From 79e81157a87aff69eb5aba6f7c6c52fd2c016d52 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 23 Oct 2026 07:06:17 +0000 Subject: [PATCH] deploy: 3df901dce8599a4535ac5ebf6a3744ed129a557f --- 404.html | 2 +- WiFi/index.html | 2 +- _javascript/firmware_picker.js | 5 +++-- bootloader_version_stock_audit/index.html | 2 +- cad_scan_validation/index.html | 2 +- carrier_wave/index.html | 2 +- carrier_wave_validation/index.html | 2 +- charge_voltage/index.html | 2 +- cli_build_matrix/index.html | 2 +- cli_command_availability/index.html | 2 +- cli_commands/index.html | 2 +- cli_settings_audit/index.html | 2 +- companion_contact_cache/index.html | 2 +- companion_contact_cache_results/index.html | 2 +- companion_lost_reply/index.html | 2 +- companion_offline_queue/index.html | 2 +- companion_protocol/index.html | 2 +- companion_radio_full/index.html | 2 +- companion_usb_ascii_validation/index.html | 2 +- companion_usb_client_validation/index.html | 2 +- docs/index.html | 2 +- double_frequency_v4_validation/index.html | 2 +- esp32_memory_budget/index.html | 2 +- esp32_partition_catalog/index.html | 2 +- esp32_wifi_partition_migration/index.html | 2 +- espnow_bridge_setup/index.html | 2 +- faq/index.html | 2 +- filter_tool/index.html | 2 +- firmware_memory_budget/index.html | 2 +- firmware_picker/index.html | 13 +++++++------ fleet_management/index.html | 2 +- flood_filtering/index.html | 2 +- four_fixed_tx_dwell_validation/index.html | 2 +- four_fixed_tx_single_pass_validation/index.html | 2 +- full_companion_features/index.html | 2 +- full_companion_usb_switcher/index.html | 2 +- full_profiles/index.html | 2 +- full_usb_logging_validation/index.html | 2 +- gps_tracking/index.html | 2 +- halo_keymind_settings/index.html | 2 +- .../index.html | 2 +- hardware_validation_checklist/index.html | 2 +- heltec_t114/index.html | 2 +- heltec_v4_wifi_partition_migration/index.html | 2 +- home_text_spacing/index.html | 2 +- host_cli_service/index.html | 2 +- index.html | 2 +- kiss_modem_protocol/index.html | 2 +- lora_ota_automation/index.html | 2 +- management_decoder/index.html | 2 +- management_reports/index.html | 2 +- mbedtls-tls-footprint/index.html | 2 +- mesh_pocket_gui/index.html | 2 +- mixed_scan_validation/index.html | 2 +- mixed_sf_bw_validation/index.html | 2 +- nimble_companion_trial/index.html | 2 +- nimble_companion_trial_results/index.html | 2 +- notifications/index.html | 2 +- nrf52-usb-ready-fix/index.html | 2 +- nrf52_bluetooth_debug/index.html | 2 +- nrf52_companion_storage_recovery/index.html | 2 +- nrf52_power_management/index.html | 2 +- nrf52_sleep_event_validation/index.html | 2 +- number_allocations/index.html | 2 +- old-releases/1.17.1.5-doc-audit/index.html | 2 +- old-releases/1.17.1.5/index.html | 2 +- old-releases/1.17.1.6/index.html | 2 +- old-releases/1.17.1.7/index.html | 2 +- old-releases/index.html | 2 +- optional_screens/index.html | 2 +- ota_easy/index.html | 2 +- ota_meshtower_v2_sdcard/index.html | 2 +- ota_nrf52_bootloader_update/index.html | 2 +- ota_nrf52_qspi/index.html | 2 +- ota_protocol/index.html | 2 +- ota_user_guide/index.html | 2 +- packet_format/index.html | 2 +- pair_4p6_300us_validation/index.html | 2 +- pair_preamble32_validation/index.html | 2 +- payloads/index.html | 2 +- power_saving_diagnostics/index.html | 2 +- pr7_review_validation/index.html | 2 +- preamble_detection_investigation/index.html | 2 +- preset_test/index.html | 2 +- qr_codes/index.html | 2 +- radio_chirp_math_validation/index.html | 2 +- radio_dwell_policy_validation/index.html | 2 +- radio_profile_switch_validation/index.html | 2 +- radio_profiles/index.html | 2 +- radio_profiles_validation/index.html | 2 +- radio_receive_calibration/index.html | 2 +- rak3401_1.17.1.7_legacy24/index.html | 2 +- rak3401_mota_chain/index.html | 2 +- releases/1.17.1.5-doc-audit/index.html | 2 +- releases/1.17.1.5/index.html | 2 +- releases/1.17.1.6/index.html | 2 +- releases/1.17.1.7/index.html | 2 +- releases/1.17.1.8/index.html | 2 +- releases/1.17.1.9/index.html | 2 +- research/companion_contact_cache/index.html | 2 +- .../esp32_full_build_audit_2026-10-06/index.html | 2 +- research/esp32_memory_budget/index.html | 2 +- .../firmware_choice_followup_2026-10-06/index.html | 2 +- research/firmware_memory_budget/index.html | 2 +- research/index.html | 2 +- research/linux_chromium_web_serial_tty/index.html | 2 +- research/nrf52_bluetooth_debug/index.html | 2 +- research/rak3401_1.17.1.7_legacy24/index.html | 2 +- research/rak3401_mota_chain/index.html | 2 +- role_feature_switches/index.html | 2 +- room_services/index.html | 2 +- rs232_bridge_setup/index.html | 2 +- s3_memory_soak_validation/index.html | 2 +- search/search_index.json | 2 +- sensor_tracker/index.html | 2 +- .../index.html | 2 +- sf10_10_vs_100hz_full_sample_validation/index.html | 2 +- sf10_10hz_cr_detour_validation/index.html | 2 +- sf10_full_retune_twice_validation/index.html | 2 +- .../index.html | 2 +- sf10_rollback_failure_validation/index.html | 2 +- sf125_settling_limits_validation/index.html | 2 +- sf5_250_dwell_validation/index.html | 2 +- sf6_125_settling_validation/index.html | 2 +- sf6_5p1_trace_validation/index.html | 2 +- sf6_channel_scan_validation/index.html | 2 +- sf8_5p1_trace_validation/index.html | 2 +- solarxiao_power/index.html | 2 +- spiffs_regular_file_reads/index.html | 2 +- stats_binary_frames/index.html | 2 +- telemetry_decoder/index.html | 2 +- terminal_chat_cli/index.html | 2 +- .../battery_calibration_2026-10-22/index.html | 2 +- .../bootloader_version_stock_audit/index.html | 2 +- test-results/cad_scan_validation/index.html | 2 +- test-results/carrier_wave_validation/index.html | 2 +- test-results/cli_settings_audit/index.html | 2 +- .../companion_ble_refresh_2026-10-19/index.html | 2 +- .../companion_contact_cache_results/index.html | 2 +- test-results/companion_lost_reply/index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../companion_usb_ascii_validation/index.html | 2 +- .../companion_usb_client_validation/index.html | 2 +- test-results/contact_memory_2026-10-19/index.html | 2 +- .../double_frequency_v4_validation/index.html | 2 +- .../esp32_ble_radio2_sleep_2026-10-21/index.html | 2 +- .../esp32_full_power_ap_2026-10-06/index.html | 2 +- .../esp32_management_password_2026-10-19/index.html | 2 +- .../espnow_bridge_diagnostics_2026-10-22/index.html | 2 +- .../firmware_variant_consolidation/index.html | 2 +- .../four_fixed_tx_dwell_validation/index.html | 2 +- .../four_fixed_tx_single_pass_validation/index.html | 2 +- test-results/full_usb_logging_validation/index.html | 2 +- .../index.html | 2 +- .../hardware_validation_checklist/index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../heltec_t114_display_detection/index.html | 2 +- .../heltec_v2_ble_memory_2026-10-22/index.html | 2 +- .../heltec_v3_full_memory_2026-10-19/index.html | 2 +- test-results/heltec_v3_preview3_ram/index.html | 2 +- test-results/home_text_spacing/index.html | 2 +- test-results/index.html | 2 +- test-results/mbedtls-tls-footprint/index.html | 2 +- .../index.html | 2 +- test-results/mixed_scan_validation/index.html | 2 +- test-results/mixed_sf_bw_validation/index.html | 2 +- test-results/morse_rx_hil_2026-10-12/index.html | 2 +- test-results/mqtt_tls_v4_regression/index.html | 2 +- test-results/nimble_companion_trial/index.html | 2 +- .../nimble_companion_trial_results/index.html | 2 +- test-results/nrf52-bluetooth-dfu-handoff/index.html | 2 +- test-results/nrf52-usb-ready-fix/index.html | 2 +- .../nrf52_companion_storage_recovery/index.html | 2 +- test-results/pair_4p6_300us_validation/index.html | 2 +- test-results/pair_preamble32_validation/index.html | 2 +- test-results/pr7_review_validation/index.html | 2 +- .../preamble_detection_investigation/index.html | 2 +- .../heltec-v4-preview4-bluetooth-oled/index.html | 2 +- test-results/preview-1.17.1.8/index.html | 2 +- .../index.html | 2 +- test-results/radio_chirp_math_validation/index.html | 2 +- .../radio_dwell_policy_validation/index.html | 2 +- .../radio_profile_switch_validation/index.html | 2 +- test-results/radio_profiles_validation/index.html | 2 +- test-results/radio_receive_calibration/index.html | 2 +- .../index.html | 2 +- .../rak_release_fixes_2026-10-22/index.html | 13 +++++++++---- .../regulator_recovery_2026-10-18/index.html | 2 +- test-results/room_mailboxes/index.html | 2 +- .../rs232_bridge_diagnostics_2026-10-22/index.html | 2 +- test-results/s3_memory_soak_validation/index.html | 2 +- test-results/sensor_dog_tracker/index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../sf10_10hz_cr_detour_validation/index.html | 2 +- .../sf10_full_retune_twice_validation/index.html | 2 +- .../index.html | 2 +- .../sf10_rollback_failure_validation/index.html | 2 +- .../sf125_settling_limits_validation/index.html | 2 +- test-results/sf5_250_dwell_validation/index.html | 2 +- test-results/sf6_125_settling_validation/index.html | 2 +- test-results/sf6_5p1_trace_validation/index.html | 2 +- test-results/sf6_channel_scan_validation/index.html | 2 +- test-results/sf8_5p1_trace_validation/index.html | 2 +- .../index.html | 2 +- .../solarxiao_advanced_2026-10-21/index.html | 2 +- test-results/solarxiao_auto_2026-10-21/index.html | 2 +- test-results/solarxiao_basic_2026-10-21/index.html | 2 +- test-results/solarxiao_ina260_2026-10-21/index.html | 2 +- test-results/solarxiao_power_2026-10-21/index.html | 2 +- test-results/solarxiao_room_2026-10-21/index.html | 2 +- test-results/spiffs_regular_file_reads/index.html | 2 +- test-results/station_g3_g2_compatibility/index.html | 2 +- .../index.html | 2 +- test-results/tdeck_radio2_display/index.html | 2 +- .../tdeck_radio2_retune_experiments/index.html | 2 +- test-results/udp_bridge_2026-10-22/index.html | 2 +- .../udp_bridge_hardware_2026-10-22/index.html | 2 +- .../unit_test_workflow_fix_2026-10-22/index.html | 2 +- test-results/usb_serial_backpressure/index.html | 2 +- .../v4_mqtt_bellevue_g2_2026-10-21/index.html | 2 +- test-results/v4_pixel5_font_trial/index.html | 2 +- tx_beacons/index.html | 2 +- udp_bridge_setup/index.html | 2 +- upgrading_1.17.1.8_to_1.17.1.9/index.html | 2 +- usb_logging_watchdog/index.html | 2 +- usb_serial_backpressure/index.html | 2 +- v4_pixel5_font_trial/index.html | 2 +- 237 files changed, 253 insertions(+), 246 deletions(-) diff --git a/404.html b/404.html index e89891d4d..702a397b3 100644 --- a/404.html +++ b/404.html @@ -2361,7 +2361,7 @@ - + diff --git a/WiFi/index.html b/WiFi/index.html index 0a41747cf..7a3af8fa0 100644 --- a/WiFi/index.html +++ b/WiFi/index.html @@ -3614,7 +3614,7 @@ and NTP diagnostics remain infrastructure features.

- + diff --git a/_javascript/firmware_picker.js b/_javascript/firmware_picker.js index d55ccb0c7..108279c54 100644 --- a/_javascript/firmware_picker.js +++ b/_javascript/firmware_picker.js @@ -2470,7 +2470,7 @@ let bootloaderCatalog = null; let bootloaderNotice = "Loading the latest bootloader downloads..."; const filters = {}; - let showReducedSensors = false; + let showReducedSensors = true; const sensorOptions = createElement("details"); sensorOptions.className = "firmware-picker-chip-family firmware-picker-wide"; sensorOptions.hidden = true; @@ -2478,6 +2478,7 @@ const sensorLabel = createElement("label"); const sensorToggle = createElement("input"); sensorToggle.type = "checkbox"; + sensorToggle.checked = showReducedSensors; sensorLabel.appendChild(sensorToggle); sensorLabel.appendChild(document.createTextNode(" Show Reduced sensor images")); sensorOptions.appendChild(sensorLabel); @@ -2941,7 +2942,7 @@ event.preventDefault(); if (!catalog.profiles.length) return; automaticChipFamily = false; - showReducedSensors = false; + showReducedSensors = true; sensorOptions.open = false; if (linkStatus) linkStatus.textContent = ""; FACET_FIELDS.forEach(function (field) { diff --git a/bootloader_version_stock_audit/index.html b/bootloader_version_stock_audit/index.html index 86c2723a9..1b59448c9 100644 --- a/bootloader_version_stock_audit/index.html +++ b/bootloader_version_stock_audit/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/cad_scan_validation/index.html b/cad_scan_validation/index.html index 3b8318136..dca65e62c 100644 --- a/cad_scan_validation/index.html +++ b/cad_scan_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/carrier_wave/index.html b/carrier_wave/index.html index 72722bc78..d7909e847 100644 --- a/carrier_wave/index.html +++ b/carrier_wave/index.html @@ -2850,7 +2850,7 @@ reproduction steps, and the tested preamble settings.

- + diff --git a/carrier_wave_validation/index.html b/carrier_wave_validation/index.html index f594a7ee9..40e83de67 100644 --- a/carrier_wave_validation/index.html +++ b/carrier_wave_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/charge_voltage/index.html b/charge_voltage/index.html index 3a2204682..dc4ff103e 100644 --- a/charge_voltage/index.html +++ b/charge_voltage/index.html @@ -2518,7 +2518,7 @@ See Battery profil - + diff --git a/cli_build_matrix/index.html b/cli_build_matrix/index.html index 32f2f40fb..7e2d92115 100644 --- a/cli_build_matrix/index.html +++ b/cli_build_matrix/index.html @@ -3000,7 +3000,7 @@ does not contain its linked marker is rejected during the build.

- + diff --git a/cli_command_availability/index.html b/cli_command_availability/index.html index 79d5e0449..eeea2124f 100644 --- a/cli_command_availability/index.html +++ b/cli_command_availability/index.html @@ -6128,7 +6128,7 @@ selected by that manifest.

- + diff --git a/cli_commands/index.html b/cli_commands/index.html index f1412878e..fd7b5edab 100644 --- a/cli_commands/index.html +++ b/cli_commands/index.html @@ -13363,7 +13363,7 @@ is active.

- + diff --git a/cli_settings_audit/index.html b/cli_settings_audit/index.html index ac3b6d816..1c8c1236f 100644 --- a/cli_settings_audit/index.html +++ b/cli_settings_audit/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/companion_contact_cache/index.html b/companion_contact_cache/index.html index 88562f60d..000254267 100644 --- a/companion_contact_cache/index.html +++ b/companion_contact_cache/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/companion_contact_cache_results/index.html b/companion_contact_cache_results/index.html index 019141ed7..c59925d28 100644 --- a/companion_contact_cache_results/index.html +++ b/companion_contact_cache_results/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/companion_lost_reply/index.html b/companion_lost_reply/index.html index 70d5f2a3c..02a8b1296 100644 --- a/companion_lost_reply/index.html +++ b/companion_lost_reply/index.html @@ -2571,7 +2571,7 @@ responder. Normal message reception and notification behavior remain in place. - + diff --git a/companion_offline_queue/index.html b/companion_offline_queue/index.html index 5c122f01d..482c0316f 100644 --- a/companion_offline_queue/index.html +++ b/companion_offline_queue/index.html @@ -2626,7 +2626,7 @@ protocol setting to resize it at runtime.

- + diff --git a/companion_protocol/index.html b/companion_protocol/index.html index 58b76158f..103892f12 100644 --- a/companion_protocol/index.html +++ b/companion_protocol/index.html @@ -5481,7 +5481,7 @@ response = wait_for_response(PACKET_MSG_SENT) - + diff --git a/companion_radio_full/index.html b/companion_radio_full/index.html index 1eb614717..5ba5591ed 100644 --- a/companion_radio_full/index.html +++ b/companion_radio_full/index.html @@ -3943,7 +3943,7 @@ selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior. - + diff --git a/companion_usb_ascii_validation/index.html b/companion_usb_ascii_validation/index.html index ac69cf1e5..eea5c2fa7 100644 --- a/companion_usb_ascii_validation/index.html +++ b/companion_usb_ascii_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/companion_usb_client_validation/index.html b/companion_usb_client_validation/index.html index e045c9697..4c258aad3 100644 --- a/companion_usb_client_validation/index.html +++ b/companion_usb_client_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/docs/index.html b/docs/index.html index 459ebbacd..431fe07f7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2490,7 +2490,7 @@ pip install mkdocs-material - + diff --git a/double_frequency_v4_validation/index.html b/double_frequency_v4_validation/index.html index affd41e62..be4d4e4b4 100644 --- a/double_frequency_v4_validation/index.html +++ b/double_frequency_v4_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/esp32_memory_budget/index.html b/esp32_memory_budget/index.html index 9278b3aa2..b5fc6d373 100644 --- a/esp32_memory_budget/index.html +++ b/esp32_memory_budget/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/esp32_partition_catalog/index.html b/esp32_partition_catalog/index.html index ea0bb34d3..cd43d0934 100644 --- a/esp32_partition_catalog/index.html +++ b/esp32_partition_catalog/index.html @@ -2705,7 +2705,7 @@ LUT. No scheduled refresh job or release upload is installed by this script.

- + diff --git a/esp32_wifi_partition_migration/index.html b/esp32_wifi_partition_migration/index.html index dc7c666ec..3d127cca9 100644 --- a/esp32_wifi_partition_migration/index.html +++ b/esp32_wifi_partition_migration/index.html @@ -3030,7 +3030,7 @@ The 16 MiB target table creates two 6.25 MiB OTA slots. See the - + diff --git a/espnow_bridge_setup/index.html b/espnow_bridge_setup/index.html index ee6e3e9a7..d80df0a30 100644 --- a/espnow_bridge_setup/index.html +++ b/espnow_bridge_setup/index.html @@ -2942,7 +2942,7 @@ a crossover-only block. See the
- + diff --git a/faq/index.html b/faq/index.html index b748cf1cb..e82438035 100644 --- a/faq/index.html +++ b/faq/index.html @@ -5365,7 +5365,7 @@ Flash one of the Ethernet-enabled firmware variants: - + diff --git a/filter_tool/index.html b/filter_tool/index.html index 5686ff5b7..15c053a96 100644 --- a/filter_tool/index.html +++ b/filter_tool/index.html @@ -3344,7 +3344,7 @@ sized structure for every possible condition and action.

- + diff --git a/firmware_memory_budget/index.html b/firmware_memory_budget/index.html index 1beeb205b..d068ea5c7 100644 --- a/firmware_memory_budget/index.html +++ b/firmware_memory_budget/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/firmware_picker/index.html b/firmware_picker/index.html index 24385e009..20f209c3b 100644 --- a/firmware_picker/index.html +++ b/firmware_picker/index.html @@ -2772,11 +2772,12 @@ Once the exact board and role are selected, the picker skips logging, connection, and profile choices that all select the same firmware. Logging settings move into Restore your settings after flashing beneath the files. Choices that select different images remain visible. -For qualified nRF52 sensor pairs, Full is the normal recommendation. Open -Advanced: sensor compatibility → Show Reduced sensor images to select a Reduced image for -an existing update route or its smaller optional-driver set. Shared links -that explicitly select Reduced retain that selection. Both downloads remain -available; this changes the recommendation, not their OTA identities. +For qualified nRF52 sensor pairs, Full is the normal recommendation and both +Full and Reduced profiles are shown by default. Show Reduced sensor images +under Advanced: sensor compatibility starts checked, including after clearing +the picker. Uncheck it to show only the recommended Full profiles. Shared links +retain their selected profile; showing Reduced does not switch a Full selection +or change either image's OTA identity. The optional chip-family filter (ESP32, nRF52, RP2040, or STM32) can narrow the hardware list first. You can skip it: picking hardware fills it in automatically. Chip family and hardware are the only dropdowns. All remaining choices use @@ -3349,7 +3350,7 @@ and each replacement's .memory.json report before updating.

- + diff --git a/fleet_management/index.html b/fleet_management/index.html index e00e6fac2..9a4bc2d43 100644 --- a/fleet_management/index.html +++ b/fleet_management/index.html @@ -3169,7 +3169,7 @@ amplification; it cannot recover airtime already occupied by an RF transmitter.< - + diff --git a/flood_filtering/index.html b/flood_filtering/index.html index b6814a89b..c9d1e2d9c 100644 --- a/flood_filtering/index.html +++ b/flood_filtering/index.html @@ -4440,7 +4440,7 @@ inspect or reset those separately when troubleshooting.

- + diff --git a/four_fixed_tx_dwell_validation/index.html b/four_fixed_tx_dwell_validation/index.html index 1fa88d8b9..d98c942e8 100644 --- a/four_fixed_tx_dwell_validation/index.html +++ b/four_fixed_tx_dwell_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/four_fixed_tx_single_pass_validation/index.html b/four_fixed_tx_single_pass_validation/index.html index ca307637f..6f8a24d72 100644 --- a/four_fixed_tx_single_pass_validation/index.html +++ b/four_fixed_tx_single_pass_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/full_companion_features/index.html b/full_companion_features/index.html index 1c163e9a4..e658830f2 100644 --- a/full_companion_features/index.html +++ b/full_companion_features/index.html @@ -2812,7 +2812,7 @@ serve host-supplied packages without needing that receiver storage layout.

- + diff --git a/full_companion_usb_switcher/index.html b/full_companion_usb_switcher/index.html index 25d69ca23..cefafae5e 100644 --- a/full_companion_usb_switcher/index.html +++ b/full_companion_usb_switcher/index.html @@ -3051,7 +3051,7 @@ ownership in
- + diff --git a/full_profiles/index.html b/full_profiles/index.html index 9307d2930..93b20734a 100644 --- a/full_profiles/index.html +++ b/full_profiles/index.html @@ -2798,7 +2798,7 @@ network setup.

- + diff --git a/full_usb_logging_validation/index.html b/full_usb_logging_validation/index.html index 11d4e0dee..5cb50a945 100644 --- a/full_usb_logging_validation/index.html +++ b/full_usb_logging_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/gps_tracking/index.html b/gps_tracking/index.html index 04923ff6f..97a50f8af 100644 --- a/gps_tracking/index.html +++ b/gps_tracking/index.html @@ -2785,7 +2785,7 @@ fresh data is available.

- + diff --git a/halo_keymind_settings/index.html b/halo_keymind_settings/index.html index fa46ee056..494708609 100644 --- a/halo_keymind_settings/index.html +++ b/halo_keymind_settings/index.html @@ -3369,7 +3369,7 @@ set flood.retry.path 2 - + diff --git a/hardware_validation_bluetooth_stealth_2026-09-07/index.html b/hardware_validation_bluetooth_stealth_2026-09-07/index.html index 3e6229b24..947d5a25e 100644 --- a/hardware_validation_bluetooth_stealth_2026-09-07/index.html +++ b/hardware_validation_bluetooth_stealth_2026-09-07/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/hardware_validation_checklist/index.html b/hardware_validation_checklist/index.html index d31243de2..c1bb7e5a7 100644 --- a/hardware_validation_checklist/index.html +++ b/hardware_validation_checklist/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/heltec_t114/index.html b/heltec_t114/index.html index cd150221e..13fbf5d40 100644 --- a/heltec_t114/index.html +++ b/heltec_t114/index.html @@ -2446,7 +2446,7 @@ for the startup checks and hardware-testing boundary.

- + diff --git a/heltec_v4_wifi_partition_migration/index.html b/heltec_v4_wifi_partition_migration/index.html index 8900a1456..5c75d4e8b 100644 --- a/heltec_v4_wifi_partition_migration/index.html +++ b/heltec_v4_wifi_partition_migration/index.html @@ -2445,7 +2445,7 @@ route instead restores supported settings and retains the saved admin password.< - + diff --git a/home_text_spacing/index.html b/home_text_spacing/index.html index 618461853..be7701c44 100644 --- a/home_text_spacing/index.html +++ b/home_text_spacing/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/host_cli_service/index.html b/host_cli_service/index.html index b4ee69c8b..b726bf22e 100644 --- a/host_cli_service/index.html +++ b/host_cli_service/index.html @@ -2861,7 +2861,7 @@ sign serial requests.

- + diff --git a/index.html b/index.html index e28a3101f..e97ff6f7b 100644 --- a/index.html +++ b/index.html @@ -2576,7 +2576,7 @@ - + diff --git a/kiss_modem_protocol/index.html b/kiss_modem_protocol/index.html index 617b928e1..3177759b1 100644 --- a/kiss_modem_protocol/index.html +++ b/kiss_modem_protocol/index.html @@ -4228,7 +4228,7 @@ never falls back to the other profile.

- + diff --git a/lora_ota_automation/index.html b/lora_ota_automation/index.html index a422bf987..9f1c377e5 100644 --- a/lora_ota_automation/index.html +++ b/lora_ota_automation/index.html @@ -3863,7 +3863,7 @@ image: the default active-download guard preserves it until you explicitly use - + diff --git a/management_decoder/index.html b/management_decoder/index.html index bb38c1ee0..f9f96dca9 100644 --- a/management_decoder/index.html +++ b/management_decoder/index.html @@ -2605,7 +2605,7 @@ offline Python capture tool, see
Management rep - + diff --git a/management_reports/index.html b/management_reports/index.html index c22015e27..7862fda01 100644 --- a/management_reports/index.html +++ b/management_reports/index.html @@ -2981,7 +2981,7 @@ public metadata and entry/page counts remain visible.

- + diff --git a/mbedtls-tls-footprint/index.html b/mbedtls-tls-footprint/index.html index 1a5bdf4a6..6faecee93 100644 --- a/mbedtls-tls-footprint/index.html +++ b/mbedtls-tls-footprint/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/mesh_pocket_gui/index.html b/mesh_pocket_gui/index.html index 466d153d1..80194586f 100644 --- a/mesh_pocket_gui/index.html +++ b/mesh_pocket_gui/index.html @@ -2471,7 +2471,7 @@ retains the standard Pocket display.

- + diff --git a/mixed_scan_validation/index.html b/mixed_scan_validation/index.html index 61f597f3f..67a423e3f 100644 --- a/mixed_scan_validation/index.html +++ b/mixed_scan_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/mixed_sf_bw_validation/index.html b/mixed_sf_bw_validation/index.html index 36594964d..e24cbfd56 100644 --- a/mixed_sf_bw_validation/index.html +++ b/mixed_sf_bw_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/nimble_companion_trial/index.html b/nimble_companion_trial/index.html index 23bdffb4b..c2a92fc9f 100644 --- a/nimble_companion_trial/index.html +++ b/nimble_companion_trial/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/nimble_companion_trial_results/index.html b/nimble_companion_trial_results/index.html index de4eadf08..87630394d 100644 --- a/nimble_companion_trial_results/index.html +++ b/nimble_companion_trial_results/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/notifications/index.html b/notifications/index.html index 56594eca8..b428f27be 100644 --- a/notifications/index.html +++ b/notifications/index.html @@ -2887,7 +2887,7 @@ If those should stay quiet too, revoke their permission with - + diff --git a/nrf52-usb-ready-fix/index.html b/nrf52-usb-ready-fix/index.html index 8b04e242d..ecb3b051f 100644 --- a/nrf52-usb-ready-fix/index.html +++ b/nrf52-usb-ready-fix/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/nrf52_bluetooth_debug/index.html b/nrf52_bluetooth_debug/index.html index 593ba390b..783eba968 100644 --- a/nrf52_bluetooth_debug/index.html +++ b/nrf52_bluetooth_debug/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/nrf52_companion_storage_recovery/index.html b/nrf52_companion_storage_recovery/index.html index 5990f3d36..264f0af74 100644 --- a/nrf52_companion_storage_recovery/index.html +++ b/nrf52_companion_storage_recovery/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/nrf52_power_management/index.html b/nrf52_power_management/index.html index e5c8ae89c..d546f746f 100644 --- a/nrf52_power_management/index.html +++ b/nrf52_power_management/index.html @@ -3365,7 +3365,7 @@ from the battery-protection SYSTEMOFF shutdown described here.

- + diff --git a/nrf52_sleep_event_validation/index.html b/nrf52_sleep_event_validation/index.html index f7dcc07a8..457e924eb 100644 --- a/nrf52_sleep_event_validation/index.html +++ b/nrf52_sleep_event_validation/index.html @@ -2668,7 +2668,7 @@ veto path instead.

- + diff --git a/number_allocations/index.html b/number_allocations/index.html index 358cc3dd3..39638f216 100644 --- a/number_allocations/index.html +++ b/number_allocations/index.html @@ -2469,7 +2469,7 @@ - + diff --git a/old-releases/1.17.1.5-doc-audit/index.html b/old-releases/1.17.1.5-doc-audit/index.html index 269a81f7f..7c0a5f5ee 100644 --- a/old-releases/1.17.1.5-doc-audit/index.html +++ b/old-releases/1.17.1.5-doc-audit/index.html @@ -2775,7 +2775,7 @@ it does not certify all historical statements as current hardware behavior.

- + diff --git a/old-releases/1.17.1.5/index.html b/old-releases/1.17.1.5/index.html index e2ed29046..a00ce4654 100644 --- a/old-releases/1.17.1.5/index.html +++ b/old-releases/1.17.1.5/index.html @@ -2831,7 +2831,7 @@ the larger layout and do not fit a 1.25 MiB slot.

- + diff --git a/old-releases/1.17.1.6/index.html b/old-releases/1.17.1.6/index.html index 2d9a02993..fe20f37ca 100644 --- a/old-releases/1.17.1.6/index.html +++ b/old-releases/1.17.1.6/index.html @@ -2593,7 +2593,7 @@ remain available; Full alternatives require their matching partition layout.

- + diff --git a/old-releases/1.17.1.7/index.html b/old-releases/1.17.1.7/index.html index c3b4901d5..9d97165c9 100644 --- a/old-releases/1.17.1.7/index.html +++ b/old-releases/1.17.1.7/index.html @@ -2440,7 +2440,7 @@ - + diff --git a/old-releases/index.html b/old-releases/index.html index 1a0dd12ff..c84103b9c 100644 --- a/old-releases/index.html +++ b/old-releases/index.html @@ -2420,7 +2420,7 @@ - + diff --git a/optional_screens/index.html b/optional_screens/index.html index 68c311ff5..83e81a657 100644 --- a/optional_screens/index.html +++ b/optional_screens/index.html @@ -2619,7 +2619,7 @@ transmit/receive, and GPS or touch operation where fitted.

- + diff --git a/ota_easy/index.html b/ota_easy/index.html index ab82ad73e..f92250130 100644 --- a/ota_easy/index.html +++ b/ota_easy/index.html @@ -3327,7 +3327,7 @@ and container internals, see
the OTA protocol specifi - + diff --git a/ota_meshtower_v2_sdcard/index.html b/ota_meshtower_v2_sdcard/index.html index 7c0f7199d..8eab708e0 100644 --- a/ota_meshtower_v2_sdcard/index.html +++ b/ota_meshtower_v2_sdcard/index.html @@ -3031,7 +3031,7 @@ as an alias.

- + diff --git a/ota_nrf52_bootloader_update/index.html b/ota_nrf52_bootloader_update/index.html index a5db43591..640b0009d 100644 --- a/ota_nrf52_bootloader_update/index.html +++ b/ota_nrf52_bootloader_update/index.html @@ -2908,7 +2908,7 @@ Use USB/BLE DFU or SWD for initial provisioning and local recovery.

- + diff --git a/ota_nrf52_qspi/index.html b/ota_nrf52_qspi/index.html index cde5225eb..a4f99bf40 100644 --- a/ota_nrf52_qspi/index.html +++ b/ota_nrf52_qspi/index.html @@ -3351,7 +3351,7 @@ card insertion has been field-tested.

- + diff --git a/ota_protocol/index.html b/ota_protocol/index.html index 464c81a91..7ac5e0f09 100644 --- a/ota_protocol/index.html +++ b/ota_protocol/index.html @@ -4422,7 +4422,7 @@ reject v2+BOOTLOADER, v3 without exact FULL|SIGNED|BOOTLOADER, and - + diff --git a/ota_user_guide/index.html b/ota_user_guide/index.html index e2a0ebdac..da78f54ed 100644 --- a/ota_user_guide/index.html +++ b/ota_user_guide/index.html @@ -3542,7 +3542,7 @@ that only contains what changed). You get them by:

- + diff --git a/packet_format/index.html b/packet_format/index.html index 244e3fdf9..01a0636c0 100644 --- a/packet_format/index.html +++ b/packet_format/index.html @@ -2998,7 +2998,7 @@ support only a subset of the path encodings and payload types described here.

- + diff --git a/pair_4p6_300us_validation/index.html b/pair_4p6_300us_validation/index.html index fac2a905b..9977882eb 100644 --- a/pair_4p6_300us_validation/index.html +++ b/pair_4p6_300us_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/pair_preamble32_validation/index.html b/pair_preamble32_validation/index.html index de06645ef..1126f121e 100644 --- a/pair_preamble32_validation/index.html +++ b/pair_preamble32_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/payloads/index.html b/payloads/index.html index b7b406b44..519b2becb 100644 --- a/payloads/index.html +++ b/payloads/index.html @@ -4273,7 +4273,7 @@ relay an opaque OTA packet when their routing policy permits it.

- + diff --git a/power_saving_diagnostics/index.html b/power_saving_diagnostics/index.html index 6013a357f..f3d0c07d6 100644 --- a/power_saving_diagnostics/index.html +++ b/power_saving_diagnostics/index.html @@ -2907,7 +2907,7 @@ to validate the TX improvement and quantify power use.

- + diff --git a/pr7_review_validation/index.html b/pr7_review_validation/index.html index d70bc21c7..2369e4929 100644 --- a/pr7_review_validation/index.html +++ b/pr7_review_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/preamble_detection_investigation/index.html b/preamble_detection_investigation/index.html index f4f5b354a..6bdcc87ad 100644 --- a/preamble_detection_investigation/index.html +++ b/preamble_detection_investigation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/preset_test/index.html b/preset_test/index.html index 30ebf488f..2fc55eac3 100644 --- a/preset_test/index.html +++ b/preset_test/index.html @@ -2912,7 +2912,7 @@ clock - + diff --git a/qr_codes/index.html b/qr_codes/index.html index 21126db8b..6f181b0c9 100644 --- a/qr_codes/index.html +++ b/qr_codes/index.html @@ -2535,7 +2535,7 @@ - + diff --git a/radio_chirp_math_validation/index.html b/radio_chirp_math_validation/index.html index d9f307335..9f6aaef95 100644 --- a/radio_chirp_math_validation/index.html +++ b/radio_chirp_math_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/radio_dwell_policy_validation/index.html b/radio_dwell_policy_validation/index.html index f29bb9b6e..0a265d81d 100644 --- a/radio_dwell_policy_validation/index.html +++ b/radio_dwell_policy_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/radio_profile_switch_validation/index.html b/radio_profile_switch_validation/index.html index ae049a45e..07934bd9c 100644 --- a/radio_profile_switch_validation/index.html +++ b/radio_profile_switch_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/radio_profiles/index.html b/radio_profiles/index.html index a7fcff9be..6731fa1d3 100644 --- a/radio_profiles/index.html +++ b/radio_profiles/index.html @@ -4302,7 +4302,7 @@ single-profile mode.

- + diff --git a/radio_profiles_validation/index.html b/radio_profiles_validation/index.html index 28a309888..82142f283 100644 --- a/radio_profiles_validation/index.html +++ b/radio_profiles_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/radio_receive_calibration/index.html b/radio_receive_calibration/index.html index fc4ab08fd..c86bb2b52 100644 --- a/radio_receive_calibration/index.html +++ b/radio_receive_calibration/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/rak3401_1.17.1.7_legacy24/index.html b/rak3401_1.17.1.7_legacy24/index.html index 6cc2c18d0..1bce0a0fd 100644 --- a/rak3401_1.17.1.7_legacy24/index.html +++ b/rak3401_1.17.1.7_legacy24/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/rak3401_mota_chain/index.html b/rak3401_mota_chain/index.html index ef7149913..91162cc39 100644 --- a/rak3401_mota_chain/index.html +++ b/rak3401_mota_chain/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/releases/1.17.1.5-doc-audit/index.html b/releases/1.17.1.5-doc-audit/index.html index 72df5f26f..153d69683 100644 --- a/releases/1.17.1.5-doc-audit/index.html +++ b/releases/1.17.1.5-doc-audit/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/releases/1.17.1.5/index.html b/releases/1.17.1.5/index.html index 491aac679..34b5a8588 100644 --- a/releases/1.17.1.5/index.html +++ b/releases/1.17.1.5/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/releases/1.17.1.6/index.html b/releases/1.17.1.6/index.html index 82d0e5281..f26c87f69 100644 --- a/releases/1.17.1.6/index.html +++ b/releases/1.17.1.6/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/releases/1.17.1.7/index.html b/releases/1.17.1.7/index.html index 7149071aa..f01e80767 100644 --- a/releases/1.17.1.7/index.html +++ b/releases/1.17.1.7/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/releases/1.17.1.8/index.html b/releases/1.17.1.8/index.html index c2f9ce4ca..0db1fbc5d 100644 --- a/releases/1.17.1.8/index.html +++ b/releases/1.17.1.8/index.html @@ -2556,7 +2556,7 @@ - + diff --git a/releases/1.17.1.9/index.html b/releases/1.17.1.9/index.html index 42d63191f..de26d46a0 100644 --- a/releases/1.17.1.9/index.html +++ b/releases/1.17.1.9/index.html @@ -2738,7 +2738,7 @@ phone, operating system, bootloader or starting firmware version.

- + diff --git a/research/companion_contact_cache/index.html b/research/companion_contact_cache/index.html index 21986332e..c399adf66 100644 --- a/research/companion_contact_cache/index.html +++ b/research/companion_contact_cache/index.html @@ -2779,7 +2779,7 @@ policy. The second invocation verifies the persisted paths after reboot.

- + diff --git a/research/esp32_full_build_audit_2026-10-06/index.html b/research/esp32_full_build_audit_2026-10-06/index.html index 3e15c8d35..387432a8e 100644 --- a/research/esp32_full_build_audit_2026-10-06/index.html +++ b/research/esp32_full_build_audit_2026-10-06/index.html @@ -2899,7 +2899,7 @@ qualification; build success alone does not cover them.

- + diff --git a/research/esp32_memory_budget/index.html b/research/esp32_memory_budget/index.html index f55f093ae..b7518a4cf 100644 --- a/research/esp32_memory_budget/index.html +++ b/research/esp32_memory_budget/index.html @@ -2503,7 +2503,7 @@ the Arduino 2.x repeater releases unused Bluetooth controller RAM in - + diff --git a/research/firmware_choice_followup_2026-10-06/index.html b/research/firmware_choice_followup_2026-10-06/index.html index 1a153d2f7..aa4cf1742 100644 --- a/research/firmware_choice_followup_2026-10-06/index.html +++ b/research/firmware_choice_followup_2026-10-06/index.html @@ -2707,7 +2707,7 @@ applications unless a separate combined application is designed and tested.

- + diff --git a/research/firmware_memory_budget/index.html b/research/firmware_memory_budget/index.html index f8d99cc4c..50a6ffef0 100644 --- a/research/firmware_memory_budget/index.html +++ b/research/firmware_memory_budget/index.html @@ -3678,7 +3678,7 @@ for artifact hashes, tests, upgrade behavior and hardware limits.

- + diff --git a/research/index.html b/research/index.html index 66812e55c..c9a14d213 100644 --- a/research/index.html +++ b/research/index.html @@ -2515,7 +2515,7 @@ - + diff --git a/research/linux_chromium_web_serial_tty/index.html b/research/linux_chromium_web_serial_tty/index.html index 39f6e0e28..2d6be1739 100644 --- a/research/linux_chromium_web_serial_tty/index.html +++ b/research/linux_chromium_web_serial_tty/index.html @@ -2542,7 +2542,7 @@ for the context-manager example and the hardware-free pseudoterminal test.

- + diff --git a/research/nrf52_bluetooth_debug/index.html b/research/nrf52_bluetooth_debug/index.html index 0c34dbefa..1e464941a 100644 --- a/research/nrf52_bluetooth_debug/index.html +++ b/research/nrf52_bluetooth_debug/index.html @@ -2566,7 +2566,7 @@ SoftDevice radio timing and invalidates the test.

- + diff --git a/research/rak3401_1.17.1.7_legacy24/index.html b/research/rak3401_1.17.1.7_legacy24/index.html index 2694ef79d..52a3f4dc5 100644 --- a/research/rak3401_1.17.1.7_legacy24/index.html +++ b/research/rak3401_1.17.1.7_legacy24/index.html @@ -2814,7 +2814,7 @@ checkout directories that share the same PlatformIO package installation.

- + diff --git a/research/rak3401_mota_chain/index.html b/research/rak3401_mota_chain/index.html index e1d6c2d5f..486c78d57 100644 --- a/research/rak3401_mota_chain/index.html +++ b/research/rak3401_mota_chain/index.html @@ -3433,7 +3433,7 @@ because its number appears first.

- + diff --git a/role_feature_switches/index.html b/role_feature_switches/index.html index 0f72c224f..d93cf1d81 100644 --- a/role_feature_switches/index.html +++ b/role_feature_switches/index.html @@ -3262,7 +3262,7 @@ and
Full Companion feature guide provi - + diff --git a/room_services/index.html b/room_services/index.html index ec44ee478..04da239b7 100644 --- a/room_services/index.html +++ b/room_services/index.html @@ -3355,7 +3355,7 @@ a physical LoRa/Wi-Fi transfer.

- + diff --git a/rs232_bridge_setup/index.html b/rs232_bridge_setup/index.html index 46e0fb002..115c2ab3b 100644 --- a/rs232_bridge_setup/index.html +++ b/rs232_bridge_setup/index.html @@ -2687,7 +2687,7 @@ corresponding peer and message checks.

- + diff --git a/s3_memory_soak_validation/index.html b/s3_memory_soak_validation/index.html index ca923b899..2a6cc554c 100644 --- a/s3_memory_soak_validation/index.html +++ b/s3_memory_soak_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/search/search_index.json b/search/search_index.json index 54a44e6c9..2d3e387e5 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Home","text":"

View this page on MeshCore Docs.

"},{"location":"#meshcore-documentation","title":"MeshCore documentation","text":"

Choose firmware with the firmware picker. For help with a device already in service, start with the FAQ.

"},{"location":"#current-release","title":"Current release","text":""},{"location":"#documentation","title":"Documentation","text":"

The navigation groups the remaining guides under Documentation, including board-specific updates, networking, power, and protocol references.

"},{"location":"#tools","title":"Tools","text":""},{"location":"#engineering-records","title":"Engineering records","text":"

Found a mistake or missing information? Edit the documentation.

"},{"location":"WiFi/","title":"WiFi","text":"

View this page on MeshCore Docs.

"},{"location":"WiFi/#wifi-and-mqtt-by-firmware-type","title":"WiFi and MQTT by Firmware Type","text":"

MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:

The firmware role and the build profile are separate choices. USB logging does not itself provide MQTT. MQTT-capable Full Companions include that feature without mqtt in their filename; check the artifact capability manifest. See feature switches by role for the different Companion and infrastructure controls and the USB web console.

"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does *_repeater Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active *_repeater_observer_mqtt Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default *_room_server Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active *_room_server_observer_mqtt Yes Yes Runs the room server and uplinks radio traffic *_companion_radio_wifi Yes No Exposes the MeshCore companion protocol on TCP port 5000 *_companion_radio_wifi_mqtt Yes Yes Runs both the TCP companion interface and the MQTT uplink *_companion_radio_full* ESP32, subject to board/transport selection Where compiled on ESP32 Includes supported USB/wireless transports; configure MQTT through WebConfig Dedicated USB, BLE, or serial companion (not Full) No No Uses the transport named by the target instead *_repeater_bridge_espnow Build-dependent on ESP32 No Uses ESP-NOW for its bridge; bridge.format wrapped|raw selects legacy bridge framing or primary-ESP-NOW compatibility; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface *_sensor Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface *_lora_ota_no_external_sensors On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact start ota browser uploader

Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.

"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"

The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.

For a received packet, the flow is:

LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n                                  \\----> normal MeshCore filtering/handling\n

Packets transmitted by the node can also be queued according to mqtt.tx. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.

On infrastructure observer builds, MQTT publication and LoRa repetition are separate:

Fresh MQTT observer settings are:

Up to six broker configurations can be saved. The number that can be active at once depends on the target and available memory. A non-PSRAM ESP32 should normally use no more than two TLS/WSS brokers; some classic ESP32 targets are reliable with only one. Excess configured slots remain saved but show as inactive in get mqtt.status.

The bridge has a bounded packet queue for broker or WiFi outages and reconnects automatically. A non-PSRAM build holds 6 packets and a PSRAM build holds 50. When the queue is full, the oldest item is replaced; after five minutes with no broker connected, stale queued packets are flushed. MQTT packet delivery is best effort rather than durable storage. Each enabled slot publishes independently, so one failed broker does not intentionally stop the other slots.

"},{"location":"WiFi/#automatic-network-outage-power-saving","title":"Automatic network outage power saving","text":"

ESP32 MQTT firmware uses one shared outage timer for MQTT and NTP. If no broker demonstrates a working connection for five minutes, the MQTT worker stops its clients and NTP request before suspending background WiFi. Saved WiFi and broker settings remain enabled. Failed requests, a WiFi association, and NTP replies alone do not restart the outage timer or keep a failed MQTT uplink awake.

After 30 minutes, the firmware opens a recovery window lasting up to two minutes. A broker acknowledgment or a connection that stays up for 30 seconds returns the node to normal operation. Otherwise WiFi is suspended for another 30 minutes. An explicit management request can ask for an earlier probe. Automatic NTP requests have a 15-second timeout; failed requests wait 30 minutes instead of repeatedly running a blocking server search. Successful NTP retains the daily refresh schedule.

Active browser administration, setup/OTA access points, connected Companion clients, and an attached WiFi OTA seeder protect their network access. An idle WebConfig listener does not prevent the cooldown. An enabled SNMP agent keeps a working LAN connection available. ESP-NOW keeps its radio and channel; only the failed infrastructure connection is suspended. LoRa continues operating, and when CPU power saving is enabled, its normal sleep eligibility returns after background WiFi shuts down. USB console and logging sleep rules still apply separately. This policy does not automatically turn off working services or expire an active setup access point.

See MQTT_IMPLEMENTATION.md for the complete preset list, custom broker configuration, topic formats, authentication, diagnostics, and memory limits.

"},{"location":"WiFi/#mqtt-observer-setup","title":"MQTT observer setup","text":"

WebConfig serves gzip only, including its bootstrap and captive-portal success pages. Static WiFi setup and browser OTA pages use the same format. Builds use Google Zopfli 0.4.3 with 1,000 iterations; validated results are cached under .pio/gzip-html. There is no Brotli dependency or second WebConfig page copy. Browsers decompress the pages automatically over ordinary HTTP. WebConfig returns HTTP 406 when a client explicitly excludes gzip. Its large page closes the TCP connection gracefully after the final acknowledged bytes so browsers do not report a completed download as aborted. The bootstrap uses ES5 syntax and Promise-based loading, including on Android 5.1.1 with Chrome 43; newer JavaScript syntax must not prevent the page loading.

Most full-size ESP32 MQTT observer builds have the shared WebConfig portal. On a fresh device with no saved SSID:

  1. Join MeshCore-Setup-XXXX with the node's admin password (factory default password). A display's WiFi QR code uses the hotspot's current credential.
  2. If the captive page does not open, browse to http://192.168.4.1/.
  3. Enter WiFi, radio, identity, and MQTT settings.
  4. Select Save & Reboot.
  5. After reboot, check get wifi.status and get mqtt.status.

ESP32 management uses one saved admin password for WebConfig login, browser firmware uploads, and setup WiFi. Choose 8\u201315 printable ASCII characters so the hotspot can use it for WPA2. Other valid admin passwords (1\u201315 UTF-8 bytes, without CR or LF) leave the hotspot open and require an HTTP admin login. There is no separate setup WiFi password. A protected first-time setup AP with no saved SSID retains its provisioning flow; recovery with a saved SSID requires the existing admin login. Deliberately passwordless images retain their local no-password policy. The portal uses plain HTTP, so use a trusted network.

Changing the password ends existing browser sessions after the save succeeds. The submitting page keeps the save result and tries the new password once, then asks you to sign in if needed. A partial save shows which settings failed and does not reboot. If the setup hotspot restarts, reconnect with the new password when it is eligible for WPA2. An unconfirmed password save keeps the previous runtime password active, but the saved state after reboot may differ; read the reported error before retrying.

Credentials for other systems or different permissions remain separate:

Credential Why it does not become the node admin password Router WiFi and MQTT broker login These are issued by the network or broker, which may serve many devices. Bluetooth pairing PIN BLE pairing requires its numeric PIN format. Guest and room passwords Members and guests should not receive node administration access. Channel, bridge, fleet and alert keys These are shared with peers or recipients and have their own protocol requirements. SNMP community and management-report credentials Monitoring systems receive these credentials; they should not also receive the node admin password.

Admin login limits apply on both LAN and recovery AP: five failures lock the failing address for 30 seconds after its fifth failure. All addresses also share a budget of six failures, refilling one allowance every 6 seconds; while empty, even correct credentials receive HTTP 429. A success clears only its own address's count. The room browser uses a separate password budget; a WebConfig session does not bypass it.

On an expanded FULL profile with no saved SSID, this automatic setup AP has an absolute 30-minute window. Browser activity or a phone left associated with the AP does not extend it. If no SSID has been saved when the window expires, WebConfig closes and the ESP32 WiFi radio remains off automatically for the remainder of that boot. A reboot or power cycle starts a new 30-minute setup window; the timeout is deliberately not written to preferences. An administrator can still override the automatic cutoff with an explicit start webconfig command.

Once an SSID is saved, the provisioning cutoff no longer applies. When WiFi is selected by logging.output wifi|both, the station stays enabled and keeps trying the saved network indefinitely: ESP automatic reconnect remains on and the explicit fallback advances through 15, 30, 60, 120, then 300-second retry intervals, remaining at five minutes until it reconnects. MQTT broker retries use their own backoff; a repeatedly failing broker eventually receives one probe every 30 minutes. Selecting logging.output off|usb keeps the MQTT bridge off, so saved credentials alone do not force WiFi on unless WebConfig is also enabled explicitly.

The equivalent MQTT observer CLI setup is:

set wifi.ssid Your WiFi Name\nset wifi.pwd Your WiFi Password\nset mqtt.iata SEA\nset name MyObserver\nreboot\n

The SSID and password values are the rest of the command line. Spaces are allowed and quotes must not be added. SSIDs may contain at most 31 characters. ESP32 infrastructure MQTT builds with WebConfig share one canonical WiFi configuration between this CLI and the browser portal. Passwords accept 0-63 characters or exactly 64 hexadecimal characters for a raw WPA PSK. Leave the password value empty for an open network. Reboot after changing credentials to apply them; an already-running MQTT worker keeps its current connection settings until it stops and restarts.

Older observer-only credentials remain usable until canonical WiFi settings are saved. The first CLI edit preserves the other legacy credential while saving the complete pair. Once canonical settings exist, empty, incomplete or unreadable canonical settings do not revive old observer credentials. Historical SSID-only canonical settings continue to represent an open network. Builds without this WebConfig integration retain their legacy 63-character password limit; this change does not add MQTT support to currently disabled Pico targets.

On these ESP32 MQTT/WebConfig builds, an empty CLI SSID clears the configured network while retaining saved broker settings:

set wifi.ssid\nreboot\n

Use this to return the node to unconfigured provisioning. The WebConfig setup form still requires a nonempty SSID. After saving a new network, verify get wifi.status, get mqtt.config.valid and get mqtt.running; credentials alone do not prove a broker connection. A local get wifi.pwd can inspect the password, while an authenticated remote command returns only set/unset.

Useful checks are:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget mqtt.enabled\nget mqtt.rx\nget mqtt.tx\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n

To change a configured observer through WebConfig without leaving the portal enabled after every reboot:

start webconfig\n

This uses the LAN when WiFi is connected. To force the setup AP, the MQTT bridge must release WiFi first:

set mqtt.enabled off\nstart webconfig ap\n

After changing WiFi credentials, use Save & Reboot or reboot. For broker-only changes that keep the existing network, stop the temporary portal and restart the bridge:

stop webconfig\nset mqtt.enabled on\n

Current MQTT observer artifacts use the expanded FULL partition profile so WebConfig and the complete role CLI are retained. Install the matching merged image over USB once when moving a device from the legacy partition layout.

"},{"location":"WiFi/#wifi-companion-setup","title":"WiFi companion setup","text":"

Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off. Bluetooth starts where the build supports it, and saved WiFi enablement is preserved across upgrades. Before using the setup AP or station, enable WiFi through the USB or Bluetooth CLI:

get wifi.enabled\nset wifi.enabled 1\n

ESP32 simultaneous-transport builds apply the change without a reboot. SenseCAP Indicator exclusive-transport builds and Pico W require a reboot. WiFi-only compatibility builds without any other CLI transport retain WiFi on so they are not left unreachable. Saving an SSID/password alone does not enable WiFi on an opt-in build. Use set wifi.enabled 0 to save it off again. This controls infrastructure WiFi, not a primary ESP-NOW mesh radio.

A *_companion_radio_wifi build replaces the BLE or USB companion link with the MeshCore companion protocol over TCP port 5000. The phone or computer must be able to reach the device on the same LAN.

Most ESP32 *_companion_radio_full targets keep all three Companion links at once: USB, BLE, and TCP port 5000. The two SenseCAP Indicator Full targets are the exception: USB remains available, while a saved next-boot selector starts exactly one secondary wireless Companion transport, either BLE or infrastructure WiFi/TCP. On the ESP-NOW Indicator, this selector never stops the primary ESP-NOW mesh radio. Full ESP32 images also provide a source-only LoRa mOTA service on ports 5001 and 5002 whenever their WiFi transport is active. See the full Companion guide for its build, terminal mode, and complete update-source workflow.

The LilyGo T-Beam 1W Full Companion maps one press of the physical BOOT button (GPIO0) to a persistent WiFi on/off toggle. Turning WiFi off closes WebConfig, Companion TCP, mOTA, and MQTT network services while USB, BLE, the display, GPS, and LoRa remain available. Another BOOT press turns WiFi back on even after reboot. WiFi/WebConfig starts first; BLE starts two seconds later so the ESP32-S3 can reserve the setup server's internal heap before both radios run together.

The companion loads runtime credentials saved in NVS. A non-placeholder compile-time WIFI_SSID/WIFI_PWD can be used as a first-boot fallback, but saved credentials take priority. With no credentials, its WebConfig portal starts in setup-AP mode. With credentials, the WebUI is enabled by default on the station IP.

Full Companion can also be provisioned from its USB terminal or TCP port 5002:

get wifi.ssid\nget wifi.status\nset wifi.ssid SlowFi\nset wifi.pwd your-password\n

Credential writes are persisted immediately. After the reply has drained, the Companion restarts its WiFi station with the saved pair. A port-5002 client is expected to disconnect and can reconnect at the new LAN IP. USB masks the password as it is entered; get wifi.pwd is intentionally unavailable. The standalone credential store accepts an empty password, ordinary passphrases up to 63 characters, or an exact 64-character hexadecimal WPA/WPA2 PSK. A non-hexadecimal 64-character value and every longer value are rejected.

After the saved station first connects following boot, every ESP32 WiFi Companion requests a fresh UTC time from NTP. A verified reply updates both the MeshCore clock and any detected hardware RTC; the next explicit refresh is due 24 hours after that success. A failed request times out without blocking the radio loop and retries after five minutes. While WiFi is disconnected or only the setup AP is available, the request waits for a station connection. Builds with a configured MQTT bridge use its boot-and-daily NTP owner instead, so two firmware services never reconfigure the ESP32 SNTP client at the same time. A later WiFi reconnect does not add another successful-sync request; it preserves the existing 24-hour deadline. Until a fresh internet reply succeeds, LoRa clock consensus remains available as the fallback.

On Full Companion targets whose primary mesh radio is ESP-NOW, ESP-NOW, the setup AP, and the infrastructure-WiFi station must share one 2.4 GHz channel. The persisted channel defaults to 1 and can be inspected or changed from the text terminal:

get espnow.channel\nset espnow.channel 6\nreboot\n

Valid values are 1 through 13; use only a channel permitted in your region and supported by the router. set espnow.channel saves the new value but does not move the running radio; reboot the node to apply it. Every primary ESP-NOW node that should communicate with it must use the same channel, and the configured router's 2.4 GHz radio must be fixed to that channel as well. A router left on automatic channel selection can move the infrastructure connection away from the ESP-NOW mesh.

espnow.channel configures the node's primary ESP-NOW mesh radio. It is distinct from bridge.channel, which applies only to the separate ESP-NOW bridge feature. WiFi power saving does not let ESP-NOW and infrastructure WiFi operate on different channels. On an ESP32 Full build whose primary radio is ESP-NOW, wifi.powersave max is unavailable because a station using maximum modem sleep can miss ESP-NOW broadcasts; use min for coexistence. The primary mesh transport holds the driver's RF wake reference continuously, so min does not suspend its ESP-NOW receiver.

Espressif long-range WiFi (LR) trades speed for reach: its raw rates are 250 or 500 kbit/s, with up to about 1 km in suitable line-of-sight conditions. LR traffic needs compatible Espressif hardware at both ends. An LR-enabled access point sends LR beacons that ordinary phones, routers and Raspberry Pis cannot receive. A mixed b/g/n+LR station can still join an ordinary router, using normal WiFi rates for that connection. MeshCore's setup and OTA access points therefore use ordinary b/g/n. See Espressif's LR compatibility guide.

ESP-NOW compatibility also depends on the bytes and PHY used above the common Espressif transport. The historical *_repeater_bridge_espnow firmware wraps each MeshCore packet with a magic value, checksum, and secret-based XOR, while Generic_ESPNOW and SenseCapIndicator-ESPNow send the serialized MeshCore packet directly using the ESP-NOW LR PHY. Updated bridge firmware can select either behavior without changing the board target:

get bridge.format\nset bridge.channel 1\nset bridge.format raw\n

On a Heltec V4, the ordinary companion_radio_full image remains a LoRa-primary Companion and does not instantiate this bridge. Select and flash the existing heltec_v4_repeater_bridge_espnow role before using these commands. Treat that Companion-to-repeater change as a role/partition migration: back up the node and use the exact V4 merged install artifact.

wrapped remains the default and interoperates with existing bridge nodes that use the same channel and bridge.secret. raw interoperates with primary-ESP-NOW nodes and other raw bridges; it requires channel 1-13, enables LR reception, forces LR transmission, and ignores bridge.secret. Receive parsing is strict to the selected format, so migrate peers deliberately rather than expecting one bridge to accept both. Match bridge.channel on the LoRa gateway to the primary nodes' espnow.channel; a primary-node channel change takes effect after its reboot.

Raw mode retains MeshCore's normal packet parsing, authentication/encryption, and the bridge's duplicate suppression, but it does not provide the wrapper's secret-based network isolation. That XOR wrapper is itself only lightweight isolation, not cryptographic security. Multiple LoRa/ESP-NOW gateways covering the same nodes can temporarily amplify duplicates, even though seen-packet tracking prevents an individual bridge from immediately echoing a packet back.

If the configured network remains unavailable for two minutes, the companion opens its setup AP so the credentials can be repaired. It continues retrying the saved network. WiFi modem sleep has its own persisted wifi.powersave setting and is independent of the Companion device power-saving setting. The WiFi card in Companion WebConfig exposes none, min, and max. Fresh Cascade-profile builds select min; target-default builds select none. A saved setting takes precedence after an upgrade.

WiFi-only Companions can use all three modes. A Full Companion that runs WiFi and BLE simultaneously requires at least minimum modem sleep. It reports min when an old or target-default none value is found and rejects a new none selection. When ESP-NOW is also the primary mesh radio, max is unavailable: the access point does not buffer ESP-NOW broadcasts for a sleeping station, so maximum modem sleep can lose them. Firmware caps a previously saved max value to and reports it as min, and rejects a new max selection.

The SenseCAP Indicator Full targets select BLE or infrastructure WiFi instead of running both. On a WiFi-selected boot, the LoRa layout accepts none|min|max, while the ESP-NOW layout accepts none|min and still rejects max. On a BLE-selected boot infrastructure WiFi is off; the LoRa layout accepts min|max for the saved WiFi setting, while ESP-NOW + BLE requires min. Device power saving remains independent of this modem policy.

ESP32 WiFi Companions also have a saved admin password for LAN WebConfig and browser OTA, initially password. Their get password command reports status without revealing it. The credential is stored in NVS alongside the WiFi settings and survives a filesystem erase. Use password <new_password> through an available administrative connection to change it.

The WebConfig Advanced card exposes device power saving on WiFi Companion, repeater, and room-server builds. It also exposes RX power saving on radio chips that support receive duty cycling. RXPS can select continuous receive, levels 1-10, automatic or explicit 16/32-symbol timing assumptions, or manual receive/sleep windows. The timing assumption does not change the radio's actual wire preamble. The WebConfig WiFi card exposes WiFi modem power saving. All three settings are persisted across reboots.

The three settings are independent. A WiFi Companion keeps its transports available while device power saving reduces CPU and GPS idle power. An infrastructure node can sleep when device power saving is enabled, so its WiFi services may be temporarily unavailable. RXPS only duty-cycles the LoRa receiver. Fresh Cascade-profile builds default to device power saving on, RXPS on at level 8 with a 16-symbol preamble, and WiFi modem power saving at min on every build that includes ESP32 WiFi. The RXPS preamble value is a saved timing assumption, not the wire length: starting with v1.17.1.5, SF5-SF8 packets normally use a 32-symbol physical preamble. Firmware selects 64 for the whole SF/BW tuple only when 32 cannot enable RXPS at any level and 64 can (SF5/BW250 and SF6/BW500), then 128 only when neither shorter length works (SF5/BW500).

When ENABLE_OTA is included, a WiFi companion also listens on:

These ports do not replace the companion protocol on TCP 5000. On a companion_radio_full build, port 5002 is the same role-specific text terminal available over USB, including chat, remote administration, radio and power settings, WiFi/WebConfig management, tempradio, and the source-only ota commands. Other OTA-enabled Companion builds expose the same local maintenance commands as binary command 0x42, plus ota ... commands. LoRa staging and installation on the Full Companion itself remain disabled.

Port 5002 is plaintext and has no independent login gate. Use it only on a trusted LAN or temporary setup network, especially when entering a remote-node admin password with the terminal's login command.

FULL ESP32 builds share the port 5001 folder seeder. It starts whenever that role has a usable WiFi station or setup access point and stops when WiFi stops. For example, a FULL repeater can run start webconfig to join its saved network, then accept:

motatool serve --dir ./motas --tcp <repeater-ip>:5001 -v\n

The TCP connection supplies .mota files for the node to advertise and relay over LoRa; it is not a raw .bin uploader. start ota continues to provide the direct browser uploader on HTTP port 80. Sign in with HTTP Basic username admin and the node's current admin password. A FULL role without WebConfig but with browser OTA support can use the MeshCore-OTA access point raised by start ota; its seeder address is 192.168.4.1:5001. The runtime OTA hotspot uses the same WPA2 eligibility rule as setup WiFi. Temporary partition migration and recovery applications retain their own documented access policy.

For browser uploads from an existing 1.17.1.8 ESP32 node, see the 1.17.1.9 WiFi upgrade guide. It covers the node AP, old-page completion behavior, and choosing an application-only image or the exact partition-migration package.

Only one external folder link can be active. A TCP client is rejected while ota folder on is using USB serial, and disconnecting motatool automatically removes the TCP folder. Port 5001 has no login layer, so expose it only on a trusted LAN or temporary setup network. Firmware target and hash checks still apply at the receiving node, along with its configured signature/trust policy.

The HTTP admin login does not protect trusted TCP consoles or delegated remote CLI access. Those administrative paths can change the node's admin password; limit them to trusted users and networks.

When the shared seeder is running, get wifi.status appends its live state:

OTA TCP 5001: listening\nOTA TCP 5001: client connected\n

The first state means WiFi is usable and the node is waiting for motatool serve --tcp. The second means a motatool folder is currently attached and available for LoRa OTA service.

"},{"location":"WiFi/#wifi-companion-with-mqtt","title":"WiFi companion with MQTT","text":"

A *_companion_radio_wifi_mqtt build combines both systems:

The companion owns WiFi connection and recovery in this build. The MQTT bridge waits for that connection rather than creating a second one. Stopping MQTT does not disable the TCP companion service.

MQTT-capable Full Companions also combine these services. The WebConfig page contains MQTT settings, and the USB/TCP and browser terminals accept the same mqtt.* settings as infrastructure for shared features. Use set mqtt.enabled on|off to control MQTT without discarding broker settings. Use set logging.output off|usb|wifi|both to select both outputs together.

"},{"location":"WiFi/#webconfig-without-mqtt","title":"WebConfig without MQTT","text":"

Full-size ESP32 repeater and room-server builds can include WebConfig even when MQTT is absent. The portal then shows node, radio, and WiFi-related controls but removes the MQTT wizard step and MQTT tab.

In this case WebConfig owns WiFi only while it is needed. Stopping the portal disconnects WiFi and turns the WiFi radio off. There is no persistent MQTT connection keeping WiFi active.

Unified FULL USB + WiFi and FULL logging-fallback repeater/room-server builds both provide these CLI controls and status checks. The unified profile includes the MQTT bridge; the fallback is used only where no matching MQTT environment exists:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget webui\nset wifi.ssid SlowFi\nset wifi.pwd your-password\nset wifi.powersave none\nset wifi.cli on\n

get wifi.status distinguishes an unconfigured node, an inactive WiFi radio, a station connection attempt, the setup AP, a connection failure, and a working LAN connection. For a LAN connection it reports the SSID, IP address, and RSSI. If the shared OTA seeder is active, the same reply also reports whether TCP port 5001 is listening or has a motatool client attached. get wifi.powersave reports the saved standalone WebConfig setting as none, min, or max. An inactive status is normal when webui is off: run start webconfig to connect temporarily. Standalone credentials can be changed through WebConfig or with the listed CLI commands. Changing the SSID or password stops an active WebConfig session; start it again to connect with the new values. Use set wifi.pwd with no value for an open network. get wifi.pwd returns the saved password on a local connection; LoRa callers cannot read it. Standalone WiFi accepts ordinary passphrases up to 63 characters and exact 64-character hexadecimal WPA/WPA2 PSKs; other 64-character values and all longer values are rejected. MQTT observer WiFi passwords remain limited to 63 characters by their fixed persisted layout.

get webui starts with the saved boot setting, then reports the current session. For example, > off, http://192.168.1.130/ means automatic WebConfig startup is saved as off, but a temporary session started by start webconfig is currently active at that URL.

"},{"location":"WiFi/#webconfig-cli-terminal","title":"WebConfig CLI terminal","text":"

The terminal defaults to on. Enable or disable it from an existing admin CLI:

set wifi.cli on\nset wifi.cli off\nget wifi.cli\n

The saved on setting becomes active only when the WiFi station client is connected and WebConfig is running in LAN mode. It is never exposed on the open setup access point. When active, the WebConfig page has a CLI tab. Repeater and room-server commands use their local CLI after admin authentication. The browser can run get password, get prv.key, erase, set freq, stats-core, stats-radio, stats-radio-diag, stats-packets, and explicit WiFi/MQTT credential getters. It also streams get acl and raw log dumps back to the browser. Log reads pause for slow clients and resume as output is read, using a fixed 160-byte file chunk. A dump snapshots the file length, so live logging cannot extend it indefinitely. The browser keeps the complete requested dump, including files larger than 64 KiB.

Companion uses the same complete terminal as USB and Full Companion TCP port 5002, including card, import meshcore://..., list, to, messages, remote login, and delayed replies. It uses the Companion's trusted LAN. See Companion browser terminal for contact import, terminal ownership, and disconnect behavior.

Type a command at the prompt, or paste multiple lines and confirm the sequence. Blank lines and comments are ignored. The device reports the supported batch limit. The browser sends each line after the previous command finishes and continues polling for incoming text after the sequence. help lists the device's actual terminal commands; Tab completes command names and settings.

The up/down arrow keys recall commands entered during the current browser session. Commands available in the terminal still depend on the firmware role and build profile. Commands such as stop webconfig, set wifi.cli off, WiFi credential changes, and reboot operations stop the remaining block and can disconnect the page before it receives their final reply.

"},{"location":"WiFi/#build-profiles","title":"Build profiles","text":"

build.sh produces several profiles. A profile changes the features compiled into a selected target; it does not change that target into another firmware role.

This fork defaults every build.sh target to the USA Cascadia radio preset and the Cascade firmware profile. The script resolves the live USA/Canada entry by name, so its changing number in the downloaded menu does not affect builds. If the preset service is unavailable, it falls back to 910.525 MHz / BW62.5 / SF7 / CR5. Use --radio-preset target --profile default only when a build intentionally needs the target's original radio and profile defaults. --radio-preset usa-cascadia is the stable explicit name; legacy numbered choices remain accepted but their meaning can change when the service reorders or adds presets.

Build profile WiFi/MQTT behavior Standard Uses the selected target's role. Where USB is a safe plaintext console, the same artifact embeds debug/packet logging behind persistent get/set usb.logging. Ordinary legacy-slot ESP32 repeater/room-server artifacts omit WebConfig when needed to fit. ESP32 MQTT observer and ESP-NOW bridge targets are automatically promoted to FULL; WiFi-companion targets keep their companion partition profile. Legacy logging No separate artifact is emitted; USB logging is part of the ordinary image. Logging output itself is not a direct MQTT uplink. MQTT Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. Non-companion ESP32 MQTT observers always use FULL expanded partitions. FULL ESP32 USB + WiFi Uses the board's MQTT target with USB packet logging and direct WiFi MQTT together, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. get/set logging.output off\\|usb\\|wifi\\|both persists the active paths. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. FULL ESP32 logging fallback Uses the board's non-MQTT target only when no matching WiFi MQTT environment exists. It keeps debug and packet logging, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. Persistent usb.logging off also provides normal output-off operation, so ESP-NOW FULL roles need no second non-logging image. LoRa-OTA no-external-sensors A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and up to 254 neighbors, subject to target RAM limits.

Repeater profiles use up to 254 neighbors. Selected Generic E22, Heltec V2, Meshadventurer, T-Beam OTA/MQTT, and TLora MQTT profiles use 50 to preserve internal-DRAM reserves. Consult the artifact capability manifest and ESP32 memory budget for the exact target.

The interactive Option 1 FULL everything choice and the standalone FULL command select the unified USB + WiFi image when a matching MQTT target exists; otherwise they select the logging fallback. The build matrix no longer emits any separate standard logging image, or a non-MQTT FULL twin for a role covered by the unified image. Expanded Full infrastructure profiles include LoRa OTA, WebConfig where supported, up to 254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM limits still apply.

FULL images change the ESP32 partition layout. Flash the matching *-merged.bin once when installing that layout. A partition-layout change can invalidate NVS, including saved WiFi, MQTT, name, and admin settings. Later updates using the same layout normally preserve them.

"},{"location":"WiFi/#wifi-power-behavior","title":"WiFi power behavior","text":"

MQTT observers and FULL standalone ESP32 repeater/room-server builds support:

set wifi.powersave none\nset wifi.powersave min\nset wifi.powersave max\n

Fresh Cascade-profile builds default to min; target-default builds use none, which gives the most predictable MQTT and radio performance at the highest power use. min and max reduce power but may add latency or reduce reliability on busy nodes. A saved operator setting takes precedence on an upgrade. ESP32 WiFi Companions expose the same values in their WebConfig WiFi card and USB text terminal. Full Companion also accepts the text commands from TCP port 5002. The normal binary Companion protocol can read or write the setting over USB, BLE, or TCP port 5000 without entering terminal mode. A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects none because coexistence requires WiFi modem sleep. An ESP32 Full Companion whose primary mesh radio is ESP-NOW also rejects max; maximum modem sleep can cause the station to miss ESP-NOW broadcasts because the access point does not buffer them. A previously saved max value is capped to and reported as min. The SenseCAP Indicator exception uses the active-mode rules above rather than assuming BLE and infrastructure WiFi are active together. ESP-NOW and infrastructure WiFi still must use the same fixed channel.

MQTT observer targets normally limit ESP32 WiFi transmit power to 11 dBm unless the board configuration overrides MQTT_WIFI_TX_POWER. This setting affects WiFi only, not LoRa transmit power.

"},{"location":"WiFi/#companion-wifi-compatibility-settings","title":"Companion WiFi compatibility settings","text":"

ESP32 WiFi Companions, including Full Companion, provide two independent, saved compatibility controls. Defaults remain bgn and normal; neither setting changes the running connection until a reboot. They are available through the USB terminal, framed Companion CLI over USB/BLE/TCP, and the browser command terminal where included.

get wifi.protocol\nset wifi.protocol bg\nreboot\n

wifi.protocol bgn permits ordinary 802.11b/g/n. bg excludes 802.11n for access-point compatibility testing; it does not change LoRa or radio2. B/G-only is rejected when it conflicts with a primary ESP-NOW radio or an active long-range ESP-NOW owner. The temporary setup AP retains its existing B/G/N compatibility policy.

The separate WiFi-only boot skips Bluetooth controller/host initialization, not merely advertising. USB and LoRa remain available; existing Bluetooth names, PINs and bonds are not erased. Enable and configure WiFi first, and retain USB access when testing an unreliable WLAN:

set wifi.enabled 1\nset wifi.mode wifi-only\nset wifi.powersave none\nreboot\nget wifi.status\n

Saving none while a WiFi-only reboot is pending queues it for that boot; it never disables modem sleep while the current Bluetooth stack is active. get wifi.status reports the connection, SSID, IP and RSSI (dBm), followed by saved/boot/live radio settings in the same bounded reply:

> connected ssid=MyWiFi ip=192.168.11.102 rssi=-74\nprotocol=bgn/bgn/0x07 mode=normal ps=min coex=yes ota=5001:listen\n

protocol lists saved / boot-selected / live SDK bitmap (0x03 = B/G, 0x07 = B/G/N). For example, protocol=bg/bgn/0x07 shows a B/G-only change waiting for reboot. mode shows one value when saved and boot choices match, or saved / boot-selected when they differ, such as wifi-only/normal. ps is the live modem-sleep mode; coex indicates whether Bluetooth coexistence is required. ota is off, <port>:listen, or <port>:client. n/a means the SDK could not read that value; it does not claim the radio is off. IP and RSSI are omitted when not applicable. Individual getters remain available, but no separate detailed-status command is needed.

Test B/G-only and WiFi-only separately to identify which change helps. A successful local test does not establish compatibility with an unavailable AP. Turning WiFi off/on, or using set bluetooth off, is not equivalent to a true WiFi-only reboot. During that boot, Bluetooth cannot be re-enabled until wifi.mode normal is saved and the device is rebooted.

To restore the usual Cascade combination:

set wifi.mode normal\nset wifi.protocol bgn\nset wifi.powersave min\nreboot\n

The SenseCAP Indicator's existing exclusive companion.transport wifi|ble selector remains authoritative; it does not expose wifi.mode.

"},{"location":"WiFi/#recognizing-the-wrong-firmware","title":"Recognizing the wrong firmware","text":"

get wifi.status, get wifi.ssid, get wifi.powersave, and get wifi.cli are available on MQTT observers and on FULL non-MQTT repeater/room-server builds with WebConfig. ESP32 WiFi Companions also expose all four commands, credential setters, and start webconfig [ap] through their USB text terminal. Their browser CLI defaults to on in station/LAN mode. Full Companion also exposes its complete role-specific text terminal on TCP port 5002, including chat and streaming commands. MQTT commands such as get mqtt.status and set mqtt1.preset ... require MQTT code in the image, on Companion or infrastructure. Unknown settings return Error: unknown setting: <name>. Older firmware that used the discontinued compact CLI can instead report Unsupported in this firmware when a command was cut for space.

Check the complete firmware filename and role. In particular:

Rolling firmware back does not restore settings erased by a full flash or a partition-table change. If the correct MQTT target still has no configuration, provision WiFi and MQTT again.

"},{"location":"WiFi/#troubleshooting","title":"Troubleshooting","text":"

For an MQTT observer:

get wifi.status\nget mqtt.enabled\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n

For a connection that works initially but later stops reporting, inspect each configured slot's pub_ack count in get mqttN.diag. Accepted publishes are not proof of delivery: a QoS 1 publish is confirmed only when the broker acknowledges it. A quiet radio channel alone is not a failed MQTT connection.

The shared MQTT bridge recovers failed transports on its owning task, including errors without a subsequent disconnect callback and connection attempts that never finish. It also watches accepted QoS 1 publishes for missing broker acknowledgments. Recovery retains the existing broker configuration, memory admission and reconnect backoff; it does not reboot the node or cycle WiFi. See the MQTT recovery regression for the deadlines, test coverage and hardware limitations. Older preview images do not gain these changes until their firmware is updated.

Common causes are:

For a WiFi companion, find its station IP in the router, connect the client to TCP port 5000, and use the MeshCore-Setup-XXXX AP at http://192.168.4.1/ if it cannot join the saved network. The hotspot uses the admin password when eligible for WPA2, as described above. Current firmware uses standard b/g/n for the setup AP, including ESP-NOW-capable targets. A running LR mesh conflicts with a conventional access point; current firmware refuses that AP startup and defers LR startup while an AP is active. A primary ESP-NOW target keeps its infrastructure station and mesh on the persisted espnow.channel (channel 1 by default). Its configured 2.4 GHz router and every other primary ESP-NOW node must use that same channel. MQTT-capable Companions also accept get mqtt.enabled, get mqtt.running, and get mqtt.status in their USB/TCP and browser terminals. Extended slot and NTP diagnostics remain infrastructure features.

"},{"location":"bootloader_version_stock_audit/","title":"Bootloader version stock audit","text":"

View this page on MeshCore Docs.

"},{"location":"bootloader_version_stock_audit/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"cad_scan_validation/","title":"Cad scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"cad_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"carrier_wave/","title":"Carrier wave","text":"

View this page on MeshCore Docs.

"},{"location":"carrier_wave/#carrier-wave-rf-checks","title":"Carrier-wave RF checks","text":"

The constrained wio-e5-mini_repeater omits Morse and CW tools to fit its existing application region. Its packet radio and radio2 are unchanged. Other boards keep their existing carrier-tool support.

"},{"location":"carrier_wave/#tx-only-morse-messages","title":"TX-only Morse messages","text":"

Morse TX mode keys an unmodulated carrier into dots and dashes. It does not send LoRa packets or automatically retransmit channel chat or private messages. The default is 902.100 MHz at 25 words per minute (WPM). Nothing transmits until an explicit morse send command is accepted.

Command Action morse send TEST DE N0CALL Send the text once using the saved Morse frequency and speed. morse stop Stop Morse immediately and restore packet reception. get morse Show idle/active state, frequency, WPM and remaining time. get morse.freq Show the Morse TX frequency in MHz. set morse.freq 902.100 Save the separate Morse TX frequency, rounded to 1 kHz. get morse.wpm Show the saved sending speed. set morse.wpm 12 Slow transmission for easier hand decoding; supported range is 1\u201360 WPM.

The default 25 WPM can be changed; for an easy-to-copy test, set 10\u201312 WPM. Standard timing uses one unit for a dot, three for a dash, one between elements, three between letters and seven between words, with a dot of 1200 / WPM milliseconds. Letters are sent case-insensitively. Messages are bounded to 80 ASCII characters, must contain supported Morse characters, and must fit within 180 seconds at the chosen speed. Supported punctuation is . , ? ' ! / ( ) & : ; = + - _ \" $ @; tabs, newlines and non-ASCII text are rejected. Invalid messages are rejected before the transmitter is keyed. There is no automatic callsign insertion.

The Morse frequency is independent of radio, tempradio, radio2 and tempradio2. Before sending, the firmware rejects a frequency that overlaps either packet-radio profile, including a guard outside its occupied bandwidth. It does not automatically find another channel or change your mesh settings. Choose another morse.freq if 902.100 MHz overlaps either profile. A frequency accepted as a saved value must still be supported by the actual radio at send time. Morse frequency/speed changes are rejected while Morse or CW is active.

There is one physical transceiver: packet reception and transmission pause throughout the message, including the spaces between dots and dashes. Normal receive/profile scanning is restored afterward. Use local USB/BLE for immediate status and stopping; a reply to a remote LoRa CLI command waits until the message finishes. Changing either packet profile during Morse can abort it to preserve the frequency-separation safety checks.

An unmodulated carrier has no selectable LoRa bandwidth. This uses native CW on SX1262/SX1268/LLCC68/STM32WLx and LR1110/LR1121/LR2021 radios, and zero-deviation FSK CW on SX1276-family radios with reset/recovery support, rather than pretending the narrowest receive filter sets the transmitted bandwidth. Morse sidebands depend on keying speed and transmitter edges. This is not a guarantee of a particular occupied RF bandwidth or freedom from interference. Unsupported radios reject the command without beginning transmission. The chips' configured TX ramps use 3400 microseconds on the SX126x-family and 304 microseconds on the LR-family to reduce keying splatter. These are driver settings, not measured occupied-bandwidth results. Direct FEM gain changes are blocked while Morse owns the radio.

Frequency and speed are saved in a separate checked /morse_prefs file; identity, contacts and existing radio preferences are unchanged. An active message is never restored after reboot. TX uses the board's existing set tx power setting and hardware power limits. Unreadable or corrupt saved Morse settings without a usable backup block sending rather than silently transmitting on the default frequency. After storage recovers, verify and save morse.freq (and the desired WPM); a verified save makes sending available again.

Use an appropriate antenna or dummy load, lawful frequency/power and any required operator identification. The default frequency is a convenience, not permission to transmit or a claim that the channel is clear. Check the local band plan and listen first.

"},{"location":"carrier_wave/#timed-morse-reception-and-retained-captures","title":"Timed Morse reception and retained captures","text":"

Supported radios can temporarily listen for keyed carriers and decode the power envelope into text. This is a separate manual session, not another MeshCore packet channel or an authenticated sender. Nothing decoded is automatically transmitted, relayed or inserted into a channel/DM inbox.

Reception requires a saved timing hint of 15-60 WPM. The default remains 25 WPM. Slower speeds of 1-14 WPM remain available for transmission; listening rejects them with a receive-specific error instead of silently changing the saved setting. Set morse.wpm to at least 15 before listening.

Command Action morse listen 1 Listen for one minute at the saved morse.freq, using morse.wpm as the timing hint. morse listen 0.5 Listen for 30 seconds. Fractions are allowed. morse listen 5 Listen for the maximum five minutes, then restore packet reception. morse stop End TX or listening early. A received capture is retained. get morse Show TX/RX state, actual RX filter bandwidth, remaining time and capture length/truncation. morse stats Show retained receiver sample counts, noise/min/max/last power, timing unit, largest sample gap and read failures. morse read Read the first retained capture page. morse read 128 Read beginning at character offset 128; use the offset returned by your previous page. morse clear Clear the retained capture when no Morse session is active.

Listening is bounded to 1\u2013300 seconds for every role. Keep the first 0.5 seconds quiet while the receiver learns its noise floor; start the sending radio at least one second after listening begins. Start with matching WPM on the sender and receiver. Noise, fading, interference and timing mismatch can produce errors; decoded text has no sender identity or integrity check.

Automatic detection retains sustained noise peaks learned during the quiet interval, after initial receiver settling. It keeps that quiet envelope across marks and gaps rather than dropping the threshold back toward the average noise level. There is no fixed per-board RSSI cutoff. A stable quiet floor retains the six-dB acquisition margin; a noisier environment requires a signal above its learned excursions. The envelope relaxes gradually when conditions become quieter. Stop and start listening to run the initial learning interval again in a new environment.

The receiver uses a continuous native FSK receive filter and live received signal strength (RSSI) to follow the carrier's on/off envelope. It does not transmit or require a packet preamble, sync word, CRC or direct-data pin:

Radio family Current Morse RX filter SX1276-family 2.6 kHz SX1262/SX1268/STM32WLx, LLCC68 4.8 kHz LR1110/LR1121, LR2021 4.8 kHz

Small-flash STM32 production builds disable Morse reception by default (MESH_ENABLE_MORSE_RX=0) because the receiver does not fit alongside their existing features. Morse TX, frequency/WPM settings and stopping remain available. Their listen/read/stats/clear commands return an unsupported-RX error. The STM32 filter above describes an explicitly enabled custom build, not RX support in those default production images.

get morse reports the actual configured bandwidth; this is not a measured selectivity or decoder-speed guarantee. Unsupported hardware rejects listening without changing the radio. A board also needs a proven complete LoRa-modem restoration path before native FSK RX is enabled. Narrow filters require sufficiently aligned oscillator frequencies. Passive listening may use either packet profile's frequency because it does not transmit; TX overlap protections still apply. FEM and saved Morse settings cannot be changed while a session owns the radio.

Morse sessions defer periodic sensor/GPS processing, screen redraws and background storage work so they do not interrupt envelope timing. Pending work and its deadlines are retained and resume when the session ends. Buttons, command connections, watchdogs and battery shutdown checks remain active. morse stats reports gap in milliseconds: the largest observed gap between receiver samples, including unavoidable safety work. Large gaps can limit reliable speed even when the RF signal is strong.

Repeater, Room, Sensor and Companion roles retain a bounded 4096-character RAM capture after their normal packet receiver resumes. A successful new listening session replaces it; a failed start does not. morse clear or a reboot removes it. If the capture fills, listening stops early and truncated=1/capture-quota status is shown instead of continuing to discard incoming text. This does not change node keys, ACLs, stored radio settings or other files.

Capture replies fit one CLI response and begin with a page header such as > 0000/0250 next=0125 truncated=0 followed by text. Send morse read 125 to continue at the returned offset; next=END means the last page. Exact page length depends on the requesting transport's reply capacity, so follow next rather than assuming a fixed page size. Captures remain readable until you start another listening session or clear them.

A Full Companion's owned buffered USB ASCII terminal or browser terminal shows decoded progress as [Morse RX] ..., paused while USB input is being edited or an earlier reply is draining. Complete records use that terminal's existing ordered output queue. Raw UART/HWCDC/TCP terminals use get morse/morse read instead of unsolicited progress. Binary Companion USB/BLE frames never receive raw text. Companion apps and other clients can request the same capture through their CLI using morse read; it is not presented as an authenticated chat message. There is no automatic LCD/inbox injection.

One transceiver cannot listen for Morse and MeshCore LoRa simultaneously. During listening, normal packet reception/transmission is paused. An authorized remote morse listen first queues its reply on the existing packet-radio settings and starts only after the exact reply copies finish transmitting, with at least one successful transmission. It does not infer that the remote client received the reply. Failed queue admission/all-copy failure, permission revocation, changed Morse settings or a 30-second handoff deadline cancel the request without starting RX. A briefly busy radio can retry admission for up to 10 seconds after reply transmission, within that same handoff deadline. For remote captures, send morse listen 1, start the Morse sender after the queued reply and initial quiet learning interval, wait beyond the selected minute (plus any admission delay), then request get morse and morse read over LoRa. Use a local console to stop early. Hardware recovery failures hold exclusive ownership until the receiver can be safely restored; get morse reports recovery pending.

Lab images compiled with MESH_MORSE_HIL=1 additionally provide morse trace [offset]. A page begins > offset/length t=total n=next x=wrapped and contains up to eight elapsed_ms,rssi_dbm,flags; records. n=END ends the retained window; x=1 means earlier entries were overwritten. Offsets address the oldest of the latest 512 retained records; retrieve after stopping or check that t stays unchanged between pages. Flags are raw key 1, committed key 2, debounce candidate 4 and noise learning 8. This diagnostic command and its trace storage are absent from ordinary production builds.

"},{"location":"carrier_wave/#continuous-carrier-checks","title":"Continuous carrier checks","text":"

cw and cw2 transmit a continuous, unmodulated carrier at the selected profile's frequency and the configured TX power. They are available in the shared ASCII CLI on SX1276-family, SX1262-family, and LR1110 builds, including repeaters, Companions, room servers, and sensors that use that CLI.

Command Action cw on Use the active primary frequency for 10 seconds. cw on 2.5 Use the active primary frequency for 2.5 seconds. cw2 on 0.75 Use the active second profile for 0.75 seconds. cw off or cw2 off Stop the carrier immediately. Either name stops it. get cw or get cw2 Show which carrier is active and its remaining seconds.

set cw ... and set cw2 ... are also accepted. Durations range from 0.001 to 60 seconds, rounded to milliseconds. The deadline runs in the main loop, so it is not a precision pulse generator or a hardware cutoff.

cw2 requires an active radio2 or tempradio2 profile in rxtx mode. An rx or disabled second profile cannot transmit a carrier. cw uses tempradio when that is the current primary profile. There is one physical transmitter: a carrier uses one profile at a time and ignores the cross-TX setting. Stop it before selecting the other carrier. Repeating on on the same profile restarts the duration.

Packet transmission, reception, profile scanning, CAD, and receive recovery checks pause during the carrier. The firmware keeps the MCU awake to service the deadline, controls the board's external amplifier, and resumes normal reception and profile scanning afterward. It rejects entry during a packet or a busy radio. Profile retuning waits until the carrier stops; replacement or expiry of the active second profile stops its carrier. Nothing about CW is saved across reboot.

The normal set tx <dBm> command also works during CW for meter sweeps. It keeps the board's power limits and PA calibration, resumes the carrier after changing power, and does not extend the CW timeout. TX power retains its usual saved behavior. SX1276 temporarily uses FSK with zero frequency deviation, then rebuilds the LoRa configuration before resuming reception.

Use the local USB or BLE console for immediate replies and manual stopping. An on-air CLI reply must wait until the carrier ends because LoRa packets cannot transmit during the test.

A second radio on the same frequency can detect the carrier by measuring live signal strength (RSSI, short for received signal strength indicator): how much radio energy the receiver sees right now, even when it cannot decode a packet. A less-negative reading in dBm means a stronger signal. Compare it before, during, and after transmission. The normal stats-radio command shows the last received packet's RSSI, not this live reading, so it cannot by itself confirm a continuous carrier. CAD and packet counters are not carrier detectors either. A rising live reading confirms RF activity, not calibrated output power, frequency accuracy, antenna match, or SWR. Afterward, exchange ordinary LoRa packets to verify both RX and TX recovery.

The reproducible bench receiver that provides this live reading is tools/hil/cw_observer.cpp, built with tools/hil/cw_observer.ini on a separate RAK4631. It never transmits on its own. See the hardware validation for measured results, reproduction steps, and the tested preamble settings.

"},{"location":"carrier_wave_validation/","title":"Carrier wave validation","text":"

View this page on MeshCore Docs.

"},{"location":"carrier_wave_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"charge_voltage/","title":"Charge voltage","text":"

View this page on MeshCore Docs.

"},{"location":"charge_voltage/#battery-charge-voltage","title":"Battery charge voltage","text":"

The charge.voltage commands change the battery charger's constant-voltage target on supported boards. They are available through the shared text CLI in the companion, repeater, room-server, sensor, and secure-chat roles. They do not change the battery voltage display, low-voltage shutdown, battery chemistry, or charge current.

get charge.voltage\nget charge.voltage.options\nset charge.voltage 4.1\n

Values are in volts. Query the options first: supported targets depend on the PMU fitted to the board. The firmware rejects values that the detected PMU cannot represent; it does not round them to a supported target.

On T-Beam boards, a successful change is saved in NVS and reapplied after a restart. The firmware reads the PMU setting back before reporting success. Boards without a saved setting retain their existing startup behavior: the full PMU driver sets 4.2 V, while the compact classic T-Beam observer driver leaves the charger setting untouched. A failed hardware write, readback, or preference save is reported as an error.

If the saved target cannot be read, is invalid, or cannot be restored, startup does not overwrite it with a default target. get charge.voltage reports the live charger voltage with (boot restore failed). A successful saved change clears that warning. This cannot guarantee restoration while the PMU or storage is failing, but it avoids silently resetting a saved lower target to 4.2 V.

"},{"location":"charge_voltage/#supported-boards","title":"Supported boards","text":"Board family Detected PMU Charge targets (V) LilyGo T-Beam SX1262 or SX1276 AXP192 4.10, 4.15, 4.20, 4.36 LilyGo T-Beam SX1262 or SX1276 AXP2101 4.00, 4.10, 4.20, 4.35, 4.40 LilyGo T-Beam S3 Supreme SX1262 AXP2101 4.00, 4.10, 4.20, 4.35, 4.40

Targets above 4.2 V require batteries rated for the selected charge voltage.

The classic T-Beam driver detects the fitted AXP192 or AXP2101. Radio type does not determine which PMU is present. T-Beam 1W uses a different power design and does not support this setting.

Neither AXP192 nor AXP2101 supports a 3.65 V target. The lowest supported target is 4.10 V for AXP192 and 4.00 V for AXP2101. These PMUs cannot be configured as LiFePO4 chargers by changing this setting.

"},{"location":"charge_voltage/#heltec-mesh-solar","title":"Heltec Mesh Solar","text":"

Mesh Solar reports charge.voltage as unsupported because its CN3795 charger's constant-voltage target is set by hardware. Heltec supplies separate Li-ion (I) and LiFePO4 (F) hardware versions, with 4.2 V/cell and 3.6 V/cell charging respectively. Cell-count soldering and switches must also match the battery configuration. A software battery-type setting does not convert one hardware version into the other. See Heltec's usage guide and MeshSolar datasheet, sections 4.1 and 4.4.

The BQ4050 on this board is a battery gauge and protection controller. Its ChargingVoltage() is a requested voltage for a compatible smart charger. The pinned Mesh Solar library calls that parameter eoc, but writing it does not change the CN3795's voltage-feedback circuit. Valid charge termination also requires the charging current to taper, so lowering eoc alone does not create a reliable lower-voltage charge cutoff. Charge-overvoltage protection is a separate safeguard, not the charger's normal constant-voltage target. See TI's BQ4050 technical reference manual, sections 4.5, 4.6, and 4.11.

These commands do not write Mesh Solar's BQ4050 data flash or change its battery type, protection limits, cell count, or charge-FET configuration.

"},{"location":"charge_voltage/#battery-profiles","title":"Battery profiles","text":"

The nRF52 battery.profile, battery.full, and battery.empty settings control voltage-based reporting and protection. They do not change any charger target. See Battery profile commands.

"},{"location":"cli_build_matrix/","title":"Cli build matrix","text":"

View this page on MeshCore Docs.

"},{"location":"cli_build_matrix/#cli-availability-by-firmware-build","title":"CLI Availability by Firmware Build","text":"

MeshCore command availability is determined in three layers:

  1. Role - repeater, room server, sensor, companion, bridge, or KISS modem.
  2. Build profile - standard, logging, OTA, or FULL.
  3. Compiled hardware features - WiFi, MQTT, GPS, external sensors, PSRAM, Ethernet, and similar optional support.

The complete command descriptions are in CLI Commands. This page describes the commands intentionally omitted or limited by build profile. The CLI Command Availability Matrix expands this summary into separate nRF52 and ESP32 command tables.

"},{"location":"cli_build_matrix/#selecting-a-build-profile","title":"Selecting a build profile","text":"

The runtime settings profile and the feature profile are separate switches:

bash build.sh build-firmware RAK_3401_repeater \\\n  --profile cascade \\\n  --build-profile auto\n

--profile default|cascade selects saved-setting defaults. It does not select which code is linked. --build-profile auto|standard|full controls features and partition policy:

Selection Behavior auto nRF52 repeater, room-server, and sensor builds publish the required pair: Full supported sensors + LoRa OTA and Reduced sensors + LoRa OTA. Both must pass flash, RAM, packaged-application OTA, and layout checks; a reduced image cannot substitute for a failing full image. The same policy applies to canonical bulk/release builds and external-QSPI/SD recipes. ESP32 boards with a qualified expanded profile use it; other standalone ESP32 repeaters and room servers attempt the complete recipe in their current application region, with a measured flash/partition-overflow fallback to their reduced LoRa OTA recipe. Other roles keep every capability declared by their resolved PlatformIO recipe and fail instead of silently removing one that does not fit. Compiler errors and missing-capability checks never trigger or conceal a reduced build. standard Preserves the deployed/portable partition contract and documented reductions. nRF52 infrastructure still publishes both OTA sensor profiles in that same exact layout; this is not an override to remove OTA or skip the full profile. full Requires a qualified ESP32 expanded-partition target (or an explicitly named Full Companion). Install a matching merged image when this changes the partition table.

Successful builds also emit <firmware>.capabilities.json. The sidecar records the effective profile, logical OTA target, actual PlatformIO base, artifact name target, promised capabilities, and every reduction selected by the script. The build fails if a promised linked marker is absent. Current invariants include retry.preset for repeater/room-server roles, WebConfig for the Indicator and ESP32 Full Companion, TempRadio/OTA controls for install-capable Companions, host-mOTA controls for Full Companion, and the advanced flood-rule engine for Full room servers. Resume mode will not accept an old or failed artifact without a verified sidecar.

For a single ESP32 repeater or room-server target, auto selects its matching combined Full MQTT/USB/WiFi image when that image covers the plain Full recipe. The direct observer and Full-matrix paths use the same full-usb-wifi-ota artifact and runtime output controls. The ordinary bulk release publishes one Full OTA identity per exact ESP32 board and role. Audited plain targets keep their identity while compiling the combined observer/ESP-NOW sources; G2 keeps its deployed observer identity. T-Beam SX1262/SX1276 choose the observer in the ordinary release. TLora V2 retains two Full images because UART, MQTT and ESP-NOW together exceed its internal RAM budget: the normal repeater provides UART plus ESP-NOW, and the observer provides MQTT plus ESP-NOW. T-Beam plain Full recipes remain available as explicit alternatives with different routing capacities. Build one with bash build.sh build-firmware <target> --full-exact, then deliberately install it over LoRa with ota pull <MID8> flash and ota install on compatible hardware and partition layouts.

The ordinary ESP32 release publishes Full images. Standard and reduced OTA recipes, including those that fit a 1.25 MiB portable application slot, remain available through explicit builds for recovery and migration. Select --build-profile standard to retain an exact target's original partition contract. They are omitted from the ordinary release and interactive board menu. That menu also omits transport/default aliases covered by Full and development utilities; it retains distinct hardware, roles, storage, and measured capacity exceptions. Exact legacy names remain accepted by build-firmware and build-matching-firmwares.

For the normal-role partition migrations, the release publishes a canonical Full image under the normal target identity. Use its qualified wireless expansion package when one exists, or install its matching merged image over USB. mOTA rejects an incompatible partition signature until that migration is complete, then later Full updates use the normal target ID. Moving from a plain target to a differently named sibling can also change the LoRa OTA identity; partition expansion does not by itself authorize that transition.

Legacy supported ESP32 Wi-Fi repeaters can use the Wi-Fi partition migration bridge to preserve their identity and install an expanded Full image without a USB flash. The default cascade settings profile enables device and LoRa RX power saving; saved preferences take precedence, and active USB/network services can prevent device sleep. --profile default instead uses the upstream power-saving defaults.

"},{"location":"cli_build_matrix/#role-comes-first","title":"Role comes first","text":"Role Text administration CLI Repeater Full repeater administration surface, subject to the profile differences below Room server Room-server administration surface, subject to the profile differences below Sensor Sensor command surface; it does not acquire the repeater administration tree Full, serial, Ethernet, USB, BLE, or WiFi companion Uses the companion protocol; Full combines every qualified transport for that exact board KISS modem Uses the KISS/TNC frame interface, not the repeater text CLI Bridge Uses its base role plus commands for the bridge transport compiled into that target

A command belonging to a different role is not considered a profile cut. For example, adding FULL features to a sensor does not turn it into a repeater administrator.

Repeater profiles use up to 254 neighbor entries. Measured internal-DRAM limits reduce selected Generic E22, Heltec V2, Meshadventurer, T-Beam OTA/MQTT, and TLora MQTT profiles to 50. Check the artifact capability manifest and ESP32 memory budget for the exact target.

See local maintenance commands for USB, BLE/binary Companion, TCP/Ethernet, and browser CLI privileges. A direct connection can access local diagnostics and explicit credential getters; relaying a command over LoRa keeps its remote restrictions.

"},{"location":"cli_build_matrix/#profile-matrix","title":"Profile matrix","text":"Build/profile Command availability Standard non-MQTT repeater or room server Keeps the normal role CLI and, where USB is a safe plaintext console, embeds packet logging behind persistent get/set usb.logging and separately saved get/set usb.debug verbosity. USB debug defaults off and requires compiled diagnostic support. The explicitly selected portable policy can omit WebConfig and browser WiFi OTA, so those commands are unavailable and the omission is recorded in the capability manifest. Legacy standard logging No longer emitted separately. Its behavior is compiled into the ordinary artifact. Size-constrained STM32 targets embed packet logging without verbose MESH_DEBUG. LoRa-OTA (-ota-) LoRa OTA adds the ota ... commands; it does not otherwise reduce the role CLI. ESP32 no_external_sensors artifacts retain the compact browser WiFi uploader, the complete CLI, and up to 254 neighbors, subject to recorded internal-DRAM reductions. nRF52 repeater/room-server/sensor release pair full-ota retains the complete supported sensor recipe; reduced-ota omits declared optional sensor drivers. Both include verified LoRa OTA, use the same logical target identity and exact storage/layout contract within the pair, and retain the role's normal commands. Repeaters/room servers also verify retry.preset. RAK3401 and RAK4631 reduced builds retain GPS where compatible and INA219/INA226/INA260/INA3221 I2C voltage/current monitors. External QSPI/SD targets also publish both choices. Companion/Terminal Chat and KISS roles do not enter this policy. ESP32 MQTT observer or ESP-NOW bridge Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. FULL ESP32 USB + WiFi Uses the matching MQTT target with packet logging, USB debug defaulting off, and the complete command surface supported by that role and hardware. Debug verbosity can be enabled only for diagnostics compiled into that image. get/set logging.output off\\|usb\\|wifi\\|both selects and persists the active output paths; get/set usb.debug separately saves USB verbosity. FULL ESP32 logging fallback Uses the matching non-MQTT target when no compatible MQTT sibling exists, preserving its hardware and role CLI. Full LoRa repeaters also include runtime ESP-NOW with set espnow.enabled on\\|off and get espnow.running; newly combined modes default off. Dedicated images remain only for measured capacity exceptions. USB debug defaults off; get/set usb.debug selects verbose output independently of packet capture. nRF52 dual-CDC Full Companion Fresh installs expose only interface 00; it starts as an ASCII terminal and automatically hands a complete < frame to framed Companion. The same interface also carries exclusive serial mOTA traffic. set usb.logging on reboot adds output-only interface 02 for ASCII RAW/RX/TX records and optional debug. Alternatively, set usb.logging stream reboot keeps CLI replies and packet records on interface 00 for unmodified one-port serial bridges; Binary Companion and serial mOTA require logging off on that port. BLE and source-only LoRa OTA remain available. get/set usb.logging persistently controls whether the logging interface is present; get/set usb.debug changes verbosity without a reboot. Send CLI/control queries to interface 00, not the log endpoint. ESP32 single-TTY Full Companion Every ESP32 Full image starts with the ASCII terminal on its one USB TTY and automatically hands a complete < frame to framed Companion. On 1.17.1.5, run set powersaving off first. set usb.logging on switches that TTY to an input-capable plaintext RAW/RX/TX logging terminal and makes framed Companion unavailable on USB; set usb.logging off stops logging but leaves the TTY in normal ASCII mode. get/set usb.debug separately controls saved verbose diagnostics, defaulting off. The terminal stop token or a valid incoming framed probe then performs the ordinary switch to Binary Companion. A saved logging-on setting starts directly in that logging terminal and disables automatic frame detection. BLE, WiFi, and source-only LoRa OTA remain available. ESP32 Full uses Arduino-ESP32 2.x where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but still expose only one TTY. no_external_sensors Trims selected optional environmental/ranging drivers and their settings; it does not remove generic I2C, core repeater discovery, routing, or runtime RS-232 commands. RAK3401 and RAK4631 profiles retain the four common INA I2C voltage/current monitors. GPS-preserving RAK nRF52 OTA profiles retain their GPS commands and provider; RAK4631 defaults the bridge to UART 2 because RAK12501/L76K GPS uses UART 1. Legacy target suffixes remain stable for OTA identity compatibility.

The four retained INA drivers are entries in the optional environmental-sensor table, not the complete set of RAK I2C consumers. Compatible reduced profiles also retain the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 I2C GPS as separate board peripherals. RAK12501/L76K GPS uses UART Serial1. The explicit RAK4631 Serial1 bridge omits the combined GPS provider, including the otherwise non-UART RAK12500 path.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42. To use both devices on one bus, leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and build with -DTELEM_INA3221_ADDRESS=0x43.

logging, OTA, and FULL describe independent build features in historical filenames. Current standard artifacts use no -logging- infix because their USB logging is runtime controlled. USB packet capture and debug verbosity share the canonical artifact, not a new build/profile name. usb.logging remains the master; verbose diagnostics need both that switch and saved usb.debug on. Fresh and legacy preferences default debug to off. Setting debug on cannot restore code compiled out of an image.

"},{"location":"cli_build_matrix/#canonical-bulk-build-policy","title":"Canonical bulk-build policy","text":"

Bulk and release-matrix commands omit legacy names whose behavior is already available from a canonical image:

The old aliases still work with build-firmware and build-matching-firmwares. Dedicated LoRa OTA repeater images are not collapsed outside the MeshTower consolidation above; they retain their exact storage, bootloader, role, and target identity contracts. Companion boards keep transport-specific canonical images only when no exact Full recipe has passed the combined flash/RAM qualification. ESP32 deliberately uses one TTY: Binary Companion and plaintext USB logging are mutually exclusive there. nRF52 retains its optional second CDC port.

The OTAFIX 2.4.11 release is the current bootloader reference, not an all-board qualification claim. Consult its exact-board manifest and unavailable-profile reasons. Internal application storage accepts exact-base in-place deltas that fit the workspace; full application packages need the matching external NOR or microSD profile. The retired internal-only MeshTower V2 bootloader requires one local installation of the combined SD/internal profile. Its changed identity/layout must not be crossed by an ordinary LoRa application or bootloader update.

"},{"location":"cli_build_matrix/#complete-cli-policy","title":"Complete CLI policy","text":"

The compact ESP32 CLI has been removed. retry.preset is a checked invariant for every repeater and room-server artifact, not a FULL-only command. MQTT observers and ESP-NOW bridges are automatically promoted to FULL builds with expanded partitions rather than dropping administration commands. This keeps tempradio, LoRa OTA, power saving, RXPS, logging, statistics, sensor, ACL, routing, and advanced radio commands whenever the role and compiled hardware support them.

Some commands still have hardware or feature limits. MQTT neighbor-table publishing and discover.scopes require the compiled WITH_MQTT_NEIGHBORS feature. PSRAM boards enable it automatically, and selected non-PSRAM variants opt in with MQTT_NEIGHBORS_WITHOUT_PSRAM. discover.neighbors does not require MQTT or PSRAM.

"},{"location":"cli_build_matrix/#discovery-invariant","title":"Discovery invariant","text":"

discover.neighbors sends the zero-hop node-discovery request used to refresh the repeater neighbor table. It is available in every repeater build profile, including standard, logging, OTA, unified FULL, and FULL logging-fallback builds.

The exact command is:

discover.neighbors\n

It accepts no options and returns:

OK - Discover sent\n

Some MQTT room-server targets also expose the command as part of their compiled neighbor-table feature. It is not a cross-role guarantee: companion, KISS, sensor, and ordinary room-server firmware use different interfaces or do not maintain the repeater administrator neighbor table.

"},{"location":"cli_build_matrix/#feature-dependent-commands","title":"Feature-dependent commands","text":"

Even in a FULL build, a command can be unavailable when its underlying feature does not exist on that target:

When diagnosing an unavailable command, check the role first, then the filename profile, then the target's compiled hardware features and its adjacent .capabilities.json file. A current artifact that promises the command but does not contain its linked marker is rejected during the build.

"},{"location":"cli_command_availability/","title":"Cli command availability","text":"

View this page on MeshCore Docs.

"},{"location":"cli_command_availability/#cli-command-availability-matrix","title":"CLI Command Availability Matrix","text":"

This page is the command-by-command companion to CLI Availability by Firmware Build. Each command name links to its detailed documentation.

The tables cover the text administration CLI used by repeater, room-server, sensor, and bridge firmware. Full Companion also has its own ASCII USB/TCP terminal; see feature switches by role. Companion apps use the companion protocol, KISS firmware uses framed KISS/TNC commands, and terminal-chat firmware has its own interface, so those build roles are not represented in the main matrices. The Companion section below lists its framed maintenance commands. Infrastructure get password is available locally in every profile; LoRa requests cannot read it. get acl and stored packet log are infrastructure features, not Companion contact or live USB-logging commands.

Room users also have an owner-only command set described in Personal mailboxes: mail settings, mail mode, mail allow, mail deny, mail delivery, mail check, mail inbox/mail list, mail read, mail ack, and mail delete. Authenticated room writers can use mail send; readers cannot send. These work as encrypted CLI requests or ordinary direct !mail ... messages to the room. Regular users can also read get room.board and get room.board.read; board publishing and deletion remain administrative.

Build columns mean:

USB packet output and verbose diagnostics are separate saved controls in current source. Where usb.logging is available, get/set usb.debug on|off selects debug intent without changing packet capture or adding a build variant. Debug defaults off for fresh and older preference images; effective output requires usb.logging and usb.debug on plus compiled diagnostic support. See live USB logging for transport restrictions and Full Companion RAW output.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42; they cannot coexist on one bus at those addresses. Leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

Cell values mean:

Runtime state can still make an included command fail. Examples include no GPS fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without .mota apply support.

"},{"location":"cli_command_availability/#companion-framed-cli","title":"Companion framed CLI","text":"

GPS-equipped Sensor builds also provide the local-only dog tracker settings: get tracker, get/set tracker.owner, get/set tracker.mode, get/set tracker.interval, get/set tracker.lost.interval, get/set tracker.path, and tracker check. They are excluded from compact STM32 defaults. LoRa CLI cannot change them.

Companion builds are not represented by the role/profile matrices below. Their local terminal and command 0x42 (CMD_RUN_CLI_COMMAND) expose these settings over the normal binary USB, BLE, or TCP connection:

Command Availability stats-core, stats-radio, stats-radio-diag, stats-packets Local terminal and binary command 0x42 erase Local terminal and binary command 0x42; reboot after erasing stored settings set freq <MHz> Local terminal, binary command 0x42, and authorized LoRa CLI; reboot to apply get public.key Companion text terminal and binary command 0x42; read-only, no private-key export feature required get prv.key Local terminal and binary command 0x42; requires private key export enabled get password ESP32 WiFi Companion reports management-password status without revealing it; other Companions report no admin password; infrastructure returns its password locally password <new-password> ESP32 WiFi Companion and infrastructure: change the saved admin password; ESP32 setup WiFi, WebConfig and browser OTA reuse it get/set lost.reply Local Companion CLI and framed command 0x42 when MESH_ENABLE_LOST_REPLY is enabled; saved off, no or yes private-message auto response get wifi.pwd, get mqttN.password, get mqttN.token Local terminal and binary command 0x42; corresponding WiFi/MQTT feature required board Every Companion text terminal and command 0x42 version Every Companion text terminal and protocol-v14 command 0x42; returns the untruncated build identity get storage.layout Every Companion text terminal and command 0x42; reports the running platform's storage layout memory ESP32 Full Companion text terminal and command 0x42 get/set radio.rxgain Companion with a supported radio get/set radio.fem.rxgain Companion on a board with controllable LoRa FEM LNA get/set wifi.powersave ESP32 WiFi Companion; active transport constraints still apply get/set bluetooth.name Companion firmware set pin <0-999999> Companion text terminal, command 0x42, and authorized LoRa CLI; saved, reboot to apply; affects Bluetooth-capable builds get/set bluetooth.mac Every Companion build with Bluetooth get/set display.touch Touchscreen Companions using the shared UI; text terminal and framed CLI; off after reboot get/set display.inbox Shared Companion UI; history (default), pending, or unread; saved across reboot uf2reset Every nRF52 Companion text terminal and local command 0x42; local only

Deprecated binary aliases remain receive-only for older clients; new clients should use command 0x42. See Companion radio binary protocol.

"},{"location":"cli_command_availability/#nrf52","title":"nRF52","text":"Area Command Scope Standard Logging LoRa OTA Operational reboot All text CLI roles Yes Yes Yes Operational poweroff; shutdown Board power-off support Yes Yes Yes Operational uf2reset Local connection; supported UF2 boards Local Local Local Operational clkreboot All text CLI roles Yes Yes Yes Operational clock sync All text CLI roles Yes Yes Yes Operational clock All text CLI roles Yes Yes Yes Operational time <epoch_seconds> Clock only moves forward Yes Yes Yes Operational advert Advert-capable role Yes Yes Yes Operational advert.zerohop Advert-capable role Yes Yes Yes Operational erase Local connection Local Local Local Information ver All text CLI roles Yes Yes Yes Information board All text CLI roles Yes Yes Yes Diagnostics get storage.layout Internal layout and configured QSPI or SD storage Yes Yes Yes Diagnostics memory ESP32 only No No No Diagnostics sensor Hardware wiring summary Yes Yes Yes Diagnostics powerlog Reset, shutdown, and boot-voltage summary Yes Yes Yes Neighbors neighbors Role with a neighbor table Yes Yes Yes Neighbors neighbor.remove [pubkey_prefix] Role with a mutable neighbor table; omit prefix to clear all Yes Yes Yes Neighbors discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Neighbors discover.scopes MQTT observer with compiled neighbor support No No No Statistics clear stats All full-parser text CLI roles Yes Yes Yes Statistics stats-core Local connection Local Local Local Statistics stats-radio Local connection Local Local Local Statistics stats-radio-diag Local connection Local Local Local Statistics stats-packets Local connection Local Local Local Statistics get telemetry.temp/volt/volt.i2c; optional GPS history Non-STM32 repeater or room server; I2C command requires a detected voltage monitor; GPS requires a provider; remote access requires administrator Yes Yes Yes Statistics set telemetry.gps; get/set/send telemetry.tx Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator Yes Yes Yes Logging log start; log stop; log erase Storage-backed roles retain data; other roles can return empty data Yes Yes Yes Logging log Local connection Local Local Local Logging get/set usb.logging; unified FULL get/set logging.output Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; nRF52 Full Companion can add a reboot-controlled second CDC Yes Yes No Logging get/set usb.debug Same USB-control availability; saved intent defaults off; actual diagnostics need compiled support and USB master on Yes Yes No Logging get/set usb.watchdog off\\|on\\|auto Current source; recovery requires native USB, live logging, and durable storage; Auto qualifies from USB stats polling Yes Yes Feature Logging get usb.watchdog.last Current source; latest USB watchdog event with advisory timestamp, uptime, action, reasons, and durability; native USB required Yes Yes Feature Radio get radio; set radio ... All text CLI roles Yes Yes Yes Radio get tx; set tx <dbm> Board TX-power limits apply Yes Yes Yes Radio tempradio ...; normalradio Full parser Yes Yes Yes Radio get/set/del radioat; get/set/del tempradioat Full parser Yes Yes Yes Radio get freq; set freq <mhz> Local or authorized LoRa CLI Yes Yes Yes Radio get/set radio.rxgain Supported radio Feature Feature Feature Radio get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Radio get/set radio.fem.txgain Controllable LoRa FEM Feature Feature Feature Radio get/set radio.rxps; get rxps.wd RX power-saving support Feature Feature Feature System get/set name All text CLI roles Yes Yes Yes System get/set lat All text CLI roles Yes Yes Yes System get/set lon All text CLI roles Yes Yes Yes System get/set prv.key get requires a local connection Yes Yes Yes System get password; password <new_password> get requires a local connection; setter requires administrator Yes Yes Yes System get/set guest.password Role with guest administration Yes Yes Yes System get/set owner.info All text CLI roles Yes Yes Yes System get/set adc.multiplier Board ADC override support Feature Feature Feature System send text.flood <message> Repeater Yes Yes Yes System host <text>; get host Repeater with host bridge; host requires a remote administrator and a USB/MQTT service Yes Yes Yes System get/set battery.alert; get battery.alert.region Repeater Yes Yes Yes System get/set battery.alert.low; get/set battery.alert.critical Repeater Yes Yes Yes System get/set rx.watchdog Repeater Yes Yes Yes System get/set system.watchdog nRF52 Yes Yes Yes System get public.key All text CLI roles Yes Yes Yes System get role All text CLI roles Yes Yes Yes System get powersaving; set powersaving on/off Supported repeater board Feature Feature Feature System get/set reboot.interval Full parser Yes Yes Yes Clock sync get/set clock.sync.*; clock.sync.mesh now Repeater, sensor, and room server; clock.sync.internet needs MQTT repeater Yes Yes Yes Routing get/set repeat Forwarding-capable role Yes Yes Yes Routing get/set repeat.trace Repeater only; saved repeat-off trace exception, default off Yes Yes Yes Routing get/set path.hash.mode Role that supports path-hash selection Yes Yes Yes Routing get/set loop.detect Repeater Yes Yes Yes Routing get/set txdelay All text CLI roles, including bridges Yes Yes Yes Routing get/set direct.txdelay Full parser Yes Yes Yes Routing get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Routing get/set dutycycle Full parser Yes Yes Yes Routing get/set af Full parser Yes Yes Yes Routing get/set int.thresh All text CLI roles Yes Yes Yes Routing get/set cad Radio CAD support Feature Feature Feature Routing get/set agc.reset.interval All text CLI roles Yes Yes Yes Routing get/set radio.watchdog MQTT observer No No No Routing get/set multi.acks Full parser Yes Yes Yes Routing get/set flood.advert.interval Repeater Yes Yes Yes Routing get/set advert.interval Advert-capable role Yes Yes Yes Routing get/set flood.max Repeater Yes Yes Yes Routing get/set flood.max.unscoped Repeater Yes Yes Yes Routing get/set flood.max.advert Repeater Yes Yes Yes Routing get/set flood.channel.data; get/set flood.channel.data.hops Repeater Yes Yes Yes Routing get/set/del flood.channel.scope* Repeater Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) Yes Yes Yes Routing get/set/del flood.moderation* Repeater Yes Yes Yes Routing get/set outpath Repeater or room user, own remote-client context Yes Yes Yes Routing get/set altpath Repeater or room user, own remote-client context Yes Yes Yes ACL setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes ACL get acl [page] Repeater, room server, or sensor; admin over LoRa Yes Yes Yes ACL get/set allow.read.only Room server Yes Yes No Room get/set topic Room server Yes Yes No Regions region load; region save Role with region storage Yes Yes Yes Regions region allowf; region denyf Role with region storage Yes Yes Yes Regions region get; region list; region Role with region storage Yes Yes Yes Regions region home; region default Role with region storage Yes Yes Yes Regions region put; region def; region remove Role with region storage Yes Yes Yes Retry get/set direct.retry; get/set direct.retry.heard Role with basic retry support Yes Yes Yes Retry get/set retry.preset Role with retry support Yes Yes Yes Retry get/set flood.retry.count; get/set flood.retry.path; get/set flood.retry.group.path Repeater Yes Yes Yes Retry get/set flood.retry.advert; get/set flood.retry.prefixes; get/set flood.retry.ignore Repeater Yes Yes Yes Retry get/set flood.retry.bridge; get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Retry get/set direct.retry.count; get/set direct.retry.base; get/set direct.retry.step Role with retry support Yes Yes Yes Retry get/set direct.retry.margin; get/set direct.retry.cr Role with retry support Yes Yes Yes Retry get/set/clear recent.repeater; get recent.repeaters Repeater Yes Yes Yes GPS get gps; set gps on/off Compiled GPS Feature Feature Feature GPS gps sync Compiled GPS Feature Feature Feature GPS get/set gps.sync.interval Compiled GPS and provider; 1-336 hours Feature Feature Feature GPS gps setloc Compiled GPS Feature Feature Feature GPS gps advert [none/share/prefs] Compiled GPS Feature Feature Feature Sensors get/set telemetry.access Sensor-capable role Feature Feature Limited Sensors sensor list [start] Compiled sensor manager; reduced OTA trims target-selected optional environmental/ranging drivers Feature Feature Limited Sensors sensor get; sensor set Compiled sensor setting; reduced OTA trims target-selected optional environmental/ranging drivers Feature Feature Limited Sensors io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No Bridge get bridge.type Compiled bridge Feature Feature Feature Bridge get/set bridge.enabled; get bridge.running Compiled bridge Feature Feature Feature Bridge get/set bridge.delay Compiled bridge Feature Feature Feature Bridge get/set bridge.source Compiled bridge Feature Feature Feature Bridge get/set bridge.baud RS-232 bridge Feature Feature Feature Bridge get/set bridge.uart RS-232 bridge Feature Feature Feature Bridge get/set bridge.channel ESP-NOW is ESP32 only No No No Bridge get/set bridge.format ESP-NOW bridge No No No Bridge get/set bridge.secret ESP-NOW is ESP32 only No No No Board get bootloader.ver nRF52 bootloader metadata Yes Yes Yes Board get pwrmgt.support; get pwrmgt.source; get pwrmgt.bootreason; get pwrmgt.bootmv Board power-management implementation Feature Feature Feature Board get/set pwrmgt.bootlock; get/set pwrmgt.cutoff nRF52 power-management boards with a text CLI Feature Feature Feature Board get/set battery.profile; get/set battery.empty; get/set battery.full; get/set adc.multiplier nRF52 power-management boards with a text CLI; relative ADC calibration Feature Feature Feature Ethernet eth.status Ethernet target Feature Feature No Browser OTA start ota [ap]; stop ota ESP32 browser uploader No No No WebConfig start webconfig [ap]; stop webconfig; get/set webui ESP32 WebConfig No No No WiFi get/set wifi.ssid; set wifi.pwd; get wifi.status; get/set wifi.powersave ESP32 WiFi No No No WiFi get/set wifi.cli ESP32 WebConfig No No No LoRa OTA ota help; ota ?; ota h LoRa OTA build No No Yes LoRa OTA ota; ota status; ota st LoRa OTA build No No Yes LoRa OTA ota stats LoRa OTA build No No Yes LoRa OTA ota ls; ota neighbors; ota nbrs; ota updates; ota n LoRa OTA build No No Yes LoRa OTA ota get; ota pull; ota download LoRa OTA build; internal-store nRF52 installs in-place deltas, while matched SD/QSPI nRF52 also accepts full images No No Yes LoRa OTA ota install; ota apply; ota applydelta Compatible bootloader and completed update No No Yes LoRa OTA ota rescue install <base_hash16> Legacy internal-flash nRF52 LoRa OTA build with failed app-side EndF validation; shared bootloader-update builds require valid EndF No No Feature LoRa OTA ota bootloader; ota bootloader install <MID8> <HASH16> Allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2 microSD nRF52 repeater with ABI-3 exact-board OTAFIX No No Feature LoRa OTA ota cancel; ota drop; ota stop LoRa OTA build No No Yes LoRa OTA ota announce; ota adv LoRa OTA build No No Yes LoRa OTA ota self; ota id Firmware with EndF trailer No No Yes LoRa OTA ota qspi; ota storage nRF52 build with raw-QSPI OTA staging Feature Feature Feature LoRa OTA ota folder; ota fold on needs compiled folder transport No No Feature LoRa OTA ota config; ota cfg; ota set LoRa OTA build No No Yes LoRa OTA ota key; ota keys LoRa OTA build No No Yes LoRa OTA ota dev ... Developer diagnostics No No Yes MQTT get/set mqttN.preset MQTT observer No No No MQTT get/set mqttN.server; get/set mqttN.port; get/set mqttN.username; get/set mqttN.password MQTT observer No No No MQTT get/set mqttN.token; get/set mqttN.topic; get/set mqttN.audience MQTT observer No No No MQTT get mqttN.diag MQTT observer No No No MQTT get/set mqtt.origin; get/set mqtt.iata; get mqtt.presets MQTT observer No No No MQTT get mqtt.stats MQTT observer No No No MQTT get/set mqtt.status; get/set mqtt.packets; get/set mqtt.raw; get/set mqtt.interval MQTT observer No No No MQTT get/set mqtt.rx; get/set mqtt.tx MQTT observer No No No MQTT get/set mqtt.neighbors; get/set mqtt.neighbors.interval MQTT observer with compiled neighbor support No No No MQTT get/set mqtt.ntp MQTT observer No No No MQTT get mqtt.ntp.diag Full MQTT observer No No No MQTT get/set timezone; get/set timezone.offset MQTT observer No No No MQTT get/set mqtt.analyzer.us; get/set mqtt.analyzer.eu Legacy MQTT aliases No No No MQTT get/set mqtt.owner; get/set mqtt.email MQTT observer; get requires a local connection No No No MQTT get mqtt.config.valid MQTT observer No No No SNMP get/set snmp; get/set snmp.community MQTT target compiled with SNMP No No No Alerts get/set alert; get/set alert.psk; get/set alert.hashtag; get/set alert.region; get/set alert.wifi; get/set alert.mqtt; get/set alert.interval MQTT observer No No No Alerts alert test [message] MQTT observer with configured alert channel No No No TLS tls.bundletest <host> MQTT target with embedded certificate bundle No No No Manifest OTA ota check MQTT target with OTA_MANIFEST_BASE No No No Manifest OTA ota update MQTT target with OTA_MANIFEST_BASE No No No"},{"location":"cli_command_availability/#esp32","title":"ESP32","text":"Area Command Scope Standard Logging LoRa OTA FULL unified FULL fallback Operational reboot All text CLI roles Yes Yes Yes Yes Yes Operational poweroff; shutdown Board power-off support Yes Yes Yes Yes Yes Operational uf2reset nRF52 only No No No No No Operational clkreboot Full parser Yes Yes Yes Yes Yes Operational clock sync All text CLI roles Yes Yes Yes Yes Yes Operational clock All text CLI roles Yes Yes Yes Yes Yes Operational time <epoch_seconds> Clock only moves forward Yes Yes Yes Yes Yes Operational advert Advert-capable role Yes Yes Yes Yes Yes Operational advert.zerohop Advert-capable role Yes Yes Yes Yes Yes Operational erase Local connection Local Local Local Local Local Display get/set display.mode; display.timeout; display.usb.mode; display.usb.timeout Display-equipped roles; pairing modes require BLE Companion Feature Feature Feature Feature Feature Display get/set display.flip MQTT observer with supported display driver No No No Feature No Information ver All text CLI roles Yes Yes Yes Yes Yes Information board All text CLI roles Yes Yes Yes Yes Yes Diagnostics get storage.layout Detected flash size and live partition table Yes Yes Yes Yes Yes Diagnostics memory ESP32 heap and PSRAM summary Yes Yes Yes Yes Yes Diagnostics sensor Hardware wiring summary Yes Yes Yes Yes Yes Diagnostics powerlog Reset-reason summary Yes Yes Yes Yes Yes Neighbors neighbors Role with a neighbor table Yes Yes Yes Yes Yes Neighbors neighbor.remove [pubkey_prefix] Role with a mutable neighbor table; omit prefix to clear all Yes Yes Yes Yes Yes Neighbors discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Yes Yes Neighbors discover.scopes MQTT observer with compiled neighbor support No No No Feature No Statistics clear stats Full parser Yes Yes Yes Yes Yes Statistics stats-core Local connection Local Local Local Local Local Statistics stats-radio Local connection Local Local Local Local Local Statistics stats-radio-diag Local connection Local Local Local Local Local Statistics stats-packets Local connection Local Local Local Local Local Statistics get telemetry.temp/volt/volt.i2c; optional GPS history Non-STM32 repeater or room server; I2C command requires a detected voltage monitor; GPS requires a provider; remote access requires administrator Yes Yes Yes Yes Yes Statistics set telemetry.gps; get/set/send telemetry.tx Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator Yes Yes Yes Yes Yes Logging log start; log stop; log erase Storage-backed roles retain data Yes Yes Yes Yes Yes Logging log Local connection Local Local Local Local Local Logging get/set usb.logging; unified FULL get/set logging.output Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; every ESP32 Full Companion uses an input-capable single-TTY logging terminal with framed USB Companion disabled while logging Yes Yes No No Yes Logging get/set usb.debug Same USB-control availability; saved intent defaults off; actual diagnostics need compiled support and USB master on Yes Yes No No Yes Logging get/set usb.watchdog off\\|on\\|auto Current source; recovery requires native USB, live logging, and durable storage; Auto qualifies from USB stats polling Yes Yes Feature Yes Yes Logging get usb.watchdog.last Current source; latest USB watchdog event with advisory timestamp, uptime, action, reasons, and durability; native USB required Yes Yes Feature Yes Yes Radio get radio; set radio ... All text CLI roles Yes Yes Yes Yes Yes Radio get tx; set tx <dbm> Board TX-power limits apply Yes Yes Yes Yes Yes Radio tempradio ...; normalradio Full parser Yes Yes Yes Yes Yes Radio get/set/del radioat; get/set/del tempradioat Full parser Yes Yes Yes Yes Yes Radio get freq; set freq <mhz> Local or authorized LoRa CLI Yes Yes Yes Yes Yes Radio get/set radio.rxgain Supported radio Feature Feature Feature Feature Feature Radio get/set espnow.channel Primary ESP-NOW mesh radio Feature Feature Feature Feature Feature Radio get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Feature Feature Radio get/set radio.fem.txgain Controllable LoRa FEM Feature Feature Feature Feature Feature Radio get/set radio.rxps; get rxps.wd Full parser and RX power-saving support Feature Feature Feature Feature Feature System get/set name All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set lat All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set lon All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set prv.key get requires a local connection Yes Yes Yes Yes Yes System get password; password <new_password> get requires a local connection; setter requires administrator Yes Yes Yes Yes Yes System get/set guest.password Role with guest administration Yes Yes Yes Yes Yes System get/set owner.info Full parser Yes Yes Yes Yes Yes System get/set adc.multiplier Board ADC override support Feature Feature Feature Feature Feature System send text.flood <message> Repeater Yes Yes Yes Yes Yes System host <text>; get host Repeater with host bridge; host requires a remote administrator and a USB/MQTT service Yes Yes Yes Yes Yes System get/set battery.alert; get battery.alert.region Repeater Yes Yes Yes Yes Yes System get/set battery.alert.low; get/set battery.alert.critical Repeater Yes Yes Yes Yes Yes System get/set rx.watchdog Repeater Yes Yes Yes Yes Yes System get/set system.watchdog nRF52 only No No No No No System get public.key Full parser Yes Yes Yes Yes Yes System get role All text CLI roles Yes Yes Yes Yes Yes System get powersaving; set powersaving on/off Supported repeater board Feature Feature Feature Feature Feature System get/set reboot.interval Full parser Yes Yes Yes Yes Yes Clock sync get/set clock.sync.*; clock.sync.mesh now Repeater, sensor, and room server; clock.sync.internet needs MQTT repeater Yes Yes Yes Yes Yes Routing get/set repeat Forwarding-capable role Yes Yes Yes Yes Yes Routing get/set repeat.trace Repeater only; saved repeat-off trace exception, default off Yes Yes Yes Yes Yes Routing get/set path.hash.mode Full parser Yes Yes Yes Yes Yes Routing get/set loop.detect Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set txdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Routing get/set direct.txdelay Full parser Yes Yes Yes Yes Yes Routing get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Routing get/set dutycycle Full parser Yes Yes Yes Yes Yes Routing get/set af Full parser Yes Yes Yes Yes Yes Routing get/set int.thresh All text CLI roles Yes Yes Yes Yes Yes Routing get/set cad Radio CAD support Feature Feature Feature Feature Feature Routing get/set agc.reset.interval All text CLI roles Yes Yes Yes Yes Yes Routing get/set radio.watchdog MQTT observer No No No Yes No Routing get/set multi.acks Full parser Yes Yes Yes Yes Yes Routing get/set flood.advert.interval Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set advert.interval Full common parser Yes Yes Yes Yes Yes Routing get/set flood.max Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.max.unscoped Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.max.advert Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.channel.data; get/set flood.channel.data.hops Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set/del flood.channel.scope* Repeater role handler Yes Yes Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater role handler Yes Yes Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater role handler; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) Yes Yes Yes Yes Yes Routing get/set/del flood.moderation* Repeater role handler Yes Yes Yes Yes Yes Routing get/set outpath Repeater or room user, own remote-client context Yes Yes Yes Yes Yes Routing get/set altpath Repeater or room user, own remote-client context Yes Yes Yes Yes Yes ACL setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes Yes Yes ACL get acl [page] Repeater, room server, or sensor; admin over LoRa Yes Yes Yes Yes Yes ACL get/set allow.read.only Room server Yes Yes No Yes Yes Room get/set topic Room server Yes Yes No Yes Yes Regions region load; region save Role with region storage Yes Yes Yes Yes Yes Regions region allowf; region denyf Role with region storage Yes Yes Yes Yes Yes Regions region get; region list; region Role with region storage Yes Yes Yes Yes Yes Regions region home; region default Role with region storage Yes Yes Yes Yes Yes Regions region put; region def; region remove Role with region storage Yes Yes Yes Yes Yes Retry get/set direct.retry; get/set direct.retry.heard Role with basic retry support Yes Yes Yes Yes Yes Retry get/set retry.preset Role with retry support Yes Yes Yes Yes Yes Retry get/set flood.retry.count; get/set flood.retry.path; get/set flood.retry.group.path Repeater Yes Yes Yes Yes Yes Retry get/set flood.retry.advert; get/set flood.retry.prefixes; get/set flood.retry.ignore Repeater Yes Yes Yes Yes Yes Retry get/set flood.retry.bridge; get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Feature Feature Retry get/set direct.retry.count; get/set direct.retry.base; get/set direct.retry.step Role with retry support Yes Yes Yes Yes Yes Retry get/set direct.retry.margin; get/set direct.retry.cr Role with retry support Yes Yes Yes Yes Yes Retry get/set/clear recent.repeater; get recent.repeaters Repeater Yes Yes Yes Yes Yes GPS get gps; set gps on/off Compiled onboard GPS Feature Feature Feature Feature Feature GPS gps sync Compiled onboard GPS Feature Feature Feature Feature Feature GPS get/set gps.sync.interval Compiled GPS and provider; 1-336 hours Feature Feature Feature Feature Feature GPS gps setloc Compiled onboard GPS Feature Feature Feature Feature Feature GPS gps advert [none/share/prefs] Compiled onboard GPS Feature Feature Feature Feature Feature Sensors get/set telemetry.access Sensor-capable full parser Feature Feature Limited Feature Feature Sensors sensor list [start] Compiled sensor manager; no_external_sensors trims target-selected environmental/ranging drivers, not generic I2C Feature Feature Limited Feature Feature Sensors sensor get; sensor set Compiled sensor setting; no_external_sensors trims target-selected environmental/ranging drivers, not generic I2C Feature Feature Limited Feature Feature Sensors io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No No Feature Bridge get bridge.type Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.enabled; get bridge.running Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.delay Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.source Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.baud RS-232 bridge Feature Feature Feature No Feature Bridge get/set bridge.uart RS-232 bridge Feature Feature Feature No Feature Bridge get/set bridge.channel ESP-NOW bridge No No No Feature Feature Bridge get/set bridge.format ESP-NOW bridge No No No Feature Feature Bridge get/set bridge.secret ESP-NOW bridge No No No Feature Feature Board get bootloader.ver nRF52 only No No No No No Board get pwrmgt.support; get pwrmgt.source; get pwrmgt.bootreason; get pwrmgt.bootmv nRF52 only No No No No No Ethernet eth.status Ethernet target Feature Feature No No Feature Browser OTA start ota [ap]; stop ota Compiled browser uploader No No Yes Feature Feature WebConfig start webconfig [ap]; stop webconfig; get/set webui Compiled WebConfig No No No Feature Feature WiFi get/set wifi.ssid; set wifi.pwd; get wifi.status; get/set wifi.powersave MQTT WiFi, or standalone FULL WebConfig; standalone has no get wifi.pwd No No No Yes Feature WiFi get/set wifi.cli Compiled WebConfig No No No Feature Feature LoRa OTA ota help; ota ?; ota h LoRa OTA build No No Yes Yes Yes LoRa OTA ota; ota status; ota st LoRa OTA build No No Yes Yes Yes LoRa OTA ota stats LoRa OTA build No No Yes Yes Yes LoRa OTA ota ls; ota neighbors; ota nbrs; ota updates; ota n LoRa OTA build No No Yes Yes Yes LoRa OTA ota get; ota pull; ota download LoRa OTA build No No Yes Yes Yes LoRa OTA ota install; ota apply; ota applydelta Compatible completed update No No Yes Yes Yes LoRa OTA ota rescue install <base_hash16> Legacy internal-flash nRF52 LoRa OTA build with failed app-side EndF validation; shared bootloader-update builds require valid EndF No No Feature No No LoRa OTA ota bootloader; ota bootloader install <MID8> <HASH16> Allowlisted nRF52 internal/QSPI repeater feature No No No No No LoRa OTA ota cancel; ota drop; ota stop LoRa OTA build No No Yes Yes Yes LoRa OTA ota announce; ota adv LoRa OTA build No No Yes Yes Yes LoRa OTA ota self; ota id Firmware with EndF trailer No No Yes Yes Yes LoRa OTA ota qspi; ota storage nRF52 build with raw-QSPI OTA staging No No No No No LoRa OTA ota folder; ota fold on needs compiled serial or TCP folder transport No No Feature Feature Feature LoRa OTA ota config; ota cfg; ota set LoRa OTA build No No Yes Yes Yes LoRa OTA ota key; ota keys LoRa OTA build No No Yes Yes Yes LoRa OTA ota dev ... Developer diagnostics No No Yes Yes Yes MQTT get/set mqttN.preset MQTT observer No No No Yes No MQTT get/set mqttN.server; get/set mqttN.port; get/set mqttN.username; get/set mqttN.password MQTT observer No No No Yes No MQTT get/set mqttN.token; get/set mqttN.topic; get/set mqttN.audience MQTT observer No No No Yes No MQTT get mqttN.diag MQTT observer No No No Yes No MQTT get/set mqtt.origin; get/set mqtt.iata; get mqtt.presets MQTT observer No No No Yes No MQTT get mqtt.stats MQTT observer No No No Yes No MQTT get/set mqtt.status; get/set mqtt.packets; get/set mqtt.raw; get/set mqtt.interval MQTT observer No No No Yes No MQTT get/set mqtt.rx; get/set mqtt.tx MQTT observer No No No Yes No MQTT get/set mqtt.neighbors; get/set mqtt.neighbors.interval MQTT observer with compiled neighbor support No No No Feature No MQTT get/set mqtt.ntp MQTT observer No No No Yes No MQTT get mqtt.ntp.diag MQTT observer No No No Yes No MQTT get/set timezone; get/set timezone.offset MQTT observer No No No Yes No MQTT get/set mqtt.analyzer.us; get/set mqtt.analyzer.eu Legacy MQTT aliases No No No Yes No MQTT get/set mqtt.owner; get/set mqtt.email MQTT observer; get requires a local connection No No No Yes No MQTT get mqtt.config.valid MQTT observer No No No Yes No SNMP get/set snmp; get/set snmp.community MQTT target compiled with SNMP No No No Feature No Alerts get/set alert; get/set alert.psk; get/set alert.hashtag; get/set alert.region; get/set alert.wifi; get/set alert.mqtt; get/set alert.interval MQTT observer No No No Yes No Alerts alert test [message] MQTT observer with configured alert channel No No No Yes No TLS tls.bundletest <host> MQTT target with embedded certificate bundle No No No Feature No Manifest OTA ota check MQTT target with OTA_MANIFEST_BASE No No No Manifest No Manifest OTA ota update MQTT target with OTA_MANIFEST_BASE No No No Manifest No"},{"location":"cli_command_availability/#supplemental-command-notes","title":"Supplemental command notes","text":"

These short entries cover implemented commands that do not yet have their own section in the primary command reference.

"},{"location":"cli_command_availability/#memory","title":"memory","text":"

ESP32 only. Prints free heap, minimum heap, largest allocation, queue depth, internal heap, and PSRAM totals.

"},{"location":"cli_command_availability/#sensor-hardware-summary","title":"sensor hardware summary","text":"

Prints the configured I2C pins and GPS serial pins. This is a wiring/build diagnostic, distinct from sensor list, sensor get, and sensor set.

"},{"location":"cli_command_availability/#powerlog","title":"powerlog","text":"

Prints the last reset reason. nRF52 also prints the captured shutdown reason and boot voltage.

"},{"location":"cli_command_availability/#stats-radio-diag","title":"stats-radio-diag","text":"

Local-serial diagnostic that prints the extended radio diagnostic counters.

"},{"location":"cli_command_availability/#radiorxps-and-rxpswd","title":"radio.rxps and rxps.wd","text":"

get radio.rxps reports RX power-saving state and timing. set radio.rxps ... changes that configuration. get rxps.wd reports the soft and hard RX power-saving watchdog counters.

"},{"location":"cli_command_availability/#rebootinterval","title":"reboot.interval","text":"

get reboot.interval shows the scheduled reboot interval. Use set reboot.interval <hours> for 1-255 hours, or set reboot.interval 0 to disable it.

"},{"location":"cli_command_availability/#sensor-io","title":"Sensor io","text":"

The simple-sensor role exposes its board GPIO word as hexadecimal. io reads it; io <hex> replaces it; and io r<hex>, io s<hex>, or io t<hex> clear, set, or toggle selected bits.

"},{"location":"cli_command_availability/#get-mqttndiag","title":"get mqttN.diag","text":"

Prints the runtime diagnostic summary for MQTT slot N, including its connection state and failure history.

pub_ack counts actual broker acknowledgments, not merely accepted writes. Recent firmware also shows rec:<count>/<reason> for transport recovery and ack_wait:<seconds>s while an accepted QoS 1 publication awaits acknowledgment. Reasons include error, state, handshake, write, and puback. recover means cleanup is pending, not that the broker is healthy. Optional fields are omitted if they cannot fit completely in the reply. rec counts completed client cleanup; a higher count alone does not prove the subsequent connection or delivery succeeded.

"},{"location":"cli_command_availability/#get-mqttstats","title":"get mqtt.stats","text":"

Prints the MQTT bridge runtime statistics summary.

"},{"location":"cli_command_availability/#get-mqttconfigvalid","title":"get mqtt.config.valid","text":"

Reports whether the saved MQTT configuration has the minimum values needed to run.

"},{"location":"cli_command_availability/#tlsbundletest","title":"tls.bundletest","text":"

tls.bundletest <host[:port]|url> tests the embedded TLS certificate bundle against a remote host without changing the MQTT configuration.

"},{"location":"cli_command_availability/#manifest-ota-check-and-ota-update","title":"Manifest ota check and ota update","text":"

On MQTT targets with OTA_MANIFEST_BASE, ota check fetches and checks the target manifest. ota update downloads and applies the compatible update selected by that manifest.

"},{"location":"cli_commands/","title":"Cli commands","text":"

View this page on MeshCore Docs.

"},{"location":"cli_commands/#cli-commands","title":"CLI Commands","text":"

For SolarXiao Base/Advanced/MPPT automatic detection, manual overrides, live battery/MCU readings and charger controls, see SolarXiao carrier profiles (get solar, get solar readings, set solar.board auto, get mppt, set mppt).

See two LoRa profiles for radio2, tempradio2, scheduling, RX-only mode, cross-transmission policy, and optional radio preambles.

For a saved MediaMesh second profile, use set radio2 mediamesh (911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, automatic preamble), or set radio2 mediamesh-range for 125 kHz. These leave the primary profile and crossing policy unchanged. Both radio2 and radio2.cross default to off. See MediaMesh presets.

For supported Full Companions, see Meshtext channel chat for mt- channels and the explicit send mt.nodeinfo <channel slot> command. Chat remains plain text. A separate GPS-free NodeInfo is sent after the first successful chat transmission per used channel/identity per boot; the explicit command can also request one. Automatic metadata waits for a 250 ms gap after the latest successful MT transmission; explicit requests bypass that gap. MT heard-repeat retries use the Companion's flood.retry.count, with the metadata cap and flood.retry.advert gate for NodeInfo.

See carrier-wave RF checks for cw on [seconds], cw2 on [seconds], and stopping a carrier with off.

For keyed Morse TX, use morse send TEST DE N0CALL and morse stop. Defaults are 902.100 MHz and 25 WPM; set morse.freq <MHz> and set morse.wpm <1\u201360> save separate frequency/speed settings. Small-flash STM32 Companion builds retain Morse TX but do not include Morse RX by default; listen/read/stats/clear return an unsupported-RX error there. The constrained wio-e5-mini_repeater omits Morse TX/RX and CW tools entirely; normal packet TX/RX and radio2 remain available. get morse shows status. See TX-only Morse messages for supported characters, limits, overlap checks and the temporary pause in packet reception. Nothing transmits automatically.

For timed Morse RX, use morse listen <minutes> (fractions allowed, 1 second through 5 minutes). morse stop ends TX or RX. All roles retain the bounded capture in RAM for morse read [offset]; follow each page's next value until next=END. morse clear removes it when idle. A new successful session or reboot replaces/removes the capture. Buffered USB/browser Full Companion ASCII terminals show decoded progress; other terminals and binary clients request it through the CLI, without adding unidentified text to authenticated channel/chat messages. Normal LoRa is paused during listening, so a remote administrator must wait for the selected session to end before reading the capture over LoRa. See timed reception for arming time, real chip bandwidths and truncation behavior.

For copy/paste on/off recipes and the differences from Full Companion, see feature switches by role. The USB web console opens the default ASCII terminal at 115200 baud; it does not require on-device WebConfig or WiFi.

This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.

See CLI Availability by Firmware Build for the role and profile matrix. Commands depend on compiled features; some portable builds omit WebConfig while retaining the complete role CLI and compact WiFi updater.

See CLI Command Availability Matrix for the command-by-command nRF52 and ESP32 build tables.

The first word of a command is case-insensitive, so set, Set, and SET are equivalent, as are get, Get, and the other command verbs. The case of arguments such as node names, passwords, and keys is left unchanged.

Repeater retries carrying the same logical request ID and original command bytes reuse the cached result, even when command parsing trims whitespace or splits arguments in place. Changing the timestamp alone does not execute the operation again. Normal authentication and stale-request checks still apply. Deferred commands and host-service replies remain bound to the full authenticated public key, even if permission changes move ACL entries. Removing or revoking the requester cancels pending execution; a running setperm can still acknowledge its own completed change to the original sender.

Use the site search or your browser's Find command with everyday wording such as tx retries, retry attempts, serial logging, or tx power. Search terms below are alternative wording to help find a command, not additional CLI aliases. Enter the syntax shown under Usage; supported command aliases are listed there explicitly.

"},{"location":"cli_commands/#navigation","title":"Navigation","text":""},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"

Search terms: restart, restart node, reboot device.

Usage: - reboot

Note: No reply is sent.

"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"

Search terms: turn off device, shut down, power off.

Usage: - poweroff, or - shutdown

Note: No reply is sent.

"},{"location":"cli_commands/#enter-the-uf2-bootloader-nrf52-only","title":"Enter the UF2 bootloader (nRF52 only)","text":"

Search terms: bootloader mode, USB firmware update, UF2 mode.

Usage: - uf2reset

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

Note: Reboots directly into the UF2 bootloader on supported nRF52 boards. This includes the Repeater, Room Server, Sensor, Companion, and Terminal Chat local serial command surfaces. It is never accepted as a remote mesh command.

"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"

Usage: - clkreboot

Note: No reply is sent.

"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"

Usage: - clock sync

"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"

Usage: - clock

"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"

Usage: - time <epoch_seconds>

Parameters: - epoch_seconds: Unix epoch time

"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"

Usage: - advert

"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"

Usage: - advert.zerohop

"},{"location":"cli_commands/#ask-a-usb-connected-host-service-over-lora-repeater-only","title":"Ask a USB-connected host service over LoRa (Repeater Only)","text":"

Usage:

host hands one remote LoRa CLI request to a service on the repeater's USB host and returns that service's reply over LoRa. It does not execute the text inside the firmware. The service has 4 seconds to prove the exact request is still pending, then 6 seconds to execute and reply. A second remote command receives busy while either phase is pending.

Request text is limited to 155 UTF-8 bytes, or 152 bytes when a companion uses its legacy three-byte correlation prefix. The complete LoRa reply is limited to 162 bytes. USB records use Base64URL text, a random 64-bit nonce, and the repeater identity's Ed25519 signature. Before executing anything, the service must return a random one-time challenge and receive the repeater's signed live proof. This avoids any dependency on repeater/Pi clock agreement. Every host.reply USB command must carry the matching request ID and nonce; it is not accepted from LoRa or the Ethernet CLI.

The included Raspberry Pi endpoint supports exact commands for help, cpu-temp, hostname, uptime, load, memory, disk-free, and clock status. It also provides strictly validated clock sync and clock set <unix_epoch> recovery actions, opt-in network restart and reboot actions, action status <operation_id>, and run <alias> [arguments] for locally allowlisted executables with typed arguments. Clock changes and host recovery actions are disabled by default, arbitrary executables are never accepted, and no action uses a shell. Clock set accepts only canonical unsigned decimal epochs from 2020 through 2099; a root-owned Unix-socket service revalidates the request and authenticates the local service account with SO_PEERCRED before changing system time. Network restart and reboot use a separate root-owned Unix-socket broker with fixed systemd units; installing clock control does not grant those actions. The endpoint never uses sudo for privileged host actions. See LoRa CLI host service for the complete meshcoretomqtt setup and security model.

"},{"location":"cli_commands/#lora-ota-speed","title":"LoRa OTA speed","text":"
get ota.speed\nset ota.speed <0.05..3>\n

1 preserves the current OTA timing. Values below 1 slow OTA traffic; values above 1 shorten adjustable delays. For example, 0.5 doubles those delays and 3 divides them by three. Decimal values such as .05 work. ota config speed <factor> is an equivalent setter. get ota.speed and ota config show both the saved value and the effective packet pacing factor. Automatic loss response may make packet spacing slower without changing the saved setting. The setting can be changed during a transfer.

Apply it on the source, destination, and relays. It controls all LoRa OTA packet types on both profiles, relay timing, discovery, and OTA adverts. Slower settings extend recovery waits and space queued OTA packets while allowing ordinary traffic through. Faster settings retain the physical retry allowance and the configured airtime budget, so throughput is not guaranteed to rise in direct proportion. Proof turnaround keeps its physical allowance. Ordinary messages, node adverts, and local staged-image verification keep their timing. Available in OTA-enabled builds for all roles, including Companions. See the OTA guide.

"},{"location":"cli_commands/#start-or-stop-an-over-the-air-ota-firmware-update","title":"Start or stop an Over-The-Air (OTA) firmware update","text":"

Search terms: WiFi OTA, wireless firmware update, OTA uploader, update firmware.

Usage: - start ota - start ota ap - stop ota

On nRF52, start ota invokes Bluetooth DFU with the matching bootloader and application DFU ZIP. The WiFi/AP instructions below apply to ESP32.

On ESP32, start ota enables WiFi as needed and serves the web upload page on the station IP when connected to WiFi; otherwise it raises the MeshCore-OTA access point. The reply gives the URL to open and tells you which network to use. start ota ap always selects the access point, including when OTA is already running on a LAN address. This is useful with client-isolated networks.

The runtime browser uploader uses HTTP Basic authentication: username admin, password the node's saved admin password. The OTA hotspot reuses that password for WPA2 when it is 8\u201315 printable ASCII characters; otherwise it is open and the HTTP login still applies. Temporary partition migration and recovery apps retain their documented access policy.

Starting OTA automatically stops WebConfig and reports WebConfig stopped in the reply. The existing LAN connection stays up; a setup hotspot is replaced by MeshCore-OTA. WebConfig's delayed HTTP cleanup cannot stop OTA's WiFi. Use the USB/Bluetooth or LoRa command line to start the uploader; WebConfig's own terminal cannot deliver the handoff reply after shutting itself down. Stop OTA before starting WebConfig again.

FULL ESP32 builds also expose the .mota folder seeder on TCP port 5001 whenever WiFi is usable. This listener is independent of port 80, so a host can run motatool serve --dir ./motas --tcp <node-ip>:5001 while WebConfig is active. The TCP connection auto-attaches and detaches the folder; do not run ota folder on at the same time because that command selects the USB-serial folder transport.

"},{"location":"cli_commands/#browser-configuration-portal-esp32-repeater-and-room-server","title":"Browser configuration portal (ESP32 repeater and room server)","text":"

Search terms: web UI, web interface, WiFi settings page, configuration website, WebConfig.

Usage:

set webui on is the persistent master switch. It starts the portal now and again after future reboots; set webui off closes it and disables that boot start. get webui reports the saved on/off state plus whether the portal is inactive, joining WiFi, serving a setup AP, or serving a LAN URL. The default is off on repeater and room-server builds. On display-equipped repeaters, an otherwise-unused triple click toggles the same saved setting. Consequently, > off, http://192.168.1.130/ means the saved boot setting is off while a temporary WebConfig session is currently active at that URL.

start webconfig is a temporary start that does not change the saved switch. It serves the shared WiFi, radio, flood, loop, and status page and reports its URL. MQTT builds also show the MQTT tab and wizard step; non-MQTT builds remove them entirely. Sign in with the node's admin password. If the node has no saved WiFi SSID, the command starts the MeshCore-Setup-XXXX captive AP at http://192.168.4.1/ instead.

If a saved WiFi network is unavailable and the node falls back to its setup AP, sign in with the existing admin password before changing settings. Joining that AP does not grant permission to replace the password. The hotspot uses the same admin password for WPA2 when it is 8\u201315 printable ASCII characters. Other valid admin passwords leave the hotspot open and require the HTTP admin login. Protected first-time setup without a saved SSID keeps its provisioning flow. ESP32 WiFi Companions also have a saved admin password, initially password. Deliberately passwordless images retain their local no-password policy; use them only on a trusted network.

start webconfig ap forces captive-AP mode. It will not interrupt an active MQTT bridge, so run set mqtt.enabled off first. Use stop webconfig to close either mode for the current boot. (stop webconfig does not change a saved webui on.) LAN mode otherwise remains active until reboot. On expanded FULL builds, an unconfigured automatic setup AP receives one absolute 30-minute window per boot, then powers WiFi off even if a client remains attached; rebooting starts a new automatic window. An administrator can explicitly run start webconfig again without rebooting. Once an SSID is saved, the cutoff no longer applies and the selected WiFi/MQTT mode keeps reconnecting. Other setup sessions retain their profile's idle timeout.

Full Repeater and Room Server suspend unconfigured network bridges during automatic first-boot setup. Automatic ESP-NOW retries remain suspended after the AP closes, so they cannot keep an unused WiFi driver running. Saved bridge choices remain intact; explicitly enabling the bridge resumes it for this boot. Manual WebConfig/WiFi starts preserve an explicitly enabled ESP-NOW bridge, including when set 2.4ghz on restores the previously selected services. Stopping a browser OTA session restores only bridges that session actually paused, rather than starting services from saved intent alone.

Every ESP32 build with WebConfig supports the browser command terminal, including WiFi Companion and Full Companion. Companions use their complete USB/TCP terminal on the trusted LAN, including contact import, chat, recipient selection, and delayed replies. ESP32 roles require their WebConfig admin login before browser terminal access; the HTTP login does not protect TCP.

To add a Companion contact, run import meshcore://<full-contact-card-data>, then list to confirm it appears. Get the complete card with card on the other Companion. A public key alone is insufficient. See the terminal command guide for to, send, login, and cmd examples and session ownership.

The saved wifi.cli setting defaults to on. Use set wifi.cli off to disable the CLI tab. get wifi.cli reports off, on, waiting for WiFi client, or on, active. The saved setting becomes active only in station/LAN mode while the WiFi client is connected. It is deliberately unavailable on the setup access point. The tab displays each reply in the browser, including local diagnostics, infrastructure get password, private-key and WiFi/MQTT credential reads, and erase. ESP32 Companion's get password reports status without the secret. Infrastructure get acl and raw log stream to the requesting browser, with backpressure for slow clients. These maintenance commands retain their LoRa restrictions. set freq works both here and through authorized LoRa CLI. The bounded /api/cli endpoint accepts local maintenance commands; use /api/terminal for complete ACL and log listings. Select Command block to paste up to 100 commands with one command per line. Blank lines are ignored, and every nonblank line must fit the normal 159-byte CLI command limit. The browser sends the lines sequentially and waits for each reply before sending the next line. The queue exists only in that browser page; closing it stops any commands that have not yet been sent. A lost connection also stops the remaining block.

ESP32 WiFi Companions also expose AP-compatibility controls through the same local/framed command parser:

The same reply includes SSID, IP address and RSSI when connected, protocol=<saved>/<boot>/<live bitmap>, mode=<saved>[/<boot>], live modem sleep (ps), Bluetooth coexistence (coex), and ota=off|<port>:listen|<port>:client. The boot mode is omitted only when it matches the saved mode; differing values mean a reboot is needed. No detail argument is required.

Both controls are saved for the next reboot, without interrupting the command connection. bg excludes 802.11n and cannot override an ESP-NOW/LR owner. wifi-only requires wifi.enabled 1 and skips Bluetooth stack initialization, while retaining USB and LoRa. To test without modem sleep, run set wifi.mode wifi-only, set wifi.powersave none, then reboot; none is safely deferred until that reboot if Bluetooth currently owns the radio. Restore with set wifi.mode normal, set wifi.protocol bgn, set wifi.powersave min, then reboot. Keep USB recovery available when testing a WLAN that may not work. The Indicator keeps its existing companion.transport selector instead of wifi.mode. See Companion WiFi compatibility settings.

Up/down arrow keys recall commands from the current browser session in single-command mode. A command that stops WebConfig, changes its WiFi connection, disables wifi.cli, or reboots the node stops the remaining block and can close the page before its reply is collected.

On unified FULL USB + WiFi and FULL logging-fallback ESP32 repeater/room-server builds, get wifi.ssid reports the saved standalone WebConfig network and get wifi.status reports whether WiFi is unconfigured, off, connecting, running the setup AP, failed, or connected. A connected result includes the SSID, LAN IP, and RSSI. When the shared OTA seeder is running, the reply appends OTA TCP 5001: listening or OTA TCP 5001: client connected. WiFi being off is normal while WebConfig is inactive; run start webconfig when a temporary connection is wanted. get wifi.powersave reports the saved standalone setting as none, min, or max. Fresh Cascade-profile builds default to min; target-default builds use none. A saved setting takes precedence after an upgrade.

The WiFi set commands work on MQTT observers and on FULL standalone ESP32 repeater/room-server builds. On a standalone build, changing the SSID or password stops an active WebConfig session; run start webconfig again to use the new credentials. set wifi.pwd with no value selects an open network. Standalone WiFi also accepts an exact 64-character hexadecimal WPA/WPA2 PSK; ordinary passphrases remain limited to 63 characters. Other 64-character values and all longer values are rejected. MQTT observer WiFi passwords retain their fixed 63-character limit. Power-save changes are applied immediately when WiFi is running and otherwise take effect on the next connection. get wifi.pwd returns the saved password on a local connection. It remains unavailable or masked over LoRa, and the configuration form continues to mask stored secrets.

ESP32 WiFi Companion WebConfig exposes the same wifi.powersave values in its WiFi card. Every ESP32 WiFi Companion with WebConfig exposes the standalone wifi.ssid, wifi.status, wifi.powersave, wifi.cli, and WebConfig command families through its USB text terminal. Full Companion exposes the same role-specific terminal on TCP port 5002. Credential writes reply before restarting the WiFi station, so a TCP client should expect to reconnect at the new address; USB password input is masked. Binary Companion clients can use USB, BLE, or TCP port 5000 without the terminal-start token: send command 0x42 (CMD_RUN_CLI_COMMAND) followed by the same CLI text, such as get wifi.powersave, get wifi.pwd, stats-core, or set wifi.powersave min. Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off and Bluetooth on where supported. Legacy WiFi-only builds without another CLI transport retain WiFi on so they are not left unreachable. Use get wifi.enabled and set wifi.enabled 1 to inspect or persistently enable WiFi; set wifi.enabled 0 disables it. Saved enablement survives upgrades, and credentials alone do not turn it on. ESP32 simultaneous-transport builds apply the change from the main loop; exclusive-transport builds and Pico W apply it after reboot. This does not disable a primary ESP-NOW mesh radio or add Bluetooth to a WiFi-only build.

ESP32 WiFi Companions also accept get wifi.tcp over USB, BLE, or the browser CLI. This checks the app's TCP listener (normally port 5000), not just the WiFi association. listening means the socket is open; waiting-ip means no usable station or setup-AP address yet; retrying means listener startup failed and will be retried automatically; off means no enabled listener request. reg=1 confirms the interface is registered for Companion commands. client=1 means a TCP client is connected. acc, rx, and tx count accepted clients, complete received frames, and complete transmitted frames since boot; the value after / is the last frame's hexadecimal opcode. fail, err, and retry show listener failures, the last listener-start socket error, and seconds until retry. drop gives the last client-disconnect reason. These counters contain no message contents or credentials. An open listener does not prove a router or guest-network firewall permits access from the phone.

Listener retries wait 1, 2, 4, 8, 16, then at most 30 seconds. They do not reconnect WiFi, interrupt BLE/WebConfig/OTA, replace a healthy app connection, or re-enable explicitly disabled WiFi. get display.wifi includes tcp=1 only when the listener is actually open; services=1 alone is not proof.

The ESP32 TCP transport services input even while radio logs or other outgoing frames are pending, so unsolicited traffic cannot starve the app handshake. Socket sends are non-blocking. A client that makes no outbound progress for 10 seconds while data is pending is disconnected (drop=tx-timeout), without blocking the other transports. drop=tx-error identifies a fatal send error. Commands wait when all reply slots hold delivery-required data, rather than executing with nowhere to return their reply. A partly sent frame stays pinned until it finishes; input and output remain separate TCP streams.

See local maintenance commands for the complete access rules. WiFi-only Companions accept all three modes. A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects none because coexistence requires modem sleep. Companion device power saving and LoRa radio.rxps remain independent. On an ESP32 Full Companion whose primary mesh radio is ESP-NOW, max is also unavailable: a station using maximum modem sleep can miss ESP-NOW broadcasts, which the access point does not buffer for it. A previously saved conflicting value is capped to and reported as min, and a new conflicting selection is rejected. The primary mesh radio also holds the ESP-IDF WiFi wake reference continuously so unsolicited ESP-NOW frames remain receivable; selecting min does not put that primary receiver to sleep.

SenseCAP Indicator Full is the exclusive-secondary exception. On a WiFi-selected boot, LoRa accepts none|min|max and ESP-NOW accepts none|min. On a BLE-selected boot infrastructure WiFi is not started; LoRa accepts min|max for the saved WiFi setting, while ESP-NOW + BLE requires min. USB and the primary LoRa or ESP-NOW radio remain available in every mode.

"},{"location":"cli_commands/#view-or-change-the-primary-esp-nowwifi-channel","title":"View or change the primary ESP-NOW/WiFi channel","text":"

Usage:

Parameters:

Default: 1

This command is available when ESP-NOW is the node's primary mesh radio. The setting is persisted. A set reply reports that reboot is required; until reboot, the running ESP-NOW radio remains on its previous channel. On builds that also provide ordinary WiFi, its station and setup AP share that channel.

After reboot, ESP-NOW, the setup AP, and the infrastructure-WiFi station use the selected channel. Every primary ESP-NOW node that must communicate with this node, plus the configured router's 2.4 GHz radio, must use the same fixed channel. WiFi power saving does not allow the transports to use different channels. On an ESP32 Full build with primary ESP-NOW, wifi.powersave max is unavailable because maximum modem sleep can miss ESP-NOW broadcasts; use min for coexistence. The firmware keeps the primary ESP-NOW receiver awake while still using the min WiFi/Bluetooth coexistence policy.

espnow.channel is the channel of the primary ESP-NOW mesh transport. It is separate from bridge.channel, which configures only an ESP-NOW bridge on a firmware role whose primary mesh radio is LoRa.

The browser portal is not compiled into the two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt targets because it does not fit while retaining the two app slots required for LoRa OTA. Their normal CLI settings remain available.

"},{"location":"cli_commands/#switch-the-ota-release-channel-observer-builds","title":"Switch the OTA release channel (observer builds)","text":"

The selector is saved only after the preferences transaction succeeds. A queued ota update keeps the channel it checked, even if ota branch is changed before the download starts. Switching channels offers the target image even when its commit matches the running image; build numbers are not comparable across channels.

Before an image is made bootable, its compatibility tag must confirm support for this fork's saved-settings format and required transports. Upstream observer images without the keymind1 capability are refused, including images that use the different /mqtt.json format. A refusal leaves the current image running; select a compatible channel to continue OTA. Production and beta URLs are supplied by the build recipe, not accepted as arbitrary CLI URLs.

"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"

Search terms: factory defaults, reset settings, erase configuration.

Usage: - erase

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

Warning: This is destructive!

"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"

Search terms: nearby nodes, neighbor list, neighbour list.

Usage: - neighbors

Note: The output of this command is limited to the 8 most recent adverts.

Note: Each line is encoded as {pubkey-prefix}:{timestamp}:{snr*4}

"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"

Usage: - neighbor.remove <pubkey_prefix> - neighbor.remove - remove all neighbors

Parameters: - pubkey_prefix: An even-length hexadecimal prefix (up to the full 64-character public key). All neighbors matching it are removed. Invalid keys are rejected.

Omitting the prefix clears the table. An empty or whitespace-only prefix is also accepted for compatibility with the mobile app's Remove All Neighbors action. Newly heard zero-hop adverts can populate the table again.

"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"

Usage: - discover.neighbors

This command is available in every repeater build profile, including portable MQTT, standard, logging, OTA, unified FULL, and FULL logging-fallback artifacts. It does not require MQTT or PSRAM.

"},{"location":"cli_commands/#discover-neighbor-scopes-mqtt-observer-neighbors-feature","title":"Discover neighbor scopes (MQTT observer, neighbors feature)","text":"

Refreshes the zero-hop neighbor table, then queries each neighbor for its region scopes and publishes the assembled table to the MQTT neighbors topic once.

Usage: - discover.scopes

Note: Requires an MQTT observer build with the neighbors feature compiled in (all PSRAM boards, plus non-PSRAM boards built with MQTT_NEIGHBORS_WITHOUT_PSRAM). Elsewhere it replies Err - neighbors not enabled in this build. If a discover.neighbors refresh is already in flight, the scope pass is queued behind it.

"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#list-rate-limited-flood-adverts","title":"List Rate-Limited Flood Adverts","text":"

Usage: get flood.advert [page] or get flood.advert key <index>

Lists tracked advert origins currently subject to a forwarding limit. Page 1 is the default, with three entries per page so replies fit both USB and admin LoRa CLI. Each row shows its index, the first 12 public-key hex characters, the limit reason, and the minimum wait before another new advert can pass the automatic limiter. Ordinary forwarding filters still apply.

get flood.advert\nget flood.advert 2\nget flood.advert key 1\n

Reasons are quota (the prefix's three-hour forwarding allowance is spent), history (distinct receive history is full), and bad (the full key is under the stricter one-advert-per-12-hours rule). A bad key remains listed when wait=0s: one forward is eligible, but its seven-day recovery is not complete. If several limits apply, bad takes precedence over history, then quota; the displayed wait accounts for all applicable limits.

The key form returns the complete 64-hex public key, reason, wait, shared prefix forwarding count/quota, shortest received hop count, and remaining bad-list recovery time. Different full keys with the same 12-hex prefix remain separate entries. Indices refer to the current live list and can change as history expires; read the full key before using a targeted clear.

Queries do not clear history, spend quota, or refresh abuse/recovery timers. Elapsed history expires normally. Origins rejected only because every table slot protects abuse history are not retained and cannot be enumerated. Available on repeaters, room servers, and forwarding sensors through their ordinary local/admin CLI. Companions do not have this table.

"},{"location":"cli_commands/#clear-automatic-flood-advert-history","title":"Clear Automatic Flood Advert History","text":"

Usage: clear flood.advert all or clear flood.advert <64-hex-full-public-key>

Clears automatic advert forwarding quotas, first strikes, and seven-day bad-list history for all keys or one exact full key. Available on repeaters, room servers, and forwarding sensors through their ordinary local/admin CLI (including admin LoRa CLI). A missing selector, a key prefix, or a malformed key is rejected. It does not change saved settings or manual flood rules. Reboot also clears this RAM-only history. See automatic advert limits.

"},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"

Usage: clear stats

"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"

Usage: - stats-core

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"

Search terms: signal strength, signal quality, RSSI, SNR, radio noise.

Usage: stats-radio

last_rssi is the strength of the last received packet. It is not a live signal-strength meter and will not show an unmodulated carrier; see the carrier-wave guide for a separate receiver that can measure one.

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"

Usage: stats-packets

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#read-repeater-and-room-server-telemetry-history","title":"Read repeater and room-server telemetry history","text":"

Repeater and room-server firmware record one UTC-aligned sample every 30 minutes. Temperature and battery voltage retain 336 samples (seven rolling days). Each detected INA219, INA226, INA260, or INA3221 voltage channel retains 192 samples (four rolling days). GPS-capable builds retain three GPS days by default; repeaters request a seven-day default at startup. Builds without a GPS provider omit the GPS history commands to conserve flash. All history and any runtime retention change are held in RAM and reset after a reboot.

Radio noise-floor history is recorded separately every 15 minutes, retaining 768 positions (eight rolling days) per profile. It uses idle RSSI calibration estimates rather than received-packet RSSI. Main-radio and radio2 history are kept separate; failed measurements retain a Missing position in the timeline.

The feature is omitted from flash-constrained STM32 repeater and room images.

Usage:

Parameters:

Local serial and remote administrator CLI sessions can read the history on both roles. Collection uses the MCU temperature, battery voltage, external I2C voltage monitors, and an already-valid onboard GPS fix. It does not wake GPS, so an off, sleeping, or unfixed GPS produces a missing location sample without changing its power-saving schedule.

Noise history combines three fresh estimates collected roughly five minutes apart in each UTC-aligned quarter-hour. If any is at or below -100 dBm, use the minimum. Otherwise, if any is at or above -80 dBm, use the maximum. Otherwise, average all three. Thus a bucket containing both low and high values uses the minimum. Minimums round down, maximums round up, and averages round to the nearest encoded step. Incomplete buckets are Missing. A busy channel can prevent a fresh estimate; old calibration results are not reused as fresh measurements.

External voltage history allocates 360 bytes for each detected monitor channel (1,080 bytes for all three INA3221 inputs). The allocation reserves all detected inputs so a sensor connected later can start recording, but all-zero inputs are omitted from command replies and LoRa transmission.

Packed noise history uses 480 bytes per retained profile, plus its collection state. It does not add a 15-minute transmission schedule. USB and administrator LoRa CLI return the same Base64 format in the existing reply buffer.

Replies contain > followed by standard padded Base64. After decoding, all multi-byte integers are little-endian. Packed fields are written most significant bit first, oldest sample first.

Use the browser-based Telemetry history decoder to turn a reply into a timestamped table or downloadable CSV without uploading the data.

telemetry.tx is disabled by default. Its enabled state and cadence are stored separately from the shared data.tx path and region. data.tx defaults to zero-hop direct plus region=auto, but it does not enable telemetry or any other producer. For compatibility, set telemetry.tx direct|path updates the shared path and enables the default 2d schedule; new configuration should use set data.tx path ... followed by set telemetry.tx schedule .... An automatic run waits until 165 half-hour positions are available, then sends one maximum-size temperature packet, one maximum-size battery-voltage packet, and up to three IVB1 packets for every populated I2C voltage channel, plus the collected noise-floor snapshots. GPS is never included in this raw transmission. The history remains boot-local, so the first scheduled run after a reboot needs about 82.5 hours to fill. On the default two-day interval, each 82.5-hour temperature or battery packet overlaps its predecessor by 34.5 hours. Packets are paced two seconds apart. A queue failure is retried after 30 minutes only for the packet that did not queue; successfully queued packets are not duplicated.

send telemetry.tx now is an administrator-only test action. It queues temperature and battery-voltage snapshots plus the available I2C voltage chunks over the configured path. It also queues one main-radio N1B1 packet once a noise bucket has completed and one N2B1 packet when radio2 is enabled and has a completed bucket. Missing positions are included; an all-Missing history is still sent. It uses all currently available base positions up to 165 and all available I2C positions up to 192 per populated channel, plus the latest 255 positions per noise profile (63 hours 45 minutes). It works while the schedule is off and does not move the next scheduled send time. RAW_CUSTOM direct packets do not enter the normal encrypted direct-message retry mechanism.

Full temperature and battery snapshots are 184-byte RAW_CUSTOM payloads. Manual tests can be shorter while history fills. Both formats are binary, not Base64 and not encrypted:

Bytes Meaning 0-3 ASCII magic TTB1 for temperature or TVB1 for voltage 4-11 First eight bytes of the repeater public key 12-15 First sample UTC epoch, unsigned 32-bit little-endian 16-17 Sample interval in minutes, unsigned 16-bit little-endian (30) 18 Sample count (1-165) 19 onward Oldest-to-newest encoded samples for the selected series

Temperature codes reserve 0 for no reading, 1 for below -50 C, and 2 for above +77 C. Codes 3 through 130 represent exact whole degrees from -50 C through +77 C; decode them as code - 53. Voltage codes use the same encoding as the paged voltage payload documented below.

Noise snapshots reuse this 19-byte telemetry header. Their magic is N1B1 for the main radio or N2B1 for radio2; the interval is 15, the count is 1-255, and data begins at byte 19. Samples are packed 5-bit codes, oldest first and most significant bit first, with zero padding in the last byte. The payload is 19 + ceil(count * 5 / 8) bytes: 179 bytes for 255 positions. No GPS coordinates are included in a noise packet. Noise packets are additional RAW_CUSTOM telemetry snapshots, not a new MeshCore packet type.

External voltage snapshots use a separate packed layout. A full four-day channel takes three 140-byte IVB1 payloads of 64 points each:

Bytes Meaning 0-3 ASCII magic IVB1 4-11 First eight bytes of the source public key 12-15 First sample UTC epoch, unsigned 32-bit little-endian 16-17 Sample interval in minutes, unsigned 16-bit little-endian (30) 18 Cayenne LPP voltage channel 19 Sample count (1-64) 20 onward Oldest-to-newest packed 15-bit voltage codes

Code 0 means missing or disconnected. Codes 1-32767 represent 0.02 V through 655.34 V in 0.02 V steps; decode millivolts as code * 20.

Match bytes 4-11 to the first 16 hex characters of the repeater public key shown by its advert or get public.key. This compact identifier is useful for association but is not authenticated and can be spoofed. An MQTT observer can upload the received raw packet to LetsMesh Analyzer /packets; its packet hex contains the direct-route header and path followed by this payload.

Temperature payload (0x11, 61 bytes):

Bytes Meaning 0 Format/type 0x11 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7-18 48 packed 2-bit temperature statuses 19-60 48 packed 7-bit temperatures

Temperature status codes are 0 none, 1 value, 2 below range, and 3 above range. For status 1, the 7-bit temperature is an exact whole-degree integer from -50 C through +77 C; decode it as code - 50. Low values use code 0, and high values use code 127. The separate status map is required because 7 bits contain exactly 128 codes, leaving no spare code for none, low, or high when the complete range is represented at 1 C resolution. No fractional temperature is stored or transmitted.

Voltage payload (0x12, 55 bytes):

Bytes Meaning 0 Format/type 0x12 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7-54 48 8-bit voltage codes

Voltage codes reserve 0 for no reading, 1 for below 1.88 V, and 255 for above 4.40 V. Codes 2-254 represent 1.88 V through 4.40 V in 0.01 V steps; decode millivolts as 1880 + (code - 2) * 10.

External I2C voltage payload (0x14, 98 bytes):

Bytes Meaning 0 Format/type 0x14 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7 Cayenne LPP voltage channel 8-97 48 packed 15-bit voltage codes

The codes have the same 0.02 V through 655.34 V meaning as IVB1.

Noise-floor payload (0x15 main radio, 0x16 radio2; up to 117 bytes):

Bytes Meaning 0 Format/type 0x15 or 0x16 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (15) 6 Sample count (1-176) 7 onward Oldest-to-newest packed 5-bit noise-floor codes

The same codes are used by N1B1 and N2B1:

Code Meaning 0 -120 dBm (also clamps lower values) 1 -119 dBm 2-29 -118 through -64 dBm, in 2 dB steps 30 MAX: above -64 dBm 31 Missing: fewer than three fresh, valid estimates

The last byte's unused low bits are zero. A full page encodes as 156 Base64 characters, plus the existing > reply prefix. The decoder keeps radio1 and radio2 separate and rejects wrong lengths, intervals, or padding. MAX is a status, not an invented numeric -62 dBm reading.

When a Companion CLI command includes its three-character correlation prefix (for example 01|), noise replies retain that prefix and omit the > prompt. A full 01|Base64 response then fits in 160 bytes including its terminating NUL. The decoder accepts either reply style.

GPS payload (0x13, 101 bytes):

Bytes Meaning 0 Format/type 0x13 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (24) 7-10 Page origin latitude in signed degrees times 10^7 11-14 Page origin longitude in signed degrees times 10^7 15 Origin sample index, or 255 when the page has no GPS fix 16 Flags; bit 0 means at least one differential was clipped 17-100 24 records: signed 14-bit north then signed 14-bit east

GPS differentials use signed 14-bit two's-complement values at 10-meter resolution and are applied to the preceding decoded valid point. The origin sample begins at the header coordinates. A no-fix slot encodes 0,0 and does not advance the reference; a stationary valid fix also quantizes to 0,0. When a page has no fixes, its origin is 0,0, origin index is 255, and all differentials are 0,0. Values outside -8192 through 8191 are clipped and set flag bit 0.

"},{"location":"cli_commands/#set-companion-tx-routing-per-contact-or-channel","title":"Set Companion TX routing per contact or channel","text":"
set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nget tx.channel 0\nset tx.channel #wardriving radio\n

Both command families accept auto, radio, radio2, both, or off. auto uses the global crossing rules. Explicit choices override radio2.cross, but radio2 must still be enabled in rxtx to transmit there. radio2 never falls back to the primary profile; both uses whichever of its selected profiles can transmit. Set a target back to auto to remove its override.

Use an exact contact/channel name, a zero-based channel index, or key:<12-64 hex digits> for a unique contact public-key prefix. Duplicate names/prefixes are rejected. get tx.user and get tx.channel show override counts; queries with a target show its saved choice and current effective profile selection. Choices are saved with the contact/channel and work for phone-app and terminal messages. A contact's off setting also blocks its ACKs and addressed responses.

See Companion routing details and examples for temporary profiles, persistence, and the traffic covered by each setting.

"},{"location":"cli_commands/#companion-meshtext-rebroadcasting","title":"Companion Meshtext rebroadcasting","text":"
get mt.rebroadcast\nset mt.rebroadcast on|off\n

Choose on or off after set. This saved option defaults to off and is also shown in get mt.status. Only broadcast text successfully decoded with a configured channel key can be relayed; unknown-key, non-text and direct messages are excluded. Relays wait five additional actual packet airtimes plus flood jitter, on top of existing radio waits. Hearing another forwarded copy cancels the pending relay. mt.maybe_rebroadcast is an equivalent alias. See optional decoded-text rebroadcasting for hop limits, cancellation and queue behavior.

"},{"location":"cli_commands/#set-companion-media-and-ssh-application-tx-profiles","title":"Set Companion media and SSH application TX profiles","text":"
get tx.media\nset tx.media auto|radio|radio2|both|off\nget tx.ssh\nset tx.ssh auto|radio|radio2|both|off\n

Choose one value after set. Both default to auto and are saved across reboot. Explicit choices override a channel route for the matching application data only; they do not change ordinary text, adverts or contact ACKs. tx.media auto selects only the active rxtx MeshCore second profile, including tempradio2, when available. Otherwise, it uses the normal channel/global route. tx.ssh auto keeps that normal route. radio2 requires an active rxtx MeshCore profile and never falls back to the primary. both uses the available profiles; off blocks sending only. Queries include the effective active= selection and warn if radio2 is unavailable for TX.

Media covers Open TAK's binary photo/voice and fetch packets. SSH requires an application to opt into the new 0x5353 channel-data type or raw 53 53 48 01 envelope; existing untagged SSH/IP tunnel apps are not detected automatically. See the application routing contract for the exact framing, precedence and limits.

"},{"location":"cli_commands/#esp32-full-companion-ethernet-tunnel","title":"ESP32 Full Companion Ethernet tunnel","text":"
get ethernet\nget ethernet.radio\nset ethernet.radio <MHz>,<kHz>,<SF>,<CR>[,<preamble>]\nset ethernet on\nset ethernet off\n

This separate ethernet mode carries the entire raw Ethernet link over radio2, including SSH, while primary radio stays MeshCore. It defaults off, saves its settings, and requires an explicitly configured supported RF tuple. Supported ESP32 Full SX1262/SX1276 builds expose trusted-LAN KISS-over-TCP on station port 5003. It does not add an SSH server, use tx.ssh tags, or bridge Ethernet into MeshCore. Conflicting radio2 owners are not overwritten. See Ethernet setup, host commands and limits.

"},{"location":"cli_commands/#set-repeater-room-server-and-sensor-reply-profiles","title":"Set repeater, room-server, and sensor reply profiles","text":"
get tx.reply\nset tx.reply both\nset tx.reply both force\nset tx.reply radio\nset tx.reply auto\n

Persistent choices: auto, radio, radio2, both (default), and off. Explicit choices override radio2.cross for local replies, including CLI, ACKs, telemetry, room subscription posts, and served LoRa OTA responses. Append force to both or radio2 to permit reply TX on an active RX-only second profile. It never enables a disabled profile or ordinary forwarding. Omitting force removes that override. off suppresses remote replies too.

See reply routing and OTA examples for temporary profiles, receive-only exceptions, and queued reply handling.

"},{"location":"cli_commands/#set-companion-display-rotation","title":"Set Companion display rotation","text":"

SSD1306 Full Companion builds support a persisted runtime orientation:

get display.rotation\nset display.rotation 0\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\n

The values are clockwise degrees. 0 clears the override and restores the board's compiled default. Unsupported display drivers return an error.

"},{"location":"cli_commands/#select-rak-oled-controller","title":"Select RAK OLED controller","text":"

Display-capable RAK4631 and RAK3401 images with OLED controller detection support a saved selection in Companion, Repeater, Room Server and Sensor roles:

get display.controller\nset display.controller auto\nset display.controller ssd1306\nset display.controller sh1106\n

Choose auto for normal use. If a panel is shifted, wraps at its edge or is otherwise incorrectly detected, select its controller explicitly and restart the node. Changes are saved immediately and apply after restart; the current screen keeps its active controller until then. get display.controller reports the configured choice, active choice and whether a restart is needed. A custom firmware build is not needed to change this choice.

Automatic detection lets the same RAK4631 image serve WisMesh Pocket v2 and v3. Both controllers retain rotation support and use one 1 KiB framebuffer.

Detection checks the configured DISPLAY_ADDRESS (default 0x3C). Only repeated SH1106 status readings select that controller; ambiguous responses keep SSD1306, preserving compatibility with older RAK1921 panels. With no saved choice, the default is automatic detection unless the build sets OLED_CONTROLLER_OVERRIDE=1 (SSD1306) or =2 (SH1106). This build-time override remains a fallback for existing custom builds. Any saved selection takes precedence, including auto, which restores detection even when that fallback forces a controller.

See the Pocket v3 issue.

"},{"location":"cli_commands/#show-touchscreen-touch-areas","title":"Show touchscreen touch areas","text":"

All touchscreen Companion builds using the shared touch UI support temporary diagnostic outlines, including both SenseCAP Indicator rendering sizes:

get display.touch\nset display.touch on\nset display.touch off\n

The default is off at every boot. Enabling wakes the display without selecting anything. Dotted yellow borders mark the actual scaled tap targets; the area under a held finger turns green and returns to yellow on release. Disabling removes the borders on the next redraw. Pairing screens suppress the overlay while they consume navigation. This option does not change the font, touch calibration, navigation actions, or saved settings, and is not persisted.

Use the text terminal or Companion framed CLI (0x42) through an available USB, BLE, or TCP connection. Non-touchscreen, missing-display, and legacy UI builds report Error: touchscreen diagnostics unsupported. No Indicator-only build flag is needed.

"},{"location":"cli_commands/#set-display-modes-and-timeouts","title":"Set display modes and timeouts","text":"

Companion message sound, vibration, LED, screen and GPIO rules have separate CLI controls. See the notification builder and CLI reference, including per-contact/channel rules, trusted-contact/room !notify permission, and USB test controls.

Display-equipped Companions, repeaters, room servers, and sensors have four saved settings. The WebConfig Display card exposes the same controls:

Setting CLI Default Battery mode get/set display.mode button-pairing on BLE Companions; button elsewhere Battery timeout, seconds get/set display.timeout 900 on e-paper; 15 elsewhere USB mode get/set display.usb.mode Same as battery mode USB timeout, seconds get/set display.usb.timeout 900 on e-paper; 15 elsewhere

Modes are off, on, button, pairing, button-pairing, and automatic. Pairing modes are available only on builds supporting BLE pairing.

All display-equipped Companion builds show the screen at boot in the default button or button-pairing mode, on battery or USB power. It stays on for the selected profile's timeout (15 minutes on e-paper, 15 seconds elsewhere). Button input restarts that timer. This also applies to previously saved button modes. Saved off and pairing-only modes still suppress the boot screen. Repeaters, room servers, and sensors do not gain this Companion-only normal-UI boot wake.

On display-equipped Companions, repeaters, room servers, and sensors, a small Starting... screen appears as soon as the board, primary filesystem, and display are initialized, before radio initialization and serial-settle delays. If a new identity is needed, it changes to Generating key with an animated spinner during radio entropy collection. OLED/TFT updates are paced at 150 ms; e-paper uses a slower one-second cadence. Saved off and pairing-only modes still suppress this startup screen. The normal UI's boot timeout starts only when that UI is ready, so key generation does not consume it. Filesystem recovery and the display driver's own initialization can still delay the first visible frame; the firmware cannot draw before those operations finish.

Startup shows Loading identity while reading a saved identity. Generating key and its spinner are used only when creating a replacement/new identity; they end before flash persistence, which shows Saving identity. A normal boot with a valid saved identity never regenerates it. An erased filesystem does require a new identity unless one is restored.

On Full Companion screens, an unpaired Bluetooth PIN takes priority over a USB/TCP connected label. Button wake shows the PIN banner without requiring USB to be disconnected; a second navigation press can leave this preview. An actual phone pairing request wakes pairing-enabled display modes and keeps the banner visible until completion or its two-minute expiry. Bluetooth scanning alone is not a pairing request.

Timeouts are whole seconds from 1 to 3600. Use mode on for a permanent display rather than timeout 0. The timeout is retained but unused in off, on, and pairing modes. Settings apply immediately, including while blanked; changing power source applies the other profile without carrying over a wake from the previous profile. Settings survive reboot and filesystem-preserving updates in /display_prefs; erasing the filesystem restores the defaults. The 15-minute e-paper default applies to battery and USB profiles on all roles. Existing saved timeouts stay unchanged. To use 15 minutes on an existing node, set both display.timeout and display.usb.timeout to 900; choose a timed mode such as button or button-pairing if the current mode is on or off. An existing observer timeout that differs from its old 60-second default is migrated once to both power profiles; an old zero timeout becomes mode on.

set display.mode button-pairing\nset display.timeout 15\nset display.usb.mode on\nset display.usb.timeout 60\n

USB profile selection uses the board's external-power detector, with USB-host detection as a fallback. VBUS-capable boards detect chargers without a data connection. Heltec V4 R8 uses either a detected USB host or a calibrated battery voltage above 4.21 V. The voltage estimate is checked every five seconds and clears at 4.20 V or below; host detection takes effect immediately. This estimate can miss a charger while the battery is low, depends on ADC calibration, and cannot distinguish USB from solar charging. Other boards without a power detector may require a USB host to recognize USB power. Wi-Fi/MQTT connection or BLE pairing is not evidence of USB power. E-paper panels can retain their last image when powered off.

"},{"location":"cli_commands/#set-companion-inbox-behavior","title":"Set Companion inbox behavior","text":"

The shared Companion UI (including T114 and R8) defaults to History: the last 32 received text previews remain available on the radio after an app downloads them. Pending separately counts frames waiting for the app, including non-text frames. Downloading a frame clears it from Pending; it does not mean someone read it. The phone's delivery queue is independent of the 32-entry preview history and is never drained by reading on the radio.

get display.inbox\nset display.inbox history\nset display.inbox pending\nset display.inbox unread\n
Mode Display behavior history (default) Shows HISTORY with the retained preview count, plus Pending. Downloaded previews remain browseable. pending Shows INBOX with the app queue count and limits the reader to previews still waiting for the app. Non-text frames count toward Pending but have no text preview. unread Shows UNREAD with the number of retained previews not yet displayed on the radio, plus Pending. Opening a preview on a lit display marks it read; app downloads do not. Read previews remain browseable.

All modes retain up to 32 previews internally; switching from pending back to history makes retained downloaded previews visible again. Evicted or rejected queue entries are not shown as pending. Previews and local read state are in RAM and reset on reboot. The mode applies immediately and survives reboot in /display_prefs; existing version-1 display settings load with history as the default. Legacy and tiny Companion UIs report Error: inbox modes unsupported.

Incoming messages automatically open their preview only when no client is connected. While connected, new messages neither select another page nor move the preview being read; history stays accessible by button or touch. The home screen identifies connected USB and Bluetooth clients (or both), and TCP, Ethernet, or serial clients where supported. Actual BLE pairing prompts retain priority over connection status.

"},{"location":"cli_commands/#set-display-rotation","title":"Set display rotation","text":"

Supported Companion displays and new Heltec V4 R8 TFT Repeater, Room Server, and Sensor images accept a saved rotation:

get display.rotation\nset display.rotation 0\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\n

0 restores the board's compiled orientation. On the R8 TFT, the other values turn clockwise from that default. A quarter turn changes between portrait and landscape, and the observer dashboard adapts to the new screen dimensions. The change applies without rebooting. Changing rotation while the display is off leaves it off; the next wake applies the pending panel rotation without resetting the shared touch controller. WebConfig offers the same choices under Display \u2192 Rotation when the running image supports rotation.

The stock R8 Expansion Kit V2 does not include an accelerometer. Its GNSS and touch controllers do not measure the screen's physical orientation, so this firmware offers manual rotation rather than an automatic orientation option. Automatic orientation would require a supported motion sensor and its mounting orientation.

Companion saves this in its existing node preferences. Infrastructure roles use a separate checked settings file, leaving existing node/MQTT layouts compatible. Old published images retain their original supported commands.

"},{"location":"cli_commands/#set-mqtt-observer-display-flip","title":"Set MQTT observer display flip","text":"

Supported observer displays, including the R8 OLED and ST7789 panels, can also be turned 180 degrees relative to their compiled orientation:

get display.flip\nset display.flip off\nset display.flip on\n

0 and 1 are accepted aliases for off and on. This is intentionally different from display.rotation: observer display.flip is an additional relative 180-degree mounting choice. Use display.rotation to switch between portrait and landscape on new R8 TFT images. The value is harmless on an observer display driver that does not support flipping.

Flip survives reboot and firmware updates that preserve the filesystem; erasing flash restores the off default. The boot log reports the saved flip state on display-enabled observer builds.

"},{"location":"cli_commands/#logging","title":"Logging","text":"

Non-compact builds compiled with MESH_PACKET_LOGGING emit a RAW: line for received radio frames that reach the raw capture hook while usb.logging is on, including Full Companion. Frames rejected by the early route policy are not exposed to raw capture. RAW: contains the complete received frame as uppercase hexadecimal, not only its decoded payload. Packet records do not require verbose USB debug output. Keep the host reader attached and draining the serial port: capture is best effort, not durable storage. Native-USB transports use bounded queues and drop records rather than block radio processing when the host stops reading; DROP:<count> markers report capture loss when output capacity returns.

Successfully parsed received frames also emit the decoded RX summary, including signal, timing, hash, type, route, and payload information. Frames that cannot be decoded, or cannot obtain a packet-pool slot, can still emit their RAW: line. Transmitted packets emit the decoded TX summary. RAW and RX summary are separate records, not an atomic pair; a queue drop or disconnect can leave a summary without its RAW bytes. USB log consumers must not associate a stale RAW record with the next unrelated summary. Size-constrained MESH_PACKET_LOGGING_COMPACT images instead use R<hex> and T<hex> records; that compact stream is not the full ASCII RAW/summary format expected by ordinary USB-to-MQTT parsers.

Ordinary non-OTA artifacts compile packet logging into the canonical image and control its live USB output at runtime; no separate -logging- artifact is emitted. Canonical RAK3401/RAK4631 repeater, room and sensor OTA pairs, plus Tower SD repeater and Tower room pairs, also retain packet logging in both Full-sensor and Reduced-sensor artifacts. Verbose debug stays off by default; get/set usb.logging controls packet output. Their capability manifests must verify both logging.usb.packets and logging.usb.control in the packaged application. Specialized storage/transport aliases and other space-constrained OTA-only targets retain their target-specific logging policy.

For other roles, use the separately named -ota- artifact when required. A -full-usb-wifi-ota- artifact combines USB packet logging, direct WiFi MQTT, LoRa OTA, and the expanded FULL feature set. A -full-logging-ota- artifact is emitted only when that hardware/role has no matching WiFi MQTT environment.

"},{"location":"cli_commands/#control-live-usb-logging","title":"Control live USB logging","text":"

Search terms: serial logging, USB debug log, enable logging, disable logging, debug output.

Usage:

get usb.logging\nset usb.logging on\nset usb.logging off\nset usb.logging on reboot\nset usb.logging off reboot\nget usb.debug\nset usb.debug on\nset usb.debug off\n

Current source also provides get usb.watchdog and saved set usb.watchdog off|on|auto. Missing watchdog state defaults to Auto, which requires 14 continuous healthy logging-client days before saving On. Existing five-minute USB stats polls confirm recent logging-client activity; a plugged cable/open TTY alone does not. See the USB logging watchdog for staged five-minute USB recovery, physical-fault-only MCU resets, persisted backoff, status fields, and safe shutdown deferrals. Older release binaries need an upgrade before these commands exist.

get usb.watchdog.last shows the latest USB watchdog event: recovery action, fault-reason bitmask, advisory node-clock epoch, uptime at the event, sequence, and whether the record passed durable readback. It survives reboot on durable storage. reboot-requested records intent, not proof that a physical reset occurred; a later safety veto records reboot-cancelled. See the watchdog documentation for storage and timestamp limitations.

ESP32 1.17.1.5 USB logging procedure: disable device sleep before enabling the log stream, using separate text commands:

set powersaving off\nset usb.logging on\n

Verify with get powersaving (expect off) and get usb.logging (expect on). Both settings are saved; disabling logging later does not restore power saving. This works around the released USB sleep bug. nRF52 does not need this ESP32 workaround.

These commands are compiled into ordinary USB-loggable artifacts and every Full Companion. usb.logging is the saved USB output master and packet-log gate. usb.debug is a separate saved preference for verbose diagnostics; effective USB debug output requires both switches to be on. Packet RAW and RX/TX summaries require only the master switch. get usb.debug reports saved intent even when the master is off. Turning usb.logging off does not erase that intent; turning it back on restores debug output if usb.debug is on.

CommonCLI roles save these settings in /com_prefs; their fresh USB logging default is on. Full Companion and ordinary USB Companion start with the master off on a fresh installation so diagnostics cannot corrupt framed traffic. Verbose USB debug defaults to off for every role, including when loading older preferences without a debug field. Updates retain an existing saved usb.logging choice. Older binaries without the new usb.debug command retain their previous behavior until upgraded.

For packet-only capture, use these separate text commands:

set usb.debug off\nset usb.logging on\n

Use set usb.debug on temporarily when troubleshooting, then turn it off again to reduce USB traffic. This setting applies immediately and needs no USB-interface reboot. It does not select a firmware variant or enable code omitted from the image: verbose output still requires compiled MESH_DEBUG or other relevant diagnostic support. A packet-only build can retain the preference without gaining missing debug messages.

Debug off suppresses verbose diagnostic chatter, not functional CLI replies, USB port identity/drop markers, or retained operational warnings. Readers must still filter non-packet lines instead of treating every line as a packet.

On Full Companion these lines belong to its text terminal, not meshcli's Binary get/set parameter namespace. Open interface 00, send +++MESHCORE-TERM-START, and then issue the command. Running meshcli ... get usb.logging or get usb.debug directly can instead return Unknown var ... because that is a different protocol operation.

nRF52 Full Companion defaults logging to off and enumerates only USB interface 00, which carries Companion, terminal, and serial mOTA traffic. Enabling logging adds interface 02, its dedicated plaintext logging port, on the next boot. Disabling it removes interface 02 on the next boot. A command without the optional reboot argument saves the choice and reports that a reboot is required when the USB interface count must change. The exact set usb.logging on reboot and set usb.logging off reboot forms save the choice, send their reply, and reboot one second later only when needed. Interface 02 is logging-only, not an input-capable CLI. Send logging/debug commands to interface 00; USB debug uses the same log endpoint as packets.

On every ESP32 Full Companion, enter the USB text terminal and use set usb.logging on (preceded by set powersaving off on 1.17.1.5) to turn that TTY into a logging-repeater-style plaintext stream. Framed Binary Companion is unavailable on USB while logging is on. The TTY remains an input-capable CLI, so set usb.logging off works on the same TTY. After its reply, logging stops and the TTY remains in the normal ASCII terminal, just as it does after a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion. No reboot is needed because the USB interface count does not change.

Turning USB logging off does not disable CLI replies. nRF52 keeps Companion frames active on interface 00; ESP32 resumes the ordinary ASCII/Binary switcher after the logging terminal turns logging off. This setting does not change the node-storage capture controlled by log start and log stop.

For a USB-to-MQTT bridge, enable packet logging but leave usb.debug off. Full Companion now supplies the ASCII RAW/RX/TX packet stream needed by such consumers; it does not provide durable or lossless capture. A compatible host must also support the role's identity/control commands and replies: packet records alone do not make an existing bridge compatible. Companion now answers get public.key with its node public key. ESP32 Full's logging TTY remains input-capable and accepts that query. nRF52 Full's dedicated logging CDC is not a command/reply console (its bounded stats-poll input only renews the watchdog client lease): a host integration must use interface 00 for commands and identity and interface 02 for logs. Existing single-port bridges that require CLI replies cannot simply point at the dedicated log endpoint. This change does not add a two-port bridge driver.

Companion, Repeater, Room Server, and Sensor builds with both MQTT and USB logging provide the same saved selector for both output paths:

get logging.output\nset logging.output off\nset logging.output usb\nset logging.output wifi\nset logging.output both\n

usb emits RAW: packets for a USB-connected service such as meshcoretomqtt. wifi enables the direct MQTT bridge configured by the wifi.* and mqtt.* commands. both intentionally duplicates the radio stream to both consumers; do not point both consumers at the same broker unless the downstream setup deduplicates messages. Fresh unified Full infrastructure installs start in both mode; Full Companion starts with USB logging off. To change only MQTT, use set mqtt.enabled on|off; get mqtt.enabled checks the saved switch and get mqtt.running checks whether the MQTT service is running. Turning MQTT off preserves all broker slots and credentials. The output selector changes the USB master, not saved usb.debug intent; WiFi MQTT logging is independent of the USB debug preference.

On 1.17.1.5 ESP32, run set powersaving off before selecting set logging.output usb or set logging.output both, since those modes enable USB logging. WiFi/MQTT-only logging does not need this workaround while the Repeater/Room Server MQTT bridge is running; the released code already blocks device sleep in that state. Check get mqtt.running, not only get mqtt.enabled. WiFi modem power saving is a separate setting.

"},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"

Search terms: save logs, record received packets, RX logging, stored packet log.

The constrained wio-e5-mini_repeater omits node-storage packet logging: log start, log stop, log erase and log are unavailable. Live USB/MQTT packet logging, set usb.logging on|off and statistics remain. Existing saved log files are not automatically deleted.

Usage: log start

"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"

Usage: log stop

"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"

Usage: log erase

"},{"location":"cli_commands/#print-the-captured-log-to-the-requesting-terminal","title":"Print the captured log to the requesting terminal","text":"

Usage: log

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"

Usage: ver

On Full Companion, the Binary device-info frame retains its legacy fixed-width version field. Use version in the text terminal, or send version through Binary protocol command 0x42 (CMD_RUN_CLI_COMMAND), to read the complete untruncated firmware string together with protocol and build date.

"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"

Usage: board

"},{"location":"cli_commands/#show-the-storage-layout","title":"Show the storage layout","text":"

Usage: get storage.layout

Reports a compact, read-only summary of the storage compiled into the running firmware:

The command does not write, format, or resize storage. A raw-QSPI or SD build may briefly probe, wake, or mount its configured external storage to read its capacity. Remote use follows the existing administrator-command permissions.

"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#select-mcu-and-lora-regulator-modes","title":"Select MCU and LoRa regulator modes","text":"
get mcu.regulator\nset mcu.regulator auto|dcdc|ldo\nget radio.regulator\nset radio.regulator auto|dcdc|ldo\nreboot\n

These are independent controls. auto follows the board's existing default; dcdc requests its switching regulator and ldo requests its linear regulator. Settings are saved for the next reboot. The getter reports the saved choice, active mode, automatic fallback, crash streak and whether recovery is available. Setting a mode again, including the same value, clears that regulator's learned fallback and crash streak. The other regulator keeps its settings.

Choices depend on the board's verified circuit. MCU selection is available on nRF52 boards that already initialize their supported REG1 DC-DC regulator; it does not control REG0, an external PMIC, or ESP32/RP2040 processor regulators. Radio selection covers supported SX1262, LR1110/LR1121 and Meshnology W12 LR2021 implementations. A board with only verified LDO support rejects dcdc. Unverified LR2021 switching-regulator circuits retain their existing mode.

On platforms with retained crash recovery, three consecutive qualifying crashes while a domain is using DC-DC select LDO for that domain on the next boot. Each crash requires a valid last battery sample of at least 50%, no more than 60 seconds old at the last heartbeat. Low/unknown battery readings, stale samples, ordinary reboots, sleep wakes and unknown reset causes break the streak. Five minutes of healthy operation also clears the streak; a later crash starts a new one. LDO fallback stays selected until a mode is explicitly set again; recovery never switches back to DC-DC automatically.

ESP32 WebConfig shows Power regulators under Radio \u2192 Advanced when supported, with separate processor/radio choices and active/fallback status. Only supported options are offered. Clear fallback on restart reapplies the current choice through the normal save flow. Saving either control restarts the node after the save succeeds. See regulator recovery and hardware checks.

"},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"

Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>

Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz. Most targets allow 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500. LR1110 targets allow 62.5, 125, 250, 500. - sf: Spreading factor (5-12) - cr: Coding rate (5-8)

Set by build flag: LORA_FREQ, LORA_BW, LORA_SF, LORA_CR

Default: 869.618,62.5,8,5

get radio returns exactly four comma-separated fields for compatibility with existing Companion apps. On dual-radio builds, use get radio2 and get radio.timing for profile and timing diagnostics.

Note: Requires reboot to apply. If RXPS is enabled and the saved minimum level/preamble cannot safely cover the new radio timing, the command reply reports the effective level and preamble, or RXPS continuous-fast when no level through 10 is safe. Slower settings recalculate from the saved RXPS minimum and return to it exactly when it is safe.

"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"

Search terms: tx power, transmit strength, radio output power, dBm.

Usage: - get tx - set tx <dbm>

Parameters: - dbm: Requested radio-chip power in dBm. The valid range depends on the radio family, selected PA path, and any board-specific external-PA limit.

Set by build flag: LORA_TX_POWER

Default: Varies by board

Notes: This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. Setting a value too high may violate the laws in your country.

The command strictly rejects malformed values and saves the new preference only after the active radio driver accepts it. On LR2021, the chip range is -9 to 22 dBm below 1500 MHz and -19 to 12 dBm above 1500 MHz. A lower board-specific external-PA limit still takes precedence. A saved profile is clamped to the applicable limit when its frequency changes; temporary radio settings use a safe effective power without replacing the saved preference.

"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"

Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins> - normalradio

Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)

Notes: - tempradio is not saved to preferences and clears on reboot. - On repeaters, remote tempradio and normalradio wait for their exact reply copies to finish, with at least one transmitted. This also covers the set alias, case-insensitive verbs, and client correlation prefixes. Failed replies cancel an unconfirmed temporary handoff; hard lease expiry still takes priority. - normalradio cancels pending and active temporary-radio windows, then restores the saved radio tuple after its CLI reply has drained on the current channel. Permanent radioat entries are not removed. - On repeaters, the reply includes total minutes plus NdNhNm (days, hours, minutes) and the watchdog policy for that session. For example:

OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours\nOK - temp params for 3075 mins (2d3h15m); rx.watchdog=12hours\nOK - temp params for 180 mins (0d3h0m); rx.watchdog=off (temp<12hours)\n

Repeater timing overrides apply only while a temporary window is running:

Temporary duration No-RX reboot watchdog Flood advert interval Direct/local advert interval Up to and including 3 hours Off Saved normal interval 1 hour More than 3, less than 12 hours Off 3 hours 1 hour 12 hours or longer 12 hours 3 hours 1 hour

The duration is the requested window length, not the decreasing time remaining. The first temporary adverts are scheduled from successful radio activation; hourly local adverts still run when a flood advert is also due. get advert.interval and get flood.advert.interval report the effective intervals. The web configuration form continues to show and edit the saved normal advert settings. These include any changes made during a temporary session. Expiry, cancellation, reboot, or power loss removes the temporary overrides. Normal mode uses the saved advert intervals and the 24-hour watchdog policy. An explicitly saved normal-mode rx.watchdog off remains off outside temporary sessions.

"},{"location":"cli_commands/#schedule-radio-parameter-changes","title":"Schedule radio parameter changes","text":"

Usage: - set radioat <freq>,<bw>,<sf>,<cr>,<start_time> - get radioat [n|all] - del radioat [n|all] - set tempradioat <freq>,<bw>,<sf>,<cr>,<start_time>,<end_time> - get tempradioat [n|all] - del tempradioat [n|all]

Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - start_time: UTC Unix epoch seconds, or +N for N whole minutes from now - end_time: UTC Unix epoch seconds, or +N for N whole minutes from now - n: Scheduled entry number from get radioat or get tempradioat

Notes: - Both relative endpoints use the same command-time snapshot of the node clock. For example, set tempradioat 910.5,500,8,5,+5,+15 starts in 5 minutes and reverts in 15 minutes (10 minutes active). set radioat 910.5,500,8,5,+5 schedules a saved change in 5 minutes. Bare numbers remain absolute timestamps; absolute and relative endpoints can be mixed. Relative offsets must be positive. The existing scheduler and clock behavior are unchanged. - get radioat and get tempradioat list all entries when n is omitted. - del radioat and del tempradioat delete all entries when n is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - radioat saves the new radio preferences when it fires. tempradioat applies temporarily, then reverts to the saved radio preferences. - tempradioat uses the repeater timing table above, with duration calculated from end_time - start_time. Its acceptance reply includes total minutes, NdNhNm, and the planned rx.watchdog setting. Queuing a future entry does not change the running watchdog or advert intervals; the overrides start only after the temporary radio parameters are successfully applied. If restoration needs retries after expiry, the timing overrides still end with the window. - A successful scheduling reply notes any RXPS effective-level/preamble change required by the scheduled tuple. RXPS continuous-fast means the tuple is accepted but will use continuous receive because no safe duty-cycle level is available. Returning to slower settings recalculates from the saved RXPS minimum.

With the measured RXPS model and the default 1600 us TCXO startup delay (2600 us total transition), a requested minimum level 1 gives:

SF BW (kHz) Wire preamble Effective timing preamble Effective RXPS level RX / sleep 7 500 32 32 2 5594 / 3022 us 6 250 32 32 2 5844 / 3022 us 5 125 32 32 2 5844 / 3022 us 5 250 64 64 1 3829 / 5100 us 6 500 64 64 1 3829 / 5100 us 5 500 128 128 1 2563 / 6647 us 5 62.5 32 16 6 11922 / 2975 us

These values include the actual transmitted preamble, explicit header, and a one-symbol timer margin. SF5/SF6 need two more sync symbols than SF7, so their receive windows differ even when symbol duration and sleep time match.

The wire preamble remains compatible with the existing Cascade convention: 32 symbols at SF5-SF8, extended to 64 for SF5/BW250 and SF6/BW500, or 128 for SF5/BW500; SF9-SF12 use 16. Local RXPS settings and oscillator delays never change the sender's preamble choice. The receiver may use a shorter catch assumption, but its header timeout covers the full transmitted preamble.

Automatic adjustment stops at guarded level 8. If no guarded timing fits, the radio receives continuously until the tuple changes. For example, a board configured with the previous 5000 us TCXO delay uses continuous RX at SF7/BW500; it cannot fit the new guard within that tuple's 32-symbol wire preamble. Experimental levels 9 and 10 require an explicit selection.

Earlier firmware passed 16/16 packets in each direction between Heltec V4 and WisMesh Tag at SF5/BW250/64 and SF5/BW500/128, CR5, 909.950 MHz. That result established the existing wire-preamble convention; it does not validate the new timing model. All senders to an RXPS receiver must use a compatible wire preamble. LoRa OTA treats v1.17.1.5 as that capability boundary.

"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"

Search terms: radio frequency, LoRa frequency, MHz.

Usage: - get freq - set freq <frequency>

Parameters: - frequency: Frequency in MHz

Default: 869.618

Note: Requires reboot to apply

Connections: USB, browser/TCP/Ethernet CLI, Companion binary command 0x42 (USB/BLE/TCP), and authorized LoRa CLI. set freq saves the frequency; bandwidth, spreading factor, and coding rate stay as configured. Infrastructure roles also clamp transmit power to the board's limit for that frequency. Like set radio, it requires permission to configure the destination node.

"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"

Search terms: RX boost, receive gain, receiver sensitivity, boosted reception.

Usage: - get radio.rxgain - set radio.rxgain <state>

Parameters: - state: on|off

Default: Target-specific. Most SX1262 and LR1110 targets default to on; Station G2/G3 targets default to off.

Notes: - The saved setting is applied immediately and persists across reboots. - Periodic AGC resets restore the saved runtime setting; they do not replace it with the target's compile-time default. - Existing installations retain their previously saved value after an upgrade.

"},{"location":"cli_commands/#view-or-change-rx-duty-cycle-power-saving","title":"View or change RX duty-cycle power saving","text":"

Search terms: RX power saving, receiver sleep, radio power saving, RXPS.

Usage: - get radio.rxps - get radio.rxps.config - get radio.rxps.rfrx_disabled - get rxps.wd - set radio.rxps.rfrx_disabled <state> - set radio.rxps off - set radio.rxps on - set radio.rxps conservative - set radio.rxps balanced - set radio.rxps max - set radio.rxps max preamble <16|32> - set radio.rxps overdrive - set radio.rxps overdrive preamble <16|32> - set radio.rxps riskyWorkingMax - set radio.rxps riskyWorkingMax preamble <16|32> - set radio.rxps <1-10> - set radio.rxps level <1-10> - set radio.rxps level <1-10> preamble <16|32> - set radio.rxps <rx_us> <sleep_us>

Parameters: - rx_us, sleep_us: Receive and sleep durations in microseconds (1000-30000000). - level: Guarded levels 1-8, followed by experimental levels 9 and 10. Higher guarded levels allow more sleep. - preamble: RXPS timing assumption in symbols; 16 or 32. This does not change the radio's actual transmitted preamble. - state: on or off.

Notes: - get rxps.wd reports the radio watchdogs' soft and hard recovery counts. See receive calibration and recovery for noise sampling, CAD handling, and the continuous-RX mode check. - get radio.rxps.config adds the persisted level and preamble assumption to the on/off and timing values. Deployment tools use it to restore a level-based preference without converting it to fixed manual timings. - radio.rxps.rfrx_disabled is a runtime-only diagnostic setting and resets to off after reboot. - Its default off state keeps the host-controlled SX1262 receive path enabled during RX duty-cycle mode. Setting it to on reproduces the old missing-RF_RX behavior and can significantly reduce receive sensitivity, making remote commands harder to receive. - radio.rxps.rfrx_disabled is supported only on SX1262 targets with a host-controlled RX enable pin. - on and conservative select level 3, balanced selects level 6, and max selects level 8; each named profile assumes a 16-symbol preamble. - The guarded catch ladders for levels 1-8 are 15,14,13,12,11,10,9,8 symbols for preamble 16 and 24,20,16,14,12,10,9,8 for preamble 32 or longer. Generated sleep leaves another 0.2 symbols of margin. The receive timer covers the actual wire preamble, sync, explicit header, and one extra symbol. - overdrive selects level 9; riskyWorkingMax selects level 10. Both operate outside the guarded timer condition. Upstream bench measurements found packet loss at level 10; neither profile is a general guarantee of reliable delivery. They are never selected by automatic level adjustment. - Named experimental profiles accept preamble 32; numeric levels keep the existing automatic-preamble selection. Manual timings bypass the level model. - MC_TCXO_DELAY_US defaults to 1600 us. Initialization applies it and checks TCXO/calibration errors. A board can override it for its oscillator. - Fresh Cascade-profile builds start with RXPS on at level 8 and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom <rx_us> <sleep_us> timings remain fixed. - get radio.rxps keeps the legacy on/off, RX, and sleep reply. The new get radio.rxps.config reply adds the saved level and preamble assumption. Radio-change replies and the Full Companion terminal status report any effective level/preamble adjustment. Effective preamble 64 or 128 appears only when the active tuple's physical wire preamble has that length. - The selected mode is applied immediately, persisted, and restored after reboot.

"},{"location":"cli_commands/#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM receive-path gain state on supported boards","text":"

Usage: - get radio.fem.rxgain - set radio.fem.rxgain <state>

Parameters: - state: on|off

Notes: - This controls the external LoRa FEM receive-path LNA where the board supports it. - This is separate from radio.rxgain, which controls the radio chip receive gain mode. - On Station G3, transmit temporarily bypasses the LNA. A gain change made during transmission is applied when receive mode resumes; get reports the configured receive preference. Startup and terminal sleep keep the LNA off.

"},{"location":"cli_commands/#view-or-change-the-lora-fem-transmit-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM transmit-path gain state on supported boards","text":"

Usage: - get radio.fem.txgain - set radio.fem.txgain <state>

Parameters: - state: on|off

Notes: - This controls a software-selectable external LoRa FEM transmit gain where the board supports it. - Heltec T096 routes TX through its KCT8103L PA with no populated TX bypass. get radio.fem.txgain reports on (fixed TX PA); set radio.fem.txgain cannot toggle it. Use set tx <dBm> to adjust SX1262 drive. The PA's CSD control shuts down the FEM and is not a lower-gain TX mode. See the T096 schematic and KCT8103L product description. - On Station G3, remove the PA PL1 jumper to allow software control. on selects PA PL1 high/short and off selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. - Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. - The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. get reports the configured state, which may lead the hardware until the node next transmits.

"},{"location":"cli_commands/#select-the-station-g3-physical-radio-slot","title":"Select the Station G3 physical radio slot","text":"
get radio.slot\nset radio.slot auto\nset radio.slot 1\nset radio.slot 2\nreboot\n

Station G3 SX1262 builds support both physical RF sockets in the same image. The default, auto, probes both sockets at boot without transmitting. If both work, it prefers the last successfully used socket; on a fresh installation, it prefers socket 1. A forced 1 or 2 never silently uses the other socket.

get radio.slot reports the saved selection, active socket, detected sockets, and whether a reboot is needed. Changes are saved immediately but applied only after reboot, never during a transmission. The selector does not change keys, node settings, or the time-shared radio2 profile. The legacy _r2 build name means physical socket 2, not a motherboard revision; both build names now support this selector.

If no usable socket is found, startup waits with both LNAs disabled instead of reboot-looping. A limited USB recovery console accepts !g3 get radio.slot, !g3 set radio.slot auto|1|2, !g3 reboot, and !g3 help. The !g3 prefix is required in this recovery console; it is not the normal running CLI. KISS firmware uses binary slot commands during normal operation, documented in the KISS protocol.

If G2 firmware works on a G3 but the G3 image behaves differently, see the G2/G3 compatibility checks for the verified pin assignments, startup fixes, and saved amplifier settings.

"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"

Usage: - get name - set name <name>

Parameters: - name: Node name

Set by build flag: ADVERT_NAME

Default: Varies by board

Note: Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.

"},{"location":"cli_commands/#turn-companion-sound-and-vibration-on-or-off","title":"Turn Companion sound and vibration on or off","text":"

get buzzer and set buzzer on|off control the Companion's buzzer. Devices with a vibration motor also support get vibration and set vibration on|off. Changes take effect immediately, are saved across reboots, and produce no confirmation tone. Muting an output stops an alert already playing.

Fresh settings default to sound off and vibration on. Existing saved choices are retained. The same preferences are used by the device buttons and UI. Unsupported hardware reports an error.

These are the device mute controls. Programmable notify.sound and notify.vibration rules also respect them; an enabled rule cannot bypass a muted output. The notification output masks still select which alert outputs a rule may use. Explicit button mute/unmute confirmations retain their existing behavior.

Adapted from upstream PR #3603.

"},{"location":"cli_commands/#turn-indicator-leds-on-or-off","title":"Turn indicator LEDs on or off","text":"
get leds\nset leds off\nset leds on\n

Available in Companion, repeater, room-server, and sensor firmware. The setting is saved across reboots and defaults to on. off suppresses software-controlled transmit, Bluetooth, status, and notification LEDs. on restores their normal behavior; it does not leave every LED lit. A build with DISABLE_BLE_LED keeps the Bluetooth indicator disabled.

Supported outputs include ordinary and inverted GPIO LEDs, addressable RGB LEDs, and board-controlled expander LEDs on nRF52, ESP32, RP2040, and STM32 boards. Screen backlights and manually controlled torches keep their own controls. Power and charging LEDs wired directly to a charger cannot be switched off by firmware. Bootloader and early startup indicators may still appear before the saved setting is loaded.

This extends the SenseCAP Solar LED switch from upstream PR #3605. With LEDs enabled, RAK-family Companions blink their Bluetooth LED while advertising and leave it off once connected, following upstream PR #2694.

"},{"location":"cli_commands/#turn-wifi-esp-now-or-all-24-ghz-services-on-or-off","title":"Turn WiFi, ESP-NOW, or all 2.4 GHz services on or off","text":"
get wifi\nset wifi off\nset wifi on\nget espnow\nset espnow off\nset espnow on\nget 2.4ghz\nset 2.4ghz off\nset 2.4ghz on\nset 2.4ghz on all\n
Control Effect wifi Infrastructure WiFi, setup AP, TCP/WebUI, and WiFi MQTT services. ESP-NOW remains available. espnow The ESP-NOW bridge or primary ESP-NOW mesh transport in this build. Infrastructure WiFi remains available. 2.4ghz off Stop WiFi, Bluetooth, and ESP-NOW; remember which were enabled. LoRa and wired transports remain available. 2.4ghz on Restore the states saved by the first off. Repeated off preserves that snapshot. With no previous off, keep the current states. 2.4ghz on all Enable every service available in this build and boot. 2.4ghz on force is an alias.

These commands work in Companion, repeater, and room-server firmware where the services are available. Changes last until reboot; saved WiFi credentials, Bluetooth preferences, and bridge settings stay intact. get 2.4ghz lists each service as on, off, or unavailable. An nRF52 Companion controls its Bluetooth and reports WiFi/ESP-NOW as unavailable. Exclusive WiFi/Bluetooth builds retain their boot transport restriction; on all cannot reclaim a released Bluetooth stack.

An off command requires a management connection that will survive it. Bluetooth can provide the fallback for wifi off; USB, Ethernet, or a CLI request over LoRa can provide it for 2.4ghz off. WiFi association, a listening socket, and USB power alone do not count. On USB hardware without an observable open-client session, send the command through USB itself. To bypass the guard:

set wifi off force\nset espnow off force\nset 2.4ghz off force\n

Shutdown starts after the reply has had time to drain (normally 250 ms, up to two seconds for pending transport I/O). Network services stop before the shared WiFi driver. A normal shutdown is cancelled if its fallback connection disappears before it starts. The get commands report pending changes and startup/teardown failures. After 2.4ghz off, use 2.4ghz on or on all before enabling an individual service. The all/force suffix on on applies only to 2.4ghz.

WiFi and ESP-NOW share one hardware channel. A running ESP-NOW bridge pins setup and station connections to bridge.channel; starting the bridge alongside an existing WiFi connection requires that same channel. A mismatch reports a failed start and preserves the WiFi connection.

"},{"location":"cli_commands/#turn-bluetooth-on-or-off-companion","title":"Turn Bluetooth on or off (Companion)","text":"
get bluetooth\nset bluetooth off\nset bluetooth off force\nset bluetooth on\n

get ble and set ble ... are aliases. This controls the running Bluetooth service on ESP32 and nRF52 Companions: off stops advertising and disconnects Bluetooth clients; on enables it again. Both choices are saved and survive reboots. USB remains available when Bluetooth is off. Older saved preferences without this setting retain the previous default of Bluetooth on.

Successful local commands report (saved). If saving fails, the command reports an error and leaves Bluetooth unchanged. When requested over Bluetooth, off first reports (save pending) so its reply can drain; the firmware rechecks the alternate connection and saves the preference before disconnecting. If that connection disappears or the save fails, shutdown is cancelled. get bluetooth reports the current runtime state. Broad 2.4ghz controls still apply to the current boot only and do not change this saved Bluetooth preference.

Plain off requires another active management connection. A connected USB terminal or binary client, TCP client, or browser terminal can provide that connection. WiFi association, a listening server, and USB power alone do not. On USB hardware without an observable open-host session (HWCDC or a UART bridge), issue the command through that USB client so the request itself proves it is active.

Append force to off to permit disconnecting the only active connection. Use USB or another management transport and set bluetooth on to regain access afterward. Reboot alone will not re-enable saved-off Bluetooth. force is accepted only as the final argument to off.

Commands sent through Bluetooth allow its reply to drain before shutdown (normally 250 ms, at most two seconds). get bluetooth shows off pending during that wait. A normal shutdown is cancelled if the other management connection disappears before it applies; force bypasses that check too.

Bluetooth must be available in the current build and boot transport mode. An exclusive-WiFi boot still requires set companion.transport ble and a reboot before Bluetooth can be enabled.

"},{"location":"cli_commands/#view-or-change-the-independent-bluetooth-name-companion","title":"View or change the independent Bluetooth name (Companion)","text":"

Usage:

get ble.name and set ble.name ... are accepted aliases. This command is specific to Companion firmware. The default is MeshCore-<advert name>; default (or clear) removes a custom override. Names are limited to 31 valid UTF-8 bytes without control characters and take effect after reboot.

A binary Companion client should carry the same text in command 0x42 (CMD_RUN_CLI_COMMAND), which works over USB, BLE, or TCP without entering USB terminal mode. See Companion radio binary protocol for the frame and reply format.

"},{"location":"cli_commands/#set-the-bluetooth-pairing-pin-companion","title":"Set the Bluetooth pairing PIN (Companion)","text":"

Usage:

Choose a six-digit code for a fixed Bluetooth pairing PIN. For example:

set pin 654321\nreboot\n

The command saves the PIN across reboots and replies > pin is now 654321. Reboot to activate the new code; the current Bluetooth session keeps its existing PIN until then. A failed save reports an error and retains the previous saved setting.

set pin 0 clears the saved override, rather than setting the pairing code to 000000 or disabling Bluetooth. After reboot, the build's default policy applies: normally 123456 without a display, or a generated PIN on eligible display-equipped builds.

This command is available in the Companion text terminal, binary command 0x42 (CMD_RUN_CLI_COMMAND), and authorized LoRa CLI; a Bluetooth-capable build is needed to use the PIN. Existing bonded clients may reconnect without being asked for a PIN. Forget the device on the phone and pair again if you need to enter the new code.

"},{"location":"cli_commands/#view-or-change-the-bluetooth-address-ble-companion","title":"View or change the Bluetooth address (BLE Companion)","text":"

Usage:

get ble.mac and set ble.mac ... are accepted aliases. The setting is available on every Companion build that includes Bluetooth.

Value Result after reboot C2:11:22:33:44:55 Save and use that custom address. random Generate one random address now, save it, and reuse it on later boots. random-every-boot Generate a new address once at each boot. random everyboot is also accepted. random-after-connect Keep one random address through unused boots. After an authenticated connection, arm a new address for the next boot. default or clear Remove the override and use the chipset's factory Bluetooth address.

A custom value must be a valid BLE random-static address in XX:XX:XX:XX:XX:XX form. Its first byte must be from C0 through FF, and you are responsible for keeping addresses unique. Hyphens are also accepted on input; status output uses uppercase colons.

Every change requires a reboot and invalidates the phone's previous view of the device. Forget the old device entry and pair again. random-every-boot also clears the Companion's saved peer bonds during each startup, so pairing and PIN entry are expected again after every reboot.

random-after-connect keeps the same address across any number of reboots when nobody successfully authenticated. As soon as an authenticated BLE connection succeeds, the next boot rotates the address and clears stale bonds. The marker is saved while connected, so a normal reboot, power switch, or battery pull after that connection all produce the same next-boot rotation.

The address setting does not enable or disable stealth. Use the separate bluetooth.stealth flag below. default changes only the address policy.

"},{"location":"cli_commands/#view-or-change-bluetooth-stealth-ble-companion","title":"View or change Bluetooth stealth (BLE Companion)","text":"
get bluetooth.stealth\nset bluetooth.stealth on\nset bluetooth.stealth off\n

get ble.stealth and set ble.stealth on|off are short aliases. This flag defaults to off and is independent of the Bluetooth address policy. Enabling or disabling it keeps the configured custom/random/factory address policy unchanged and requires a reboot. Repeating on keeps an existing stealth pairing; repeating off does not alter address settings.

For a custom address with stealth:

set bluetooth.mac C2:11:22:33:44:55\nset bluetooth.stealth on\nreboot\n

Use set bluetooth.mac random instead of the first line for one saved random address. Both combinations keep their address and bond across later boots. Rotating policies still rotate: random-every-boot reopens pairing at every boot, while random-after-connect reopens it on the first boot after an authenticated connection. Stealth stays enabled; each new address begins with a fresh pairing window so it cannot get hidden behind an unusable bond.

With stealth on, the node is normally discoverable until the first authenticated pairing. After that, nRF52 uses directed advertising to the saved peer. ESP32 uses a minimal unnamed advertisement and a controller allowlist because its common Arduino BLE API does not expose the directed-advertising peer field. Both reject connection and scan requests from other peers. BLE cannot be completely radio silent and still accept a connection, so a passive radio sniffer may still observe packets. To deliberately clear the saved stealth peer, send set bluetooth.stealth off, then set bluetooth.stealth on, then reboot over USB or an existing authenticated connection. To leave stealth, set it off and reboot. If the saved node-side bond is lost, recovery reopens pairing. The former set bluetooth.mac stealth command is no longer accepted.

This changes only the running MeshCore Companion application. On nRF52, the OTAFIX bootloader still uses its own address, which is visible only while the board is in BLE DFU/recovery mode. USB and LoRa updates are unaffected. Bluetooth update tools must follow that separate bootloader address after the handoff; they must not assume that a custom application address remains the DFU address. See the hardware observations for the tested XIAO handoff and host-cache caveats.

"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"

Usage: - get lat - set lat <degrees>

Set by build flag: ADVERT_LAT

Default: 0

Parameters: - degrees: Latitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"

Usage: - get lon - set lon <degrees>

Set by build flag: ADVERT_LON

Default: 0

Parameters: - degrees: Longitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"

Usage: - get prv.key - set prv.key <private_key>

Parameters: - private_key: MeshCore expanded private key in hex format (128 hex characters)

Local connection only: get prv.key. Companion also requires ENABLE_PRIVATE_KEY_EXPORT=1; use binary command 0x42 or the text terminal. Infrastructure set prv.key retains its administrator access rules.

Infrastructure repeaters with ENABLE_PRIVATE_KEY_EXPORT=1 also support backup prv.key <16-hex-nonce> for authenticated remote administrators. It returns KEYBACKUP <nonce> <128-hex-private-key> only as an end-to-end encrypted direct CLI reply. This separate command does not make get prv.key remotely available. Treat the reply as a secret: clients must not put it in chat history or debug logs and should encrypt any saved backup. An administrator password alone is not a safe backup-file encryption key. Firmware does not retain this reply in its remote CLI retry cache. Companion firmware must advertise get key.backup.transport as ephemeral-routed-v1 before the phone requests it; that build delivers the reply only to the live attached client that requested it, without offline queuing or terminal echo. Older Companion firmware is not a safe transport for this backup flow.

Note: Requires reboot to take effect after setting

"},{"location":"cli_commands/#view-or-change-this-nodes-admin-password","title":"View or change this node's admin password","text":"

Usage: - get password - password <new_password>

Parameters: - new_password: New admin password (on ESP32, 1\u201315 UTF-8 bytes without CR or LF)

Set by build flag: ADMIN_PASSWORD

Default: password

get password returns the current admin password only on a local connection: USB, Ethernet, or the authenticated LAN browser CLI. It is rejected over LoRa. ESP32 WiFi Companion instead reports management-password status without revealing the secret. Its password is stored in NVS, survives a filesystem erase, and defaults to password. Other Companion builds retain their existing no-admin response. This command does not report the Bluetooth PIN.

On ESP32, empty and overlong new passwords are rejected rather than truncated. Leading spaces are part of the password. For ESP32 setup WiFi protection, choose 8\u201315 printable ASCII characters; other accepted passwords use an open hotspot with an HTTP admin login. WebConfig and runtime browser OTA share this admin credential (OTA's HTTP Basic username is admin).

A confirmed password change invalidates existing browser sessions. The page that submitted it keeps a save receipt, tries the new password once, and reads the same result again after login; it does not resend commands. If another setting fails, the result remains visible and reboot is withheld. An unconfirmed password save keeps the previous runtime password active, but the saved state after reboot may differ; the error reports this uncertainty.

Trusted TCP consoles and delegated remote CLI remain administrative paths and can change this password. The HTTP credential does not protect those ports or replace radio ACL permissions.

Note: The password setter replies with confirmation.

Note: Any node using this password will be added to the admin ACL list.

"},{"location":"cli_commands/#enable-or-disable-repeater-guest-logins","title":"Enable or disable repeater guest logins","text":"

Availability: Repeater firmware. Local administrative consoles and authenticated remote admins can use these commands.

get guest\nset guest on\nset guest off\n

Default: off, including when upgrading from firmware without this setting. The choice is saved and takes effect without a reboot. set guest off also removes existing guest sessions immediately. Turning it back on requires guests to log in again; admin and assigned ACL access are unaffected.

When enabled, guests must supply the configured guest.password. Its default is blank, so enabling guests without setting a password permits passwordless guest access. A guest can read repeater status and basic telemetry but cannot run administrative commands. This setting does not disable ordinary packet forwarding or public discovery queries.

For example:

set guest.password a-private-code\nset guest on\nget guests\nset guest off\n

Room-server login settings are unchanged.

"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"

Usage: - get guest.password - set guest.password <password>

Parameters: - password: Guest password

Set by build flag: ROOM_PASSWORD (Room Server only)

Default: <blank>

Repeater guest logins must also be enabled with set guest on. Changing this password takes effect immediately; use set guest off to remove existing guest sessions. Setting the password to off makes it the literal password off.

"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"

Usage: - get owner.info - set owner.info <text>

Parameters: - text: Owner information text

Default: <blank>

Note: | characters are translated to newlines

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#calibrate-the-battery-reading-against-a-meter","title":"Calibrate the battery reading against a meter","text":"

Availability: Repeater, Room Server, Sensor, Companion and Terminal Chat firmware on boards with a supported battery ADC. Use an administrative CLI connection; infrastructure roles also accept authenticated remote commands.

battery calibrate 4.18\nget adc.multiplier\nbattery calibrate reset\n

Measure the battery voltage with a voltmeter and enter that reading in volts. The firmware calculates new multiplier = current multiplier * meter voltage / reported voltage, applies it and saves it for restart. A reference is required; the command does not assume a chemistry or a 4.2 V full battery. Enter the voltage of the battery input the board actually measures, including pack voltage where that input supports a pack. For example, battery calibrate 13.2 supplies a 13.200 V reference; it does not change the board's measurement range.

Calibration supports the ESP32, nRF52, RP2040 and STM32 boards with implemented battery-voltage ADC conversions, including the Wio Tracker L2's external ADS1115. An unsupported board or unavailable/saturated reading returns an error. Digital PMU/fuel-gauge and battery-module readings retain their hardware scaling; boards without a battery-voltage input cannot be calibrated through this command.

The meter reference must be within 50% of the reported voltage. Newly supported ADCs accept a relative multiplier from 0.500 to 1.500, where 1.000 retains their original conversion. Existing board-specific absolute multipliers and saved values keep their established meaning. The five legacy absolute drivers are Heltec V4, V4 R8, RC32, Meshnology W12 and ProMicro.

battery calibrate reset restores and saves the board's default conversion. set adc.multiplier 0 also resets it. A successful numeric calibration reply shows the effective multiplier, including any rounding by the board. A failed save reports an error and attempts to restore the previous runtime setting.

Calibration corrects voltage reporting and the battery percentages/protection that use that reading. On nRF52 boards with voltage policy it also applies to the boot reading and running cutoff. It does not adjust charger hardware or create a current measurement. This is a single-point scale correction, not an ADC offset or nonlinear calibration.

"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"

Usage: - get adc.multiplier - set adc.multiplier <value>

Parameters: - value: ADC multiplier (0.0-10.0)

Default: 0.0 (value defined by board)

Note: On newly supported ADC boards this is a relative 0.500-1.500 correction, with 0 restoring the default. Existing absolute-multiplier boards retain their coefficient units and accept 0 or a positive value up to 10. On nRF52 boards with power management and a boot-voltage setting, this is a saved relative calibration: 1.000 keeps the board conversion, 1.050 raises all reported battery millivolts by 5%, and 0 resets to 1.000. The allowed range is 0.500-1.500. This affects boot protection, running cutoff, telemetry, battery alerts, and percentage. Calibrate against a voltmeter before enabling a low cutoff. Boards without a supported ADC return unsupported.

"},{"location":"cli_commands/#send-a-repeater-flood-text","title":"Send a repeater flood text","text":"

Usage: - send text.flood <message>

Parameters: - message: Text to send to the shared #repeaters flood channel, prefixed with this node's name. Any : in the node name is sent as ; so the prefix delimiter stays unambiguous.

Example:

send text.flood checking ridge link\n
"},{"location":"cli_commands/#view-or-change-battery-alert-state","title":"View or change battery alert state","text":"

Usage: - get battery.alert - get battery.alert.region - set battery.alert on [region] - set battery.alert off

Parameters: - region: Optional named region scope. When omitted, the repeater selects the single deepest (most narrow) named region in the configured hierarchy. If multiple regions tie for deepest, specify one explicitly.

Defaults: - battery.alert: off - battery.alert.region: <unset>

Notes: - Enabling fails until at least one usable named region is defined. Alerts are never sent as unscoped floods. If the selected region is later removed, alerts stop until battery alerts are enabled again with a valid region. - Region hierarchy edits are not persistent until region save is run. After region def west pnw wa w-wa sea, run region save before enabling the alert if the hierarchy must survive a reboot. - A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected. - The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the #repeaters channel in the selected region. - Once an alert finishes transmitting, another battery alert is suppressed for at least 12 hours. A queue rejection, stale-queue drop, or radio send failure does not start the cooldown. Battery recovery or toggling alerts off and back on does not bypass a completed alert's cooldown during the same boot. - With region def west pnw wa w-wa sea, set battery.alert on selects sea; set battery.alert on w-wa overrides that default. - get battery.alert.region returns the selected scope, for example > sea. - The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a pending alert keeps the repeater awake until the packet is handled.

"},{"location":"cli_commands/#view-or-change-battery-alert-thresholds","title":"View or change battery alert thresholds","text":"

Usage: - get battery.alert.low - set battery.alert.low <1-100> - get battery.alert.critical - set battery.alert.critical <0-99>

Defaults: - battery.alert.low: 20 - battery.alert.critical: 10

Note: The low threshold must be greater than the critical threshold. Alerts at or below the critical threshold use CRITICAL BATTERY in the message; both severities use the same 12-hour resend cooldown.

"},{"location":"cli_commands/#pause-forwarding-when-the-battery-is-low","title":"Pause forwarding when the battery is low","text":"

Search terms: low battery repeating, battery flood cutoff, save battery, automatic repeat off.

Repeater, Room Server, and Sensor firmware can automatically pause forwarding below a configured battery percentage. Both controls default to off, including when upgrading. They are independent of battery alerts.

wio-e5-mini_repeater omits these controls and the adjustable battery voltage range below to fit its application partition. Battery alerts and basic battery readings remain available.

Usage:

set repeat.battery on enables the saved repeat threshold, initially 10%. set flood.battery on enables the saved flood threshold, initially 20%. A number both enables the control and sets its threshold. off disables it while retaining that threshold for the next on. Changes are saved and take effect without a reboot.

Below the repeat threshold, the node stops LoRa retransmissions of received traffic. Below the flood threshold, it stops LoRa retransmissions of received flood traffic but may still relay routed traffic if repeating is otherwise allowed. Neither control stops the node's own transmissions or replies to administration requests. The saved manual repeat flag, flood limits, and forwarding rules remain unchanged; a battery recovery does not override a manual restriction.

Received packets entering from a bridge are subject to the same LoRa transmit limits. Exporting radio reception to WiFi, ESP-NOW, or RS232 is unchanged.

Forwarding resumes automatically after the measured percentage reaches two percentage points above the applicable threshold, capped at 100%. With the initial thresholds, flood forwarding pauses below 20% and resumes at 22%; all relay forwarding pauses below 10% and resumes at 12%. This recovery margin avoids repeatedly switching forwarding on and off near a threshold.

Battery voltage is sampled every 30 seconds while either control is enabled, and immediately when its configuration changes. get battery.forward reports the voltage, estimated percentage, whether the reading is valid, and which battery controls are currently suppressing forwarding. A missing or invalid reading does not suppress forwarding: accepted readings are 2000-20000 mV. Voltage alone does not establish that a battery is physically connected.

To enable both default thresholds:

set repeat.battery on\nset flood.battery on\nget battery.forward\n

To choose different thresholds and later disable the feature:

set repeat.battery 15\nset flood.battery 25\nset repeat.battery off\nset flood.battery off\n
"},{"location":"cli_commands/#set-the-battery-percentage-voltage-range","title":"Set the battery percentage voltage range","text":"

The forwarding thresholds use a voltage-derived percentage. On infrastructure builds without the nRF52 voltage policy, the saved range is adjustable with:

get battery.empty\nget battery.full\nset battery.empty 3000\nset battery.full 4200\n

Values are millivolts, from 2000 to 20000, with full at least 100 mV above empty. The default range comes from the board's compiled battery endpoints, normally 3000 mV for 0% and 4200 mV for 100%; boards with a two-cell pack retain their compiled pack range. The percentage is interpolated between those endpoints and limited to 0-100%. These settings describe the battery range; they do not calibrate the voltage measurement or alter a charger.

Supported nRF52 power-management builds use their existing saved battery.profile, battery.empty, and battery.full values instead. Their endpoint range remains 2000-4200 mV, with at least 100 mV between endpoints. See nRF52 battery protection for profiles and their separate boot-lock and cutoff effects. set adc.multiplier adjusts voltage calibration on boards that support it; see battery reading calibration.

Percentage remains an estimate: a voltage reading under transmit load may differ from an idle reading. Check the reported voltage against a voltmeter before relying on automatic forwarding limits.

"},{"location":"cli_commands/#enable-or-disable-the-rx-inactivity-watchdog-repeater-only","title":"Enable or disable the RX inactivity watchdog (Repeater Only)","text":"

Usage: - get rx.watchdog - set rx.watchdog on - set rx.watchdog off

Default: on, with a 24-hour no-RX timeout in normal mode.

Notes: - In normal mode, the repeater reboots after 24 hours without a received packet passing MeshCore parsing. Each such reception restarts this timeout; checks use a sliding deadline rather than waiting for another 24-hour checkpoint. - While tempradio or tempradioat is running, windows of at least 12 hours use a 12-hour no-RX timeout. Shorter windows disable this reboot watchdog for the duration of the session. This temporary policy also overrides a saved normal-mode off setting. - Startup, a successful temporary-radio activation/replacement, expiry, cancellation, or manually changing this setting starts a fresh observation window. Expiry is processed before the watchdog, so a temporary window ending exactly at its watchdog deadline restores normal timing first. - get rx.watchdog reports both the effective on/off state and timeout or short-session disabled reason. set rx.watchdog on|off saves the normal-mode preference; temporary-session rules remain active until that session ends. Existing saved preferences are retained when upgrading; the new default applies to fresh settings or older preference files without this field. - The check uses the last successful MeshCore parse time, recorded before receive delays and routing filters. It does not require the packet to be addressed to this repeater or its encrypted payload to be decrypted; duplicate receptions also count. This checks MeshCore packet structure, not sender authentication. - It does not poll, sample, or wake the radio or CPU. A due check waits for the next normal loop/wake. With RX power saving enabled, qualifying packets received during normal listening windows count as activity; the watchdog does not alter the RX/sleep timing. - Transmitting, radio interrupts, and raw LoRa packets rejected by the MeshCore parser do not reset the timer. Packets dropped before parsing because the packet pool is full do not count either. This whole-board reboot setting is separate from the automatic radio-only recovery watchdog, which still measures successful raw radio reads.

"},{"location":"cli_commands/#enable-or-disable-the-nrf52-system-watchdog","title":"Enable or disable the nRF52 system watchdog","text":"

Usage: - get system.watchdog - set system.watchdog on - set system.watchdog off

Default: on

Notes: - This nRF52-only hardware watchdog resets the device if the application loop stops for 60 seconds, including an indefinite SoftDevice flash-write wait. - Enabling takes effect without a reboot. - The nRF52 hardware cannot stop a watchdog after it has started. Disabling is persisted immediately, then the current firmware stops feeding it so the board performs one watchdog restart within 60 seconds. It remains off after that restart. - This setting is nRF52-only. The SoftDevice flash deadlock does not apply to ESP32, whose existing watchdog behavior is unchanged.

"},{"location":"cli_commands/#estimate-and-correct-infrastructure-node-time-after-startup","title":"Estimate and correct infrastructure-node time after startup","text":"

Usage: - get clock.sync - get clock.sync.status - get clock.sync.status.table - get clock.sync.status.<1-16> - get clock.sync.mesh - set clock.sync.mesh <on|off> - get clock.sync.mesh.edge - set clock.sync.mesh.edge <on|off> - clock.sync.mesh now - get clock.sync.internet - set clock.sync.internet <on|off> - get clock.sync.drift - set clock.sync.drift <30-86400> - get clock.sync.samples - set clock.sync.samples <3-16>

Defaults: - clock.sync.mesh: on for all repeater, sensor, and room-server builds - clock.sync.mesh.edge: on - clock.sync.internet: off - clock.sync.drift: 600 seconds (10 minutes) - clock.sync.samples: 9

When either source is enabled, the node makes its first clock-bootstrap attempt after 30 minutes of uptime, or immediately when the configured number of fresh evidence sources has been collected, whichever comes first. A successful estimate changes the RTC only when the absolute difference is greater than clock.sync.drift; correction can move the clock forward or backward. A valid estimate within the threshold counts as a successful sync without changing the clock. Seven days after each successful estimate, the node evaluates time again; the seven-day deadline therefore starts from the last successful estimate rather than from boot. This is a lazy uptime deadline: the check runs on the first normal loop/wake after it becomes due and does not wake the device by itself. If no source or consensus is available, the node retries every 30 minutes, and newly collected evidence triggers another immediate evaluation once the configured source count is present. Every reboot starts with the initial bootstrap attempt. An existing saved setting always overrides the platform default.

clock.sync.mesh now bypasses the startup/seven-day deadline and queues a LoRa-only consensus evaluation on the next normal loop, even when the internet source is also enabled. It uses any currently fresh samples without clearing the 16-slot table. If there is not yet enough evidence, mesh collection remains open and the next attempt follows the normal 30-minute retry. The command requires clock.sync.mesh to be on and does not bypass CLI, GPS, or NTP suppression; it also retains the normal quorum, timestamp-validity, and drift checks. The separate clock command only displays the current RTC and does not request a synchronization attempt.

clock.sync.mesh collects signature-verified advert timestamps and MAC-valid, decrypted Public-channel plain-text timestamps. In normal path mode, collection occurs only after the packet passes every forwarding filter. Sources are deduplicated by advert public key or case-insensitive Public-channel display name. Every fresh sample must also have a different full received path; all direct, zero-hop receptions count as the same empty path. This prevents repeated packets or multiple names arriving over one route from increasing the vote count.

For a node at the edge of the network where every packet arrives through one relay path, set clock.sync.mesh.edge on changes the evidence requirement from distinct receive paths to distinct sources. Signature-verified adverts are deduplicated by public key, and Public-channel timestamps are deduplicated by case-insensitive display name. Repeated packets from one source still count once. Edge mode observes this verified evidence on the receive path, independently of the forwarding decision, so repeat off and forwarding filters do not prevent clock collection. Other packet types are observed normally but cannot be clock evidence because they do not provide a suitable authenticated Unix timestamp. Changing edge mode clears the in-memory sample table so evidence collected under the other policy is not reused. The setting is persistent and defaults on.

At least the configured number of distinct fresh evidence sources (nine by default) and a strict majority of all fresh samples must fall within ten minutes of the median. In normal mode each source must use a distinct receive path. In edge mode signed adverts are distinct by public key and Public-channel messages are distinct by display name, but all may use the same receive path. The effective quorum is therefore the larger of clock.sync.samples and half the fresh sample count plus one. For example, a 9-vs-7 split can succeed but an 8-vs-8 split cannot. The median is used. clock.sync.samples accepts 3 through 16; samples older than two hours are ignored. Status reports mode=paths or mode=edge and labels the collected evidence as paths or sources. It reports reason=need-more-paths or reason=need-more-sources when fewer than the configured number exist, and reason=no-consensus when enough evidence exists but the effective quorum does not agree. Mesh collection begins immediately after boot. Following a successful estimate, it resumes two hours before the next seven-day deadline so only evidence that can still be fresh at evaluation time is processed.

get clock.sync.status reports whether the clock was set and a reason when it was not. Common reasons include waiting-deadline, need-more-sources, need-more-paths, no-consensus, within-drift, mesh-off, and suppression by CLI, GPS, or internet time. It also reports whether collection is active, the fresh evidence count, the number of occupied table slots, and the next evaluation deadline.

get clock.sync.status.table shows the active sample table in compact form. Each item is slot:type:id-prefix:age, where type A is a signed advert and type P is a Public-channel message. A trailing ! marks a stale sample. If the compact reply is truncated, query any slot with get clock.sync.status.<1-16>. The detail view reports the full source and path hashes, age-adjusted epoch, difference from the local clock, and freshness.

A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by build.sh and that time plus ten calendar years. Direct developer builds that bypass build.sh fall back to the compiler timestamp. Validation happens before a slot is selected or written, so an advert or Public-channel timestamp outside that window is not recorded as a clock sample.

Before voting, the node advances each packet timestamp by an estimated transit time. The estimate sums the radio airtime at the original packet length and at each progressively longer relay length, plus the expected midpoint of the random flood-forward delay at every prior hop. The normal elapsed time since the radio recorded local receipt is then added when consensus is evaluated, so local signature/decryption/filter processing time is included too. This is better than using hop count alone because LoRa airtime changes with packet length and radio settings. Transit compensation is capped at the ten-minute consensus window. It cannot know sender queueing, channel contention, or a remote relay's non-matching txdelay, so the consensus window and median still absorb residual error.

If a clock sync or time <epoch> CLI command successfully sets the clock, or a GPS provider writes a valid GPS time, LoRa-derived clock collection and correction are suppressed for the rest of that boot. Turning clock.sync.mesh off and back on does not clear this safety latch; only a reboot does. get clock.sync.mesh and get clock.sync.status report whether CLI or GPS time caused the suppression. On a WiFi MQTT build, a successful NTP sync is authoritative and also suppresses LoRa correction for the rest of that boot. Source selection starts fresh after a reboot, so LoRa remains the fallback when NTP cannot obtain internet time during that boot.

Public-channel display names are not authenticated and can be spoofed. Received path hashes are also truncated, unauthenticated routing hints; requiring unique paths prevents ordinary duplicate-route inflation but is not a cryptographic identity check. Signed adverts authenticate the advert contents but do not prove that the advertising node's own clock is correct. Mesh time is therefore a consensus estimate, not an authoritative time service. Edge mode intentionally gives up receive-path diversity. Public-channel display names can be spoofed, so one sender can claim multiple names and inflate the edge-mode vote count.

clock.sync.internet is available on WiFi MQTT repeater-observer builds. Its initial and seven-day queries run on the MQTT/WiFi task and are read-only until the repeater applies the configured drift test. Failed queries retry after 30 minutes. On other infrastructure-node builds, the preference can be stored but status reports that internet time is unavailable. MQTT builds retain their existing startup NTP behavior required for MQTT/TLS/JWT operation; this setting controls the additional delayed drift checks. Startup NTP is always preferred when it succeeds, regardless of this setting.

Sensor and room-server builds support mesh clock consensus and report clock.sync.internet as unavailable. Changing any clock.sync.* setting starts a new attempt for the current boot. Settings are persistent in /clock_sync; samples and schedule state are not.

A backward correction is intentionally allowed, but peers that already recorded a later timestamp from this node may temporarily reject its lower timestamps as replays until corrected time passes the previously observed value.

Example:

set clock.sync.drift 600\nset clock.sync.samples 3\nset clock.sync.mesh on\nset clock.sync.mesh.edge on\nclock.sync.mesh now\nget clock.sync.status\n
"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"

Usage: get public.key

Returns this node's identity public key without changing it or exposing its private key. Companion supports the query through its text terminal and framed CLI command 0x42 (CMD_RUN_CLI_COMMAND); its reply is > followed by exactly 64 uppercase hexadecimal characters. Private-key export support is not required. On nRF52 Full Companion, send the query to control interface 00, not the output-only logging interface 02.

"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"

Usage: ver

"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"

Usage: get role

"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag","title":"View or change this node's power saving flag","text":"

Search terms: battery saver, device power saving, low power mode.

Usage: - get powersaving - set powersaving on - set powersaving off

Parameters: - on: enable power saving - off: disable power saving

Default: on for fresh Cascade-profile builds and Companion firmware; off for other infrastructure profiles

Note: get powersaving, set powersaving on, and set powersaving off are shared by Companion and infrastructure. The saved preference controls whether power saving is allowed; active USB, logging and network services may keep the hardware awake. Actual sleep depends on the board.

On ESP32 infrastructure boards using an external USB-to-UART chip, such as the Heltec V3's CP2102, the native USB host guard cannot detect an attached computer. Current source blocks MCU light sleep while the raw UART console driver is enabled, preserving the first web-console command after idle even with all bridges and USB logging stopped. This guard has no idle timeout and also covers a computer's first connection after boot. These builds enable that console at startup, so MCU light sleep is unavailable while it remains enabled, including on battery. set powersaving on still enables sensor/GPS power-saving policies; the CPU continues normal idle yielding while keeping the console available. A running MQTT, RS232 or ESP-NOW bridge keeps infrastructure awake even when get powersaving reports on; that readback alone does not prove MCU sleep. Negotiated RS232 standby can stop an unresponsive UART owner without changing its saved enabled flag; see rs232.autosleep below. Other sleep blockers still apply. Adding UART wake alone would still discard the characters that trigger wake, so it cannot preserve an unmodified web console's first command. See the ESP-IDF UART wake documentation.

For the 1.17.1.5 G3 USB-disconnect report, use set powersaving off as the workaround. The released ESP32 sleep code can lose native USB after two minutes when the terminal is closed, even with a computer attached. The source fix blocks ESP32 light sleep while a native USB host is attached or live USB logging is enabled; ordinary CPU idle/yield remains available. The G3 button also wakes the device for at least two minutes. See the G3 release note for verification steps and the distinction between the fix and the published binaries.

Companion firmware defaults this setting to on. Full Companion accepts the command from its local USB terminal and exposes the same setting in WebConfig. On ESP32, it lowers the CPU clock to 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. USB and each active wireless transport remain available; SenseCAP Indicator Full keeps only its selected BLE or infrastructure-WiFi secondary transport active. set powersaving off restores the board's normal CPU clock and disables the GPS duty cycle. This device setting is separate from LoRa RXPS (radio.rxps) and WiFi modem power save (wifi.powersave). Infrastructure uses the same commands; its hardware and active-service sleep guards determine when the node can sleep.

"},{"location":"cli_commands/#diagnose-what-prevents-power-saving","title":"Diagnose what prevents power saving","text":"

Companion, Repeater, Room Server and Sensor provide on-demand diagnostics:

set powersaving.diag on\nget powersaving.diag\nget powersaving.diag calls\nget powersaving.diag time\nget powersaving.diag usb.session\nget powersaving.diag bridge\nget powersaving.diag 3\nset powersaving.diag off\nclear powersaving.diag\n

Diagnostics default to off and use only RAM. They do not change the saved power-saving flag, USB/DTR behavior, radio settings, logging or bridge state, and are lost on reboot. on starts a new window after off; repeated on keeps the current window. off freezes the counters for later inspection. clear resets them without changing whether collection is enabled. There is no automatic diagnostic log stream or flash write.

The default/state page shows the last actual sleep decision (why, mask), its age, reasons seen, total blocked time and eligible time in milliseconds. Before the first observed decision it reports unobserved. Pages 3 through 8 list every reason's transition count and blocked milliseconds; use an exact reason name for a separate readback. Role guards preserve their normal short-circuit order, so they report the first guard actually checked that prevented sleep, not an exhaustive simultaneous inventory. Inner board vetoes can add another reason. Reason times can overlap; blocked_ms on the state page counts their union instead of adding them twice.

Reasons include policy.off, startup, usb.session, usb.logging, usb.recovery, wireless, button, morse, radio2, ota, calibration, radio.watchdog, queue, retry, scheduled, serial, bridge, bridge.cleanup, bridge.wake, network, mesh, tracker, board and no.wake. Not every board/role has every guard. mesh includes pending protocol/storage work; scheduled can include a deadline too close for the existing whole-second sleep policy. RS232 merely being compiled or saved as enabled is not a blocker while its UART is suspended; running, cleanup and due wake are distinguished where those guards apply. USB power from a charger is not a host session. Logging enabled alone is not an nRF52 MCU sleep veto; on ESP32, the existing logging and raw-console guards can veto light sleep.

ESP32 pages 9 and 10 count SDK call entries in sixteen 16-second buckets starting at start_ms. They also report entries outside the first 256 seconds and flags for outside entries (1), saturation (2) or missing timestamps (4). Use a whole bucket contained within an independently timed interval to prove SDK calls occurred during that interval; whole-window totals are insufficient. See power-saving diagnostics for the timing limits.

The calls page counts actual instrumented wait calls, returns, short returns, board vetoes and SDK errors. time reports time spent inside those wait calls in microseconds, including SDK and interrupt work, not CPU sleep residency or electrical current. Returns under 100 us are marked short, not failed. An eligible decision with many short calls can indicate wake/event activity; an eligible ESP32 automatic-sleep path may yield without a direct instrumented wait, so zero calls alone is not proof of a fault. detail records the last board veto/error detail (SDK error code, or board-specific guard detail). Counters saturate instead of wrapping; observation timestamps must be sampled within one millis() wrap (about 49 days).

Opening USB to read the results can itself add a host/serial blocker. For a battery test, enable collection, close the terminal, wait, then read the accumulated pages. Stop collection promptly if preserving that window. A meter is still needed to diagnose radio RX current, RXPS duty cycle, FEM/LNA gain, GPS, displays, regulator losses and other loads that can consume power even when the MCU sleeps correctly. See power-saving diagnostics.

Native USB ESP32 Full Companions can also use short light-sleep intervals when the BLE/WiFi drivers, ESP-NOW, GPS UART, logging, and USB host sessions are inactive and no work is pending. Compiling those transports into Full does not itself prevent sleep. Turning off BLE advertising alone leaves its controller running and therefore retains idle behavior rather than manual light sleep.

An active radio2 requires continuous software scanning. With power saving on, ESP32 Companion, Repeater, Room and Sensor builds do not insert the ordinary 1 ms idle delay while dual-radio scanning is active. This avoids delaying sub-millisecond scan visits; it does not make active dual-radio scanning a sleep mode.

The verified Meshnology W12 layout enables the LR2021 radio's DC-DC regulator after LoRa and Morse receive initialization, including recovery. This setting is independent of powersaving and does not remove radio features. Other LR2021 layouts retain their existing regulator behavior unless their recipe explicitly enables LR2021_USE_DCDC after confirming the required inductor; the X1 is not opted in without that hardware confirmation.

"},{"location":"cli_commands/#view-or-set-the-reboot-interval-repeater-and-room-server","title":"View or set the reboot interval (Repeater and room server)","text":"

Usage: - get reboot.interval - set reboot.interval <hours>

Parameters: - hours: 0-255; 0 disables scheduled reboots.

Default: 0 (disabled)

"},{"location":"cli_commands/#control-an-exposed-gpio","title":"Control an exposed GPIO","text":"

Availability: ESP32 Repeater, Room Server, Bridge, and Sensor firmware. Companion firmware does not expose these commands. On nRF52, the commands are enabled only for Sensor builds on the Heltec T096, ProMicro, RAK3401, and RAK4631. GPIO expanders are not supported.

Usage:

Examples:

An integer duration has seconds as its default unit, so 5 means 5 seconds. Add ms for milliseconds (5ms); an explicit s suffix is also accepted (5s). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). on or off without a duration remains in that state until another command, reset, or reboot.

reset changes the pin to an input with no pull resistor (high impedance). It does not reboot the node. It also cancels any pending timer for that pin. A new timed command for the same pin cancels and replaces the previous timer. GPIOs begin in reset; states and timers are not saved and are lost on reboot.

The immediate reply confirms the applied state and any pending transition. When a timed transition finishes, a command issued over the authenticated remote CLI receives a second report such as > GPIO 16 timer complete: off. Retries of the same authenticated timed command are recognized and do not restart its countdown.

The pin number is the Arduino pin number used by that target (the normal GPIO number on ESP32 and the board's D/pin index on nRF52). The available-pin list is build-specific. Radio, flash/PSRAM, USB, serial console, display, GPS, I2C, buttons, LEDs, battery measurement, power control, bridge, Ethernet, watchdog, and other pins claimed by the firmware are rejected. A pin must also be physically broken out on your board; get gpio cannot detect wiring or an attached peripheral that is not represented by the firmware configuration.

Electrical warning: GPIOs use 3.3 V logic and have limited drive current. Do not power a relay, motor, solenoid, or other load directly from a GPIO. Use a suitable transistor, MOSFET, optocoupler, or driver with the required protection components.

"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-set-the-direct-path-override-for-the-current-remote-client","title":"View or set the direct path override for the current remote client","text":"

Usage: - get outpath - get outpath path - set outpath <hop1_hex,hop2_hex,...> - set outpath path - set outpath direct - set outpath clear - set outpath flood

Parameters: - hopN_hex: Hop hash with 2, 4, or 6 hexadecimal characters. Every hop must use the same width.

Notes: - These commands require remote client context and update the caller's ACL entry. - Room Server readers and writers may manage their own outpath and altpath without administrator permission. See room return routes for the command list, session persistence and alternate delivery behavior. - get outpath path reports the reciprocal PAYLOAD_TYPE_PATH received after the caller's latest flood login without changing the selected output route. Because that packet is asynchronous, an immediate query can report > path pending; retry shortly. The observation window expires after one minute, and the captured login path is not automatically selected. - set outpath path copies that observed route to outpath and saves it. It returns an error without changing outpath if no route was received. - direct selects a zero-hop route for a directly reachable caller. - clear forgets the override, replies > outpath cleared, and allows normal path discovery to repopulate it. - flood forces replies to use flood packets until the client logs in again.

"},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"

Search terms: enable repeating, disable repeating, packet forwarding, stop relaying.

Usage: - get repeat - set repeat <state>

Parameters: - state: on|off

Default: repeat on.

repeat off disables relay forwarding, not locally generated traffic or configuration replies. Repeaters can separately opt in to the routed-trace exception below.

"},{"location":"cli_commands/#allow-routed-traces-while-repeating-is-disabled-repeater-only","title":"Allow routed traces while repeating is disabled (repeater only)","text":"
get repeat.trace\nset repeat.trace on\nset repeat.trace off\n

Default: off, including upgrades from older preference files. The setting is saved and takes effect without reboot. Failed saves leave the active choice unchanged. Room Server, Sensor, and Companion do not gain this exception.

To keep ordinary forwarding disabled while allowing routed trace diagnostics:

set repeat off\nset repeat.trace on\n

Only direct TRACE packets with a matching next hop may be relayed. Ordinary messages, adverts, group data, ACKs, and control packets remain subject to repeat off; flood traces remain invalid. Existing trace length, path-capacity, duplicate, radio-profile, and channel/duty-cycle checks still apply. This is a diagnostics opt-in, not authenticated-admin-only forwarding: trace packets retain the protocol's existing authentication behavior.

get repeat and the disabled status bit continue to report repeating off. With repeat on, traces use the ordinary forwarding policy regardless of repeat.trace. Changing the exception to off while repeating is off blocks subsequent relayed trace transmissions, including queued copies and retries; a trace already on air is allowed to finish. Locally initiated traffic and management replies are not cancelled.

The dynamic configuration field is repeat.trace_off (0 or 1). It is appended to /com_prefs; all field offsets in the published upstream layout stay unchanged.

"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"

1.17.1.5 infrastructure bug: affected Repeater, Room Server, and Sensor images accept get path.hash.mode but report unknown config for the setter. The command was not renamed. The source correction restores the setter; installed images need an updated build. See the setting-dispatch audit.

Usage: - get path.hash.mode - set path.hash.mode <value>

Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)

Default: 0

Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14

Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.

"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"

Usage: - get loop.detect - set loop.detect <state>

Parameters: - state: - off: no loop detection is performed - minimal: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) - moderate: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - strict: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)

Default: off

Note: When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14

Example: If preference is loop.detect minimal, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"

Search terms: flood forwarding delay, flood retransmit delay, flood TX delay.

Usage: - get txdelay - set txdelay <value>

Parameters: - value: Transmit delay factor (0-2)

Default: 0.5

Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"

Search terms: direct forwarding delay, direct retransmit delay, direct TX delay.

Usage: - get direct.txdelay - set direct.txdelay <value>

Parameters: - value: Direct transmit delay factor (0-2)

Default: 0.3 (Repeater) - 0.2 (Room Server, Sensor)

Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.

"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"

Usage: - get rxdelay - set rxdelay <value>

Parameters: - value: Receive delay base (0-20)

Default: 0.0

Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.

"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"

Usage: - get dutycycle - set dutycycle <value>

Parameters: - value: Duty cycle percentage (1-100)

Default: 50% (equivalent to airtime factor 1.0)

Examples: - set dutycycle 100 - no duty cycle limit - set dutycycle 50 - 50% duty cycle (default) - set dutycycle 10 - 10% duty cycle - set dutycycle 1 - 1% duty cycle (strictest EU requirement)

Note: Added in firmware v1.15.0

"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"

Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.

Usage: - get af - set af <value>

Parameters: - value: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: - af = 1 -> ~50% duty - af = 2 -> ~33% duty - af = 3 -> ~25% duty - af = 9 -> ~10% duty You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).

Default: 1.0

"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"

Usage: - get int.thresh - set int.thresh <value>

Parameters: - value: Interference threshold value

Default: 0.0

"},{"location":"cli_commands/#enable-or-disable-hardware-channel-activity-detection-cad","title":"Enable or disable hardware Channel Activity Detection (CAD)","text":"

Usage: - get cad - set cad <on|off>

Description: When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of int.thresh - either, both, or none may be active.

Fresh Cascade-profile builds default CAD to off across all roles, including Companion. Saved CAD settings take precedence after an upgrade. Use set cad off to disable it on an existing node or set cad on to enable it explicitly. The repeater applies the saved toggle to the radio during its periodic noise-floor service. With one runnable packet, a busy result uses the normal CAD retry delay and allows roughly four seconds of continuous busy results. As the runnable transmit queue grows, both delays are divided by its depth: retry spacing will not fall below 50 ms and the busy ceiling will not fall below 500 ms. Reaching the busy ceiling records a CAD-timeout error and attempts the next queued transmission rather than waiting indefinitely. Future-scheduled packets do not accelerate CAD. get cad includes the hardware busy-result count.

Parameters: - on|off: Enable or disable hardware CAD

Default: off

"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"

Usage: - get agc.reset.interval - set agc.reset.interval <value>

Parameters: - value: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable.

Default: 0.0

With device powersaving on and a nonzero AGC interval, Companion, Repeater, Room Server, and Sensor firmware use adaptive reset scheduling. The saved interval is the starting/minimum interval. After every two admitted maintenance attempts without an observed radio recovery, the interval doubles, capped at 60 seconds (or the saved interval if it is already longer). For example, 8 -> 16 -> 32 -> 60 seconds, with two attempts at each step. An observed radio recovery returns the schedule to the saved interval. Changing the interval or power-saving mode starts a fresh schedule; adaptive history is not persisted.

get agc.reset.interval still reports the saved starting interval, not the current adaptive interval. 0 disables periodic AGC maintenance even with power saving on. With power saving off, fixed-interval behavior is retained. This follows device powersaving, independently of LoRa radio.rxps.

Adaptive scheduling does not treat a quiet mesh, strong packets, a stable noise floor, or a successful transmission as proof of full receiver sensitivity. TX cannot continually postpone adaptive maintenance. Pending RX/TX, a BUSY chip outside known RX duty cycling, carrier/Morse ownership, and an active noise-floor block can defer it; deferred attempts do not advance backoff and normally retry after 50 ms. When a failed receive start already has a later retry deadline, adaptive AGC waits at least until that deadline too, allowing sleep during persistent receive-start failures. The 60-second bound is a scheduling interval, not permission to interrupt a packet. An armed RXPS radio uses the existing explicit wake/reset path, avoiding retry polling that could miss every short listen window. Existing radio recovery watchdogs remain active. These conservative intervals have host regression coverage, but are not hardware-qualified sensitivity or battery-life results; noisy sites may require a short fixed interval with device power saving off.

"},{"location":"cli_commands/#periodic-transmit-only-text-beacons","title":"Periodic transmit-only text beacons","text":"

See TX beacons for the 12 independent MeshCore Public or Meshtext slots. set beacon.interval 12|24|36|48 selects the shared cycle; the default is 24 hours. Slots are staggered and disabled until configured.

"},{"location":"cli_commands/#view-or-change-the-radio-watchdog-interval-mqtt-observer-only","title":"View or change the radio watchdog interval (MQTT observer only)","text":"

Usage: - get radio.watchdog - set radio.watchdog <minutes>

Parameters: - minutes: 0 to use the standard 30-minute soft recovery interval, or 1-120 minutes to override it

Default: 5

Note: This setting belongs to the MQTT observer runtime and is not available on a standalone FULL repeater. It sets the interval without a successfully received packet before soft radio recovery. Transmissions and interrupts alone do not restart the timer. Setting it to 0 removes the observer override; automatic radio recovery remains active. The normal hard recovery threshold is 12 hours, where the radio supports it, and recovery does not reboot the whole board. On quiet meshes, increasing the soft interval can reduce unnecessary recoveries when no traffic is expected.

"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"

Usage: - get multi.acks - set multi.acks <state>

Parameters: - state: 0 (disable) or 1 (enable)

Default: 0

"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"

Usage: - get flood.advert.interval - set flood.advert.interval <hours>

Parameters: - hours: Interval in hours (3-168)

Default: 47 (Repeater, Room Server) - 0, disabled (Sensor)

"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"

Usage: - get advert.interval - set advert.interval <minutes>

Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)

Default: 2 on a factory-fresh node, then 0 (disabled) once configured.

Note: A new install ships with a 2 minute zero-hop advert interval. Saving any setting resets an interval below the 60 minute minimum to 0, on the assumption that the node has now been deliberately configured. To keep zero-hop adverts running, set an explicit value in the 60-240 range.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"

Search terms: hop limit, maximum hops, max hops, flood distance.

Usage: - get flood.max - set flood.max <value>

Parameters: - value: Maximum flood hop count (0-64)

Default: 64

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"

Usage: - get flood.max.unscoped - set flood.max.unscoped <value>

Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)

Default: 0xFF - indicates it hasn't been set, will track flood.max until it is.

Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"

Usage: - get flood.max.advert - set flood.max.advert <value>

Parameters: - value: Maximum flood hop count (0-64) for an advert packet

Default: 8

"},{"location":"cli_commands/#forward-flood-group-data-packets-on-repeaters","title":"Forward flood group data packets on repeaters","text":"

Usage: - get flood.channel.data - get flood.channel.data.hops - set flood.channel.data <on|off> - set flood.channel.data.hops <all|1-7>

Parameters: - on: Retransmit received flood GRP_DATA channel packets. - off: Do not retransmit received flood GRP_DATA channel packets. - all: When flood.channel.data is off, block GRP_DATA at any received flood hop count. - 1-7: When flood.channel.data is off, repeat GRP_DATA at this hop count or lower and block longer paths.

Default: flood.channel.data on; flood.channel.data.hops h=all

Forwarding behavior: Repeater firmware only. The repeater still receives and logs the packet when logging is enabled; this only blocks retransmission. On generalized repeaters these commands manage an ordinary visible FPF7 type=grp_data drop row. off with all maps to hops=all; off with N maps to hops=N+1+. The 240 KB compact FPF6 profiles retain the legacy hard gate. Because it is an ordinary FPF7 row, a matching higher-priority stop rule can exempt traffic from it. The compact get flood.filter list marks the managed row with ~data. Flood group text (GRP_TXT) is unaffected by this setting.

get flood.channel.data includes the active hop gate as h=all or h>N.

"},{"location":"cli_commands/#bridge-and-radio-crossover-filters","title":"Bridge and radio crossover filters","text":"

mode= selects where a generalized flood.rule / flood.filter row applies:

Mode Where the rule applies radio Ordinary LoRa forwarding; flood-only unless route=direct or route=any is selected. This is the default mode. bridge Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. cross Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. bridge,cross Both transport paths using one rule. cross,bridge is equivalent.

The third built-in repeater rule blocks authenticated #wardriving text and data at all hops on bridge and crossover paths, including temporary-radio sessions. It leaves same-profile traffic subject to the separate four-hop rule. set radio2.cross on does not override this filter.

# Factory slot: inspect before replacing an existing rule.\nget flood.rule.3\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n# On an existing device, add it without overwriting an occupied slot.\nset flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\n

Saved tables are preserved on upgrade; new defaults are seeded only when the table is first initialized. Bridge/cross modes accept drop, rate, priority and stop actions; scope rewrites, retry actions and TX timing are rejected. These modes cover direct as well as flood packets, in both crossing directions. The default authenticates the channel key, avoiding false drops from short channel-hash collisions. FULL room servers support explicit transport rules; companions and compact FPF6 profiles do not expose this table.

See filtering documentation for persistence, queue/rate semantics, exceptions, and how to remove one or both blocks.

"},{"location":"cli_commands/#block-selected-flood-channels-with-fpf7","title":"Block selected flood channels with FPF7","text":"

The separate flood.channel.block command and 15-row table have been retired. Generalized repeaters use the 63-row FPF7 forward phase for authenticated channel blocks:

set flood.rule type=any channel=#test hops=all drop\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule type=any channel=9cd8fcf22a47333b591d96a2b848b73f hops=4+ drop\nget flood.rule\ndel flood.rule.2\n

type=any with a channel condition can authenticate only GRP_TXT and GRP_DATA, so it does not match other payload types. Use hops=all to block at every received hop count. To preserve the old h=N meaning of repeating through N hops and blocking longer paths, use hops=N+1+; old h=4 is therefore hops=5+.

New generalized repeater tables seed slot 2 with the second example. Existing FCB2 rows are imported once into free FPF7 slots and the retired file is then removed. The fixed-size STM32WL FPF6 build cannot match authenticated channels.

"},{"location":"cli_commands/#force-a-transport-scope-onto-floods","title":"Force a transport scope onto floods","text":"

Usage: - get flood.channel.scope - get flood.channel.scope.<n> - set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - del flood.channel.scope.<n> - del flood.channel.scope all

fs is the short family name for all these commands. Short options are p=bl (path=blacklist), p=b1 through p=b6 (path=bucket:1 through path=bucket:6), and tx=s/tx=f (tx=slow/tx=fast).

Parameters: - n: Slot number within the table compiled for the target. Roomy ESP32 builds provide 1-255; classic ESP32 repeaters and nRF52/other normal constrained builds provide 1-31; very-tight STM32WL repeaters provide 1-15; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides 1-4. - channel: public, a public #channel, or a 128/256-bit channel key in hex. - txt:*: Unauthenticated fallback for otherwise-unmatched GRP_TXT and GRP_DATA. Plain * is an alias for txt:*. - login:*: Type-based wildcard for the remote-login/admin family: REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH (0x00, 0x01, 0x02, 0x07, and 0x08). It classifies the outer type; a transit repeater cannot authenticate whether a packet is actually part of a login session. - other:*: Type-based wildcard for every remaining flood payload type, including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved types, and raw custom. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - scope=<name>: Regionless alternative to region. The public name is normalized with a leading #, and its 128-bit transport key is derived directly from that hashtag exactly as for flood.filter scope=<name>. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - path=blacklist: Optional. Require the received path to match the passive flood.filter.blacklist ID table. No flood.filter drop row needs to be enabled. One exact listed ID qualifies a 3-byte path. A 2-byte path requires two matching received path entries, while a 1-byte path never qualifies. - path=bucket:<1-6>: Optional alternative to path=blacklist. Match IDs in the selected persistent flood.retry.bucket. Each bucket holds up to 17 three-byte IDs and remains usable when flood.retry.bridge is off. It uses the same 3-byte, 2-byte, and 1-byte thresholds as path=blacklist. recent.repeater freshness and flood.retry.ignore do not affect this passive match. - tx=slow: Optional. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and schedule retransmission with the maximum supported txdelay factor of 2.0 after changing the scope. The default is fast; tx=fast may be supplied explicitly when replacing a slow row.

Default: No forced scopes.

Remote ACL permission 4 (region/scope manager) can use all get, set, and del flood.channel.scope forms. Filter managers and other non-admin roles cannot change this table.

Without .n, set updates the row for the same exact channel key or wildcard class with the same path selector, otherwise it uses the first empty slot. This permits an ordinary fallback and separate blacklist or bridge-bucket rows for the same channel. With .n, it replaces that slot. The three wildcard classes are independent and consume one slot each. get flood.channel.scope reports active/total slot counts; use the numbered form for row detail. Keyed rows are displayed by the first four bytes of their derived channel hash because channel secrets are never returned. Regionless targets are displayed with their normalized leading #.

This acts on received ROUTE_TYPE_FLOOD and ROUTE_TYPE_TRANSPORT_FLOOD packets. An unscoped packet gains the configured scope; an already-scoped packet has its existing transport codes replaced. For GRP_TXT and GRP_DATA, all exact channel-key rows are tried first and must validate the packet MAC/decryption. Matching path-qualified exact rows are tried before ordinary exact fallback rows. A region-backed row whose target is missing or unusable is skipped; later exact rows and then txt:* are tried. Exact keyed rows with a usable target therefore beat txt:* regardless of slot number. Within each wildcard class, path-qualified rows similarly precede ordinary fallback rows. login:* and other:* select their non-overlapping outer-type families without decrypting the payload. The lowest usable slot wins within each priority tier.

Standard traceroute is direct-routed and is therefore outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and region/unknown-code gates still apply.

On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region or direct hashtag key and packet payload, and sets transport code 1 to zero. This occurs before region enforcement, forwarding filters, and the seen-packet lookup. For an already-scoped packet, the selected code replaces both incoming transport-code fields. Direct routes are never rewritten. A packet converted from unscoped is no longer subject to flood.max.unscoped; all rewritten packets remain subject to normal payload handling, flood.max, flood.filter, loop detection, and moderation. Assigning a scope does not make a packet type forwardable if the core would otherwise reject it. By default, if the selected scope differs and the rewritten packet is accepted for forwarding, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Adding tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), keeps the ordinary queue priority, and uses the maximum txdelay factor of 2.0 for the retransmission. As with ordinary txdelay, the actual transmit delay is randomized from zero through the resulting window; factor 2.0 gives a maximum of ten packet airtimes. Neither mode preempts an active radio transmission or bypasses CAD and airtime-budget limits. Selecting the scope already carried by the packet is a no-op and does not grant special transmit treatment.

A region-backed target must be locally flood-allowed and remains subject to the normal region gate. A scope=<name> target is trusted for this matched receive pass even though it has no region-list entry, matching the behavior of flood.filter scope=<name>. It does not create a region, consume a region slot, or change the allow/deny state for unrelated packets carrying the same transport code.

If a region-backed row's target has been removed or has no usable key, the repeater tries the next applicable row. For group packets this means later authenticated exact rows followed by txt:*; wildcard duplicates likewise fall through to the next usable slot. When no usable mapping exists, the packet retains its original unscoped or scoped route.

LoRa OTA remains functional when other:* is configured. OTA packets are given that target's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. A region target must allow flooding; a direct target follows the regionless trust behavior above. The OTA core itself is dormant outside that window; no default flood filter row is needed for that behavior. Forced scope does not make OTA operate outside the window.

Capacity cost: Each rule slot retains its 36-byte runtime and persistent record. A separate 32-byte-name table holds up to the smaller of the rule count or 32 distinct regionless targets; very-tight STM32WL builds hold one reusable direct target. Region-backed targets do not consume this table. The four-slot minimum uses 272 bytes RAM and a 278-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 572 bytes RAM and a 578-byte file. The 31-slot table uses 2,108 bytes RAM and a 2,114-byte file. Roomy ESP32 builds use 255 rule slots and 32 direct-target slots: 10,204 bytes RAM and a 10,210-byte file. Classic ESP32 LoRa-OTA builds that cannot afford the 255-rule table use 31 slots instead. Both configured regions and regionless targets can be reused by any number of rules.

Duplicate behavior: Mesh dedup hashes payload type and payload bytes; it does not hash route type, transport codes, or the ordinary flood path. Adding a transport scope therefore does not create a new duplicate identity. If the same payload later arrives scoped, unscoped, or through a different region, it is still the same seen packet. TRACE is the exception only in that its encoded path_len byte is also hashed.

While equivalent non-TRACE flood copies are waiting in rxdelay, the normal receive-quality timing still selects the packet to process, but that winner receives a scope from the queued scoped copies with the same dedupe identity. If the copies carry different locally allowed scopes, the scope from the shortest received path wins. Unknown and denied transport codes are not candidates and therefore cannot overwrite an unscoped winner. With equal path lengths, the deeper child region wins because it is narrower. A remaining tie keeps queue order. The winner's own path, SNR reading, and scheduled time are not changed, and an already-scoped winner may have its code replaced by the better queued scope.

The comparison is deferred until dequeue so each copy retains its original scope and path for arbitration. It can only use copies still present in rxdelay; it cannot replace a packet that already won the dedupe race. Flood-form TRACE participates in this arbitration; direct traceroute does not enter the flood queue.

A packet that matches a fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses the inbound rxdelay queue. A tx=slow row remains in that queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.

Examples:

region put west\nregion save\nset flood.channel.scope #local west\nset flood.channel.scope.2 txt:* west tx=slow\nset flood.channel.scope.3 login:* west\nset flood.channel.scope.4 other:* west\nget flood.channel.scope\nget flood.channel.scope.1\ndel flood.channel.scope.2\n

A regionless exact mapping needs no region command:

set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope\nget flood.channel.scope.1\n

For example, if an authenticated #rgdata packet arrives carrying scope #usa, that rule replaces #usa with #BlackHole86 before forwarding. The rule also assigns #BlackHole86 when the packet is unscoped or carries any other scope; it is a channel-to-target mapping, not an incoming-scope filter.

To use bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, while assigning west to every other authenticated public packet:

set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n

Additional 3-byte IDs may be added to bucket 1 later; any one exact hit qualifies the east row. This use is passive and does not require flood.retry.bridge to be enabled. The separate blacklist selector remains available for tables shared with flood.filter path=blacklist rules.

"},{"location":"cli_commands/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"

Usage: - get flood.channel.scope.require - get flood.channel.scope.require.<n> - set flood.channel.scope.require <public|#channel|128/256-bit-key> - set flood.channel.scope.require.<n> <public|#channel|128/256-bit-key> - del flood.channel.scope.require.<n> - del flood.channel.scope.require all

fsr is the short family name, with the same arguments and region/scope-manager permissions.

Default: Empty; normal global region enforcement remains active.

Once this table contains a row, received flood GRP_TXT and GRP_DATA packets use selective region enforcement. A packet authenticating against a listed channel key must already carry a transport scope matching a locally flood-allowed region. Listed channels arriving unscoped, with an unknown code, or with a denied region are not retransmitted. This tests the original incoming scope before any flood.channel.scope or flood.filter scope= rewrite. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.

Other group channels bypass the region/unknown-code gate while the table is active. They remain subject to every other forwarding control, including repeat, flood.max*, packet filters, loop detection, payload validation, and moderation. Non-channel flood payload types retain normal global region enforcement.

Channel matching validates the packet MAC/decryption with the configured key; the visible one-byte channel hash is only a prefilter. Public hashtag channels use their derived public key. Without .n, setting an existing key updates it and a new key uses the first empty slot. Numbered set replaces that slot. get ...<n> reports a four-byte derived prefix and key size without exposing the key. The table uses the same build-dependent slot count as flood.channel.scope.

Remote ACL permission 4 can manage this table. Deleting its final row restores normal global region enforcement for group channels.

Example:

set flood.channel.scope.require #bot\nget flood.channel.scope.require\nget flood.channel.scope.require.1\n
"},{"location":"cli_commands/#change-persistent-flood-rules-in-the-field","title":"Change persistent flood rules in the field","text":"

For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Flood Filtering and Moderation.

Usage:

Generalized rows also accept sender=, text=, len=, route=, prob=, enabled=, and anchored path= chains. These use the same table and ordering; see the parameters and short forms below.

The extended table is available on repeaters with the rule engine enabled and on FULL-profile ESP32 room servers. A FULL room server exposes fr, flood.rule, and flood.filter, has 31 slots, and requires an administrator for remote changes. It does not have the repeater's passive path blacklist, so flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= match on a room server. Standard room-server profiles do not compile this table.

Parameters:

The payload names follow the MeshCore packet-format allocation:

Value Short name Full name 0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA (this fork's LoRa OTA extension; reserved upstream) 0x0D 13 reserved 0x0E 14 reserved 0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM

Route scope: mode=radio defaults to the two flood route values: ROUTE_TYPE_TRANSPORT_FLOOD (0x00, flood plus transport codes) and ROUTE_TYPE_FLOOD (0x01, unscoped flood). Direct routes 0x02 and 0x03 require an explicit route=direct or route=any on a radio row. Bridge/cross rules cover both route classes by default at their transport boundaries.

Behavior: Match fields within one row are ANDed. Radio rows are matched against the same immutable receive-time packet, before any rule changes its scope. Bridge/cross rows match the packet at their admission boundary; a TX packet can already have an appended hop or rewritten scope. Each mode evaluates only its applicable rows, processed in descending priority. At equal numeric priority, authenticated channel matches precede raw hashes, which precede an unrestricted channel matcher; lower slot wins after that. The first matching stop row is included and all lower-order FPF7 matches are discarded. A stop cannot undo an earlier drop or bypass hard forwarding gates or the other policy phases. A row with path=blacklist must meet the path condition as well as its other conditions; blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. In contrast, prefix= begins at the first received path entry and preserves order. A matching drop row prevents retransmission. The highest-order remaining matching scope/region row wins; matching drop and rate rows remain independent and can still block the rewritten packet. Scope rewriting happens before region enforcement and is trusted even when its name is absent from the local region list. It does not bypass repeat, flood.max, other drop rows, loop detection, or moderation.

Retry selection uses that same ordered, stop-truncated match set. With at least one active retry row, a received flood starts a retry sequence only when one of those matching rows includes retry. This selector cannot override flood.retry.count, path/type attempt caps, flood.retry.advert, disabled forwarding, a drop decision, or any other hard forwarding gate. Locally originated floods retain the normal global retry behavior.

With require=region, a failed check makes that scope row ineligible. It leaves the packet unchanged and does not set the filter-scope trust bypass, so an unknown or denied incoming region is rejected normally unless another independent scope rule rewrites it. Later eligible filter scope rows may still match.

By default, when a scope row will change the packet's transport codes, the packet bypasses inbound rxdelay; its retransmission then uses zero txdelay and the highest outbound queue priority. With tx=slow, the rewrite instead uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), normal queue priority, and the maximum txdelay factor of 2.0. The actual randomized transmit wait ranges from zero to ten packet airtimes. Selecting the scope already carried is a no-op and does not grant special treatment. An active radio transmission is not preempted, and CAD and airtime-budget limits still apply.

The packet is still received and can still be logged. Rules are persistent data and can be changed over serial or authenticated remote CLI without an OTA or reboot. flood.rule and flood.filter address the same table on extended builds; FPF6 files are migrated in memory and the next save writes FPF7. New sender/text, length, route, probability, enabled-state, or extended-path fields use FPF8 instead. Old firmware without FPF8 support cannot load that table; remove the new fields before downgrading and keep a copy of the rules. FPF1-FPF5 files are rejected and filtering fails open. FPF7/FPF8 store canonical region names rather than transient numeric region IDs. Removing, reordering, or reusing a region ID therefore cannot silently retarget a rule. If a saved input or target region name is absent, that input match or rewrite is inert; restoring the same region name reactivates it. While either tempradio or tempradio2 is active, only rows explicitly marked suspend=tempradio are skipped. Temporary radio is a radio state, not an OTA mode; normal payload types can also use the temporary channel. Other rows remain in force, and suspended rows resume after both temporary sessions end. A malformed persisted table fails open (no general rules are applied).

Within one receive evaluation, rows using the same channel key share the initial authentication verdict; content patterns also decrypt into temporary scratch for matching. The cache and plaintext are discarded after the packet. Raw hash:XX rows skip authentication and compare only the visible byte.

Per-channel retry examples:

# Use bridge-bucket retry only for authenticated Public and #hamradio traffic.\nset flood.retry.bridge on\nset flood.rule.2 type=any channel=public retry\nset flood.rule.3 type=any channel=#hamradio retry\n\n# If the channel key/name is unavailable, select visible channel hash A7.\n# This is an unauthenticated 1-byte hint, not a channel identity.\nset flood.rule.4 type=any channel=hash:A7 retry\n

Because Public's visible hash is 0x11, a bare hash:11 drop row also matches Public. To exempt authenticated Public while dropping other packets that carry the same visible byte, put the exact Public rule first and stop lower-priority rules after it:

# Retry and preserve authenticated Public; drop other channel-hash 11 packets.\nset flood.rule.2 type=any channel=public retry stop\nset flood.rule.3 type=any channel=hash:11 drop\n

The Public row matches only after its MAC/decrypt check succeeds. A colliding channel therefore misses that stop and reaches the raw-hash drop row. Both rows use the default numeric priority, but authenticated-channel specificity automatically orders Public first even if its slot number is higher. Without stop, both rows match Public and the sticky drop action wins. Omit retry from the Public row when only the forwarding exemption is wanted. An operator can deliberately reverse this order by assigning the hash row a higher numeric priority.

Deleting or replacing the last active retry row restores the legacy global retry eligibility. Firmware that predates the retry/hash:XX FPF7 extension cannot preserve tables containing those rows; remove them before downgrading.

Default rows: Repeater firmware and FULL ESP32 room-server firmware seed a new flood-filter table with ota all suspend=tempradio in slot 1. This blocks repeated LoRa OTA (0x0C) floods at every received hop unless either temporary radio profile is active. The OTA core independently refuses OTA receive, relay, and transmit outside temporary radio. The row is editable and deletable; once the table is saved, deletion is persistent. Restore the exact seeded row with:

set flood.filter.1 0x0C all suspend=tempradio\n

Omitting all is equivalent. Omit .1 as well to reuse an identical rule or the first empty slot instead of replacing slot 1.

Generalized repeaters also seed slot 2 with the authenticated #wardriving radio hop limit and slot 3 with the bridge/crossover block:

set flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n

These numbered commands replace those slots. Saved tables are preserved on upgrade; use the unnumbered third command to add the new rule without replacing an occupied slot. See restoring the three defaults.

Remote-admin lockout warning: There are no hidden payload-type or short-hop exceptions. FPF7 drop and rate rows may block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so. Transit repeaters cannot decrypt these outer types to distinguish an admin exchange from ordinary peer traffic. Keep a serial or other recovery path and stage broad deny/rate rules carefully.

Without .n, set reuses an identical rule or uses the first empty slot. With .n, it replaces that slot, which is the intended way to change a row's match or action. get flood.filter or get flood.rule gives a compact list. Use the numbered form for full details, including channel, prefix, original-scope condition, action, timing, rate, and temporary-radio suspension.

fr is a shorter alias for flood.rule, with the same explicit-action requirement. Short and long field names can be mixed in either command:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\nget fr.3\n

The two setters above save the same row. get fr.3 returns the complete short setter; get fr lists the table. Unnumbered set fr ... reuses an identical row or adds one to the first empty slot. del fr.N and del fr all have the same behavior as their long forms.

Content, exceptions, and state changes use that same setter:

set fr grp_txt c=#local x=\"^BEACON\" d\nset fr grp_txt c=#local n=\"^Alice$\" pri=200 a\nset fr grp_txt c=#local d\nset fr.4 e=off\nset fr.4 e=on\n

The Alice rule stops lower-order general drops; separate moderation and hard forwarding gates still apply. Sender names remain unverified display text. Saved older prefix rules keep their start-anchored semantics; when retyping an old path=A1,B2, use prefix=A1,B2 or pa=^A1>B2.

Full token Short token type=... Positional type or t=...; * means any hops=... Positional range or h=...; * means all mode=radio/bridge/cross/bridge,cross m=r/b/c/bc (choose one value) channel=..., prefix=... c=..., p=... path=[^]ID>ID...[$] pa=... sender=..., text=... n=..., x=... len=N/N+/N-M l=... route=flood/direct/any rt=f/d/a prob=N, enabled=on/off pr=N, e=1/0 hashbytes=any/1/2/2+/3 pb=*/1/2/2+/3 (choose one value) path=blacklist p=bl (repeater only) in=any/none/scoped/allowed/unknown i=*/n/s/a/u (choose one value) in=scope:name, in=region:name i=s:name, i=r:name drop, stop, retry d, s, r allow, forward a (same action as stop) scope=name, region=name s=name, r=name rate=N/min, priority=N q=N, pri=N tx=slow, suspend=tempradio, retry f=s, f=t, f=r; combine as f=str

All existing validation and permissions apply. m=cb, m=b,c, and m=c,b also mean bridge and crossover.

Full family Short family Commands flood.moderation fm get, set, del flood.channel.scope fs get, set, del flood.channel.scope.require fsr get, set, del flood.filter.blacklist fb get, set, del flood.advert fa get, clear

Slot arguments, output, validation, and permissions are shared with the full family names. For example, get fa key 1 inspects the full advert key and clear fa all clears advert history.

The numbered long getters use this complete short setter when descriptive fields would exceed a reply. Defaults are omitted and specific payload types are printed numerically. If it still cannot fit, the reply is Err - compact command exceeds reply size, never a partial setter. Private channel keys are shown as key:XXXXXXXX; that reference can be reused while an active rule on the same device holds the key. Copying it to another device requires the original key, unless that device already has a matching rule. Unknown or ambiguous references are rejected. See the shorthand guide for examples.

On generalized repeaters, filter rows, scope-rewrite rows, the shared blacklist, and flood.channel.data compatibility state are committed in one atomic FPF7/FPF8 image. Compact FPF6 profiles retain separate files. Replacing or deleting the blacklist does not delete rows containing path=blacklist; such rows remain dormant while the list is empty. Path hashes are truncated routing identifiers, not authenticated identities, so this is a forwarding signal rather than proof that a particular repeater handled a packet.

A common use is containment of bulk internet-to-mesh dumping: list the path IDs associated with the offending gateways, then add a broad type=any hops=all path=blacklist drop row. This prevents this repeater from retransmitting matching floods; it does not delete them from local logs or prove who originated them.

The unnumbered blacklist set replaces the whole list and accepts up to 18 IDs so it fits every CLI transport. Numbered set writes up to 18 consecutive entries beginning at an existing slot or exactly the next slot, allowing an ESP32 list to grow to 255 entries in batches. Numbered deletion compacts subsequent slots. The unnumbered get reports the total and as many leading IDs as fit in one reply; use numbered get to inspect entries beyond that reply.

Standard traceroute is direct-routed and therefore outside flood.filter entirely. A custom flood-form trace participates normally: type=any, explicit trace, rewrite, rate, drop, and stop rows can all apply.

Examples:

set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\nset flood.rule.4 type=any prefix=860C rate=10/min\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\nget flood.rule.2\nset flood.filter grp_data 4+\nset flood.filter.2 PAYLOAD_TYPE_ADVERT 6+\nset flood.filter ota 2-4\nset flood.filter.1 0x0C all suspend=tempradio\nset flood.filter grp_data all suspend=tempradio\nset flood.filter grp_txt all scope=local\nset flood.filter grp_data all scope=local require=region\nset flood.filter grp_data all path=blacklist scope=local tx=slow\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter.blacklist.19 778899,AABBCC,DDEEFF\nset flood.filter any all path=blacklist\nget flood.filter.blacklist\nget flood.filter.blacklist.4\nset flood.filter any 12+\nget flood.filter\nget flood.filter.2\ndel flood.filter.2\n

The first rule authenticates #rgdata, requires more than three received hops, and adds #BlackHole86 only when no scope was present. The second rewrites the exact incoming #usa scope. The third demonstrates a two-byte pbyte source prefix and a global per-row rate cap. The fourth authenticates #rgdata at zero through two hops, applies no FPF7 action of its own, and stops lower-order FPF7 forward rows; hard gates and the rewrite/moderation phases still apply.

The fixed 240 KB STM32WL profiles leave MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the compact, persistent FPF6 flood.filter and blacklist commands. They still filter floods, but do not expose the flood.rule alias or its extended channel, prefix, input-scope, region-action, or rate fields, nor sender/text patterns, length, route, probability, enabled-state, or anchored chains. Their compiled legacy families still accept the fm/fs/fsr/fb/fa aliases; fm requires moderation support in that build. No partition size changes are required.

"},{"location":"cli_commands/#moderate-flood-group-text-by-channel-sender-and-source-path","title":"Moderate flood group text by channel, sender, and source path","text":"

Usage: - get flood.moderation - get flood.moderation.<n> - set flood.moderation <channel> <sender> <action> [action...] - set flood.moderation.<n> <channel> <sender> <action> [action...] - del flood.moderation.<n> - del flood.moderation all

fm is the short family name. Options can use d (drop), q=N (rate=N/min), h=N (hops=N), and p=... (path=...). The short rate also accepts q=N/min or q=N/m; duplicate rate/drop options remain invalid.

Parameters: - n: Moderation slot from 1 to 16. - channel: - public: Built-in Public channel. - #channel: Derive the well-known hashtag-channel key. - A 128-bit or 256-bit channel key in hex, for any other/private channel. - sender: Exact group-text display name. Matching is ASCII case-insensitive. Quote names containing spaces, for example \"Field User\". - drop: Do not forward matching messages. Equivalent to rate=0/min. - rate=X/min: Forward at most X matching messages per 60-second local window. This option requires an exact sender rather than *. - hops=N: Do not forward a matching message whose received flood path count is N or higher. hops=all removes this constraint. - path=H1[,H2,H3]: Match the start of the flood path. One to three hashes are accepted; every hash must have the same 1-, 2-, or 3-byte width. - path=*: Match any source path (the default).

At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. Rate counters are local to this repeater and rule, use a 60-second window beginning with the first matching message, and reset on reboot.

Configured exact names are limited to 31 bytes. A wildcard * with a hop restriction also matches longer valid sender names. The FGM1 storage format is unchanged; saves use staging, readback, and backup recovery. A save reported as unconfirmed may already have published, so rebooted settings can differ from the restored live table.

Decode and identity behavior: Moderation applies only to flood PAYLOAD_TYPE_GRP_TXT. The repeater first checks the packet's channel-hash byte, then validates and decrypts with the configured key. It extracts the text before the first : from the standard <sender>: <message> plaintext. The channel key is stored locally but is never printed by get.

The group-text sender is an unverified display name, not a public key. It can be spoofed. Combining it with the first one to three path hashes makes a more useful moderation signal, but path hashes are truncated and are not proof of the originating user. A path-qualified rule begins matching only after the packet contains all configured starting hops; it cannot identify a first hop on a zero-hop packet.

As with general filtering, matching messages are still received/logged; only retransmission is denied. There are no moderation rules by default.

Examples:

set flood.moderation public \"Noisy User\" rate=5/min\nset flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation.3 00112233445566778899AABBCCDDEEFF alice rate=10/min hops=4 path=71CE82\nset fm public \"Noisy User\" q=5\nget flood.moderation\nget flood.moderation.3\ndel flood.moderation.3\n
"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"

Usage: - setperm <pubkey> <permissions>

Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin - 4: Region/scope manager (repeater delegated region and forced-scope management) - 5: Filter manager (repeater delegated forwarding-filter management)

Filter manager scope: Permission 5 can use an explicit allowlist of non-secret operational/filter status commands and can change the forwarding controls repeat, loop.detect, flood.max*, flood.channel.data*, flood.filter*, flood.rule*, fr[.N], and flood.moderation*, including the fm, fb, and fa aliases. It cannot read guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or other admin configuration. Permission 4 is limited to region commands, flood.channel.scope* (including fs and fsr), and the same non-secret status allowlist. Both delegated manager roles are protected from least-recently-active ACL eviction like administrators.

The same filter aliases work inside enrolled signed fleet commands, including quoted sender/text patterns. Fleet signature, expiry, replay, and command-size checks still apply; ordinary channel text cannot authorize a change.

Note: Removes the entry when permissions is omitted

"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"

Usage:

Available on repeaters, room servers, and sensors. Over LoRa, an authenticated admin session is required; guests, read-only/read-write clients, and delegated region/filter managers cannot read the ACL.

Each page starts with ACL <page>/<pages> and contains up to two entries. Each entry is a two-digit hexadecimal permission byte followed by the full 64-character public key. For example, use get acl 2 for the second page. An empty list returns ACL: empty. Deleted/guest entries with permission byte 00 are omitted, matching the local listing. Pages reflect the current live ACL, so entries can move between pages if permissions or clients change.

Bare get acl keeps the full streamed listing on USB and direct network CLI, or via Companion binary command 0x42 where the role supports the command. The listing contains public keys and permissions, not passwords or shared secrets, and does not change the ACL.

"},{"location":"cli_commands/#view-repeater-guest-sessions","title":"View repeater guest sessions","text":"

Usage:

Available on repeaters through local administrative consoles or an authenticated remote admin session. Delegated region/filter managers and guest clients cannot read this list.

Each page starts with Guests <page>/<pages> (<total>) and shows one guest's full public key followed by last=<UTC epoch seconds>. An empty list returns Guests: empty. A last=0 value can occur before the node's clock is set.

These are live RAM entries, separate from the persistent ACL shown by get acl. They indicate an accepted session, not whether the guest is currently online. Entries can move between pages as clients change. A reboot or set guest off clears them.

"},{"location":"cli_commands/#view-repeater-login-attempts","title":"View repeater login attempts","text":"

Usage:

Available on repeaters through local administrative consoles, an authenticated remote admin session, or enrolled signed fleet commands. Guests and delegated region/filter managers cannot read this list.

The repeater keeps the 16 most recently attempted public keys in RAM, with the newest attempt first. Each row shows only the first 12 hexadecimal characters of the key, the latest attempt's UTC epoch time, its result, and an attempt count. There are up to two rows per page. Attempts for the same full key update its existing row and count; the oldest key is replaced when the list is full. The count stops at 4,294,967,295. Two different keys can share the displayed prefix; their records and counts remain separate.

Logins 1/1 (2)\nABCDEF123456 n=3 last=1792600000 accepted\n123456ABCDEF n=1 last=1792599900 guests-disabled\n

Results are accepted, wrong-password, guests-disabled, replay/state, and acl-unavailable. An accepted login can be an admin, an assigned ACL client, or a guest. This history records actual login requests after packet authentication; anonymous owner, region, and clock requests, incomplete requests, and packets that fail authentication do not appear. Passwords and shared secrets are never recorded.

This is recent attempt history, not a list of online users or a complete audit log. Pages can change as attempts arrive. A reboot clears it; set guest off does not. A zero timestamp means the clock was not set when the attempt arrived.

"},{"location":"cli_commands/#recover-a-repeaters-future-dated-replay-timestamp","title":"Recover a repeater's future-dated replay timestamp","text":"

This is an explicit recovery operation after correcting a bad clock, not a contact deletion. It lowers selected replay timestamps to the repeater's current UTC epoch only when they are later than that epoch. Earlier values, public keys, permissions, stored paths, and historical identity records remain. The replay file is committed before the live table changes. No extra 60-second login reservation is added by this command.

First set/synchronize and verify the repeater's clock (clock); a build-default clock without a manual or observed synchronization is not accepted. Also fix the companion's clock before its next login.

USB console:

replay reset <full-64-hex-public-key>\nreplay reset all CONFIRM\n

Authenticated LoRa admin, including a resumed admin session:

replay reset <full-64-hex-public-key>\n

The reply shows now=<epoch>, ttl=<remaining-seconds>s, and a confirmation command containing the same full key and a one-use 32-hex-character token. Verify the displayed time and send that command before the original 300-second deadline. During the first 120 seconds, repeated requests for the same key by the same admin return the same token without restarting either timer. From 120 through 300 seconds, the token is retained for confirmation only: requests do not resend or replace it. This leaves at least 180 seconds to deliver a confirmation after the last permitted token response is generated (radio transit time still counts toward expiry). At 300 seconds it expires and a new request can receive a new token. Confirmation can succeed immediately; there is no requirement to wait for the resend window to close. The displayed TTL decreases on retries and is measured when the reply is generated, not when it reaches the companion.

It can target the caller's own key or another exact key; prefixes, self, wildcards, and all are not allowed over LoRa. Tokens are bound to both the requesting admin and target, expire on reboot, and are invalid after use or a clock correction outside the five-second confirmation tolerance. A failed write requires a new confirmation. Challenge requests consult live token state instead of replaying cached challenge text; completed confirmation results remain cacheable without executing the reset again. Normal packet freshness checks still apply to retries.

Only the physical serial console grants all access. Ethernet, browser and internal command callbacks do not count as USB. Guest, read-only, region-manager and filter-manager roles cannot reset replay state. This command is implemented in repeater firmware; room-server and sensor CLI are unchanged.

Normal login and command admission checks remain in force: a fully locked-out caller that cannot send an accepted admin command needs another working admin or USB access. A corrupt/unreadable replay file fails closed and is not erased or formatted by this operation. Unknown keys do not create records.

Security trade-off: lowering a replay boundary can admit previously captured future-dated login/command packets above the new boundary, including packets that could raise it again. The one-use token prevents the recovery command itself from being repeatedly executed; it does not replace the protocol's timestamp-based replay protection. Use recovery only after verifying clocks. Setting a clock alone never automatically resets this table.

"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"

Usage: - get allow.read.only - set allow.read.only <state>

Parameters: - state: on (enable) or off (disable)

Default: off

Make a room read-only while allowing selected writers:

set allow.read.only on\nsetperm <PUBKEY_USER_1> 2\n

With read-only mode on, a client without a valid room password can join as a read-only guest, including with a blank password. Empty configured admin and room passwords disable those password credentials; they never grant Admin or Read-Write access. Permission 2 gives the specified public key read-write access. Replace <PUBKEY_USER_1> with the user's full public key and repeat setperm for every approved writer.

An existing ACL identity keeps its assigned role when logging in with a blank, stale, or room password. A valid nonempty admin password can explicitly upgrade it to Admin. This prevents a guest-password login from demoting an admin, and prevents a room password from overriding an explicit read-only assignment. Only Read-Write (2) and Admin (3) clients can post.

This does not remove write access from clients that already know the room password. Keep that password private, or replace it, if write access should be limited to the explicitly permitted users.

"},{"location":"cli_commands/#view-or-change-the-room-topic","title":"View or change the room topic","text":"

Availability: Room Server firmware. Local consoles and authenticated remote admins can use these commands.

set topic Welcome to the Bellevue room. Please keep posts local.\nget topic\nset topic\n

set topic <text> saves an announcement of up to 151 UTF-8 bytes. Individual command transports can impose a smaller input limit. get topic returns the current text prefixed with >. set topic with no text clears it.

The topic survives reboot and appears as a normal room post authored by the server, using the existing app message format. It does not require an app update or provide a separate pinned-header display. Older unread retained posts are delivered before the topic, so the companion's automatic catch-up cursor cannot jump past them. A topic ACK updates only topic delivery state, leaving the server's message-history cursor unchanged.

The server waits six seconds before delivering a new topic and attempts an unacknowledged announcement up to three times per client. It then continues ordinary post delivery; reconnecting or changing the topic renews that budget. Changing a topic while its predecessor is awaiting an ACK does not mark the replacement as received. Clearing the topic stops further announcement pushes; a packet already queued for transmission can still arrive.

Topic saves are verified before publication. A failed save reports an error and retains the previous live topic. Corrupt, unreadable, or unsupported stored topic files are preserved and block replacement until their storage issue is repaired. Ordinary room history uses the bounded RAM ring by default. Enable optional retention of the newest 32 posts with set room.history on.

See Room history, moderation, and information board for persistent history, saved ACL roles, full-key bans, per-user post/request limits, the ESP32 /room browser page, and versioned article downloads.

"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"

Usage: - region load - region load <name> [flood_flag]

Parameters: - name: A name of a region. * represents the wildcard region

Note: flood_flag: Optional F to allow flooding

Note: Indentation creates parent-child relationships (max 8 levels)

Note: region load with an empty name will not work remotely (it's interactive)

"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"

Usage: - region save

"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"

Usage: - region allowf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * allows packets without region transport codes

"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"

Usage: - region denyf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * drops packets without region transport codes

"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"

Usage: - region get <name>

Parameters: - name: Region name (or * for wildcard)

"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"

Usage: - region home - region home <name>

Parameters: - name: Region name

"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"

Usage: - region default - region default {name|<null>}

Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"

Usage: - region put <name> [parent_name]

Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)

Note: In firmware v1.15.0 and later, region put enables flooding for that region by default (you do not need a separate region allowf <name> after each put). On v1.14.0 and earlier, new regions may still require region allowf for flooding-see region allowf.

"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"

Usage: - region def <token> [<token> ...]

Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.

Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. The command is transactional: invalid names, unknown or ambiguous jumps, table overflow, and hierarchy cycles return Err - ... without changing the existing tree.

Existing regions: region def does not clear the existing tree - if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.

Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | - region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.

Example - linear chain (each token becomes a child of the previous):

region def a b c d e\nregion save\n

Example - branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):

region def a b c d|b e f\nregion save\n

Example - transactional error:

region def a b c|nope d\n

The reply is Err - unknown or ambiguous jump: nope. The existing tree is unchanged; re-run with a corrected jump.

Example - flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:

region def a|* b|* c|* d|* e|* f\nregion save\n
"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"

Usage: - region remove <name>

Parameters: - name: Region name

Note: Must remove all child regions before the region can be removed

"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"

Usage: - region list <filter>

Serial Only: Yes

Parameters: - filter: allowed|denied

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"

Usage: - region

Serial Only: For firmware older than 1.12.0

"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"

Example 1: Using F Flag with Named Public Region

region load\n#Europe F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a region named #Europe with flooding enabled - Packets from this region will be flooded to other nodes

Example 2: Using Wildcard with F Flag

region load \n* F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * with flooding enabled - Enables flooding for all regions automatically - Applies only to packets without transport codes

Example 3: Using Wildcard Without F Flag

region load \n*\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * without flooding - This region exists but doesn't affect packet distribution - Used as a default/empty region

Example 4: Nested Public Region with F Flag

region load \n#Europe F\n  #UK\n    #London\n    #Manchester\n  #France\n    #Paris\n    #Lyon\n<blank line to end region load>\nregion save\n

Explanation: - Creates #Europe region with flooding enabled - Adds nested child regions (#UK, #France) - All nested regions inherit the flooding flag from parent

Example 5: Wildcard with Nested Public Regions

region load \n* F\n  #NorthAmerica\n    #USA\n      #NewYork\n      #California\n    #Canada\n      #Ontario\n      #Quebec\n<blank line to end region load>\nregion save\n

Explanation: - Creates wildcard region * with flooding enabled - Adds nested #NorthAmerica hierarchy - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules

"},{"location":"cli_commands/#direct-retry","title":"Direct Retry","text":"

Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets.

The shared state, count, base, and step controls work on repeater, room-server, and sensor firmware. Recent-repeater/SNR controls are repeater-only because the other roles do not keep the repeater reachability table they require.

"},{"location":"cli_commands/#view-or-change-direct-retry-state","title":"View or change direct retry state","text":"

Search terms: enable tx retries, disable tx retries, stop direct retries, turn off retransmissions.

Usage: - get direct.retry - set direct.retry <state>

Parameters: - state: on|off

Default: on

Notes: - New installs and older preference files without direct retry settings default to on with the rooftop preset.

Examples:

get direct.retry\nset direct.retry on\nset direct.retry off\n
"},{"location":"cli_commands/#view-or-change-direct-retry-heard-table-gate","title":"View or change direct retry heard-table gate","text":"

Usage: - get direct.retry.heard - set direct.retry.heard <state>

Parameters: - state: on|off

Default: on

Note: This command is repeater-only. When enabled, the recent repeater table is the direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked.

Examples:

get direct.retry.heard\nset direct.retry.heard on\nset direct.retry.heard off\n
"},{"location":"cli_commands/#view-or-apply-a-retry-preset","title":"View or apply a retry preset","text":"

Search terms: retry profile, retry defaults, rooftop retries, mobile retries, infrastructure retries.

Usage: - get retry.preset - set retry.preset <preset>

Parameters: - preset: infra|rooftop|mobile

Notes: - Applies shared direct retry and flood retry defaults. - infra: fewer, slower retries for stable fixed infrastructure. - rooftop: default long retry window for weak rooftop links. - mobile: long retry count with shorter spacing for moving or changing links; flood retry count is 15. - Changing direct.retry.count, direct.retry.base, direct.retry.step, direct.retry.margin, flood.retry.count, flood.retry.path, or flood.retry.group.path makes the preset report as custom.

Examples:

get retry.preset\nset retry.preset infra\nset retry.preset rooftop\nset retry.preset mobile\n
"},{"location":"cli_commands/#flood-retry","title":"Flood Retry","text":"

Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater.

The count, path, group-data path, and advert controls work on repeater, room-server, sensor, and Companion firmware. Infrastructure forwarding must also be enabled for retries to run. Prefix, ignore, bridge, and bucket controls are repeater-only.

"},{"location":"cli_commands/#hop-zero-flood-retry-coding-rate","title":"Hop-zero flood retry coding rate","text":"

Flood packets with no path hops recorded yet use the same CR escalation ladder as direct retries. The starting point is the active radio CR, since a flood has no single next-hop repeater whose SNR can select a starting CR. Each retry uses its attempt number and the active radio setting; it does not restart the ladder from the previous retry's override. Forwarded packets with one or more recorded hops use the active radio CR on every retry, including bridge retries whose effective path length discounts ignored hops.

The initial transmission uses the active radio CR. This table lists retries after that initial transmission; any later retries remain at CR8:

Starting CR Retry 1 Retry 2 Retry 3 Retry 4 Retry 5+ CR4 CR4 CR5 CR7 CR7 CR8 CR5 CR5 CR7 CR7 CR8 CR8 CR6 CR6 CR7 CR7 CR8 CR8 CR7 CR7 CR7 CR8 CR8 CR8 CR8 CR8 CR8 CR8 CR8 CR8

The ladder is shared across presets. Presets change the retry budget, not the CR steps. At CR5, the maximum hop-zero schedules are:

Preset Configured flood count Hop-zero retries CR schedule after the initial send infra 1 2 CR5, CR7 rooftop 3 6 CR5, CR7, CR7, CR8, CR8, CR8 mobile 15 15 CR5, CR7, CR7, then CR8 for retries 4-15

These budgets apply to group text and the origin login/message family (RESPONSE, TXT_MSG, ANON_REQ, PATH). Existing payload caps, echo cancellation, and retry eligibility still apply: REQ and OTA do not use generic flood retries; other payloads allow at most one retry. An eligible advert at CR5 therefore gets only its first CR5 retry.

set flood.retry.count 0 continues to disable all automatic flood retries, including hop zero. It leaves the initial transmission and all direct-retry settings and CR schedules unchanged. Direct retry's direct.retry.cr switch and SNR thresholds continue to control direct retries only. Per-packet CR overrides are restored after transmission and do not change the saved radio setting or physical preamble.

"},{"location":"cli_commands/#view-or-change-flood-retry-count","title":"View or change flood retry count","text":"

Companion firmware supports flood.retry.count, flood.retry.path, flood.retry.group.path, and flood.retry.advert through its text terminal and framed CLI. These settings persist across reboot. Companion retains its pre-existing defaults when upgrading an older preferences file: count 15, general path gate 1, group path gate off, and self-origin advert retries on. Companion uses the configured count directly, without the infrastructure role's hop-zero and one-hop multipliers. The shared payload caps below still apply. Companion only retries its own adverts; neighbor adverts remain ineligible even with flood.retry.advert on.

The infra, rooftop, and mobile defaults and retry.preset command in this section apply to infrastructure roles. Companion does not expose repeater bridge or prefix retry filters.

On builds with Meshtext channel chat, flood.retry.count also limits retries of the node's own MT channel text. Retries reuse the original encoded packet and stop after hearing a matching forwarded copy with a lower remaining hop limit. This is not a recipient ACK or a delivery guarantee. NodeInfo permits at most one additional transmission, only with nonzero retry count and flood.retry.advert on, and waits an extra minute beyond its airtime-based delay. These settings never enable relaying of received MT traffic or apply MeshCore prefix/bridge rules to MT packets. set mt.hops 0 disables heard-repeat retries, without suppressing the original chat or its first-use NodeInfo. get mt.retry reports retry/heard/exhausted counters, pending work and the configured count without changing settings.

Search terms: flood tx retries, flood retry attempts, flood retransmissions, broadcast retries.

Usage: - get flood.retry.count - set flood.retry.count <count>

Parameters: - count: Base retry attempts after the original send, from 0 to 15. 0 disables flood retry.

Note: Infrastructure roles first calculate their retry count: path count 0 uses count * 2, path count 1 uses count * 1.5 rounded up, and path count 2 and higher uses the configured base count, with a hard cap of 15. Companion uses count directly. A shared payload policy then applies to every build: REQ never retries; GRP_TXT keeps the role-calculated count; remote-login-critical RESPONSE, TXT_MSG, ANON_REQ, and PATH packets keep up to 15 at the originating node (path count 0) and cap at 2 after entering the path; all other flood payload types cap at 1. These caps never raise a lower role-calculated count. Setting count to 0 immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.

Forwarded neighbor adverts have an additional loop guard independent of the advert retry setting. After this node completes an advert transmission and hears a downstream copy with a longer path, it does not forward that same advert again while the advert's signed timestamp is less than six hours old. Self-originated adverts, adverts without a heard echo, and adverts six hours old or older are unaffected.

An enabled self-originated advert retry waits at least one additional minute beyond the normal airtime-aware retry delay. Once a newer self advert has successfully entered the outbound queue, its retry sequence replaces queued or future retries for older self adverts; an older advert already transmitting is allowed to finish. Companion firmware permits this single slow retry for its own adverts while continuing to block retry attempts for neighbor adverts it forwards.

Defaults: - infra: 1 - rooftop: 3 - mobile: 15

Examples:

get flood.retry.count\nset flood.retry.count 0\nset flood.retry.count 15\n
"},{"location":"cli_commands/#view-or-change-flood-retry-path-gate","title":"View or change flood retry path gate","text":"

Usage: - get flood.retry.path - set flood.retry.path <count|off>

Parameters: - count: Maximum flood path hash count eligible for retry, from 0 to 63. - off: Disable the path-length gate.

Defaults: - infra: 1 - rooftop: 2 - mobile: 1

Examples:

get flood.retry.path\nset flood.retry.path 1\nset flood.retry.path off\n
"},{"location":"cli_commands/#view-or-change-the-group-data-flood-retry-path-gate","title":"View or change the group-data flood retry path gate","text":"

Usage: - get flood.retry.group.path - set flood.retry.group.path <count|off>

Parameters: - count: Maximum flood path hash count eligible for retry for group data packets (PAYLOAD_TYPE_GRP_DATA/type 6), from 0 to 63. - off: Disable only the group-data-specific gate. The general flood.retry.path gate still applies.

Default: 1 for infra, rooftop, and mobile presets.

Note: The stricter of flood.retry.path and flood.retry.group.path is used. A value of 1 allows retry sequences at path counts 0 and 1; group data at path count 2 or higher is still forwarded normally but does not start a flood retry sequence. A value of 0 allows retries only at the originating sender.

On infrastructure roles, setting flood.retry.path to 0 also sets flood.retry.group.path to off because the general zero-hop gate is already stricter. While the general gate remains 0, attempts to set the group-data gate keep it off. Applying a named retry preset restores the group-data default of 1. Companion retains the saved group gate, but the stricter general gate still wins.

Examples:

get flood.retry.group.path\nset flood.retry.group.path 1\nset flood.retry.group.path off\n
"},{"location":"cli_commands/#view-or-change-flood-retry-advert-handling","title":"View or change flood retry advert handling","text":"

Usage: - get flood.retry.advert - set flood.retry.advert <on|off>

Parameters: - on: Retry node advert floods. - off: Do not retry node advert floods.

Default: off

Examples:

get flood.retry.advert\nset flood.retry.advert off\n
"},{"location":"cli_commands/#view-or-change-flood-retry-target-prefixes","title":"View or change flood retry target prefixes","text":"

Usage: - get flood.retry.prefixes - set flood.retry.prefixes <prefixes|none|off>

Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.

Note: When set, non-bridge flood retry only accepts same-packet echoes whose last hop matches one of these prefixes. When unset, any non-ignored last hop can cancel the retry.

Examples:

get flood.retry.prefixes\nset flood.retry.prefixes A58296,860CCA,425E5C\nset flood.retry.prefixes none\n
"},{"location":"cli_commands/#view-or-change-flood-retry-ignored-prefixes","title":"View or change flood retry ignored prefixes","text":"

Usage: - get flood.retry.ignore - set flood.retry.ignore <prefixes|none|off>

Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.

Note: Non-bridge flood retry does not cancel on same-packet echoes whose last hop matches this list. Bridge mode also excludes these prefixes from bucket and other hits.

Examples:

get flood.retry.ignore\nset flood.retry.ignore 71CE82,C7618C\nset flood.retry.ignore none\n
"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-mode","title":"View or change flood retry bridge mode","text":"

Usage: - get flood.retry.bridge - set flood.retry.bridge <on|off>

Note: Bridge mode retries until each configured fresh bucket, plus the non-source other bucket, has been heard or the retry count is exhausted. If prefixes in different buckets share their first byte, configuration commands return a warning because a 1-byte path cannot distinguish those buckets. The configuration remains valid: an ambiguous source is treated as belonging to every matching source bucket, and an ambiguous echo credits every matching target bucket so it cannot force retry exhaustion.

Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random 0-200% of one additional packet airtime on every attempt. This de-synchronizes repeaters that may have missed the same echo while capping the added wait at two frames.

Only one active retry sequence is kept for a given logical flood packet. An identical flood can still transmit normally, but it does not create a second sequence of extra attempts. Retry state is released if a queued packet is evicted, and the final echo window retains metadata without reserving a packet-pool entry.

Bridge reachability learned from earlier hops in a successful echo is cached separately from recent.repeater. Only the final RF hop updates recent.repeater and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.

Examples:

get flood.retry.bridge\nset flood.retry.bridge on\n
"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-buckets","title":"View or change flood retry bridge buckets","text":"

Usage: - get flood.retry.bucket.<n> - set flood.retry.bucket <n> <prefixes|none|off>

Parameters: - n: Bucket number from 1 to 6. - prefixes: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket. - none or off: Clear the bucket.

Examples:

get flood.retry.bucket.1\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 none\n
"},{"location":"cli_commands/#view-or-change-direct-retry-count","title":"View or change direct retry count","text":"

Search terms: tx retries, transmit retries, direct retries, retry count, retry attempts, retransmission count, message retries, DM retries.

Usage: - get direct.retry.count - set direct.retry.count <count>

Parameters: - count: Maximum retry attempts after the original send, from 1 to 15.

Default: 15 with the rooftop preset

Note: This setting limits retries for eligible direct-routed packets such as traces, requests, responses, and ACKs. Direct-routed text messages (type 2) are an exception: they allow up to 21 retries after the original send, regardless of direct.retry.count or the repeater short-path cap. For example, set direct.retry.count 1 limits eligible non-text packets to one retry, but does not reduce the text-message limit.

These are maximums, not a fixed number of transmissions. Retries stop early when the node hears the next hop forward the packet. The special final-hop retry sends only one duplicate because the destination does not forward the packet. Repeater non-text retries are also capped at 8 for a retry path of up to 3 hops, 12 for 4 hops, and 15 for longer paths; these caps never increase a lower configured count.

Use set direct.retry off to disable this node's direct retries, including text-message retries, and set direct.retry on to enable them again. A count of 0 is not supported. Sending apps and other nodes may have their own retry behavior; this setting does not change it.

Examples:

get direct.retry.count\nset direct.retry.count 1\nset direct.retry.count 4\nset direct.retry.count 15\n
"},{"location":"cli_commands/#view-or-change-direct-retry-base-delay","title":"View or change direct retry base delay","text":"

Search terms: retry delay, retry timeout, retransmission timeout, wait between retries.

Usage: - get direct.retry.base - set direct.retry.base <ms>

Parameters: - ms: First retry wait in milliseconds, from 10 to 5000.

Default: 175 with the rooftop preset

Explanation: - The first retry waits for base + packet-length add-on + random forwarding jitter after the preceding transmission completes. - TRACE and ANON_REQ/type 7 packets use a 3x line-time add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured base with the same packet-type add-ons. - Larger values reduce channel pressure and give slow repeaters more time. - Smaller values recover faster but create tighter retry bursts.

Examples:

get direct.retry.base\nset direct.retry.base 175\nset direct.retry.base 275\nset direct.retry.base 500\n
"},{"location":"cli_commands/#view-or-change-direct-retry-step-delay","title":"View or change direct retry step delay","text":"

Search terms: retry backoff, increasing retry delay, retry interval step.

Usage: - get direct.retry.step - set direct.retry.step <ms>

Parameters: - ms: Extra milliseconds added for each subsequent retry, from 0 to 5000.

Default: 100 with the rooftop preset

Explanation: - Retry delay is base + packet-length add-on + random forwarding jitter + attempt_index * step. - TRACE and ANON_REQ/type 7 packets use a 3x packet-length add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured step with the same packet-type add-ons. - With base=175 and step=100, the fixed portion is 175, 275, 375, 475 ms, and so on, before the packet-length add-on and random jitter. - step=0 keeps every retry at the same delay. - Larger steps spread retries over time and are safer on busy channels.

Examples:

get direct.retry.step\nset direct.retry.step 0\nset direct.retry.step 50\nset direct.retry.step 100\nset direct.retry.step 250\n
"},{"location":"cli_commands/#view-or-change-direct-retry-snr-margin","title":"View or change direct retry SNR margin","text":"

Search terms: retry signal threshold, retry SNR threshold, retry signal margin.

Usage: - get direct.retry.margin - set direct.retry.margin <snr_db>

Parameters: - snr_db: Extra SNR margin above the SF receive floor, from 0 to 40.

Default: 5.00 with the rooftop preset

Notes: - This command is repeater-only. - Unknown repeaters are still retried. - Known repeaters below the receive floor plus this margin are skipped. - Failed attempts lower the recent repeater SNR estimate by 0.25 dB.

Examples:

get direct.retry.margin\nset direct.retry.margin 0\nset direct.retry.margin 2.5\nset direct.retry.margin 5\nset direct.retry.margin 10\n
"},{"location":"cli_commands/#view-or-change-adaptive-direct-retry-coding-rate","title":"View or change adaptive direct retry coding rate","text":"

Usage: - get direct.retry.cr - set direct.retry.cr off - set direct.retry.cr on (room-server and sensor) - set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>

Parameters: - cr4_min: Minimum SNR in dB to retry at CR4. - cr5_min: Minimum SNR in dB to retry at CR5. - cr7_min: Minimum SNR in dB to retry at CR7. - cr8_max: Maximum SNR in dB that forces CR8.

Default: 10.00,7.50,2.50,2.50

Explanation: - Higher SNR uses faster coding rates. - Lower SNR uses more robust coding rates. - Repeater retry attempts escalate from the adaptive starting CR. CR4 starts as CR4, CR5, CR7, CR7, then CR8. CR5 starts as CR5, CR7, CR7, then CR8. CR7 gets two attempts, then CR8. - Repeater adaptive CR selection intentionally skips CR6. - Non-repeater retry packets start at the current radio CR and follow the same escalation pattern, clamped at CR8. With the normal CR5 radio setting this is CR5, CR7, CR7, then CR8. - Room-server and sensor firmware accept on or off; numeric SNR thresholds are repeater-only because those roles do not keep recent-repeater SNR data. - off disables per-packet direct-retry CR overrides and uses the current radio CR. - Retry packets may use a different coding rate, but they keep the radio's normal physical preamble. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - A failed unknown repeater is seeded at +2.75 dB. - Each later failure lowers the SNR estimate by 0.25 dB.

Examples:

get direct.retry.cr\nset direct.retry.cr off\nset direct.retry.cr on\nset direct.retry.cr 10.0,7.5,2.5,2.5\nset direct.retry.cr 12.0,8.0,4.0,1.0\nset direct.retry.cr 8.0,5.0,1.5,0\nset direct.retry.cr 6.0,3.0,0,-2.0\nset direct.retry.cr 20.0,12.0,6.0,2.0\nset direct.retry.cr 4.0,2.0,0,-4.0\n

Example profiles: - Conservative weak-link profile:

set direct.retry.cr 12.0,8.0,4.0,1.0\n
set direct.retry.cr 10.0,7.5,2.5,2.5\n
set direct.retry.cr 6.0,3.0,0,-2.0\n
set direct.retry.cr 20.0,12.0,6.0,2.0\n
"},{"location":"cli_commands/#view-seed-or-clear-the-recent-repeater-table","title":"View, seed, or clear the recent repeater table","text":"

Usage: - get recent.repeater - get recent.repeater <page> - get recent.repeaters <page> - get recent.repeaters search <prefix> [page] - set recent.repeater <prefix> [snr_db] - clear recent.repeater

Parameters: - prefix: Repeater path-hash prefix as 2, 4, or 6 hex characters. - snr_db: Optional SNR in dB. If omitted or invalid, defaults to 3.0. - page: 1-based result page.

Note: These commands are repeater-only.

The default capacity is 256 entries on classic ESP32, 2,048 on other ESP32 chips, 512 on nRF52, and 64 on other platforms. Builds can override it with MAX_RECENT_REPEATERS. Classic ESP32's history uses 3,072 bytes of startup heap instead of 24,576 bytes, leaving more memory for the packet pool and Wi-Fi.

Output order: - get recent.repeater lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes. - Within each prefix length, entries are sorted from highest SNR to lowest SNR. - search returns every overlapping path-hash entry. For example, searching 860C can return 86, 860C, and 860CCA; it does not return a different branch such as 86D0. - Search rows include the monotonic age of the entry's most recent recording, compacted to a whole s, m, or h field. Search pages contain up to six rows so the result remains within the remote CLI reply limit.

SNR details: - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: 75% existing SNR and 25% new heard SNR, rounded up. - Direct retry success also feeds the heard echo SNR back into the same weighted table. - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by 0.25 dB. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at +2.75 dB. - set recent.repeater <prefix> [snr_db] seeds a missing prefix or adds another weighted sample for an existing prefix. - Successful set recent.repeater replies include the stored prefix and SNR, for example OK - set A1B2C3 at 3.0 SNR. - Entries strictly older than 24 hours are removed during a sweep every three hours, so an entry can remain for at most approximately 27 hours.

Examples:

get recent.repeater\nget recent.repeater 2\nget recent.repeaters search 86\nget recent.repeaters search 860C page 2\nset recent.repeater A1B2C3 8.5\nset recent.repeater 71CE82 -3.25\nset recent.repeater A1B2C3\nclear recent.repeater\n
"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"

Search terms: enable GPS, disable GPS, turn GPS on, turn GPS off.

Usage: - get gps - set gps <state>

Parameters: - state: on|off

Default: off

Note: These command names are shared with GPS-capable Companion builds. Companion reports on or off; infrastructure includes receiver details: - off, {powered|unpowered} when GPS is manually disabled - on, {powered|unpowered}, {fix|no fix}, {sat count} sats when GPS is enabled

powered describes the supply, not proof that the receiver is tracking or asleep. Shared-power hardware can remain powered while GPS processing is off.

On repeaters, gps off cancels any acquisition already in progress. A later authorized location-telemetry request may wake the receiver, but only until its first valid fix is cached; GPS processing then stops again. Whether its supply can also turn off depends on the board's power policy below.

"},{"location":"cli_commands/#rak4631-shared-peripheral-power","title":"RAK4631 shared peripheral power","text":"

Usage:

get peripherals.mode\nset peripherals.mode gps-only\nset peripherals.mode shared\nreboot\n

Default: gps-only on RAK4631. The saved mode takes effect only after reboot; setting it does not switch power immediately. get peripherals.mode reports the saved choice, which may differ from the current boot's mode.

Ethernet/PoE builds and detected external NOR storage automatically keep the rail on, even in gps-only mode or when the network service is disabled. RAK3401 permanently protects the same rail because its radio frontend needs it; peripherals.mode is a RAK4631 setting, not an override of that protection.

On a powered shared rail, RAK12501/L76K cannot be independently switched off: GPS-off stops MCU parsing, not physical GNSS operation. Other receivers may use internal power-saving commands, but shared does not promise physical sleep. UART1 bridge ownership remains protected; changing this mode does not grant permission to connect two transmitters to the GPS UART.

"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"

Usage: - gps sync

The GPS must be enabled. When GPS power saving has put an enabled receiver to sleep, this command schedules a sync and wakes it; after set gps off, it reports gps is off without scheduling work.

On repeaters with device power saving enabled, the automatic GPS cycle wakes for up to 10 minutes and repeats seven days after GPS sleeps. A valid GPS time can correct the RTC either forward or backward. With device power saving off, an enabled GPS refreshes the RTC every 30 minutes. These are the legacy defaults; gps.sync.interval below overrides them.

"},{"location":"cli_commands/#set-the-automatic-gps-clock-sync-interval","title":"Set the automatic GPS clock-sync interval","text":"
get gps.sync.interval\nset gps.sync.interval 1\nset gps.sync.interval 24\nset gps.sync.interval 336\n

The interval is a whole number of hours, from 1 to 336 (two weeks). Larger numbers are capped, not rejected. For example:

set gps.sync.interval 999\nOK - GPS sync interval 336 hours (saved)\n

Zero, negative numbers, fractional hours, and nonnumeric values are rejected. The setting is saved across reboots and supported by GPS-equipped repeater, room-server, sensor, and Companion builds. Until it is explicitly configured, the getter reports default (board GPS sync policy) and existing board timings are retained.

The selected interval controls periodic clock acquisitions both with GPS power saving and with a continuously powered receiver. Changing it reschedules a sleeping receiver without immediately powering it on. A GPS fix can take time; an unsuccessful acquisition retains the existing bounded awake window, then backs off before trying again. Device GPS/power switches still apply.

This does not change position-update gps_interval, location telemetry/cache timings, or mesh/NTP time synchronization. Location requests can still power GPS for a fix, but do not bypass the configured clock-sync interval. Explicit gps sync remains an immediate acquisition request where that command exists.

"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"

Usage: - gps setloc

"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"

Usage: - gps advert - gps advert <policy>

Parameters: - policy: none|share|prefs - none: don't include location in adverts - share: share gps location (from SensorManager) - prefs: location stored in node's lat and lon settings

Default: prefs on every repeater, room-server, and sensor build that uses the common advert policy. A previously saved none, share, or prefs choice still overrides the first-boot default after an update.

"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-telemetry-access-mode","title":"View or change telemetry access mode","text":"

Usage: - get telemetry.access - set telemetry.access <mode>

Parameters: - mode: all|acl - all: allow telemetry requests using the requester-provided telemetry mask - acl: require ACL read-only or higher for telemetry, including GPS

Default: all

Note: all matches the previous sensor telemetry behavior.

"},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"

Usage: sensor list [start]

Parameters: - start: Optional starting index (defaults to 0)

Note: Output format: <var_name>=<value>\\n

"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"

Usage: - sensor get <key> - sensor set <key> <value>

Parameters: - key: Sensor setting name - value: The value to set the sensor to

"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":"

For a two-repeater setup and diagnostic flow, see the ESP-NOW bridge setup guide or RS-232/UART bridge guide. For an existing WiFi network, see the eight-peer UDP bridge guide.

"},{"location":"cli_commands/#wifi-udp-bridge","title":"WiFi UDP bridge","text":"

Available on supported ESP32 repeater images with WITH_UDP_BRIDGE. UDP starts off on new/upgraded installs. It requires an existing station WiFi connection, and pauses during AP/AP+STA operation, disconnection, browser OTA and ownership transitions. It does not start a network or require an MQTT broker.

get udp.enabled\nset udp.enabled on\nset udp.enabled off\nget udp.port\nset udp.port 4404\nget udp.peer.1\nset udp.peer.1 192.168.1.51:4404\nset udp.peer.1 off\nget udp.status\nget udp.stats\nget udp.stats rx\nget udp.stats tx\nget udp.stats drops\nget udp.stats test\n

Peer slots are udp.peer.1 through udp.peer.8. Each destination is one numeric unicast IPv4 address plus a port 1..65535. The listening port defaults to 4404. Multicast, loopback, unspecified/broadcast and duplicate endpoints are rejected. Both boards must list each other's listening address/port. Reserve DHCP addresses so the stored destinations remain valid.

The transport reuses bridge.secret for authenticated sessions and packet integrity; no additional password is introduced. UDP payloads are not encrypted. Counter/status output does not reveal the secret. Config writes use the existing authenticated CLI policy and a separate checked preferences file; a successful save reports intent rather than reachability.

Local-only diagnostic actions:

udp test 1\nudp test 1 A1B2C3D4\nudp resetstats\n

The peer number is 1..8; the optional ID is exactly eight hexadecimal digits without 0x. A queued/sent probe is not a delivery acknowledgement. On the destination, check get udp.stats test for an increased rx and matching rxid, then reverse the test. An authenticated peer session must be ready. The status ready count records established sessions, not current liveness; use a fresh received probe to confirm reachability. Incoming UDP enters the local LoRa mesh and is not relayed to other UDP peers. Probes do not echo, retry, allocate mesh packets or transmit on LoRa. Resetting statistics does not write preferences or restart the bridge.

Both set usb.logging on and set usb.debug on enable bounded runtime USB summaries without requiring a special debug build. Restore both previous settings after capture. CLI pages/probes work with logging disabled.

WiFi client isolation and firewall rules must allow peer-to-peer UDP; the firmware cannot bypass an isolating guest network. Isolation is an optional network policy, not an intrinsic WiFi restriction. See the setup and troubleshooting guide.

"},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"

Usage: get bridge.type

"},{"location":"cli_commands/#diagnose-the-esp-now-bridge","title":"Diagnose the ESP-NOW bridge","text":"

Available when the optional ESP-NOW bridge is compiled in. These are runtime diagnostics; no BRIDGE_DEBUG build flag is required.

Read-only commands:

get espnow.status\nget espnow.stats\nget espnow.stats rx\nget espnow.stats tx\nget espnow.stats drops\nget espnow.stats test\n

Status reports local state (off, teardown-error, driver-error, channel-mismatch or ready), then these compact fields:

Field Meaning ch Configured/actual 2.4 GHz channel; actual 0 means unavailable. fmt, src Packet format and configured LoRa packet source (rx or tx). wifi Shared WiFi enable setting, not proof of WiFi association. mode, ps, proto WiFi SDK mode and power-save integers, and protocol bitmask in hexadecimal. An unavailable mode or power-save query returns -1. q Current receive/transmit queue depths. last Last recorded SDK error stage/code, which may remain after recovery.

espnow.running on means the local bridge initialized; it does not prove a peer is reachable. The statistics pages use these keys:

Page Keys and meaning Default rx: received callback frames; mesh: inbound MeshCore packets queued; tx: outbound MeshCore packets queued; sent: successful frame callbacks. rx frames: received callback frames; decoded: parsed MeshCore packets; mesh: queued for mesh processing; crc: checksum failures; bad: malformed frames; parse: packet parse failures. tx req: mesh TX requests; queued: mesh packets queued; start: SDK-accepted frames; ok: successful frame callbacks; err: SDK start failures; cbfail: failed frame callbacks; timeout: callback timeouts. drops rxq: RX queue drops; foreign: foreign-group packets; filter/dupe: inbound filter/duplicate drops; txq: recorded local mesh TX rejection/final-failure decisions; txfilter/txdupe: outbound filter/duplicate drops; big: oversize TX drops. test queued, start, ok, fail, timeout: probe TX stages; rx, bad: valid/invalid probes received; txid, rxid: last queued/received IDs in hexadecimal.

Mesh admission means queued for normal processing, not confirmed LoRa forwarding or end-to-end delivery. Packet and frame counters have different units: fragmentation can produce several frames for one mesh packet. Counters survive bridge stop/start; reboot or the explicit reset starts a new window. Stopping or restarting the bridge clears pending queue work without classifying its delivery. These counters are not a complete end-to-end delivery ledger.

Local-only actions:

espnow test\nespnow test A1B2C3D4\nespnow resetstats\n

The optional test ID is exactly eight hexadecimal digits, case-insensitive, without 0x, spaces or trailing arguments. Omitting it uses uptime in milliseconds. The reply OK - test queued id=...; confirm on peer confirms local queueing only. Read the other updated board's get espnow.stats test and match rxid, with an increased rx, to establish one-way probe receipt. Send a different ID from the other board to test the reverse direction.

A probe is unencrypted, CRC-checked transport diagnostic data, not a MeshCore packet or a security token. It does not verify packet-format compatibility or bridge.secret, authenticate the peer or establish normal packet delivery. There is no automatic echo, retry or LoRa forwarding; older firmware ignores it. Resetting statistics does not restart the bridge or write preferences. Test/reset commands are rejected over LoRa; use a local console. Every reply fits the bounded CLI frame, and diagnostics do not print from WiFi callbacks.

For a short USB debug capture, record both get usb.logging and get usb.debug, then enable both set usb.logging on and set usb.debug on. Run the test and restore both original settings. The ordinary image emits bounded ESPNOW: updates from its main loop at most once per second when diagnostic state changes, even with BRIDGE_DEBUG=0. Updates can be skipped when USB is busy; read the counter pages for exact values. The CLI counter pages and probes do not require either logging gate.

See the two-board diagnostic flow. Do not include get bridge.secret or WiFi credentials in shared debug logs.

"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"

Search terms: enable bridge, disable bridge, serial bridge, RS232 bridge.

Usage: - get bridge.enabled - get bridge.running - set bridge.enabled <state>

Parameters: - state: on|off

bridge.enabled is the saved intent. bridge.running reports whether the bridge actually started in this boot; they can differ after a hardware conflict, missing credentials, or a transient initialization failure. Normal merged repeater images default to off; dedicated bridge images may default to on.

Full MQTT repeaters with RS-232 expose three independent transports. Use set rs232.enabled on|off and get rs232.enabled for saved UART intent; get rs232.running reports the actual UART state. mqtt.enabled and espnow.enabled select the other two transports. In MQTT images the historical bridge.enabled alias controls ESP-NOW; in non-MQTT UART images it controls RS-232. bridge.baud and bridge.uart change only UART configuration, without restarting MQTT or ESP-NOW. The new UART mode starts off when upgrading old MQTT preferences and retains explicit settings across reboots.

"},{"location":"cli_commands/#diagnose-the-rs232-bridge","title":"Diagnose the RS232 bridge","text":"

Available on repeater builds containing the optional RS-232 bridge. These runtime diagnostics require no BRIDGE_DEBUG build flag. Builds without the transport do not gain UART support from these commands.

Read-only commands:

get rs232.transport\nget rs232.stats\nget rs232.stats rx\nget rs232.stats tx\nget rs232.stats drops\nget rs232.stats test\n

The existing get rs232.status keeps its heartbeat/automatic-standby report. If no UART bridge object exists, such as after explicitly disabling it, the transport and counter commands report RS232 diagnostics unavailable; use rs232.enabled, rs232.running and rs232.status to check saved intent and power state. get rs232.transport reports the local transport state (ready, off, standby, probing or retrying) and these fields:

Field Meaning baud Configured serial baud rate. pins Selected RX/TX pin numbers. src Configured LoRa packet source, rx or tx. rxpart, rxready Bytes held in the current RX frame and whether a complete frame awaits mesh-pool space. tx Bytes remaining in the owned TX frame. heartbeat on initiates heartbeats; passive can still answer a valid ping. last Last recorded diagnostic error: none, pins, driver, test.stopped, test.busy or test.timeout. It can remain after recovery.

A running UART means local initialization succeeded, not that a peer is reachable. Counter pages separate ordinary mesh traffic from explicit probes:

Page Keys and meaning Default rx: received data frames; mesh: inbound packets admitted to mesh processing; tx: outbound mesh packets queued; sent: mesh frames fully accepted by the local UART. rx bytes: all consumed UART bytes; frames: checksum-valid data frames; crc: checksum failures; length: invalid lengths; parse: mesh-packet parse failures; timeout: incomplete-frame expiry; pool: complete frames held for mesh-pool space. tx req: mesh TX requests; queued: mesh frames accepted into the owned buffer; sent: fully UART-accepted mesh frames; bytes: all UART-accepted bytes; stall, short: blocked drain attempts or short-write events. drops noise: synchronization rejects; filter/dupe: inbound filter/duplicate drops; busy/stopped: mesh TX rejected by transport state; txfilter/txdupe: outbound filter/duplicate drops; big: invalid or oversize TX packets. test queued, sent, fail: probe TX stages and recorded failures; a late write can count as both failed and sent; rx, bad: valid/invalid probes received; txid, rxid: last queued/received IDs in hexadecimal.

Mesh admission means queued for normal processing, not confirmed LoRa forwarding or end-to-end delivery. Local write acceptance does not prove the bytes reached the peer. Byte totals include data, heartbeat and test traffic; RX bytes also include discarded noise. A frame can cause several write stalls. Counters survive same-object UART stop/start, including automatic standby. Disabling or reconfiguring the bridge can delete and replace that object, starting a new window; reboot and explicit reset also clear the counters. Stopping a transport can discard unfinished work without determining delivery, so the pages are not a complete delivery ledger.

Local-only actions:

rs232 test\nrs232 test A1B2C3D4\nrs232 resetstats\n

The optional ID is exactly eight hexadecimal digits, case-insensitive, without 0x, spaces or trailing arguments. Omitting it uses uptime in milliseconds. OK - test queued id=...; confirm on peer confirms local queueing only. On the other updated board, get rs232.stats test must show an increased rx and matching rxid. Use a different ID in the reverse direction.

Probes use a reserved UART-local control frame. They do not allocate a mesh packet, enter duplicate tracking, refresh peer liveness, authenticate a peer, or get forwarded onto LoRa. There is no automatic echo or retransmission. A probe that misses its write deadline records one failure. If no byte was accepted by the UART, it is cancelled. If a prefix was already accepted, the bridge retains and drains only the original suffix, with bounded work per main-loop pass, before another frame can start. This preserves framing for older receivers. A later completion also increments sent; the historical failure is retained and neither result proves peer receipt. A stopped, transitioning or busy UART, including a due heartbeat, rejects the test rather than waking or taking over the port. Older firmware ignores the extension. The test and counter reset do not write preferences and are rejected over LoRa; use a local console.

For a short USB capture, record both get usb.logging and get usb.debug, then enable both set usb.logging on and set usb.debug on. The ordinary image emits bounded RS232: updates from its main loop at most once per second when diagnostic state changes. Busy USB output can skip updates; use the counter pages for exact values. Restore both original settings afterward. The CLI pages and test probes need neither logging gate.

See the two-board serial diagnostic flow.

"},{"location":"cli_commands/#rs232-heartbeat-and-automatic-standby","title":"RS232 heartbeat and automatic standby","text":"

Available on repeater builds with an RS232 bridge. This is a UART-local control exchange, not an advert or a LoRa packet. Room builds without RS232 are unaffected.

Usage:

Heartbeat and negotiated automatic standby are on by default, including upgrades. An active UART pings every 30 seconds (52 bytes per ping). Stock bridges ignore that traffic and still receive the original data frames; they are not automatically put to sleep: standby is armed only after a fresh heartbeat reply proves support. Fresh, session/challenge-matched replies prove a bidirectional peer; ordinary valid incoming data can keep a legacy link awake but cannot prove heartbeat support. The status separates saved intent, actual UART state, standby/probe phase, and confirmed versus legacy/unknown peer liveness.

Enable on both updated peers:

set rs232.enabled on\nset rs232.heartbeat on\nset powersaving on\nset rs232.idle 300\nset rs232.autosleep on\nget rs232.status\n

With heartbeat, autosleep, and global powersaving all on and the peer's heartbeat support confirmed, when no peer responds for the idle interval the UART is soft-stopped while rs232.enabled stays on. It wakes after 30 seconds for a 2-second probe; every eighth probe listens for 40 seconds. That longer window spans the other side's sleep interval, so two sleeping updated peers can reconnect without manual intervention (normally within about five minutes with working wiring and timely loop servicing). Healthy peers remain running; this is dead-link power saving, not an idle-data duty cycle.

Stock receivers ignore these control frames; they are deliberately invalid zero-length stock frames with an ASCII-only remainder. They do not allocate a mesh packet or enter LoRa forwarding. Stock peers do not answer heartbeats, so default negotiated standby never arms on a stock-only link. Valid legacy data after heartbeat replies cease also disarms negotiated standby, allowing a replacement stock peer to stay awake. Use set rs232.autosleep force only to sleep an absent/unconfirmed UART peer; this explicitly bypasses stock protection and can miss one-off stock packets. If a previously confirmed updated peer is silently replaced with stock while the local UART is already sleeping, its first packet can be missed before replacement is detected. Set autosleep off before that change, or restart the local bridge to clear the old negotiated session. While the UART is stopped it cannot receive a wake byte; mesh traffic during standby is not queued for later UART delivery. Turning autosleep or global powersaving off resumes the saved-enabled UART; rs232.enabled off remains an explicit off and never automatically wakes. rs232.heartbeat off stops initiating heartbeats and prevents standby. An updated active peer can still answer a valid incoming ping in passive mode.

"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"

Usage: - get bridge.delay - set bridge.delay <ms>

Parameters: - ms: Delay in milliseconds (0-10000)

Default: 500

"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"

Usage: - get bridge.source - set bridge.source <source>

Parameters: - source: - rx: bridges received packets - tx: bridges transmitted packets

Default: tx

Note: For MQTT, use mqtt.rx and mqtt.tx to control each direction independently. Both can be enabled together. Use mqtt.enabled as the MQTT master switch; bridge.* configures RS232 and ESP-NOW bridges.

"},{"location":"cli_commands/#view-or-change-the-mqtt-master-switch","title":"View or change the MQTT master switch","text":"

Search terms: enable MQTT, disable MQTT, MQTT on, MQTT off, WiFi logging.

Usage:

These commands are the same on MQTT-capable Companion, Repeater, Room Server, and Sensor builds. The master switch is saved; turning it off disconnects MQTT while keeping broker presets and credentials. Turning it on allows those brokers to reconnect. get mqtt.running checks the service's runtime state; get mqtt.status reports individual broker connections. Enabling MQTT does not imply that a broker is connected.

Custom mqtts:// and wss:// brokers require an image with the embedded CA bundle. Without it, the slot stays disconnected and sends no credentials; use a build with the bundle or a supported preset with its pinned CA. Portable MQTT images retain those presets but do not include the general bundle for custom TLS brokers. Explicit mqtt:// and ws:// configurations remain available for trusted networks and use plaintext transport.

On ESP32 Repeater and Room Server images, set mqtt.enabled off acknowledges the stop request while the MQTT task disconnects and destroys its clients. get mqtt.stopping stays on until that cleanup completes; mqtt.running also stays on while those resources are still owned. Wait for mqtt.stopping to become off before enabling MQTT again. UART, ESP-NOW, and the radio continue running during this cleanup. OTA still waits for a completed MQTT shutdown before it can write firmware.

USB logging remains independent. On images with both outputs, set logging.output off|usb|wifi|both selects USB and MQTT together.

"},{"location":"cli_commands/#view-or-change-mqtt-rx-packet-uplinking","title":"View or change MQTT RX packet uplinking","text":"

Search terms: MQTT receive logging, MQTT RX capture, publish received packets.

Usage: - get mqtt.rx - set mqtt.rx <on|off>

Parameters: - on: uplink received (RX) packets to MQTT brokers - off: disable RX packet uplinking

Default: on

"},{"location":"cli_commands/#view-or-change-mqtt-tx-packet-uplinking","title":"View or change MQTT TX packet uplinking","text":"

Search terms: MQTT transmit logging, MQTT TX capture, publish sent packets.

Usage: - get mqtt.tx - set mqtt.tx <on|off|advert>

Parameters: - on: uplink all transmitted (TX) packets to MQTT brokers - advert: uplink only this node's own advert packets (self-originated advertisements only - forwarded adverts from other nodes are filtered out) - off: disable TX packet uplinking

Default: advert

Note: mqtt.rx and mqtt.tx take effect immediately - no restart required. Both can be enabled simultaneously.

"},{"location":"cli_commands/#view-or-change-mqtt-advert-signature-filtering","title":"View or change MQTT advert signature filtering","text":"

Usage:

Parameters:

Default: off

Note: Adverts are uploaded before the mesh validates them, so a frame corrupted over the air that still passes the LoRa CRC is published with a mangled name or location. Leaving this off keeps that corruption visible to collectors; turning it on hides it from them. Dropped adverts are counted as badsig= in get mqtt.stats. Takes effect immediately.

On Companion builds, enabling this setting opts into a marked MQTT preference record. Before rolling back to firmware without this setting, turn it off to restore the older record format and retain the saved MQTT setup. Ordinary default-off updates keep the existing record size.

"},{"location":"cli_commands/#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature","title":"View or change periodic neighbors publishing (MQTT observer, neighbors feature)","text":"

Usage: - get mqtt.neighbors - set mqtt.neighbors <on|off>

Parameters: - on: periodically discover neighbor scopes and publish the neighbor table to the neighbors topic - off: disable periodic neighbors publishing

Default: off

Note: Requires a build with the neighbors feature compiled in (all PSRAM boards, plus non-PSRAM boards built with MQTT_NEIGHBORS_WITHOUT_PSRAM); elsewhere this replies Err - neighbors not enabled in this build. Non-PSRAM builds publish at most 20 neighbours per pass to bound internal-DRAM use, and set truncated with the true total_neighbors when the table is larger. The setting is read live by the mesh loop - no restart required; enabling it triggers a discovery on the next pass. While enabled, get mqtt.status gains a trailing nbr: <next>/<last> field (time to next publish, and how the last publish went).

"},{"location":"cli_commands/#view-or-change-the-neighbors-publish-interval-mqtt-observer-neighbors-feature","title":"View or change the neighbors publish interval (MQTT observer, neighbors feature)","text":"

Usage: - get mqtt.neighbors.interval - set mqtt.neighbors.interval <hours>

Parameters: - hours: how often to publish the neighbor table (12-336, default 24)

Default: 24 (hours)

Note: Out-of-range values are rejected (not clamped). Requires a build with WITH_MQTT_NEIGHBORS; PSRAM boards enable it automatically and selected non-PSRAM variants opt in with MQTT_NEIGHBORS_WITHOUT_PSRAM.

"},{"location":"cli_commands/#view-or-change-the-ntp-server-mqtt-observer-only","title":"View or change the NTP server (MQTT observer only)","text":"

Usage: - get mqtt.ntp - set mqtt.ntp <hostname> - set mqtt.ntp none

Description: Sets the primary NTP server used for clock sync (required for JWT MQTT auth). On set, the device attempts an immediate sync of the just-configured server (primary only, so a typo fails fast) when WiFi is connected and the MQTT bridge is running.

Fallbacks: If the primary fails, the firmware tries pool.ntp.org, time.google.com, time.cloudflare.com, time.aws.com, and time.nist.gov in order (skipping duplicates).

Default: pool.ntp.org (when unset or none)

"},{"location":"cli_commands/#diagnose-ntp-server-connectivity-mqtt-observer-only","title":"Diagnose NTP server connectivity (MQTT observer only)","text":"

Usage: - get mqtt.ntp.diag

Description: Probes every configured NTP server (the custom primary, if set, plus the built-in fallbacks) and reports whether each responds. This is a pure connectivity diagnostic - it does not change the system clock.

Requires WiFi connected and the MQTT bridge running.

"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"

Usage: - get bridge.baud - set bridge.baud <rate>

Parameters: - rate: Integer baud rate from 9600 through the board's compiled BRIDGE_MAX_BAUD (commonly 500000); for example 115200. Stop the bridge with set rs232.enabled off in a combined MQTT image, or set bridge.enabled off in a non-MQTT UART image, before changing it. Enable the UART again with the matching switch.

Default: 115200

"},{"location":"cli_commands/#view-or-change-the-uart-used-by-the-bridge-rs-232-only","title":"View or change the UART used by the bridge (RS-232 only)","text":"

Usage: - get bridge.uart - set bridge.uart <port>

Parameters: - port: Hardware UART number compiled for the board. Most boards expose one fixed UART. RAK4631 accepts 1 or 2; UART 2 is the default so UART 1 can remain available to the RAK12501/L76K GPS. RAK12500 GPS uses I2C rather than this UART, but the explicit legacy Serial1 bridge omits the combined GPS provider and therefore does not expose either GPS path.

The setting is persistent and restarts an enabled bridge immediately. Normal repeater artifacts start with bridge.enabled off; configure the UART and baud rate before running set bridge.enabled on. Combined MQTT images use rs232.enabled for that independent UART setting. On the canonical RAK4631 runtime image, UART 1 is reserved even if the bounded boot probe hears no RAK12501. Silence cannot prove that a cold L76K is physically absent, and that module remains powered by the shared WB_IO2/3V3_S rail. Use UART 2. UART 1 requires an explicit no-GPS/dedicated Serial1 bridge image. This fail-closed reservation also applies when the detected GPS is an I2C RAK12500; that receiver does not electrically use UART 1, but its presence cannot rule out another silent UART module.

"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"

Usage: - get bridge.channel - set bridge.channel <channel>

Parameters: - channel: Channel number from 1 through 13 in either format. This matches the primary-ESP-NOW policy and the default regulatory range used by the supported ESP32 targets.

This controls the optional ESP-NOW bridge transport; it does not change a node's primary ESP-NOW mesh channel. Primary-ESP-NOW firmware uses get espnow.channel and set espnow.channel <1-13> instead.

"},{"location":"cli_commands/#view-or-change-the-esp-now-bridge-wire-format","title":"View or change the ESP-NOW bridge wire format","text":"

Usage: - get bridge.format - set bridge.format <format>

Parameters: - format: - wrapped: the original ESP-NOW bridge framing (magic, checksum, and XOR using bridge.secret) - raw: the exact serialized MeshCore packet used by primary ESPNOWRadio firmware

Default: wrapped

The setting is persistent and restarts an enabled bridge immediately. Receive parsing is strict: wrapped accepts only wrapped frames and raw accepts only raw MeshCore frames. Coordinate the format and channel on every ESP-NOW peer; there is no automatic dual-format receive mode because it would permit ambiguous, asymmetric bridge deployments.

Use raw to connect a LoRa-primary *_repeater_bridge_espnow node to Generic_ESPNOW, SenseCapIndicator-ESPNow, or another primary-ESP-NOW node. Raw mode enables the ESP-NOW LR PHY and sends at the same LR rate used by those targets. All nodes must use the same 1-13 channel. On a primary-ESP-NOW node, change that side with set espnow.channel <1-13> and reboot; on the bridge, use set bridge.channel <1-13>.

Raw mode ignores bridge.secret and removes the bridge wrapper's lightweight network isolation. MeshCore's own packet authentication/encryption still applies where the packet type provides it, but public frames and routing metadata remain visible. The wrapped format's XOR is isolation, not strong cryptography. Bridge duplicate tracking remains active in both formats, but deploying multiple gateways between the same LoRa and ESP-NOW coverage areas can still increase duplicate traffic while their seen-packet tables converge.

One ESP-NOW frame carries at most 250 payload bytes. In raw mode, serialized MeshCore packets up to that size remain byte-for-byte compatible with existing raw endpoints. Updated raw endpoints split 251-255-byte transport units into two versioned fragments and reassemble them by source MAC, length, and CRC; both endpoints must include this support for those sizes. Current valid MeshCore packet geometry reaches 254 bytes, while 255 is retained as transport headroom. Wrapped mode retains its legacy 246-byte maximum after the four-byte magic and checksum overhead, and drops larger packets instead of truncating them.

"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"

Usage: - get bridge.secret - set bridge.secret <secret>

Parameters: - secret: ESP-NOW bridge secret, 1-15 characters

Default: Varies by board

This setting is used only by bridge.format wrapped; raw mode ignores it.

"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"

Usage: get bootloader.ver

Applies to every nRF52 board/role that exposes this getter, including Companion, repeater, and room-server firmware. It reads the installed bootloader region selected by the MBR/UICR boot address and bounded by the chip's flash geometry; it does not search application firmware or staged update files.

The reader preserves a complete Adafruit/UF2 version string, including OTAFIX, preview, and vendor/build suffixes. Manufacturer two-component versions such as Seeed's 1.00 are preserved without inventing a patch number. If that string is absent or ambiguous, it uses the existing whole-image CRC and unique BLMF/BLM2 validation to read the embedded OTAFIX version (for example, > OTAFIX2.4.6). Some current release images, including MeshTower V2 SD 2.4.6, omit the UF2 text entirely. As a last OTAFIX-specific fallback, it accepts one unambiguous standalone version token such as v0.11.0-OTAFIX2.4.9 from only the first 40 KiB of the identified bootloader region. A corrupt canonical bootloader manifest cannot be bypassed by that printable token. The Adafruit core's startup-captured version remains the final fallback and is explicitly labelled (base); it cannot identify an OTAFIX/fork revision.

unknown remains possible for a bootloader exposing none of these version sources, invalid geometry/vectors, or unresolved ambiguity. There is no universal version field shared by every third-party bootloader. This is diagnostic only: displaying a version neither grants OTA capability nor bypasses install checks. The fix is in MeshCore application firmware; installing another bootloader alone does not update an older application's version reader.

See the stock bootloader audit for tested manufacturer images and the boards still requiring untouched factory dumps.

"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"

Usage: get pwrmgt.support

"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"

Usage: get pwrmgt.source

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"

Usage: get pwrmgt.bootreason

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"

Usage: get pwrmgt.bootmv

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#configure-battery-charge-voltage","title":"Configure battery charge voltage","text":"

T-Beam SX1262/SX1276 and T-Beam S3 Supreme support persistent charger-voltage selection through the fitted AXP192 or AXP2101 PMU:

get charge.voltage.options\nget charge.voltage\nset charge.voltage 4.1\n

Values are exact supported targets in volts. Neither T-Beam PMU supports 3.65 V. Heltec Mesh Solar's CN3795 charge voltage is set by its Li-ion or LiFePO4 hardware version, so these commands report that limitation without changing BMS settings. See charge-voltage behavior and supported targets.

"},{"location":"cli_commands/#configure-nrf52-battery-protection","title":"Configure nRF52 battery protection","text":"

Usage: get pwrmgt.bootlock, set pwrmgt.bootlock <2500-4200>

Usage: get pwrmgt.cutoff, set pwrmgt.cutoff <off|2500-4200>

Values are battery millivolts and are saved in internal storage. The boot lock defaults to each board's compiled threshold and takes effect on the next boot. Boards whose compiled boot lock is disabled also accept set pwrmgt.bootlock off. The running cutoff defaults to off; when enabled, three valid low readings at 30-second intervals shut the node down. Both checks are bypassed while externally powered. A nonzero cutoff cannot exceed the boot lock, and the boot lock cannot be disabled while a cutoff is active. A fixed 2000 mV pre-check protects flash on boards with a nonzero compiled boot lock before the saved setting can be loaded. In the custom profile, the lower limit for the boot lock and running cutoff is 2000 mV. Firmware warns when any saved voltage is below 2500 mV; that experimental range may brown out the board or interrupt flash writes. These commands are available on nRF52 boards with NRF52_POWER_MANAGEMENT and a boot-voltage configuration, in roles with a text CLI. The KISS modem has no text CLI for changing these settings.

battery.profile selects saved voltage-to-percentage endpoints and protection settings. liion uses 3000 mV = 0%, 4200 mV = 100%, the board's original boot lock, and running cutoff off. lifepo4 uses 2700 mV = 0%, 3550 mV = 100%, 2900 mV boot lock, and 2700 mV running cutoff. custom starts with the current values and allows endpoint overrides. These percentages are simple voltage estimates; they are not an accurate state-of-charge measurement under load.

WARNING: SELECTING LIFEPO4 DOES NOT CHANGE ANY BOARD'S CHARGING CIRCUIT. A 4.2 V LI-ION CHARGER WILL STILL CHARGE TOWARD 4.2 V AND CAN DESTROY A LIFEPO4 CELL. USE A LIFEPO4-COMPATIBLE CHARGER OR DISABLE/ISOLATE ON-BOARD CHARGING BEFORE CONNECTING A LIFEPO4 CELL. Check the charging hardware on every board before using this profile; firmware battery-profile and cutoff settings cannot make a Li-ion charger safe for LiFePO4.

get battery.profile\nset battery.profile lifepo4\nset battery.profile liion\nset battery.profile custom\nget battery.empty\nset battery.empty 2700\nget battery.full\nset battery.full 3550\nget adc.multiplier\nset adc.multiplier 1.050\n

The custom empty/full endpoints may be 2000-4200 mV and must be at least 100 mV apart. Setting either endpoint automatically selects custom; boot lock and cutoff remain separately adjustable. get pwrmgt.bootlock and get pwrmgt.cutoff show the values in force after selecting a profile.

For a RAK3401/RAK13302 LiFePO4 cell:

set battery.profile lifepo4\n

For example, the RAK19007 base-board datasheet specifies 4.2 V charging for a Li-ion battery. The profile does not change it.

The 2.7 V cutoff is a cell-protection choice, not a guarantee of 1 W RF output. The RAK13302 datasheet specifies 3.0 V minimum at VBAT and lists 5 V / 1 A peak for 30 dBm output. Use a cutoff at or above 3.0 V if keeping the radio within its published battery-input range matters; verify the actual voltage under transmit load.

The shutdown wake comparator has a separate fixed hardware reference. Changing these millivolt settings does not change that wake threshold; a unit may need to charge above it or have USB power restored before it wakes.

"},{"location":"cli_commands/#ethernet-when-ethernet-support-is-compiled-in","title":"Ethernet (when Ethernet support is compiled in)","text":"

Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the _ethernet firmware variants (for example, RAK_4631_repeater_ethernet) for the normal Ethernet-only configuration.

RAK_4631_companion_radio_full is a distinct combined profile: it can compile both physical RAK13800 Ethernet and an optional RAK15002 SD mOTA archive. They share CS26, so they are mutually exclusive. In that Full profile Ethernet is off at boot and is deliberately controlled only for the current boot:

get eth\nset eth on\nset eth off\nget sdcard [free]\nota cache [on|off]\n

Before set eth on, remove the SD card and finish/cancel any active OTA capture, transfer, serving, or host-folder work. The command rejects an inserted card or a busy archive instead of taking the shared bus. Turn Ethernet off before accessing or inserting the card. An insertion while Ethernet is live fails closed and still needs set eth off; it is not a supported hot-swap flow. This combined hardware path is not yet qualified.

The SD-capable Ethernet-room receiver, RAK_4631_room_server_ethernet_lora_ota_no_external_sensors, uses the same shared CS26 safety rule but has its own cooperative Ethernet control. It starts off on every reboot and is never persisted. From USB or an already established Telnet session, use either spelling:

eth on\n# or: set eth on\neth.status\neth off\n

eth on rejects a fitted card, active/cache/staged OTA work, or a retained NOR owner. eth off waits for a safe release and refuses while OTA work is busy. Remove the card before enabling Ethernet and do not insert it while Ethernet is active.

"},{"location":"cli_commands/#view-ethernet-connection-status","title":"View Ethernet connection status","text":"

Usage: - eth.status

Output: - ETH: <ip>:<port> when connected (e.g. ETH: 192.168.1.50:23) - ETH: not connected when Ethernet is not active

Notes: - Available on repeater and room server firmware only. The normal _ethernet Companion image does not expose a CLI; the combined RAK4631 Full profile above instead exposes get eth / set eth on|off. - The normal _ethernet variants obtain an IP address via DHCP automatically on boot. The combined RAK4631 Full profile does not: Ethernet starts off until set eth on succeeds. - A TCP server listens on port 23 (default) for CLI connections. - Connect with any TCP client (e.g. nc, PuTTY) to access the same CLI available over serial.

"},{"location":"cli_settings_audit/","title":"Cli settings audit","text":"

View this page on MeshCore Docs.

"},{"location":"cli_settings_audit/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_contact_cache/","title":"Companion contact cache","text":"

View this page on MeshCore Docs.

"},{"location":"companion_contact_cache/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"companion_contact_cache_results/","title":"Companion contact cache results","text":"

View this page on MeshCore Docs.

"},{"location":"companion_contact_cache_results/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_lost_reply/","title":"Companion lost reply","text":"

View this page on MeshCore Docs.

"},{"location":"companion_lost_reply/#companion-lost-status-auto-response","title":"Companion lost-status auto response","text":"

Development Companion builds containing this feature can automatically answer a private Am I lost? message with Yes or No. You choose the saved answer. The default is off, and the firmware does not infer lost status from GPS, distance, or a missing phone connection.

This is included in nRF52, ESP32 and RP2040 Companion profiles, including T1000-E and RAK Tag. Compact STM32 Companion images omit it to preserve their existing application and filesystem boundaries. A custom build can explicitly enable MESH_ENABLE_LOST_REPLY=1 when its flash budget permits; the normal flash and RAM checks still apply.

Configure it through a directly connected local Companion CLI:

get lost.reply\nset lost.reply no\nset lost.reply yes\nset lost.reply off\n

The setting survives reboot and takes effect immediately. USB, BLE and other local Companion transports can use framed command 0x42. Builds with a text terminal also accept these commands there. LoRa CLI cannot read or change this local setting. A failed save restores the previous answer and reports an error. Older saved preference images load the new setting as off.

ESP32 Companions with WebConfig also have a Lost-status auto reply selection on the radio configuration page. Choose Off, Reply No or Reply Yes and save. The control is shown only on Companion firmware with this capability. The existing mobile app protocol needs no new message type; a native app toggle is not added by this firmware change.

"},{"location":"companion_lost_reply/#dog-tracker-example","title":"Dog tracker example","text":"

The owner's Companion stores the tracker as a normal radio contact. Set the answer to no during normal operation. If the dog is lost, set it to yes. When the tracker sends Am I lost? privately to that reachable Companion, it receives the selected answer, even when the owner's phone is disconnected. The same configured answer applies to all eligible contacts on this Companion.

The opt-in Sensor dog tracker implements periodic fresh GPS acquisition, low power sleep, and owner check-ins. It uses a tagged encrypted request and response rather than parsing plain Yes or No text, so old answers cannot change a newer check-in. Store the Sensor as a persistent Sensor contact on the owner's Companion. The same saved lost.reply setting answers those checks; off returns Unknown. A fresh accepted GPS position updates the contact location through normal contact notifications.

Other clients can still use the plain text question. They must stay awake for the separate private answer; a delivery ACK is not that answer. Plain text answers have no request ID. The responding Companion must be powered on and reachable. Personal room mailboxes provide store-and-forward mail; automatic mailbox polling is not yet implemented in the Sensor tracker.

"},{"location":"companion_lost_reply/#matching-and-traffic-limits","title":"Matching and traffic limits","text":"

Only an authenticated ordinary private message from a known persistent radio contact is eligible. The contact must be an ordinary Companion/chat contact; room-server and repeater contacts are excluded. Temporary unknown senders, public channels, room chat posts and CLI messages do not trigger a response.

The exact phrase am I lost is matched without regard to ASCII letter case, with an optional final question mark and surrounding whitespace. Extra words or commands do not match. The reply is ordinary encrypted private text, Yes or No, and neither answer can trigger another automatic reply.

Automatic responses are limited to one per contact per minute and one globally per ten seconds. A bounded four-peer cache uses full public keys and the incoming request timestamp to suppress retries. Its state clears on reboot. The same retained request is not answered again; a fresh check uses a new timestamp. An old retry can receive another answer after a newer request replaces its saved timestamp or its peer slot is evicted. Queueing an answer records the request even if later delivery fails. An unanswered poll means unknown, and the tracker should trust replies only from its configured Companion's full public key.

The responder leaves packet capacity for normal receive/ACK work, uses a free ACK-tracking entry, and does not replace pending user messages or their delivery deadlines. Busy packet or ACK capacity, or a failed send, can prevent an answer; the tracker should retry later. A full offline app inbox does not disable the responder. Normal message reception and notification behavior remain in place.

"},{"location":"companion_offline_queue/","title":"Companion offline queue","text":"

View this page on MeshCore Docs.

"},{"location":"companion_offline_queue/#companion-offline-message-queue","title":"Companion Offline Message Queue","text":"

Companion firmware keeps received channel data, channel messages, and direct messages in one pending queue until a Companion client requests them with the sync-next-message command. This is volatile RAM, not message history in flash. A reboot clears it.

"},{"location":"companion_offline_queue/#default-capacities","title":"Default capacities","text":"Platform or memory profile Pending frames ESP32 with configured PSRAM 512 ESP32 without PSRAM 256 nRF52840 with at most 300 contact slots 256 nRF52840 with more than 300 contacts and the contact cache (non-mOTA-shared queue) 256 total: up to 15 held DMs use slots until accepted RAK WisMesh Tag ordinary BLE Companion 240 total: up to 15 held DMs use slots until accepted nRF52840 with more than 300 contacts but no contact cache 208 ordinary frames plus a separate 15-DM queue nRF52 Full Companion with the memory correction 256 normally; 128 while mOTA owns shared storage RP2040 256 STM32 16 (one-key DM holding is omitted on flash-constrained STM32WL builds) Known constrained classic ESP32 target override 128 Meshadventurer Full Companion 16 Constrained Full ESP32 fallback 16

An explicit target OFFLINE_QUEUE_SIZE overrides the platform default. The Heltec V2 and TLora V2 Full Companion profiles, for example, use 16 frames so their combined WiFi, BLE, and LoRa mOTA image retains enough internal DRAM. Meshadventurer SX1262 and SX1268 Full Companion use 16 frames together with 100 contacts and 30 group channels; their ordinary transport-specific images keep 128 frames and 40 channels.

The nRF52 Full memory correction applies queue sharing to every nRF52 Full Companion. It keeps 256 frames normally, retaining each board's contacts, channels, and all Full transports. The upper 128 slots temporarily hold the mOTA context when a source or TempRadio discovery session starts. Stopping or disconnecting the USB/Bluetooth source returns all 256 slots; a discovery-only session returns them when TempRadio ends. This shares a fixed memory region and avoids heap fragmentation from resizing.

Existing unread messages retain their order. If more than 128 frames are pending, mOTA refuses the loan and asks you to sync messages with a Companion app first. While the loan is active, the overflow policy below applies at 128 frames. The original 26303793 1.17.1.5 builds reserve the queue and mOTA state separately. Sharing recovers about 19 KiB on nRF52 Full. The ST7735 color-display Full profiles require at least 72 KiB of heap space at link time; OLED and headless profiles use their own memory budgets.

Standard, logging, MQTT, and Cascade build overlays retain the selected target capacity. On cache-enabled high-contact nRF52 Companions, including the RAK4631 USB and BLE profiles, ordinary messages and consent-pending DMs share one 256-frame RAM pool. With no held DMs, all 256 slots are available to ordinary messages; with 15 held DMs, up to 241 are. The app sees only ordinary messages until a sender is accepted. The oldest held DM rolls off when a sixteenth decryptable DM arrives. The shared pool is still volatile and clears on reboot. Uncached high-contact profiles keep the conservative 208-frame default and separate held queue. An explicit target OFFLINE_QUEUE_SIZE still takes precedence.

The ordinary RAK WisMesh Tag BLE profile uses 240 frames to retain the runtime RAM reserve for Bluetooth, filesystem buffers, and the main task. It keeps 350 contact slots and 40 group channels. All 240 slots are available when no DMs are held; 15 held DMs leave 225 ordinary slots. This saves 2,832 bytes compared with 256 frames. The Tag Full Companion retains 256 frames normally and 128 while mOTA borrows queue storage.

Each queue slot currently costs 177 bytes. A 256-frame queue reserves 45,312 bytes, while a 512-frame queue reserves 90,624 bytes. There is no 256-frame protocol limit: the queue length and indexes can represent 512 or more. The practical limit is available RAM and the heap and stack headroom required by the transports and display.

On ESP32 boards marked with BOARD_HAS_PSRAM, the queue is allocated from PSRAM before WiFi and BLE start. A failed 512-frame allocation retries at 256, then 128, and finally uses a 16-frame internal fallback. Full Companion prints the capacity actually allocated in its startup memory line as offline_queue=<frames>.

"},{"location":"companion_offline_queue/#full-queue-behavior","title":"Full queue behavior","text":"

The capacity is shared across Public, other channels, channel data, and direct messages. It is not a per-channel count. When the queue is full, firmware replaces the oldest queued channel frame so newer traffic can still arrive. If the full queue contains only direct messages, a new frame is dropped rather than deleting a direct message.

Queue order is preserved. Removal uses a ring index, so delivering one pending message no longer copies every remaining frame; only the less-common removal of an old channel frame from a full queue may shift entries.

Capacity is selected when firmware is compiled. There is no CLI or Companion protocol setting to resize it at runtime.

"},{"location":"companion_protocol/","title":"Companion protocol","text":"

View this page on MeshCore Docs.

"},{"location":"companion_protocol/#companion-protocol","title":"Companion Protocol","text":"

The command and response catalogs track examples/companion_radio/MyMesh.cpp. Applications should negotiate the protocol and validate lengths because older firmware exposes a subset.

This document is a practical guide to MeshCore's binary companion protocol. The same protocol frames can be carried by the enabled BLE, USB serial, Wi-Fi, or Ethernet companion interface; connection details differ by build.

On builds exposing more than one transport, delivery-required replies follow the interface which supplied the command. The multi-frame contact-list response holds that route until END_OF_CONTACTS; best-effort asynchronous observations may still be broadcast to enabled clients. Treat the device as one Companion session rather than as independent per-transport sessions.

The examples focus on BLE, but the packet formats are transport-independent.

"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"

Please see the following repos for existing MeshCore Companion Protocol libraries.

"},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"

All secrets, hashes, and cryptographic values shown in this guide are example values only.

"},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
  1. BLE Connection
  2. Packet Structure
  3. Commands
  4. Channel Management
  5. Message Handling
  6. Response Parsing
  7. Example Implementation Flow
  8. Best Practices
  9. Troubleshooting
"},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"

MeshCore Companion devices expose a BLE service with the following UUIDs:

An nRF52 Full Companion also exposes a separate LoRa mOTA source service. It does not replace or multiplex the normal Companion UART service:

All three mOTA attributes require an encrypted, MITM-authenticated connection using the Companion's six-digit PIN. The source remains inactive until the client subscribes to Device Request and explicitly starts it with command 0x4B. See Bluetooth LoRa mOTA source.

ESP32 and nRF52 Companion UART characteristics require the same PIN-protected, MITM-authenticated link. ESP32 advertises DisplayOnly capability so a central must enter the PIN shown by the Companion; a Just Works bond is insufficient.

"},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
  1. Scan for Devices

    • Scan for BLE devices advertising the MeshCore Service UUID
    • Optionally filter by device name (typically contains \"MeshCore\" prefix)
    • Note the device MAC address for reconnection
  2. Connect to GATT

    • Connect to the device using the discovered MAC address
    • Wait for connection to be established
  3. Discover Services and Characteristics

    • Discover the service with UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
    • Discover the RX characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
      • Your app writes to this, the firmware reads from this
    • Discover the TX characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
      • The firmware writes to this, your app reads from this
  4. Enable Notifications

    • Subscribe to notifications on the TX characteristic to receive data from the firmware
  5. Send Initial Commands

    • Send CMD_APP_START to identify your app to firmware and get radio settings
    • Send CMD_DEVICE_QUERY to fetch device info and negotiate supported protocol versions
    • Send CMD_SET_DEVICE_TIME to set the firmware clock
    • Send CMD_GET_CONTACTS to fetch all contacts
    • Send CMD_GET_CHANNEL multiple times to fetch all channel slots
    • Send CMD_SYNC_NEXT_MESSAGE to fetch the next message stored in firmware
    • Setup listeners for push codes, such as PUSH_CODE_MSG_WAITING or PUSH_CODE_ADVERT
    • See Commands section for information on other commands

Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.

"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"

When writing commands to the RX characteristic, specify the write type:

Platform-specific:

Recommendation: Use write with response for reliability.

"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:

  1. Request Larger MTU: Request MTU of 512 bytes if supported
    • Android: gatt.requestMtu(512)
    • iOS: peripheral.maximumWriteValueLength(for:)
    • Python (bleak): MTU is negotiated automatically
"},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"

Critical: Commands must be sent in the correct sequence:

  1. After Connection:

    • Wait for BLE connection to be established
    • Wait for services/characteristics to be discovered
    • Wait for notifications to be enabled
    • Now you can safely send commands to the firmware
  2. Command-Response Matching:

    • Send one command at a time
    • Wait for a response before sending another command
    • Use a timeout (typically 5 seconds)
    • Match response to command by type (e.g: CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO)
"},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"

For reliable operation, implement a command queue.

Queue Structure:

Error Handling:

"},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"

The MeshCore protocol uses a binary format with the following structure:

Most packets follow this format:

[Packet Type (1 byte)] [Data (variable length)]\n

The first byte indicates the packet type (see Response Parsing).

"},{"location":"companion_protocol/#commands","title":"Commands","text":"

The first byte selects the command. This is the current protocol-v14 command catalog; bytes 0x2C-0x31 are parked and 0x35 is unused.

Byte Firmware name Purpose 0x01 CMD_APP_START Start an app session and request self information. 0x02 CMD_SEND_TXT_MSG Send text to a contact. 0x03 CMD_SEND_CHANNEL_TXT_MSG Send channel text. 0x04 CMD_GET_CONTACTS Enumerate contacts, optionally modified since a timestamp. 0x05 / 0x06 CMD_GET_DEVICE_TIME / CMD_SET_DEVICE_TIME Read or set the device clock. 0x07 / 0x08 CMD_SEND_SELF_ADVERT / CMD_SET_ADVERT_NAME Advertise self or change the advertised name. 0x09 CMD_ADD_UPDATE_CONTACT Add or update a contact. 0x0A CMD_SYNC_NEXT_MESSAGE Dequeue the next pending message. 0x0B / 0x0C CMD_SET_RADIO_PARAMS / CMD_SET_RADIO_TX_POWER Set radio parameters or transmit power. 0x0D CMD_RESET_PATH Reset a contact's learned path. 0x0E CMD_SET_ADVERT_LATLON Set advertised coordinates. 0x0F CMD_REMOVE_CONTACT Remove a contact. 0x10 / 0x11 / 0x12 CMD_SHARE_CONTACT / CMD_EXPORT_CONTACT / CMD_IMPORT_CONTACT Share, export, or import contact data. 0x13 CMD_REBOOT Reboot after the required confirmation body. 0x14 CMD_GET_BATT_AND_STORAGE Read battery and storage usage. 0x15 CMD_SET_TUNING_PARAMS Set tuning parameters. 0x16 CMD_DEVICE_QUERY Negotiate protocol support and read device information. 0x17 / 0x18 CMD_EXPORT_PRIVATE_KEY / CMD_IMPORT_PRIVATE_KEY Export or import identity key material when enabled. 0x19 CMD_SEND_RAW_DATA Send an application raw-data packet. Known media and opt-in SSH envelopes honor application TX routing; other raw bytes retain normal routing. 0x1A-0x1D CMD_SEND_LOGIN through CMD_LOGOUT Manage a server connection. 0x1E CMD_GET_CONTACT_BY_KEY Look up a contact by public-key prefix. 0x1F / 0x20 CMD_GET_CHANNEL / CMD_SET_CHANNEL Read or write a channel slot. 0x21-0x23 CMD_SIGN_START through CMD_SIGN_FINISH Stream data for identity signing. 0x24 CMD_SEND_TRACE_PATH Trace a direct route. 0x25 CMD_SET_DEVICE_PIN Set or clear the device PIN. 0x26 CMD_SET_OTHER_PARAMS Set telemetry, location, ACK, and related preferences. 0x27 CMD_SEND_TELEMETRY_REQ Send the legacy telemetry request. 0x28 / 0x29 CMD_GET_CUSTOM_VARS / CMD_SET_CUSTOM_VAR Read or set custom variables. 0x2A CMD_GET_ADVERT_PATH Read a cached advertisement path. 0x2B CMD_GET_TUNING_PARAMS Read tuning parameters. 0x32 CMD_SEND_BINARY_REQ Send an application binary request. 0x33 CMD_FACTORY_RESET Factory-reset after the required confirmation body. 0x34 CMD_SEND_PATH_DISCOVERY_REQ Request path discovery. 0x36 CMD_SET_FLOOD_SCOPE_KEY Select scoped or unscoped flood behavior. 0x37 CMD_SEND_CONTROL_DATA Send zero-hop control data. 0x38 CMD_GET_STATS Read core, radio, or packet statistics. 0x39 CMD_SEND_ANON_REQ Send an anonymous request. 0x3A / 0x3B CMD_SET_AUTOADD_CONFIG / CMD_GET_AUTOADD_CONFIG Write or read automatic-contact policy. 0x3C CMD_GET_ALLOWED_REPEAT_FREQ Read allowed client-repeat frequency ranges. 0x3D CMD_SET_PATH_HASH_MODE Set path-hash width mode. 0x3E CMD_SEND_CHANNEL_DATA Send a channel binary datagram. 0x3F / 0x40 CMD_SET_DEFAULT_FLOOD_SCOPE / CMD_GET_DEFAULT_FLOOD_SCOPE Write or read the default flood scope. 0x41 CMD_SEND_RAW_PACKET Queue a fully encoded raw mesh packet. 0x42 CMD_RUN_CLI_COMMAND Run a local CLI command (protocol v14+). 0x4A CMD_EXEC_LOCAL_OTA_CONTROL Run one bounded local TempRadio or OTA command when the Companion includes the OTA CLI. 0x4B CMD_BLE_MOTA_SOURCE Query, start, or stop an nRF52 Full Companion's Bluetooth-backed LoRa mOTA source. 0x78-0x7F Deprecated hardware-setting aliases Receive-only compatibility for clients shipped before command 0x42 became the canonical settings path.

The sections below detail the most common frames. Refer to the source named above for command bodies that are not expanded here.

"},{"location":"companion_protocol/#private-text-when-only-the-sender-knows-the-recipient","title":"Private text when only the sender knows the recipient","text":"

Clients continue to send an ordinary CMD_SEND_TXT_MSG (0x02) with TXT_TYPE_PLAIN; no client protocol change is needed. For a chat contact without a recent confirmed DM, Companion firmware first sends a signed, encrypted one-key introduction that includes its full public key, then queues the normal private text after the introduction. It sends the introduction again on an application retry, which recovers if the recipient removed the sender contact. An ACK confirms that the recipient can decrypt normal DMs, so subsequent first attempts omit the extra packet until the sender reboots or the small recent-ACK cache is replaced.

The receiving Companion verifies the introduction signature. By default, dm.one_key is off: the sender is offered to the app as a synthetic PUSH_CODE_NEW_ADVERT (0x8A) but is not added as a contact. The app/user accepts through the normal add-contact command. Up to 15 verified, decryptable plain DMs are held in RAM and delivered after acceptance; the oldest rolls off first. The queue is lost on reboot. Cache-enabled high-contact nRF52 Companions share a 256-frame pool between ordinary offline messages and up to 15 held DMs, so ordinary capacity is 256 when none are held and 241 when all 15 are held. The held frames are not exposed by ordinary message sync before acceptance. Uncached high-contact nRF52 builds retain 208 ordinary frames and a separate held queue. The device sends a signed DMR1 refusal, and the sender pushes 0x91 plus the full rejecting key to a connected app, then stops further introductions for that peer until reboot. Directly attached clients may use get dm.one_key or set dm.one_key on|off; get dm.held reports the number of held DMs. on auto-accepts verified senders. The flash-constrained STM32WL Companion builds omit one-key DM support to preserve their existing filesystem boundary; these commands report Error: one-key DMs unsupported on this build there. Ordinary contact-based private messages remain available. A recipient running older firmware still needs the sender contact before it can decrypt a normal DM. This flow uses a second radio packet for the initial DM; RESP_CODE_SENT includes that scheduling delay in its timeout.

For a two-radio regression test, run tools/hil/one_key_dm.py with the USB serial paths for --sender and --recipient. --reset-contact removes the sender from recipient contacts, and --zero-hop sets the sender's route to zero hop when the devices are nearby. The test checks the synthetic advert, refusal, manual acceptance, held-message delivery, and matching ACK. --invalid-signature-first also checks that a decryptable introduction with an invalid Ed25519 signature does not add a contact.

CMD_RUN_CLI_COMMAND is followed by the local CLI text with an optional final NUL, as sent by the official app. Clients that omit it remain supported. The device returns RESP_CODE_CLI_REPLY (0x1D) followed by the reply text. This is separate from sending a remote on-air CLI command with CMD_SEND_TXT_MSG and TXT_TYPE_CLI_COMMAND. The body must contain at least one byte and must not contain an embedded NUL. An unknown command is returned as the normal CLI reply text Unknown command, not as an error frame. Clients may prefix the CLI text with any two-character correlation tag and | (for example, A7|get radio.rxgain); the reply preserves that prefix.

Direct USB, BLE, WiFi TCP, and Ethernet clients can also use this command for stats-core, stats-radio, stats-radio-diag, stats-packets, erase, and get prv.key (when private key export is enabled). WiFi/MQTT builds accept get wifi.pwd, get mqttN.password, and get mqttN.token. On ESP32 WiFi Companion, get password reports whether the management password is available without revealing it. password <new-password> changes the saved password shared by setup WiFi, WebConfig and browser OTA. Other Companion builds report that they have no admin password; infrastructure roles return their own admin password on a local CLI connection. These direct-client maintenance and secret-read privileges do not apply to CLI commands relayed over LoRa. Existing trusted Companion transports and contacts explicitly granted remote CLI access retain their command privileges. The browser login does not add a password handshake to those transports. ESP32 WiFi Companion builds expose the official app's get/set wifi.enabled, get/set wifi.ssid, and get/set wifi.pwd controls. Credentials use the shared WiFi store, and enablement is saved in node preferences. WiFi/BLE builds apply enablement from the main loop; exclusive WiFi/BLE builds apply it after reboot. get wifi.status reports the connection state and get wifi.ip reports the station address, the setup AP address, or (not connected). get wifi.tcp reports the actual app listener, interface registration, client connection and completed frame counters. It is read-only and available through the same binary CLI command over BLE or USB when TCP is unreachable. Listener startup failures recover automatically without resetting a healthy client or the WiFi connection; see the CLI diagnostics. set freq <MHz> works through command 0x42 and authorized LoRa CLI, like set radio; it saves the frequency and requires reboot to apply. See local maintenance commands for role differences and the text-terminal equivalents.

Full Companion clients can send version through this command to receive the untruncated build identity, for example Companion 1.17.1.5-... (protocol 14, build 31-Aug-2026). This deliberately supplements rather than changes the 20-byte legacy version field in RESP_CODE_DEVICE_INFO, so existing clients keep the same frame layout.

Firmware from this fork predating the upstream 0x42 allocation used 0x42-0x49 for these eight settings. This firmware accepts those values as deprecated inbound aliases so existing clients continue to work. A one-byte 0x42 frame is the legacy FEM-gain GET; 0x42 followed by text is the official CMD_RUN_CLI_COMMAND. New clients should use CMD_RUN_CLI_COMMAND for all of these settings, rather than allocating additional command bytes. For example, send 0x42 followed by get radio.rxgain or set radio.rxgain on. The reply is RESP_CODE_CLI_REPLY followed by the normal CLI reply text.

Two deprecated binary alias blocks remain receive-only for compatibility:

Setting Original alias Later fork alias GET body/reply SET body/reply FEM receive gain 0x42 / 0x43 0x78 / 0x79 No body; OK, state One byte 0/1; OK Radio receive gain 0x44 / 0x45 0x7A / 0x7B No body; OK, state One byte 0/1; OK WiFi power save 0x46 / 0x47 0x7C / 0x7D No body; OK, mode One mode byte 0-2; OK Bluetooth name 0x48 / 0x49 0x7E / 0x7F No body; OK, custom, name Zero to 31 UTF-8 bytes; OK

Each pair lists GET then SET. Here OK is RESP_CODE_OK; the remaining reply bytes have the same meanings as the CLI settings below. A bare 0x42 is the old FEM GET, while 0x42 plus at least one text byte is CMD_RUN_CLI_COMMAND. New clients must use the framed CLI form; these aliases exist only so deployed clients do not break after a firmware update.

The equivalent framed CLI commands are:

Setting Commands Radio receive gain get radio.rxgain; set radio.rxgain on|off FEM receive gain get radio.fem.rxgain; set radio.fem.rxgain on|off WiFi power save get wifi.powersave; set wifi.powersave none|min|max Bluetooth name get bluetooth.name; set bluetooth.name <name|default> Bluetooth address get bluetooth.mac; set bluetooth.mac <address|random|random-every-boot|random-after-connect|default> Bluetooth stealth get bluetooth.stealth; set bluetooth.stealth on|off

The framed form works over the normal binary USB, BLE, or TCP transport and does not need the USB terminal-start token. Unsupported settings return the same explanatory text as the local CLI.

WiFi power-save modes are:

Value Mode 0 min - minimum modem sleep 1 none - no modem sleep 2 max - maximum modem sleep

A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects WiFi mode none because coexistence requires modem sleep. A Full Companion using ESP-NOW as its primary mesh radio also rejects max, because maximum modem sleep can miss broadcasts that the access point cannot buffer. If an older image saved a conflicting value, the effective mode is capped to and reported as min. Device power saving remains independent.

The SenseCAP Indicator Full profiles run exactly one secondary wireless Companion transport per boot. Their active-mode constraints are:

Indicator mode Accepted wifi.powersave values LoRa + infrastructure WiFi none, min, max LoRa + BLE min, max; infrastructure WiFi is not started ESP-NOW + infrastructure WiFi none, min; max conflicts with primary ESP-NOW ESP-NOW + BLE min; infrastructure WiFi is not started and primary ESP-NOW remains active

The Bluetooth name can be configured over USB, BLE, or TCP. Use set bluetooth.name default to restore MeshCore-<advert name>; an empty CLI value is rejected. (clear is also accepted as an alias for default.) A custom name is limited to 31 valid UTF-8 bytes and takes effect after reboot.

Bluetooth Companion builds also accept bluetooth.mac (ble.mac is an alias). A literal address must be a BLE random-static address; random generates and saves one, random-every-boot rotates it at each startup, and random-after-connect retains it through unused boots but rotates it on the first boot after an authenticated connection. default or clear restores the factory address. The change takes effect after reboot.

bluetooth.stealth on|off (ble.stealth is an alias) is a separate flag, defaulting to off. It preserves the address policy and can be combined with custom, saved random, rotating random, or factory addresses. With stealth on, the node advertises normally until the first authenticated pairing, then permits only that bonded peer to reconnect. Address changes clear the saved peer and reopen pairing without disabling stealth. Rotating policies retain their normal rotation triggers. Repeating on does not clear an existing bond. Both flag changes require reboot. The old MAC value stealth is not accepted.

BLE still transmits directed or allowlisted advertisements; stealth is not radio silence. These are application settings; an nRF52 OTAFIX bootloader continues to advertise its separate DFU identity.

"},{"location":"companion_protocol/#bluetooth-lora-mota-source","title":"Bluetooth LoRa mOTA source","text":"

Protocol v14 lets a phone control an install-capable Companion's own LoRa OTA session. An nRF52 Full Companion can additionally act as the source for a remote repeater update without a USB computer. The normal Companion service still carries contacts, repeater login, CLI messages, and these two control commands. The separate mOTA service carries only host-folder request/response frames.

CMD_EXEC_LOCAL_OTA_CONTROL (0x4A) is followed by 1-174 printable ASCII bytes. A Companion built with the OTA CLI accepts only these local command families:

tempradio <freq_kHz>,<bw_kHz>,<sf>,<cr>,<minutes>\nnormalradio\nota ...\n

ota folder ... is deliberately rejected because USB and Bluetooth source ownership must not be changed through the wrong transport. Embedded NUL, CR, LF, other control bytes, non-ASCII bytes, empty commands, and oversized frames return ERR_CODE_ILLEGAL_ARG. A recognized command replies with RESP_CODE_OK, one unsigned reply-length byte, and exactly that many printable result bytes. Shell metacharacters are rejected as well; the text is dispatched only to the in-firmware parser and is never passed to a host shell. Firmware without the OTA CLI feature returns ERR_CODE_UNSUPPORTED_CMD.

CMD_BLE_MOTA_SOURCE (0x4B) has one action byte:

Action Meaning 0 Read status without changing it. 1 Attach and enumerate the subscribed Bluetooth host's .mota catalog. 2 Detach the Bluetooth source.

Current firmware returns eleven bytes (legacy protocol-v14 previews returned the seven-byte prefix only):

00 action flags offered_le16 advertised_le16 source_packets_sent_le32\n

Flag bit 0x01 means the encrypted GATT channel is connected and Device Request notifications are enabled. Bit 0x02 means the Bluetooth catalog is attached. Bit 0x04 means USB or another folder transport currently owns the source slot. Start without a ready subscription, or while another source link owns the slot, returns ERR_CODE_BAD_STATE. A non-nRF52 Full Companion returns ERR_CODE_UNSUPPORTED_CMD. source_packets_sent is a per-attachment count of OTA packets accepted by the Companion's LoRa transmit adapter, including catalog/manifest traffic, data, proofs, and retries. It wraps as an unsigned 32-bit value. Clients should accept the legacy seven-byte response and display the packet counter as unavailable.

After a successful start, the device sends the same bounded seeder frames used by motatool serve on Device Request:

device -> host: 'M' 'S' op args... xor(op || args)\nhost -> device: 'm' 's' op status payload... xor(all prior bytes)\n

Device requests are at most 11 bytes. A source response is at most 197 bytes. The host may split one response across multiple write-with-response operations when the negotiated ATT payload is smaller; it must preserve byte order and must not interleave another response. Bad checksums, partial frames, overflow, unsubscribe, loss of encryption, or disconnect fail closed. The firmware then detaches the catalog and stops advertising its entries. USB and Bluetooth folder sources are mutually exclusive.

A Linux reference controller and seeder is provided at tools/ble_mota/ble_mota_seeder.py. It verifies every input with motatool before offering it. A mobile implementation should apply the same complete container verification before serving files.

"},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"

Purpose: Initialize communication with the device. Must be sent first after connection.

Command Format:

Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n

Example (hex):

01 00 00 00 00 00 00 00 6d 63 63 6c 69\n

Response: PACKET_SELF_INFO (0x05)

"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"

Purpose: Query device information.

Command Format:

Byte 0: 0x16\nByte 1: Highest companion protocol version understood by the app\n

Example (hex):

16 0E\n

Response: PACKET_DEVICE_INFO (0x0D) with device information

"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"

Purpose: Retrieve information about a specific channel.

Command Format:

Byte 0: 0x1F\nByte 1: Channel index (0 through max_channels - 1)\n

Example (get channel 1):

1F 01\n

Response: PACKET_CHANNEL_INFO (0x12) with channel details

On builds with MESH_ENABLE_MESHTEXT_CHAT=1, a capable client can append a third request byte of 32 (1F <index> 20) to retrieve the complete 32-byte secret for an mt- channel. Ordinary two-byte requests, and requests for MeshCore channels, retain the original 16-byte secret response.

"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"

Purpose: Create or update a channel on the device.

Command Format:

Byte 0: 0x20\nByte 1: Channel index (0 through max_channels - 1)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n

Total Length: 50 bytes

Channel index: - Slot count is build-specific. Read max_channels from byte 3 of PACKET_DEVICE_INFO; current profiles commonly expose 1, 8, or 40 slots. - No slot number has an intrinsic public/private meaning.

Channel Name: - UTF-8 encoded - Maximum 32 bytes - Padded with null bytes (0x00) if shorter

Secret Field (16 bytes): - Supply the exact 16-byte channel key. A private channel normally uses a cryptographically random key; known public and hashtag channels use their defined or derived key. - An all-zero key is not the public-channel key.

Example (create channel \"SMS\" at index 1 with secret):

20 01 53 4D 53 00 00 ... (name padded to 32 bytes)\n    [16 bytes of secret]\n

Meshtext-only extension: With MESH_ENABLE_MESHTEXT_CHAT=1, a 66-byte SET_CHANNEL frame can supply 32 secret bytes at offsets 34\u201365 for an mt- channel. Non-MT channels and feature-disabled builds reject this variant with PACKET_ERROR; the standard 50-byte MeshCore command is unchanged. The MT backend uses AES-256 when the upper 16 bytes are nonzero, otherwise AES-128.

A legacy 50-byte SET of an existing MT channel preserves its upper 16 key bytes when the supplied lower half is unchanged. This permits an old client's GET/edit/SET round trip without silently truncating a provisioned AES-256 key. To explicitly replace or downgrade that key, send the full 32-byte variant (zero the upper half for AES-128). A client using only the legacy GET cannot export a complete AES-256 key. Stock app editors do not gain 256-bit provisioning or sharing from this firmware extension.

For local CLI provisioning without implementing the binary extension, use set mt.channel with a 32- or 64-digit hexadecimal PSK; see the private-channel commands.

MT channel changes can also fail with a bad-state error for conflicting presets, an unsuitable manual radio2 profile, ambiguous region or a busy automatic owner. See Meshtext channel chat.

Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure

"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"

Purpose: Send a text message to a channel.

Command Format:

Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel index (0 through max_channels - 1)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n

Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian)

Example (send \"Hello\" to channel 1 at timestamp 1234567890):

03 00 01 D2 02 96 49 48 65 6C 6C 6F\n

Response: PACKET_MSG_SENT (0x06) on success

"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"

Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp - applications needing either must encode them inside the binary payload.

Command Format:

Byte 0:                         0x3E\nByte 1:                         Channel index (0 through max_channels - 1)\nByte 2:                         Encoded path descriptor (0xFF = flood)\nBytes 3+:                       Encoded path bytes (omitted for 0xFF)\nNext 2 bytes (little-endian):   Data Type (`data_type`, uint16)\nRemaining bytes:                Binary payload (variable length)\n

For a direct send, the descriptor's low six bits are the hash count and its high two bits are the hash size minus one. Current mesh packets accept one-, two-, or three-byte hashes; the four-byte code is reserved. The following path therefore occupies hash_count * hash_size bytes; the descriptor itself is not a raw byte count.

Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):

3E 01 FF FF FF A1 B2 C3\n

Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Registered application/community namespaces occupy 0x0100-0xFEFF; the remaining nonzero ranges are reserved for internal or development use. See the Registered data_type values table below.

Limits: - Maximum payload length is MAX_GROUP_DATA_LENGTH = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3 = 165 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).

Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) - unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) - invalid path_len, reserved data_type (0x0000), or payload larger than MAX_GROUP_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) - outbound send queue is full; retry later

Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.

"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered data_type values","text":"

data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents - data_type is transported opaquely.

Value Constant Purpose 0x0000 DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 - 0x00FF - Reserved for internal use 0x0100 - 0xFEFF - Registered application namespaces (see number_allocations.md) 0xFF00 - 0xFFFE - Testing/development; no registration required 0xFFFF DATA_TYPE_DEV Developer/experimental namespace

To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.

"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"

Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.

Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):

Byte 0:                 0x1B (packet type)\nByte 1:                 SNR (signed int8, scaled x4 - divide by 4.0 to recover dB)\nBytes 2-3:              Reserved (clients MUST ignore)\nByte 4:                 Channel index (0 through max_channels - 1)\nByte 5:                 Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7:              Data Type (uint16 little-endian)\nByte 8:                 Data Length\nBytes 9 .. 8+data_len:  Payload\n

Path bytes are not forwarded: Only path_len is reported in the receive frame - the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6-7, regardless of path_len.

Path Length semantics differ between send and receive:

Direction path_len = 0xFF path_len != 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = hash_count x hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded pkt->path_len field as observed (no path bytes follow)

In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only - never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.

Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.

Parsing Pseudocode:

def parse_channel_data_recv(data):\n    if len(data) < 9:\n        return None\n    snr_byte = data[1]\n    snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n    channel_idx = data[4]\n    path_len = data[5]\n    data_type = int.from_bytes(data[6:8], 'little')\n    data_len = data[8]\n    if 9 + data_len > len(data):\n        return None\n    payload = data[9:9 + data_len]\n    return {\n        'snr': snr,\n        'channel_idx': channel_idx,\n        'path_len': path_len,\n        'data_type': data_type,\n        'payload': bytes(payload),\n    }\n
"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"

Purpose: Request the next queued message from the device.

Command Format:

Byte 0: 0x0A\n

Example (hex):

0A\n

Response: - PACKET_CHANNEL_MSG_RECV (0x08) or PACKET_CHANNEL_MSG_RECV_V3 (0x11) for channel messages - PACKET_CONTACT_MSG_RECV (0x07) or PACKET_CONTACT_MSG_RECV_V3 (0x10) for contact messages - PACKET_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - PACKET_NO_MORE_MSGS (0x0A) if no messages available

Note: Poll this command periodically to retrieve queued messages. The device may also send PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.

"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"

Purpose: Query device battery voltage and storage usage.

Command Format:

Byte 0: 0x14\n

Example (hex):

14\n

Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information

"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
  1. Public Channel
    • Uses a publicly known 16-byte key: 8b3387e9c5cdea6ac9e5edbaa115cd72
    • Anyone can join this channel, messages should be considered public
    • Used as the default public group chat
  2. Hashtag Channels
    • Uses a secret key derived from the channel name
    • It is the first 16 bytes of sha256(\"#test\")
    • For example hashtag channel #test has the key: 9cd8fcf22a47333b591d96a2b848b73f
    • Traffic is encrypted on air, but anyone who knows or guesses the channel name can derive the key. Hashtag channels should not be treated as private.
    • Used as a topic based public group chat, separate from the default public channel
  3. Private Channels
    • Uses a randomly generated 16-byte secret key
    • Messages should be considered private between those that know the secret
    • Users should keep the key secret, and only share with those you want to communicate with
    • Used as a secure private group chat
"},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
  1. Set Channel:
    • Read max_channels from device info, fetch those slots, and choose an unused slot (normally an empty name and zeroed key)
    • Generate or provide a 16-byte secret
    • Send CMD_SET_CHANNEL with name and a 16-byte secret
  2. Get Channel:
    • Send CMD_GET_CHANNEL with channel index
    • Parse RESP_CODE_CHANNEL_INFO response
  3. Delete Channel:
    • Send CMD_SET_CHANNEL with empty name and all-zero secret
    • Or overwrite with a new channel
"},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"

Messages are received via the TX characteristic (notifications). The device sends:

  1. Channel Messages:
  2. PACKET_CHANNEL_MSG_RECV (0x08) - Standard format
  3. PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR

  4. Contact Messages:

  5. PACKET_CONTACT_MSG_RECV (0x07) - Standard format
  6. PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR

  7. Notifications:

  8. PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued
"},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"

Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):

Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n

V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):

Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_contact_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x10:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    pubkey_prefix = data[offset:offset+6].hex()\n    offset += 6\n\n    path_len = data[offset]\n    txt_type = data[offset + 1]\n    offset += 2\n\n    timestamp = int.from_bytes(data[offset:offset+4], 'little')\n    offset += 4\n\n    # If txt_type == 2, skip 4-byte signature\n    if txt_type == 2:\n        offset += 4\n\n    message = data[offset:].decode('utf-8')\n\n    return {\n        'pubkey_prefix': pubkey_prefix,\n        'path_len': path_len,\n        'txt_type': txt_type,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x10 else None\n    }\n
"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"

Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):

Byte 0: 0x08 (packet type)\nByte 1: Channel index (0 through max_channels - 1)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n

V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):

Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel index (0 through max_channels - 1)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_channel_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x11:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    channel_idx = data[offset]\n    path_len = data[offset + 1]\n    txt_type = data[offset + 2]\n    timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n    message = data[offset+7:].decode('utf-8')\n\n    return {\n        'channel_idx': channel_idx,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x11 else None\n    }\n
"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"

Use the SEND_CHANNEL_MESSAGE command (see Commands).

Important: - The shared text envelope permits up to 160 UTF-8 bytes. For channel text, firmware prepends <sender name>: inside that envelope, so the available message body is 160 - prefix_bytes and varies with the configured name. - Count encoded UTF-8 bytes, not Unicode characters. Split a longer message at valid UTF-8 boundaries. - Include a chunk indicator (e.g., \"[1/3] message text\")

"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"

This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:

Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.

"},{"location":"companion_protocol/#response-types","title":"Response types","text":"Value Firmware name Description 0x00 RESP_CODE_OK Command succeeded. 0x01 RESP_CODE_ERR Command failed; byte 1 is the error code. 0x02 RESP_CODE_CONTACTS_START Contact enumeration started. 0x03 RESP_CODE_CONTACT One contact record. 0x04 RESP_CODE_END_OF_CONTACTS Contact enumeration ended. 0x05 RESP_CODE_SELF_INFO Device self-information. 0x06 RESP_CODE_SENT Send accepted, with route/tag/timeout data. 0x07 / 0x08 RESP_CODE_CONTACT_MSG_RECV / RESP_CODE_CHANNEL_MSG_RECV Queued legacy-format message. 0x09 RESP_CODE_CURR_TIME Current device time. 0x0A RESP_CODE_NO_MORE_MESSAGES Offline queue is empty. 0x0B RESP_CODE_EXPORT_CONTACT Exported contact bytes. 0x0C RESP_CODE_BATT_AND_STORAGE Battery and storage values. 0x0D RESP_CODE_DEVICE_INFO Protocol and build information. 0x0E RESP_CODE_PRIVATE_KEY Exported identity key, when enabled. 0x0F RESP_CODE_DISABLED Requested sensitive feature is disabled. 0x10 / 0x11 RESP_CODE_CONTACT_MSG_RECV_V3 / RESP_CODE_CHANNEL_MSG_RECV_V3 Queued message with SNR fields. 0x12 RESP_CODE_CHANNEL_INFO Channel slot information. 0x13 / 0x14 RESP_CODE_SIGN_START / RESP_CODE_SIGNATURE Signing capacity or completed signature. 0x15 RESP_CODE_CUSTOM_VARS Custom-variable data. 0x16 RESP_CODE_ADVERT_PATH Cached advertisement path. 0x17 RESP_CODE_TUNING_PARAMS Tuning parameters. 0x18 RESP_CODE_STATS Requested statistics subtype. 0x19 RESP_CODE_AUTOADD_CONFIG Automatic-contact policy. 0x1A RESP_ALLOWED_REPEAT_FREQ Allowed repeat-frequency ranges. 0x1B RESP_CODE_CHANNEL_DATA_RECV Queued channel datagram. 0x1C RESP_CODE_DEFAULT_FLOOD_SCOPE Default flood-scope data. 0x1D RESP_CODE_CLI_REPLY Text returned by CMD_RUN_CLI_COMMAND."},{"location":"companion_protocol/#asynchronous-push-types","title":"Asynchronous push types","text":"Value Firmware name Description 0x80 PUSH_CODE_ADVERT Advertisement received. 0x81 PUSH_CODE_PATH_UPDATED A contact path changed. 0x82 PUSH_CODE_SEND_CONFIRMED A sent message was acknowledged. 0x83 PUSH_CODE_MSG_WAITING One or more offline frames are waiting. 0x84 PUSH_CODE_RAW_DATA Raw application data received. 0x85 / 0x86 PUSH_CODE_LOGIN_SUCCESS / PUSH_CODE_LOGIN_FAIL Server login result. 0x87 PUSH_CODE_STATUS_RESPONSE Server status response. 0x88 PUSH_CODE_LOG_RX_DATA Radio receive log data. 0x89 PUSH_CODE_TRACE_DATA Completed trace data. 0x8A PUSH_CODE_NEW_ADVERT New advert; a verified one-key DM may use this as an addable synthetic advert without storing the contact. 0x8B PUSH_CODE_TELEMETRY_RESPONSE Telemetry response. 0x8C PUSH_CODE_BINARY_RESPONSE Binary request response. 0x8D PUSH_CODE_PATH_DISCOVERY_RESPONSE Path-discovery response. 0x8E PUSH_CODE_CONTROL_DATA Control/discovery data. 0x8F PUSH_CODE_CONTACT_DELETED Oldest contact was deleted while making room. 0x90 PUSH_CODE_CONTACTS_FULL Contact storage is full. 0x91 PUSH_CODE_ONE_KEY_DM_REJECTED One-key DM refused; bytes 1\u201332 contain the refusing contact's full public key."},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"

PACKET_OK (0x00):

Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n

PACKET_ERROR (0x01):

Byte 0: 0x01\nByte 1: Error code (optional)\n

PACKET_CHANNEL_INFO (0x12):

Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n

Note: The normal response contains the 16-byte channel secret. An opt-in 32-byte MT GET described above extends the secret through byte 65; response code, index and name fields remain unchanged.

PACKET_DEVICE_INFO (0x0D):

Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: Active BLE PIN (32-bit little-endian; includes a generated session PIN)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n

Parsing Pseudocode:

def parse_device_info(data):\n    if len(data) < 2:\n        return None\n\n    fw_ver = data[1]\n    info = {'fw_ver': fw_ver}\n\n    if fw_ver >= 3 and len(data) >= 80:\n        info['max_contacts'] = data[2] * 2\n        info['max_channels'] = data[3]\n        info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n        info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n        info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n        info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n    if fw_ver >= 9 and len(data) >= 81:\n        info['client_repeat'] = data[80] != 0\n    if fw_ver >= 10 and len(data) >= 82:\n        info['path_hash_mode'] = data[81]\n\n    return info\n

PACKET_BATTERY (0x0C):

Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n

Parsing Pseudocode:

def parse_battery(data):\n    if len(data) < 3:\n        return None\n\n    mv = int.from_bytes(data[1:3], 'little')\n    info = {'battery_mv': mv}\n\n    if len(data) >= 11:\n        info['used_kb'] = int.from_bytes(data[3:7], 'little')\n        info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n    return info\n

PACKET_SELF_INFO (0x05):

Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n

Parsing Pseudocode:

def parse_self_info(data):\n    if len(data) < 36:\n        return None\n\n    offset = 1\n    info = {\n        'adv_type': data[offset],\n        'tx_power': data[offset + 1],\n        'max_tx_power': data[offset + 2],\n        'public_key': data[offset + 3:offset + 35].hex()\n    }\n    offset += 35\n\n    lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n    lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n    info['adv_lat'] = lat\n    info['adv_lon'] = lon\n    offset += 8\n\n    info['multi_acks'] = data[offset]\n    info['adv_loc_policy'] = data[offset + 1]\n    telemetry_mode = data[offset + 2]\n    info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n    info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n    info['telemetry_mode_base'] = telemetry_mode & 0b11\n    info['manual_add_contacts'] = data[offset + 3] > 0\n    offset += 4\n\n    freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n    bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n    info['radio_freq'] = freq\n    info['radio_bw'] = bw\n    info['radio_sf'] = data[offset + 8]\n    info['radio_cr'] = data[offset + 9]\n    offset += 10\n\n    if offset < len(data):\n        name_bytes = data[offset:]\n        info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_MSG_SENT (0x06):

Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n

PACKET_SEND_CONFIRMED (0x82):

Byte 0: 0x82\nBytes 1-4: ACK code (32-bit little-endian)\nBytes 5-8: Round-trip time (32-bit little-endian, milliseconds)\n
"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"

PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:

Code Constant (firmware) Description 1 ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2 ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3 ERR_CODE_TABLE_FULL Internal queue or table is full - retry later 4 ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5 ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6 ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)

Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.

"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"

BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.

"},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
  1. Command-Response Pattern:
  2. Send command via RX characteristic
  3. Wait for response via TX characteristic (notification)
  4. Match the response by the expected response type; frames do not carry a general command sequence number
  5. Handle timeout (typically 5 seconds)
  6. Use command queue to prevent concurrent commands

  7. Asynchronous Messages:

  8. Device may send messages at any time via TX characteristic
  9. Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command
  10. Parse incoming messages and route to appropriate handlers
  11. Validate frame length before decoding

  12. Response Matching:

  13. Match responses to commands by expected packet type:

    • APP_START -> PACKET_SELF_INFO
    • DEVICE_QUERY -> PACKET_DEVICE_INFO
    • GET_CHANNEL -> PACKET_CHANNEL_INFO
    • SET_CHANNEL -> PACKET_OK or PACKET_ERROR
    • SEND_CHANNEL_MESSAGE -> PACKET_MSG_SENT
    • GET_MESSAGE -> PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS
    • SEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERROR
    • GET_BATTERY -> PACKET_BATTERY
  14. Timeout Handling:

  15. Default timeout: 5 seconds per command
  16. On timeout: Log error, clear current command, proceed to next in queue
  17. Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds)
  18. Consider longer timeout for channel operations

  19. Error Recovery:

  20. On PACKET_ERROR: Log error code, clear current command
  21. On connection loss: Clear command queue, attempt reconnection
  22. On invalid response: Log warning, clear current command, proceed
"},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n
"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16)  # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = choose_unused_slot(max_channels)\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n
"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n
"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n    packet_type = data[0]\n\n    if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n        message = parse_channel_message(data)\n        handle_channel_message(message)\n    elif packet_type == PACKET_MESSAGES_WAITING:\n        # Poll for messages\n        send_command(rx_char, build_get_message())\n
"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
  1. Connection Management:
  2. Implement auto-reconnect with exponential backoff
  3. Handle disconnections gracefully
  4. Store last connected device address for quick reconnection

  5. Secret Management:

  6. Always use cryptographically secure random number generators
  7. Store secrets securely (encrypted storage)
  8. Never log or transmit secrets in plain text

  9. Message Handling:

  10. Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received
  11. Implement message deduplication to avoid displaying the same message twice

  12. Channel Management:

    • Fetch all channel slots even if you encounter an empty slot
    • Ideally save new channels into the first empty slot
  13. Error Handling:

  14. Implement timeouts for all commands (typically 5 seconds)
  15. Handle RESP_CODE_ERR responses appropriately
"},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":""},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":""},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":""},{"location":"companion_radio_full/","title":"Companion radio full","text":"

View this page on MeshCore Docs.

"},{"location":"companion_radio_full/#full-companion","title":"Full Companion","text":"

Configure message alerts with the Companion notification builder. It generates CLI rules for contacts, rooms and channels, previews patterns, and can apply/test them over USB. Trusted contacts and rooms can send !notify strings with no GPIO and a 15-second playback limit.

For runtime on/off commands and board exceptions, see feature switches by role. Open the USB web console for the default ASCII terminal.

For practical on/off commands and update directions, see Full Companion feature switches.

companion_radio_full combines every qualified Companion transport for its platform into one artifact and acts as a host-backed LoRa mOTA source for updating other nodes. Most targets can run those transports simultaneously; the SenseCAP Indicator exception selects one secondary wireless transport per boot as described below.

Fresh Full Companion installs default to Bluetooth on and infrastructure WiFi off. WiFi remains compiled in: use set wifi.enabled 1 through the USB or Bluetooth CLI to enable it, or set wifi.enabled 0 to disable it persistently. Existing saved WiFi enablement is honored after an upgrade. The SenseCAP Indicator requires a reboot to apply its exclusive transport selection; primary ESP-NOW mesh operation is unaffected by the infrastructure-WiFi default.

The full Companion is deliberately not a LoRa OTA destination: it has no installable firmware staging store, refuses ota install, and never advertises its own firmware as an mOTA image. The RAK4631 Full variant can optionally keep a source-only RAK15002 SD archive of peer .mota files; that does not make the Companion an update destination.

Preference changes through the Companion protocol, WebConfig, or USB terminal report a storage error if saving fails, and restore their previous values. Settings that also change GPS or radio power-saving state restore that state; a busy radio rollback remains pending for recovery instead of reporting success.

Capability ESP32 full nRF52 full USB Binary Companion Yes Yes BLE Binary Companion Yes Yes USB ASCII terminal Yes Yes Dedicated USB plaintext logging No - shares the one USB TTY Yes Host-backed LoRa mOTA source WiFi TCP 5001 Exclusive USB mode or encrypted BLE; RAK4631 Full can additionally cache/serve peer images from optional RAK15002 SD WiFi Companion/WebConfig Yes No - nRF52840 has no WiFi Hardware serial Companion On targets with assigned serial pins On targets with assigned serial pins Ethernet Companion On targets with an Ethernet module On RAK4631 with RAK13800; the RAK15002 combination is explicitly one-at-a-time LoRa self-update No No Optional self-update WiFi on existing dual-app layouts; USB on single-app layouts Bluetooth DFU with a compatible bootloader; USB always supported"},{"location":"companion_radio_full/#rak4631-full-companion-optional-rak15002-archive-and-rak13800-ethernet","title":"RAK4631 Full Companion: optional RAK15002 archive and RAK13800 Ethernet","text":"

RAK_4631_companion_radio_full can cache and serve peer mOTA files from an optional RAK15002 card. This remains a source-only archive: ota install and bootloader installation are unavailable on the Companion, whether or not a card is inserted. Check the archive with ota cache [on|off]; use get sdcard [free] for card space.

The optional RAK13800 Ethernet module and RAK15002 share CS26. Ethernet is therefore off by default in this combined Full image and is controlled only for the current boot:

get eth\nset eth on\nset eth off\n

Startup and shutdown are asynchronous. get eth reports starting, retrying, online, stopping, or a failure state; accepting set eth on does not prove a DHCP lease. USB, BLE, and radio service continue while network operations wait. After requesting OFF, wait until get eth reports off before accessing SD. An already-entered vendor DHCP or renewal call cannot be cancelled midway; OFF keeps the SPI reservation until that call finishes and cleanup is safe.

Remove the SD card and end/cancel active OTA cache, transfer, serve, or host folder activity before set eth on. The command refuses unsafe states. Turn Ethernet off before inserting or accessing a card; an insertion during Ethernet use fails closed and still requires set eth off. This is not a supported hot-swap workflow. The combined RAK15002/Ethernet hardware path has not yet been hardware-qualified; see nRF52 RAK15002 storage for the receiver's separate signed-application path and loader prerequisite.

"},{"location":"companion_radio_full/#screenless-nrf52-spoken-button-confirmations","title":"Screenless nRF52 spoken button confirmations","text":"

Full Companion builds for T1000-E, RAK WisMesh Tag, MeshTracker X1, Muziworks R1 Neo and ThinkNode M3 use prerecorded spoken confirmations for their shared single-button controls. Other roles, legacy USB/BLE-only builds, screened boards and all Wio profiles retain their existing audio behavior. Set MESH_GPS_VOICE=0 at build time to retain tones on an eligible Full build.

User-button gesture Action / spoken confirmation One press Acknowledge an active notification: \"Notification cleared\"; otherwise \"Ready\" Two presses \"Advert queued\" or \"Advert failed\" Three presses \"Sound on\" or \"Sound off\" after the preference is saved Four presses \"GPS on\" or \"GPS off\" when the GPS setting is available Hold more than three seconds \"Shutting down\"; shutdown remains nonblocking while audio drains Hold during the first eight seconds of UI startup \"USB setup\" for terminal/rescue mode

MeshTracker X1's three-press gesture instead cycles \"Sound and vibration\", \"Sound only\", \"Vibration only\", and \"Silent\". The new setting must save before either live alert output changes. A failed save restores both preferences and announces \"Action failed\". An explicit mute or alert-mode change permits one courtesy confirmation without unmuting ordinary notifications. The four additional mode clips are compiled only for HAS_DRV2605 builds. Notification timers cannot cancel control speech, and the committed alert mode remains authoritative for vibration even if a notification-mask save fails.

Power-on and wake wiring remain board-specific. Speech is nonblocking and uses the existing nRF52 PWM player; each buzzer still needs a physical listening test for intelligibility. All spoken button, alert-mode and GPS recordings use Microsoft Zira Desktop with the selected compressed Companion voice profile. GPS on/off use three stored audio spans: one shared encoded GPS prefix and two phrase tails. Advert queued/failed also share an identical encoded prefix; Sound off/Sound only share one only on HAS_DRV2605 boards. The tails are not independently decodable words. Playback treats each prefix and tail as one continuous compressed stream, without resetting its codec, bit position, interpolation or DMA at the seam. The approved full-phrase PCM and PWM output remain bit-for-bit identical, including pronunciation and pauses.

The IMA audio payload is 3,881 bytes smaller on ordinary voice boards, or 4,971 bytes smaller on vibration-equipped boards, before the small segmented descriptor/decoder overhead. Tiny shared silence prefixes are not split. A representative Cortex-M4 GCC 14.2 size-optimized/LTO player-and-catalog build saves 3,732 or 4,792 bytes respectively after that overhead; final firmware savings can vary with toolchain and surrounding code. The existing generators still synthesize the two complete GPS phrases and button confirmations, then share byte-identical encoded prefixes; they do not try to identify word boundaries or splice independently spoken words. Windows Zira synthesis still requires Windows' local System.Speech tooling. R1 Neo's buzzer pin aliases its optional QSPI clock, so voice is not default-enabled when QSPIFLASH is selected. ThinkNode M4 is deliberately excluded: its former buzzer definitions incorrectly claimed I2C SDA and a battery-status LED, not verified audio hardware. Its Companion recipes no longer drive those pins as a buzzer.

The optional ESP32 RC32-without-display recipe is not part of this nRF52 extension: its fitted audio hardware needs verification and its ui-new controls require an ESP32 playback backend, not the nRF52 PWM player.

"},{"location":"companion_radio_full/#experimental-g726-speech","title":"Experimental G.726 speech","text":"

Ordinary builds still use the approved Zira IMA recordings. G.726 is a hardware test option only: set both MESH_BUTTON_AUDIO_HIL=1 and MESH_GPS_VOICE_G726_BITRATE=16 or 24. Invalid bitrates or a missing test gate are rejected. The generated catalog selects exactly one bitrate, retains the original sample counts, and omits the X1-only mode prompts on other boards.

The decoder needs 52 bytes of state, no heap or full-clip buffer, and about 1.2 KiB of Cortex-M4 code/tables. Its packing, resets and PWM lifecycle are checked against independently pinned FFmpeg output. At 24 kbps the audio payload is 25% smaller; at 16 kbps it is 50% smaller with more waveform error. Neither software comparisons nor successful builds qualify physical buzzer intelligibility or the refill deadline under Bluetooth/radio load.

scripts/generate_g726_voice.py regenerates both experimental catalogs from the approved mono 8 kHz Zira WAVs using FFmpeg's MSB-first g726 encoder. Production IMA headers cannot be selected as its output. The decoder's Sun provenance, attributed FFmpeg MIX interpolation and LGPL license are retained in src/helpers/ui/g726; no claim of independent ITU conformance is made.

"},{"location":"companion_radio_full/#nrf52-bluetooth-reconnect-qualification","title":"nRF52 Bluetooth reconnect qualification","text":"

Full Companions persist the paired client's notification subscriptions (CCCDs) alongside its bond keys. A RAM-only subscription cache is insufficient: after reboot, a phone can retain its bond and subscription while the node has disabled notifications. Bluetooth then shows Connected, but Companion replies do not reach the app. The RAM cache remains useful for warm reconnects; the persistent bond record restores subscriptions after reboot.

The nRF52 build force-includes SoftDeviceSvcCompat.h in both application and framework compilation. It prevents LTO from discarding memory reads and writes performed by Nordic supervisor calls, including queries used to save CCCDs. test/test_nrf52_svc_compat.py compiles a real ARM regression with the build's LTO and pointer-analysis options and compares it with the unprotected wrapper.

On an already paired test node, run the hardware regression with bleak:

python3 scripts/check_nrf52_ble_reconnect.py \\\n  --address D1:A0:86:CD:BB:C6 --board \"Seeed Tracker T1000-E\" \\\n  --version <expected-firmware-hash> --reboot\n

This checks app-style NUL-terminated CLI commands, reboots the selected node, and reads the saved subscription before the host subscribes again. It also observes release of a secured link with no Companion traffic, followed by a normal reconnect and working CLI. It does not clear pairing keys or change preferences. Fresh pairing with the current PIN is a separate initial gate. A link drop alone does not prove the firmware watchdog fired: confirm the cause in the node debug log or Bluetooth trace. Link timeout reasons 0x08 and 0x22 must not be counted as a confirmed firmware-requested disconnect (0x13 at the central).

"},{"location":"companion_radio_full/#sensecap-indicator-transport-and-rendering-exception","title":"SenseCAP Indicator transport and rendering exception","text":"

Both SenseCAP Indicator Full layouts keep USB available and start exactly one secondary wireless Companion transport per boot: BLE or infrastructure WiFi. The saved companion.transport selection takes effect after reboot. BLE mode does not start infrastructure WiFi, WebConfig, MQTT, TCP Companion, or network OTA services. WiFi mode never initializes BLE and releases its controller and host memory. LoRa remains the primary radio on the LoRa layout. ESP-NOW remains the primary mesh radio in both ESP-NOW modes, including BLE mode; only the infrastructure-WiFi services are omitted.

The requested 4-bit internal render profiles are:

Primary radio Secondary Companion transport Canvas LoRa Infrastructure WiFi Native 480x480 LoRa BLE Native 480x480 ESP-NOW Infrastructure WiFi Native 480x480 ESP-NOW BLE 320x320, scaled 1.5x to the 480x480 panel

All four modes retain the same 160x160 logical UI. If a contiguous native DMA-capable block is unavailable, startup keeps the device usable with the 320x320 emergency fallback and reports the canvas actually retained. Such a fallback is not a successful native-480 validation result.

"},{"location":"companion_radio_full/#build-and-install","title":"Build and install","text":"

The target is synthesized by build.sh only for an exact board recipe that has passed the combined-transport size check:

The measured ESP32 additions are M5Stack Unit C6L; XIAO C6/S3, Meshimi, WHY2025 Badge, LilyGo T-LoRa C6, T3S3 SX1262/SX1276, T-Deck, TETH Elite, classic T-Beam SX1262/SX1276, and T-Beam S3 Supreme; Heltec Wireless Tracker, Wireless Paper, E213, E290, T190, CT62, and V4 expansion-kit TFT; Generic ESP-NOW; SenseCAP Indicator ESP-NOW/LoRa; Ebyte EoRa-S3; and Meshadventurer SX1262/SX1268. The measured nRF52 additions are GAT562 Mesh Watch13, LilyGo T-Echo Lite, LilyGo T-Impulse Plus, and Wio Tracker L1 E-Ink. Their old transport-specific names remain available for explicit compatibility builds, but the Full image is the canonical release artifact.

Every ESP32 Full image includes ordinary WiFi Companion, WebConfig, and the TCP mOTA services, even when its historical build base was USB- or BLE-only. On Generic_ESPNOW and SenseCapIndicator-ESPNow, ESP-NOW is also the primary mesh radio and shares the same 2.4 GHz hardware. Those two Full images retain B/G/N for ordinary clients alongside ESP-NOW LR. Their ESP-NOW mesh, setup AP, and infrastructure connection all use one persisted channel, which defaults to 1. Configure the router's 2.4 GHz radio and every other primary ESP-NOW node for that same fixed channel. Turning Companion WiFi off stops its TCP/AP services but deliberately leaves the ESP-NOW mesh radio running on the selected channel.

Every ESP32 Full Companion with a station connection synchronizes its UTC clock from NTP after boot and explicitly refreshes it every 24 hours. The common WiFi path owns this for non-MQTT and runtime-unconfigured nodes; a configured MQTT bridge owns the same boot-and-daily schedule. Successful sync updates an attached hardware RTC as well as the ESP32 clock and suppresses LoRa clock fallback for that boot. Reconnecting WiFi preserves the existing daily deadline. A timeout leaves the existing clock and fallback intact and retries without blocking radio work.

Inspect or change the shared channel from the Full Companion text terminal:

get espnow.channel\nset espnow.channel 6\nreboot\n

The accepted range is 1 through 13; use only a channel permitted in your region and supported by the router. The setter persists the selection, but the running radio stays on its current channel until reboot. Coordinate the change across every primary ESP-NOW node and the router before rebooting, or the node will lose one or both links. This primary-radio setting is not bridge.channel; that command belongs to the separate ESP-NOW bridge feature. To attach a LoRa-primary *_repeater_bridge_espnow gateway to these nodes, match its bridge.channel and select set bridge.format raw; the bridge's backward-compatible default is the distinct wrapped format. WiFi power saving cannot put infrastructure WiFi and ESP-NOW on different channels. On these two primary-ESP-NOW Full targets, max is unavailable because maximum modem sleep can make the station miss ESP-NOW broadcasts; use min for WiFi/BLE/ESP-NOW coexistence. The firmware also holds the ESP-IDF RF wake reference for the primary mesh radio, so the ESP-NOW receiver remains continuous even though the reported coexistence setting is min.

List the available targets:

bash build.sh list | grep companion_radio_full\n

Build by using one exact listed name:

bash build.sh build-firmware heltec_v4_r8_companion_radio_full \\\n  --firmware-version v1.17.0\n\nbash build.sh build-firmware RAK_4631_companion_radio_full \\\n  --firmware-version v1.17.0\n

To build every canonical full Companion target, select the corresponding interactive menu item or run:

bash build.sh build-full-companion-firmwares \\\n  --firmware-version v1.17.0\n

Canonical Companion bulk builds also omit legacy _ps and _femoff aliases. Power saving and controllable FEM receive gain are persisted runtime settings; the old names remain available through an explicit build-firmware command for compatibility. Full Companion replaces separate USB, BLE, ordinary WiFi, hardware-serial, Ethernet Companion, Terminal Chat, and USB-only packet-logging release artifacts whenever the exact board supports those combined transports. Direct builds of the legacy targets remain available. RAK4631 repeater and room server Ethernet builds remain separate because they are different standalone roles, not Companion transports. nRF52 separates framed traffic and logs; ESP32 makes those modes mutually exclusive on its one USB TTY. In WebConfig, use the FEM RX boost switch. From the text terminal (USB, or TCP 5002 on ESP32), use:

get radio.rxgain\nset radio.rxgain off\nset radio.rxgain on\nget radio.fem.rxgain\nset radio.fem.rxgain off\nset radio.fem.rxgain on\nget radio.fem.txgain\nset radio.fem.txgain off\nset radio.fem.txgain on\n

radio.rxgain controls the radio chip's boosted receive-gain mode; the FEM commands control the external receive and transmit paths. The selected states are applied immediately and retained after reboot. FEM TX gain is reported as unsupported on boards without software-selectable PA gain.

SSD1306 display builds also persist a runtime orientation. This replaces the separate rotated Full Companion release image:

get display.rotation\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\nset display.rotation 0\n

0 resets the screen to that board's compiled default orientation.

The display clock accepts decimal hour offsets, including half-hour and quarter-hour timezones, from -12 through +14:

get tz.offset\nset tz.offset 5.75\n

This Companion display setting is separate from MQTT's timezone.offset. Invalid values leave the previous setting unchanged; a failed save restores its exact previous fractional value.

Preference migration retains the published signed one-byte hour at offset 225 and every following legacy field. Integer settings keep the existing image format. Fractional settings append a 10-byte TZF version-1 footer after the existing board and TXR footers: a little-endian 32-bit float and CRC-16/CCITT covering the first eight bytes, stored most-significant byte first. The old byte contains the hour truncated toward zero. A missing footer uses that legacy integer; malformed, truncated, nonfinite or out-of-range footers reject the complete image before changing live preferences. Older firmware with strict image-length validation cannot read a fractional image; set an integer offset and save before downgrading.

Heltec E290 and T190 now use their Full Companion artifacts for simultaneous USB, BLE, and WiFi. Their older usb_ble, USB-only, and BLE-only names remain available only as explicit compatibility builds.

Heltec V3 and base OLED V4 Full Companion also include the former direct WiFi MQTT Companion capability. Configure and enable MQTT at runtime through WebConfig; the canonical release therefore publishes the Full image instead of a second companion_radio_wifi_mqtt image. V4 TFT and expansion-kit layouts remain separate hardware images because their display and I2C wiring differs.

Device power saving is separate from LoRa RXPS. It can be changed in WebConfig with the Device power saving switch or from the text terminal:

get powersaving\nset powersaving on\nset powersaving off\n

On ESP32, WiFi modem power saving is a third independent setting. Select it in the WebConfig WiFi card, or use the Full Companion text terminal:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget webui\nget password\npassword my-admin-pass\nset wifi.ssid MyNetwork\nset wifi.pwd my-password\nset wifi.powersave min\nset wifi.powersave max\nstart webconfig\nstop webconfig\n

ESP32 WiFi Companions with WebConfig, including Full Companion, expose a CLI tab at the node's LAN address. It defaults to on; use get wifi.cli, set wifi.cli on, or set wifi.cli off. Older builds that report the browser terminal as unavailable need a firmware update. The setup AP does not expose this tab.

ESP32 WebConfig and runtime browser OTA share the saved node admin password, initially password; the OTA HTTP Basic username is admin. get password reports status without revealing it. The password is stored in NVS and survives a filesystem erase. New values must be 1\u201315 UTF-8 bytes without CR or LF. Use 8\u201315 printable ASCII characters to protect setup WiFi with the same password; otherwise the hotspot is open and requires an HTTP admin login. The display's WiFi join QR uses the hotspot's current credential. After a successful change, the submitting page keeps its save result and tries the new login once; other browser sessions must sign in again. An unconfirmed save keeps the previous runtime password active, but the saved state after reboot may differ.

The browser accepts Companion configuration commands, including get powersaving, set usb.logging on|off, WiFi controls, and MQTT settings. set mqtt.enabled on|off controls MQTT without erasing broker settings; get mqtt.enabled, get mqtt.running, and get mqtt.status distinguish the saved switch from current connections. On MQTT builds, get mqtt.stats reports free/largest heap and accepted/error publish counts. get mqttN.diag reports the connection state, transport errors, and pub_ack, the count of actual broker acknowledgments for that client. Accepted publishes are separate from broker acknowledgments. Runtime slot capacity depends on the hardware profile; the V4 OLED Full profile runs one slot even though six broker configurations can be saved. set logging.output off|usb|wifi|both selects both outputs together. The browser also uses the complete Companion terminal for card, import meshcore://..., list, to, send, login, cmd, and delayed replies, with the same syntax as USB and TCP port 5002. See the terminal command guide for contact import and session ownership. USB MOTA session commands require the USB connection; WiFi host folders use port 5001.

On the two primary-ESP-NOW Full targets, the same terminal also provides get espnow.channel and set espnow.channel <1-13>. A channel change is persisted and requires a reboot, unlike a WiFi power-save change.

SSID and password writes return their reply first, then restart the Companion WiFi station with the saved credentials. A TCP terminal therefore disconnects shortly after either write; reconnect to the IP reported by the new network. Password entry is masked over USB. get wifi.pwd reads the saved password from any local connection, including binary command 0x42; LoRa cannot read it. It may be empty for an open network, an ordinary passphrase of up to 63 characters, or an exact 64-character hexadecimal WPA/WPA2 PSK. Other 64-character values and all longer values are rejected.

The normal binary Companion connection can also read or write this setting over USB, BLE, or TCP port 5000 without entering terminal mode. The mode values are 0 for min, 1 for none, and 2 for max; see the Companion protocol. A Full Companion that runs WiFi and BLE simultaneously rejects none because coexistence requires modem sleep. A primary-ESP-NOW Full target also rejects max; unlike infrastructure traffic, peer ESP-NOW broadcasts cannot be buffered by the access point while the station sleeps. If an older image saved a conflicting value, firmware applies and reports min instead. The SenseCAP Indicator uses the active-mode constraints in the exception above: LoRa + WiFi accepts all three values, ESP-NOW + WiFi accepts none|min, LoRa + BLE accepts min|max for the inactive WiFi setting, and ESP-NOW + BLE requires min. Fresh Cascade builds select min, and an existing valid saved selection takes precedence.

On radios with RX duty-cycle support, WebConfig and the text terminal also expose the persisted RXPS setting:

get radio.rxps\nset radio.rxps off\nset radio.rxps on\nset radio.rxps level 8 preamble 16\nset radio.rxps 65625 60000\n

Fresh Cascade-profile Full Companion builds start with RXPS on at level 8 and a 16-symbol preamble. Changing it takes effect immediately and remains selected after reboot.

Companion firmware defaults device power saving to on. Version 1.17.1.2 also turns it on once when upgrading an older Companion preference file, including one written by the short-lived default-off regression. After that one-time migration, an explicit set powersaving off selection remains persistent.

On ESP32, enabling it lowers the CPU clock to 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. Disabling it restores the normal CPU clock and keeps GPS awake. Active Full Companion transports remain available in both states; on the SenseCAP Indicator this means USB plus the selected BLE or infrastructure-WiFi secondary transport. WiFi modem sleep stays enabled when BLE and the ESP32 WiFi radio are active together because coexistence requires it. Changing device power saving does not overwrite the saved WiFi power-save mode. While a native-USB host is enumerated, the platform sleep attempt is held off so USB CDC remains responsive; detaching the host releases that guard. CPU, radio-modem, and GPS power-saving settings remain active, and USB power from a charger alone does not create a Companion session. The selected state is retained after reboot.

On the LilyGo T-Beam 1W Full Companion, press the physical BOOT button once to turn the ESP32 WiFi radio and all WiFi services off or on. The screen confirms WiFi: OFF or WiFi: ON, and the selected state is retained after reboot. When WiFi is off, TCP ports 5000-5002, WebConfig, and MQTT are stopped; USB, BLE, the display, GPS, and LoRa continue to operate. Press BOOT again to restore WiFi, including the saved station or setup-AP mode. On boot, BLE starts two seconds after WiFi/WebConfig so their peak startup allocations do not overlap.

Artifacts are written to out/ by default.

Full Companion behavior is selected with independent capability macros for TempRadio, the OTA CLI, the TCP terminal, USB folder seeding, and memory diagnostics. The legacy COMPANION_RADIO_FULL flag remains an input for older target recipes, but application behavior no longer uses that umbrella as an unrelated compile guard. In particular, ESP32 WiFi/WebConfig terminal controls are compiled from their actual WiFi/WebConfig capability. On targets that run ESP32 WiFi and BLE simultaneously, BLE is started first to avoid heap fragmentation. The SenseCAP Indicator starts only its selected secondary transport. Compile-time prerequisite checks reject inconsistent feature flags. After linking, the capability sidecar verifies USB, BLE, the OTA CLI, TempRadio, and each platform's host-folder transport; it also verifies the TCP terminal, WebConfig, and WiFi seeder on ESP32, plus dedicated logging on nRF52.

On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit in one artifact. The T-Beam 1W Full Companion uses that same LilyGo factory-compatible boot layout on its 16 MB flash because this source-only role does not install updates into a second app slot. Flash the generated -merged.bin when first installing this partition layout. Other boards with 8 MB or more retain dual application partitions. Heltec V2 and TLora V2 use 100 contacts, 8 group channels, and a 16-frame offline queue in this combined profile because of internal DRAM limits. Meshadventurer SX1262 and SX1268 use 100 contacts, 30 group channels, and a 16-frame queue. These capacities preserve the required 8 KiB static internal-DRAM reserve alongside MOTA and all Full transports; their ordinary transport-specific images retain 160 contacts, 40 channels, and 128 queued frames.

The 1.17.1.5 memory replacements for Generic ESP-NOW, Heltec Wireless Paper, Wireless Tracker, CT62, V3, Tracker V2, and XIAO C3 Full Companion use 150 contacts and retain their 256-frame queue and Full transports. Export contacts before updating if you have more than 150; entries beyond the new limit may be unavailable and a later save may omit them.

Full Companions normally retain 256 pending Companion message frames. ESP32 boards with configured PSRAM retain 512 and allocate that queue from PSRAM before WiFi and BLE start. If PSRAM is unavailable at runtime, allocation falls back through 256 and 128 frames, then to a 16-frame internal buffer. The Full Companion startup memory line reports the capacity actually allocated as offline_queue=<frames>. The queue is volatile and shared by all channels and direct messages; it is not flash-backed history. See Companion offline message queue for all platform defaults and full-queue behavior.

Every corrected nRF52 Full profile keeps 256 offline frames normally and temporarily lends 128 slots to mOTA to leave room for Bluetooth tasks, displays and UI allocations. Queue sharing preserves the board's contacts, channels, and USB/Bluetooth mOTA sending. See the memory correction. If an explicit OTA source, discovery, download, install, or temporary-radio request needs that workspace while more than 128 unread frames are queued, the oldest volatile frames are consumed until 128 remain. The command reports the number removed. Read-only OTA status queries do not remove messages. Sync the queue to an app before starting OTA when those unread messages matter.

The nRF52 target inherits the board's ordinary USB Companion installation format and adds BLE plus the serial mOTA source. It does not enable an SD cache or any other board-specific storage behavior; host files are streamed as they are requested. Its image is bounded by the board's normal application region, not the smaller OTAFIX in-place workspace reserved for firmware that can update itself.

"},{"location":"companion_radio_full/#interfaces","title":"Interfaces","text":"Platform Interface Purpose Both USB, 115200 baud ASCII after boot or observable USB session reset; automatically switches on the first complete Binary Companion frame Both BLE Binary Companion; display builds show a random session PIN, while headless builds default to 123456 ESP32 TCP 5000 Binary Companion over WiFi ESP32 HTTP 80 Companion WebConfig and first-boot WiFi setup ESP32 TCP 5001 Host .mota folder from motatool serve --tcp ESP32 TCP 5002 Full Companion text terminal; same role commands as the USB terminal nRF52 USB mOTA mode Host .mota folder from motatool serve --serial nRF52 Encrypted BLE mOTA service Paired phone/tablet/Linux host .mota catalog

Delivery-required replies are returned only to the interface which supplied the latest command. A contact-list stream keeps that route locked from CONTACTS_START through END_OF_CONTACTS; commands waiting on another interface are read after the stream finishes. Best-effort asynchronous observations such as adverts remain broadcast so passive clients can refresh their views. Companion session state is device-wide, so use one active Companion application at a time. On nRF52, BLE remains available while USB is in terminal or mOTA mode.

The Companion TCP listener starts only after a usable WiFi station or setup-AP address exists. If opening the socket fails, it closes any failed-start socket and retries with a delay capped at 30 seconds, without restarting the WiFi connection or disturbing BLE, WebConfig, OTA, or a healthy accepted TCP client. Turning WiFi off or suspending it cancels the listener request; retry service cannot turn it back on. Use get wifi.tcp through BLE, USB, or the browser CLI to inspect listener state, registration, accepted clients, completed RX/TX frames, and errors without a PC port probe. get display.wifi also shows the actual tcp listener flag independently of the network-services flag. A listening socket alone cannot detect LAN firewall or guest-network isolation.

ESP32 TCP sends are non-blocking, and inbound commands are serviced alongside outgoing data rather than waiting for an empty transmit queue. Unsolicited radio logs therefore cannot keep the app handshake unread. A live client with pending output is disconnected after 10 seconds without write progress; other transports continue running. Required replies retain their queue slots, so a new command waits if no reply can be admitted. Partially sent frames remain pinned to preserve framing, while already queued replies keep the same reconnection rules below.

On WiFi TCP port 5000, reconnecting from the same IP retains queued outgoing frames. A different IP takes over immediately, but never receives the previous IP's replies: up to four queued frames are parked separately for 30 seconds from takeover. The old IP can reconnect within that window to retrieve them. Only one displaced IP's backlog is retained; if another client also needs that slot, the older backlog is discarded so the new connection is not delayed. This reuses an existing fixed buffer instead of allocating additional frame storage. Pending commands, signing sessions, and contact streams belonging to the displaced WiFi session are canceled immediately; already accepted radio message transmissions are not canceled. Queued frames are retained, not a resumable command session, and IP matching is not authentication. Turning WiFi off clears both queues. Stored message history is unaffected.

Ethernet also retains complete queued frames for the same IP. Unlike WiFi, it has no spare backlog buffer: a different IP cancels the old owner's pending operations and discards its queued frames before the new socket is exposed. This keeps Ethernet's existing four-frame memory limit. On both transports, a live same-IP replacement preserves pending operations; an observed disconnect, disabled transport, or rejected input cancels them. Already queued frames can still be replayed on a same-IP reconnect, but canceled operations are not resumed.

Partial TCP writes retain the unsent bytes without interleaving later frames. After a reconnect, an incomplete frame restarts from its header; the new TCP connection must not receive only the old connection's trailing bytes.

Binary WiFi/Ethernet frames must declare 1\u2013176 payload bytes. Illegal lengths close the connection immediately, without waiting for the announced body or executing a truncated command. An incomplete binary header or payload has a five-second deadline once reception starts; reconnect to send a fresh frame. RAK13800 worker-owned SPI intervals pause this deadline without changing the connection or its queued replies. The remaining receive budget resumes when the worker hands the controller back; an already-expired frame is not revived. Ethernet raw-line terminal input has no typing deadline, but an overlong line also closes the connection instead of executing its prefix.

USB Binary output is queued as complete length-prefixed frames. Temporary CDC or UART backpressure pauses the contact stream; a frame may drain through a smaller hardware FIFO in ordered chunks, but its remainder is retained and no later frame can interleave with it or cause it to be discarded.

Binary login, status, telemetry, path-discovery, binary-request, and trace results also have bounded producer retention when that transport queue is full. One request result and one trace result can wait independently, on their original captured interfaces. Their required SENT frames are retained and admitted before the final result. A first valid radio result gets up to ten seconds for whole-frame queue admission; a still-blocked SENT keeps its earlier ten-second deadline. Duplicate callbacks cannot replace the retained result or extend these deadlines. Disconnect, cancellation, or expiry drops an unadmitted result. Queue admission is not proof of physical host receipt.

Reflected request tags are matched against the complete peer public key. Recent request and trace history has eight slots each, reserved before radio transmission; live entries are never evicted to accept a newer request. A full history or a quarantined identifier returns ERR_BAD_STATE, so clients must back off. Retired reflected tags and trace tag/auth tuples are suppressed for at least ten seconds and through the original radio deadline plus ten seconds when longer. Login retries to the same peer have at least a thirty-second quarantine. Positively matched newer status/telemetry/binary replies still work after login, but otherwise unsolicited responses from that login peer can be suppressed during the quarantine.

These guards are finite and clear at reboot. Login replies carry a server timestamp, not an echoed request nonce; sufficiently late replies or a timestamp collision cannot always be distinguished without a protocol change. Trace tag/auth fields are correlation data, not cryptographic authentication. The guards do not authenticate a host process or retract frames already accepted by a transport. In particular, the same-IP TCP queue replay described above remains unchanged, including its lack of authentication.

When a BLE client requests pairing, a display-equipped build wakes the screen, switches to the first home page, and keeps the active six-digit PIN visible until Bluetooth connects or the two-minute pairing window expires. USB, WiFi, Ethernet, and hardware-serial connections do not suppress this screen. With no saved BLE PIN, display builds generate a new PIN at boot; builds without a physical display use 123456. Use set pin 654321 in the Companion CLI to save a fixed pairing PIN, then reboot to activate it. set pin 0 restores the default/generated PIN policy. A PIN saved through the Companion protocol also takes effect after reboot. See the PIN command reference.

On T1000-E, a USB data host automatically pauses Bluetooth, including an existing phone connection. Unplug USB to resume Bluetooth after 500 ms; a saved off setting stays off. A wall charger without a USB data host does not pause Bluetooth. See the T1000-E transport policy for status and saved-setting behavior.

The Bluetooth device name is independently configurable. In the text terminal, use get bluetooth.name and set bluetooth.name <name>; use set bluetooth.name default to restore MeshCore-<node name>. The WebConfig Node card exposes the same optional field on Bluetooth-capable ESP32 builds. Custom names replace the complete Bluetooth label rather than inheriting the prefix, accept up to 31 valid UTF-8 bytes, and take effect after reboot. This does not change the node's mesh advertisement name.

Every Bluetooth Companion also supports get bluetooth.mac and set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>. A literal address is a BLE random-static identity, random creates one persistent identity, random-every-boot rotates it on startup, and random-after-connect rotates it on the first boot after an authenticated connection while leaving unused boots unchanged. default (or clear) restores the chipset address. None of these values changes the stealth flag. Reboot and re-pair after changing modes or after a rotation. Address rotation clears stale local bonds. On nRF52 this affects the MeshCore application only, not the OTAFIX bootloader's separate BLE DFU identity.

get bluetooth.stealth and set bluetooth.stealth on|off control an independent flag, defaulting to off. Combine it with a custom address or random to retain the address and bond across boots. With stealth on, normal discovery ends after the first authenticated pairing and only the saved peer may reconnect. Rotating MAC policies still rotate and reopen first pairing when they do; stealth remains enabled. Repeating on keeps an existing bond. Toggle off, then on, then reboot to reset first pairing. All flag changes need reboot. The node still sends directed or allowlisted BLE packets; it is not radio silent.

ESP32 TCP ports 5000, 5001, and 5002 remain trusted-network services. The HTTP admin login protects WebConfig and runtime browser OTA, and does not protect these ports or delegated remote CLI. Administrative console access can change the admin password. Expose TCP services only on a trusted LAN or temporary setup network. Temporary migration/recovery apps retain their documented access policy. See WiFi setup for credential setup and reconnect behavior.

"},{"location":"companion_radio_full/#usb-binary-and-text-terminal-modes","title":"USB Binary and text terminal modes","text":"

Full Companion USB starts in the ASCII terminal after boot or an observable USB session reset. MeshCore apps and meshcli send a <-prefixed framed command, which automatically hands the untouched frame to the Binary Companion parser:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

The automatic probe runs only at an empty prompt. A complete frame confirms binary mode; an incomplete probe returns to ASCII after one second. Binary mode then remains selected until an observable USB session reset, reboot, or the explicit terminal start token. See Full Companion USB CLI and binary switcher for the byte-level state machine, logging and mOTA ownership, recovery paths, and known limitations.

Immediately after boot, an ordinary terminal can issue ASCII commands without a start token. If the device is already in Binary Companion mode, open the port with the terminal start token sent automatically:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

The input map prevents any Binary Companion control bytes received during the mode transition from changing the local terminal's character set or display state while leaving UTF-8 emoji intact. The banner confirms that terminal mode is active; do not enter the start token again after it appears.

The terminal supports Companion chat commands, including channels, channel <name-or-slot> <message>, remote administration with login <admin-password> and cmd <remote-command>, and routed trace [recipient-name-or-prefix], plus local ota, tempradio, and normalradio controls. board reports the exact hardware target. ESP32 Full Companion also provides memory, which reports heap, internal allocation, PSRAM, and offline-queue usage; it is omitted from non-ESP32 help. ESP32 builds also provide local WiFi credential, status, WebConfig, and power-save controls. Every Full Companion provides persistent get/set usb.logging and starts with logging off on a fresh installation.

"},{"location":"companion_radio_full/#esp32-single-usb-serial-port","title":"ESP32 single USB serial port","text":"

Every ESP32 Full Companion exposes one USB TTY with two exclusive modes. It starts as the ASCII terminal unless a saved logging-on preference boots directly into the logging terminal. If it is already binary, enter its text terminal with +++MESHCORE-TERM-START. For an ESP32 1.17.1.5 USB logging session, run these separate commands:

set powersaving off\nset usb.logging on\n

The first command is the release's USB power-saving workaround. The same TTY then emits plaintext packet/debug logs and continues accepting CLI commands, including set usb.logging off. Framed Binary Companion is unavailable on USB while logging owns the TTY. Turning logging off sends the command reply, stops the logs, and leaves that TTY in the normal ASCII terminal, matching a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion afterward. A saved logging-on preference boots directly into this input-capable logging terminal. BLE and Wi-Fi Companion remain available while USB is logging on simultaneous targets. On the SenseCAP Indicator, only the selected BLE or Wi-Fi Companion transport remains available alongside the logging terminal.

ESP32 Full Companion uses the repository's Arduino-ESP32 2.x platform base where the board supports it. RC32 and ESP32-C6 retain their board-required Arduino 3.x platform, but follow the same one-TTY policy. No ESP32 Full image creates an optional second CDC interface. The single-TTY behavior applies to native-USB ESP32-S3 boards and boards using a USB-UART bridge.

"},{"location":"companion_radio_full/#nrf52-dual-usb-serial-ports","title":"nRF52 dual USB serial ports","text":"

Current nRF52 Full Companion firmware can expose two CDC ACM serial interfaces on one physical USB cable:

A fresh Full Companion starts with USB logging off and therefore enumerates only interface 00. Use set usb.logging on to save logging on; the reply says that a reboot is required. Use set usb.logging on reboot to save it and have the node reboot automatically after the reply. The second interface appears after that reboot. Likewise, set usb.logging off reboot removes interface 02. The optional reboot word is accepted only in these exact command forms and triggers a reboot only when the USB logging mode or descriptor needs to change.

For serial bridges that use one port for both commands and packets, including unmodified Cisien/meshcoretomqtt, use set usb.logging stream reboot instead. This saved packet-stream mode keeps only interface 00 and emits plaintext RAW/RX/TX records together with repeater-style -> CLI replies. Configure the bridge with the stable *-if00 path. Binary Companion and serial mOTA are unavailable on that port while logging is enabled; set usb.logging off stops the stream and leaves the ASCII terminal active. BLE remains available. get usb.logging reports stream and the primary port after reboot. To return to separate logging, use set usb.logging on reboot.

These are Full Companion text-terminal commands. The superficially similar meshcli ... get usb.logging command uses the Binary Companion parameter registry and can report Unknown var usb.logging; it does not forward that line to the text terminal. Enter terminal mode on interface 00 with the +++MESHCORE-TERM-START token as described above, then issue the command.

When logging is on, Linux normally shows two /dev/ttyACM* devices. Match the stable /dev/serial/by-id/*-if00 and *-if02 links, or use a udev rule matching ID_USB_INTERFACE_NUM, rather than assuming which tty number is assigned. On Windows they appear as two COM ports; identify them by USB interface instead of depending on a particular COM number. The nRF52 bootloader temporarily exposes its normal DFU serial interface during an update.

Opening the nRF52 logging port prints MeshCore USB logging port followed by its portable identity, USB CDC 1; interface 02; Linux stable suffix: -if02. get usb.logging reports the same endpoint. Firmware cannot print the exact /dev/ttyACM* or COM* name because Linux, macOS, or Windows assigns that name after USB enumeration; use the *-if02 link on Linux to obtain the exact path. For example, readlink -f /dev/serial/by-id/*-if02 prints the host-assigned /dev/ttyACM* name.

Every ESP32-S3 Full Companion image uses DIO flash mode, including the RAK3112 and RC32 profiles. The S3 ROM supports DIO while loading the software bootloader, and some flash configurations fail before the application starts when a merged image inherits QIO. DIO trades some maximum flash-read throughput for compatibility; it does not change a board's PSRAM type or any ordinary non-Full firmware profile.

On nRF52, point MeshCore Companion software, meshcli, and motatool at interface 00. When enabled and rebooted, point a plaintext reader or USB-connected MQTT service at interface 02. On ESP32, use its only USB TTY for Binary Companion while logging is off, or for the plaintext CLI/logger while logging is on; close one consumer before switching modes. Turning logging off returns to ASCII, so the normal stop token or a valid framed probe is still required before Binary Companion owns the port.

ESP32 Full Companion exposes this same text terminal on TCP port 5002. Connect with nc DEVICE_IP 5002; no USB control token is needed. USB terminal mode and the TCP terminal share recipient, login, command, trace, and display state, so only one may own the terminal at a time. The idle startup USB prompt yields to a TCP connection when no USB data client or partial command is present, and is restored when TCP disconnects. An active USB session rejects TCP; entering USB terminal mode later closes an active TCP session. On USB-Serial-JTAG and USB-to-UART hardware, the firmware cannot observe an idle host open, so actual buffered USB activity\u2014not the physical cable alone\u2014claims ownership. Disconnecting TCP clears pending terminal-only state without cancelling Binary Companion delivery or radio retries.

Both terminal transports accept reboot. The reply is sent first and the device reboots one second later, so a script can distinguish an accepted reboot from an abruptly lost connection.

Port 5002 is plaintext and has no device-local login gate. A remote-admin password entered with login is sent across the LAN connection as typed even though the terminal does not echo it. Use port 5002 only on a trusted LAN or a temporary setup network.

For example:

channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nshow\nshow channels on\nto Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\nlogin my-admin-password\ncmd ver\ntrace\n

The terminal list [n] command displays favorite contacts first and orders each favorite/non-favorite group by its most recent advertisement. This does not alter the binary Companion contact-list protocol.

Unsolicited terminal output starts in a quiet mode: advertisements and ordinary channel messages are hidden, while #emergency messages remain visible. Use show adverts on|off, show channels on|off, and show emergency on|off to control each category independently; plain show reports their state. These runtime filters affect terminal printing only and reset to their defaults after reboot.

The to command selects the remote-administration target. path shows its saved outgoing route; path direct, path clear, or a list separated by spaces, commas, or both changes the route used by subsequent login, send, and cmd commands. Every hop must use the same 2-, 4-, or 6-digit hexadecimal width. Login passwords are masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait for the asynchronous login result before using cmd; command replies appear as CLI -> from <name> and use a response window of 300% of the route estimate. DIRECT via path <hop,...> displays the exact saved prefixes copied into the packet. Remote ACL permissions determine which commands the target accepts. The matching reply reports its round-trip time from local queueing through result reception, including radio transit and remote execution. Only one terminal cmd can be pending at a time.

Incoming unicast replies are labeled ROUTED. Their exact return prefixes are not available at the destination because each forwarder consumes its prefix; use trace to verify the return route.

With no argument, trace uses the current to recipient. A name-prefix argument traces that contact directly without changing the current recipient. The contact must already have a known direct path; results show the SNR at each hop, or a timeout if the round trip does not return.

An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces, commas, and mixed separators are accepted:

trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n

The entered route must include both the outward and return prefixes. Exact three-byte traces are not supported.

Return to Binary mode with:

+++MESHCORE-TERM-STOP\n

An observable USB session reset restores ASCII, including after a Binary client disconnects. Native USB with DTR can detect a terminal closing; ESP32 hardware USB Serial/JTAG detects bus resets and physical host loss. UART bridges usually cannot report a terminal closing, so use the start token if they remain in Binary mode. A different baud rate, including 57600, does not select ASCII.

On an ESP32 Full Companion built with OTA_FOLDER_SERIAL, motatool can keep the USB serial port open as an mOTA folder source when WiFi is unavailable:

motatool serve --serial /dev/ttyACM0 --dir ./motas -v\n

The tool sends the exact completed ota folder on line when it opens the port. Firmware recognizes it in startup ASCII or idle Binary Companion mode, then transfers the port to exclusive USB mOTA ownership before binary request/reply frames begin. The text terminal and Binary Companion are unavailable on that TTY until motatool detaches the folder. This matches the nRF52 ownership behavior described below. TCP port 5001 remains the preferred unattended source transport.

"},{"location":"companion_radio_full/#nrf52-usb-mota-mode","title":"nRF52 USB mOTA mode","text":"

The nRF52 full target has a third, exclusive USB mode for the host folder. Unmodified motatool serve --serial sends ota folder on when it opens the port. The startup ASCII terminal recognizes that exact completed line, leaves terminal mode, and gives the stream directly to exclusive mOTA handling. If the port is already in Binary Companion mode, the idle binary parser recognizes the same control sequence. The sequence is not examined inside a framed Binary Companion packet. See the switcher guide for the ownership transitions.

While mOTA mode owns USB:

No modified motatool build, terminal token, or preliminary mode change is required.

"},{"location":"companion_radio_full/#nrf52-bluetooth-mota-source","title":"nRF52 Bluetooth mOTA source","text":"

Protocol v14 also lets a phone, tablet, or Bluetooth-capable Linux host feed the .mota catalog to an nRF52 Full Companion. Normal Companion commands stay on the Nordic UART service. Firmware data uses a separate GATT service, so binary app traffic cannot be mistaken for a firmware block.

The client must pair with the Companion PIN, subscribe to the mOTA Device Request characteristic, and send CMD_BLE_MOTA_SOURCE action start over the normal Binary Companion connection. The source is available only while that encrypted MITM-authenticated connection remains active. Disconnecting, unsubscribing, overflowing a frame, or receiving malformed data automatically detaches the catalog. USB and BLE source modes are mutually exclusive.

The included Raspberry Pi reference client validates each .mota with motatool, schedules the local TempRadio window, serves until interrupted, then detaches and restores the normal radio tuple:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion \\\n  --dir ./motas \\\n  --local 'tempradio 909.950,250,5,5,120'\n

Prefer this relative tempradio form when the phone/Pi and radio clocks may disagree. It starts a duration on the Companion and does not compare their wall clocks. Use the absolute tempradioat scheduler only after synchronizing the participating nodes.

Use --pair when the Linux host has not already bonded. BlueZ must have an agent capable of entering or confirming the six-digit PIN. Use --source status without --dir for a read-only channel/status check. The complete UUID, frame, action, and status definitions are in the Companion protocol.

This reference process stands in for the phone application. A mobile app can use the same sequence while retaining its normal contact and Repeater Admin UI: log in to the destination, put each required node on the same bounded TempRadio tuple, start the local Bluetooth catalog, then send the normal remote ota ls, ota pull, and ota install commands. The destination still checks container geometry, hardware identity, hashes, signature policy, and the OTAFIX bootloader before installation.

"},{"location":"companion_radio_full/#serve-mota-images-manually","title":"Serve mOTA images manually","text":"

First put the destination, required relays, controller, and source on the same bounded TempRadio tuple. The example frequency below is not legal everywhere; choose a legal tuple supported by every participating radio.

"},{"location":"companion_radio_full/#esp32-source","title":"ESP32 source","text":"

Use the local console to start TempRadio:

nc 192.168.1.50 5002\n
tempradio 909.950,250,5,5,120\nota status\n

Then start the dedicated TCP seeder:

motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v\n
"},{"location":"companion_radio_full/#nrf52-source","title":"nRF52 source","text":"

Use the USB terminal briefly to schedule TempRadio, then return to Binary mode and close the terminal:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM1\n
tempradio 909.950,250,5,5,120\n+++MESHCORE-TERM-STOP\n

After sending the stop token, exit picocom with Ctrl-A, Ctrl-X.

Start the serial seeder on that same port:

motatool serve --dir ./motas --serial /dev/ttyACM1 --baud 115200 -v\n

motatool switches the port into mOTA mode automatically. Stop it with Ctrl-C to detach the folder. Reopen the terminal and use normalradio if the source should return early; otherwise the saved radio settings return when the bounded window expires.

As a cable-free alternative, keep the normal Companion BLE session open and run the Bluetooth reference client shown in the nRF52 Bluetooth section. Do not run the USB seeder at the same time.

Both platforms intentionally refuse firmware installation commands such as:

ota pull <id> flash\nota install\nota dev ...\n
"},{"location":"companion_radio_full/#script-a-complete-update","title":"Script a complete update","text":"

The Bash and PowerShell wrappers accept a release ZIP or ready .mota, set up TempRadio, run motatool, monitor the exact image, install it on the destination, and restore the radio path. Use a separate Companion as the controller.

For an ESP32 full source:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-tcp 192.168.1.50:5002\n

For an nRF52 full source, the script automatically detects the token-switched terminal and uses the same source port sequentially for control and seeding:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n
$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof Node' `\n  --controller-serial COM7 `\n  --source-serial COM8\n

See the start-to-finish LoRa OTA guide for package selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior.

"},{"location":"companion_usb_ascii_validation/","title":"Companion usb ascii validation","text":"

View this page on MeshCore Docs.

"},{"location":"companion_usb_ascii_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_usb_client_validation/","title":"Companion usb client validation","text":"

View this page on MeshCore Docs.

"},{"location":"companion_usb_client_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"docs/","title":"Docs","text":"

View this page on MeshCore Docs.

"},{"location":"docs/#local-documentation","title":"Local Documentation","text":"

This document explains how to build and view the MeshCore documentation locally.

"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n
"},{"location":"double_frequency_v4_validation/","title":"Double frequency v4 validation","text":"

View this page on MeshCore Docs.

"},{"location":"double_frequency_v4_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"esp32_memory_budget/","title":"Esp32 memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_memory_budget/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"esp32_partition_catalog/","title":"Esp32 partition catalog","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_partition_catalog/#esp32-release-partition-lookup-table","title":"ESP32 release partition lookup table","text":"

The canonical LUT is firmware/esp32_partition_catalog.json in this fork. scripts/precompute_esp32_partitions.py regenerates it from GitHub release assets. MeshCore Open bundles an identical copy for offline phone updates. There is no need to run a command on the phone or download old firmware there.

"},{"location":"esp32_partition_catalog/#smallest-partition-by-board","title":"Smallest partition by board","text":"

The table shows each board's smallest known application partition across all cataloged releases, roles, and profiles. It does not count smaller NVS or filesystem partitions. Sizes use MiB (1,048,576 bytes), with exact bytes below. Mixed layouts means some releases have dual-slot OTA and others do not. These are historical factory-image minimums, not measurements of your device; use get storage.layout to check its actual installed layout.

Board Open JSON Download JSON

Loading catalog JSON...

BoardSmallest app partitionOTA layouts Show JSON details JSON view Snapshot and partition layouts Release inventory Complete catalog (large)
Expand this section to view the JSON.

With JavaScript disabled, use the complete JSON file.

"},{"location":"esp32_partition_catalog/#coverage","title":"Coverage","text":"

The initial snapshot enumerates every published release (including prereleases, excluding drafts) in these repositories, without a recent-release cutoff:

Repository Releases Inspected ESP32 factory images meshcore-dev/MeshCore 94 1,998 IoTThinks/EasySkyMesh (PowerSaving) 14 336 mikecarper/MeshCore (including keymindCascade releases) 69 7,808

All 10,142 standalone -merged.bin / -cleanInstall.bin images were parsed, with nine distinct partition tables and no unresolved selected assets. PowerSaving release tags can contain several firmware patch versions; the LUT uses each asset's version, not merely its containing release tag.

Eleven releases have no standalone ESP32 factory image. These include nRF52 update chains, RP2040/nRF52-only releases, an empty release, checksum-only metadata, and the ZIP-only Station G3 KISS modem package. They remain in the release inventory with an explicit note, but supply no inferred ESP32 layout. Unpublished builds, custom partition tables, and partition tables inside ZIP archives are not covered. This is an inventory of published factory layouts, not a claim that every Git revision has a known layout.

"},{"location":"esp32_partition_catalog/#matching-and-safety","title":"Matching and safety","text":"
  1. Ask the device for get storage.layout internally. Complete, valid OTA slot measurements take precedence over the LUT. A truncated reply is not proof that a slot is absent.
  2. Otherwise match board, role, and complete firmware version. Prefer the exact build hash when reported and present. Preserve profile candidates; do not conflate PowerSaving, upstream, and fork version suffixes or V4/R8 boards.
  3. Compare the chosen application length against both OTA slots (use the smaller slot so the next update also fits). If multiple profiles disagree, only make a fit/expansion recommendation when all candidates agree.
  4. An oversized image needs a supported exact-board two-stage migration bundle. A single-app layout cannot accept the bridge via normal dual-slot OTA and needs cable installation. Unknown layouts remain explicitly unknown.

Version-based results are estimates. OTA usually replaces only the app, so a newer version can run on an older partition table. Never use this LUT alone to authorize a partition-table write. The migration bridge must validate the actual flash geometry and identity recovery on the device before the final application upload. See two-stage migration.

The phone blocks known oversized regular uploads and opens its two-step migration section. Companion updates use the same capacity guard, but the Companion screen does not yet perform partition migration; it offers a smaller exact-board image or cable migration when necessary.

"},{"location":"esp32_partition_catalog/#schema-1","title":"Schema 1","text":"

The generator reads at most the first 36 KiB of each image, including partition tables at merged-file offset 0x8000 or 0x7000. It validates partition bounds, overlaps, duplicate app subtypes and the table MD5 when supplied. GitHub asset digests are recorded as provenance; a prefix read does not verify the full asset SHA-256. This LUT is not a substitute for firmware download verification.

"},{"location":"esp32_partition_catalog/#refresh-and-tests","title":"Refresh and tests","text":"

Use an authenticated gh CLI and Python 3. The generator has bounded downloads, retries, and a disposable prefix cache keyed by asset identity/update metadata. No secrets or firmware binaries are committed.

python3 scripts/precompute_esp32_partitions.py \\\n  --output firmware/esp32_partition_catalog.json \\\n  --cache-dir /tmp/meshcore-partition-catalog-cache \\\n  --copy-to ../meshcore-open-android-5.1.1/assets/firmware/esp32_partition_catalog.json\npython3 test/test_partition_catalog.py\n

--cached-inventory deliberately reuses the saved release listing for a resumable, reproducible snapshot. Omit it to discover newly published assets. Review coverage counts and unresolved after each refresh before shipping the LUT. No scheduled refresh job or release upload is installed by this script.

"},{"location":"esp32_wifi_partition_migration/","title":"Esp32 wifi partition migration","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_wifi_partition_migration/#esp32-legacy-partition-migration-over-usb-wi-fi-or-lora","title":"ESP32 legacy-partition migration over USB, Wi-Fi or LoRa","text":"

The partition-migration bridge lets a supported ESP32 node move from a small dual-OTA layout to a verified larger dual-OTA layout without cable flashing or a full-chip erase. It fits a legacy 1.25 MiB slot. The Wi-Fi version exposes an updater for the Full image after migration. The older identity-only LoRa handoff preserves a verified old OTA receiver so it can fetch the Full image into the expanded inactive slot. On 8/16 MiB flash that bridge restores identity before handing off; its 4 MiB route keeps identity staged until the Full image's first boot. Configuration-preserving Partition Expanders instead receive the Full image themselves after restoring identity and supported settings.

The distinct USB version performs verified identity/configuration restoration before the host writes the final app. It keeps application USB CDC disabled during flash operations and prevents Arduino startup from automatically erasing damaged/full NVS. S3/C3 return to ROM download automatically once restoration settles; original ESP32 reports matching-MAC settled UART status before the host uses DTR/RTS. Neither route needs physical BOOT access, but classic boards require their working USB-UART automatic-reset circuit.

The host must verify the same physical USB device and original chip MAC, the exact target table, CRC-valid NVS restored/no-pending markers, and current OTA metadata before flashing the unchanged final app. USB reappearance, a timer, or settled status alone is not recovery proof. An interrupted migration retains its pending host record and device-side restoration markers for a safe retry.

It has explicit, byte-for-byte generated target plans for the 4 MiB MeshCore dual-OTA Full table and Arduino ESP32 default_8MB.csv and default_16MB.csv. It does not guess a table for other flash sizes. Adding a flash size later means adding one reviewed target plan and its generated table-prefix test; the bridge core is otherwise shared.

Flash size Target table app0 / app1 size Typical board 4 MiB variants/dual_ota_full_4MB.csv 0x1F0000 / 0x1F0000 ThinkNode M2/M5, LilyGo T3S3, Nibble, Heltec CT62, Waveshare LR1121 observers 8 MiB default_8MB.csv 0x330000 / 0x330000 Seeed XIAO ESP32-S3, Heltec V3 16 MiB default_16MB.csv 0x640000 / 0x640000 Heltec V4 / V4.3, Station G2

These are the supported migration plans. The 8/16 MiB tables retain more filesystem storage and a coredump partition; their application slots are not the absolute maximum permitted by the physical flash capacity.

"},{"location":"esp32_wifi_partition_migration/#bridge-wi-fi-credentials","title":"Bridge Wi-Fi credentials","text":"

Join MeshCore-Migrate using the admin password saved on the old node. This applies to the Wi-Fi bridge and the recovery AP of LoRa bridges and Partition Expanders. The firmware stores at most 15 password characters; the bridge can use a saved password of 8 to 15 printable ASCII characters for WPA2.

If the saved password is shorter than 8 characters, contains other characters, or cannot be read, the bridge uses the published fallback meshcore-migrate. Missing saved preferences, including a node still using an unsaved built-in default, also cause fallback. Anyone nearby who knows that published password can then join and upload firmware when the updater is enabled. A saved default or other public admin password is also usable, so choose a strong, nonpublic password before migration. On the old node, use password <new password> and verify that it remains valid after a normal reboot before uploading the bridge.

The Wi-Fi bridge and configuration-preserving Partition Expanders restore the saved password before starting their AP in the expanded layout. The older identity-only LoRa handoff does not restore saved settings, so its Wi-Fi recovery AP after the table change normally uses the fallback. Follow the exact route in the package README. If a device remembers MeshCore-Migrate from an earlier bridge, forget that saved network or update its password before joining. Bridge packages built before this credential change use meshcore-migrate.

"},{"location":"esp32_wifi_partition_migration/#ordinary-full-releases-and-installation","title":"Ordinary Full releases and installation","text":"

Ordinary ESP32 node releases use Full for each exact board and firmware role. The existing RAM and application-size guards still apply: a failed Full qualification fails that release target rather than publishing a smaller ordinary image. Direct --standard builds and reduced identities remain compatibility/development tools outside the ordinary release selection.

An app-only update cannot change the installed partition table. If a device already has the required layout, use the matching Full application through a supported updater. If its layout needs expansion, use a verified migration ZIP for its exact entry in --list-boards and an updater supported by the old image. Otherwise, install the exact board/role's Full -merged.bin through USB/serial using that board's flashing procedure; the merged image contains the bootloader, partition table, and application. Never upload a merged image through an app-only browser or LoRa updater.

The new Full defaults outside the migration catalog, including ESP32-C6 boards and other boards introduced with min_spiffs.csv, have no reviewed wireless expansion package in this recipe. If their Full app fits the unchanged live layout and update identity, an ordinary app update can suffice. A table change requires USB/serial until a matching migration package is reviewed. A Full image fitting physical flash does not by itself prove it can be installed through an old updater. Full Companion images with a single app slot likewise need USB/serial for local firmware replacement; their host-backed LoRa sending role does not provide a second local update slot.

"},{"location":"esp32_wifi_partition_migration/#waveshare-esp32-s3-lr1121-observer-migration","title":"Waveshare ESP32-S3 LR1121 observer migration","text":"

Only the Waveshare repeater observer and room-server observer select the larger 4 MiB table. Ordinary repeater, room, Companion and KISS recipes retain min_spiffs.csv. The two exact migration selections are:

sh scripts/build_esp32_partition_migration.sh \\\n  --board waveshare-s3-lr1121-observer-repeater \\\n  --board waveshare-s3-lr1121-observer-room-server \\\n  --version v1.18.0-test --radio-preset usa-cascadia --profile cascade --dry-run\n

Remove --dry-run only to build and verify the packages from a clean commit; this command does not flash hardware. Each observer role has its own exact-target Partition Expander. Both its Wi-Fi application and first-stage LoRa package use that same configuration-preserving bridge, not the older identity-only LoRa handoff. It accepts a reviewed source layout, stages and verifies identity and supported configuration before destructive writes, and refuses unknown layouts or saved data that cannot be preserved safely. The final table has two 1984 KiB application slots and only 64 KiB SPIFFS.

The bridge can migrate from either existing OTA slot. After reboot it must verify the new table, private key and saved configuration before enabling the LoRa receiver. It uses the saved primary radio profile and fetches only the exact observer role's Full image. The node name, ACL, radio 1/radio 2 and saved GPS preferences remain part of the configuration handoff; temporary sessions are not converted into permanent preferences. Already-expanded boards use the existing verified-image/no-handoff safety checks rather than migrating again.

The recipes reuse the real LR1121 board implementation, including its 1.8 V TCXO and HP-PA/RF-switch wiring. The first-stage image must pass the legacy 1.25 MiB size limit and both final-image and mOTA staging guards. A failed guard must fail the package, not produce a reduced image. These are build/host-test contracts, not physical qualification: test both A/B placements, restored settings and interruptions on a sacrificial Waveshare before remote deployment. The ESP32's single partition-table-sector erase/write is still not atomic; power loss in that window can require cable recovery.

"},{"location":"esp32_wifi_partition_migration/#repeatable-esp32-build-recipe","title":"Repeatable ESP32 build recipe","text":"

From a clean commit, run the shell menu in WSL/Linux:

sh scripts/build_esp32_partition_migration.sh\n

Choose one exact board/role, or all listed board/role pairs. The menu also offers all repeaters, all room servers, or all sensors separately. It asks for the firmware version, radio preset, and profile, then builds Full application images first, the available bridge(s) for each selected board, and verifies each ZIP. Only one PlatformIO process runs at a time; build.sh can clean the shared .pio/build tree between Full targets. The same menu can be run non-interactively:

sh scripts/build_esp32_partition_migration.sh --board heltec-v4 \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\nsh scripts/build_esp32_partition_migration.sh --all \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\nsh scripts/build_esp32_partition_migration.sh --role sensor \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

--all creates one verified ZIP per listed board/role pair and a release ZIP containing all of them, their hashes, and a manifest. This is a local release bundle, not an upload to GitHub Releases. It is not an archive of old firmware binaries. --list-boards shows the configured choices. The release fails instead of publishing a partial bundle if one board package is absent or fails verification.

The underlying Python recipe remains available:

python3 -B scripts/build_esp32_partition_migration.py \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

The default builds all configured board/role recipes. Add --board heltec-v4 or --board xiao-s3-wio to build one; repeat --board for a selected set. Use --dry-run to inspect the command order without building. The resulting ZIPs appear under .releases/esp32-expanded-<commit>/packages/. The recipe requires a clean checkout so each ZIP identifies the firmware commit it was built from. It creates files only; it does not flash a device or erase flash.

"},{"location":"esp32_wifi_partition_migration/#independent-usb-firmware-set","title":"Independent USB firmware set","text":"

The USB intermediate firmware has six distinct chip/flash-capacity builds:

Chip Flash capacities USB completion Original ESP32 4, 8 MiB Settled UART status, then host DTR/RTS ESP32-C3 4 MiB Automatic ROM return ESP32-S3 4, 8, 16 MiB Automatic ROM return

To build only this complete set, without rebuilding or relabeling any Full firmware release, run serially in WSL/Linux:

python3 -B scripts/build_esp32_partition_migration.py --usb-only\n

This creates meshcore-usb-partition-bridges.zip under .releases/esp32-usb-bridges-<commit>/. Its manifest records each binary's chip, capacity, bytes and SHA-256, plus the utility source commit, dirty-tree state and runtime/configuration source hashes. All six are mandatory, and both the USB migration and NVS no-autoerase guard markers are verified. Existing Wi-Fi and LoRa artifacts remain unchanged. Exact new board/role migration packages also include their matching usb-bridge.bin.

A host can pair this standalone set with an older, exact Full migration ZIP; the Full image retains its original release/source identity. Meshfirmware downloads a versioned, checksum-pinned published USB set when a qualified older package has no USB bridge. It does not use an untested latest bridge, and an ordinary update that fits does not download migration utilities. For an offline or explicitly selected set, use MESHFIRMWARE_USB_MIGRATION_SET, or place the ZIP beside the flasher at tools/meshcore-usb-partition-bridges.zip. The original Full package must still match the selected final image and prove the target layout. No app-only upload or manual partition-table write substitutes for that proof.

The catalog covers canonical ESP32 repeater, room-server, and sensor roles on 4, 8, and 16 MiB chip-family plans, including historical 1.25 MiB candidates now built with expanded tables. This includes ESP32-S3, original ESP32, and ESP32-C3 boards with an evidenced old default layout. --list-boards is the authoritative list of exact targets; specialty ESP-NOW/observer aliases are separate identities and are not packaged under their old IDs. With Wi-Fi OTA they can migrate to the same physical board's canonical role image. The Wi-Fi bridge verifies and restores its supported saved settings; other settings may need to be recreated. The LoRa route still requires an exact old target ID. The catalog omits boards with no evidenced legacy default-layout build, such as boards introduced with min_spiffs.csv or another non-1.25 MiB layout. Some listed boards use larger slots today but had historical default-layout builds. A listed package is usable only when the installed old image supports the selected updater and the bridge verifies its live source table. Menu inclusion does not override either check.

Every listed board/role has Wi-Fi and LoRa bridge recipes. Heltec V4 and XIAO S3 WIO repeaters retain their board-specific bridge names; the other entries use matching original-ESP32, ESP32-S3, or ESP32-C3 chip-family bridges. The older identity-only LoRa handoff requires a verified same-target old receiver, and its 4 MiB route requires the slot-B-only sequence below. Configuration-preserving Partition Expanders use the route described in their package README instead. A 4 MiB Full build offers 1,984 KiB per slot. The packager verifies both the Full application's slot fit and its mOTA staging overhead. It supports at most 4096 blocks of 2 KiB for the Full transfer, and records the seeder proof-scratch requirement in the manifest. Older 4 KiB-scratch seeders are limited to 1024 blocks. A package fails if its required Full LoRa transfer cannot fit the supported staging/block limits.

Other ESP32 boards need a reviewed source updater, chip/flash-size bridge, verified Full image, and board entry before they can be added. The LoRa route also needs an exact old OTA target ID and a non-overlapping receiver handoff. Matching flash capacity alone is not enough to claim a package is safe.

"},{"location":"esp32_wifi_partition_migration/#what-the-bridge-preserves","title":"What the bridge preserves","text":"

Before it touches the partition table, the bridge reads the historical /identity/_main.id SPIFFS file. It requires all 96 bytes (32-byte public key followed by its 64-byte private key), stages them in an NVS namespace that is unchanged by the source and target layouts, then changes the table. The Wi-Fi bridge and 8/16 MiB LoRa bridge restore the staged value into expanded SPIFFS, read it back byte-for-byte, and only then clear the NVS staging record. On the 4 MiB identity-only LoRa handoff, the Full application's first boot performs this restoration.

The Wi-Fi bridge also stages and verifies its supported saved configuration: node name and preferences, radio profiles, ACL and login replay state, region keys, and selected network/OTA, filtering, display, and telemetry settings. It refuses migration if the saved files cannot fit or verify in NVS. The older LoRa handoff route preserves only the private identity, so saved settings may reset. The board/role-specific Partition Expander routes preserve their supported configuration and automatically fetch the exact-target Full image; follow the package README for those routes.

The old SPIFFS image is deliberately not raw-copied into an expanded partition: SPIFFS is not safe to resize that way. Files outside the supported preservation set, including logs and temporary radio sessions, are not preserved.

Migration does erase the 4 KiB partition-table sector and the destination app sectors. Expanded SPIFFS can be reformatted. It never issues a full-chip erase.

"},{"location":"esp32_wifi_partition_migration/#eligible-source-layout","title":"Eligible source layout","text":"

The bridge only accepts a source table that has all of the following:

This means the source app-slot and SPIFFS sizes can vary; the bridge validates the live geometry instead of carrying board-name rules. It also ensures it is running from one source OTA slot. If it arrived in B, it CRC-copies itself to the future A address, selects A in OTA metadata, writes the table, and restarts. The Wi-Fi route therefore works whether the browser OTA updater placed the bridge in legacy A or B. Single-app source layouts cannot use this route.

Keep power stable while the bridge is replacing the 4 KiB table sector. A loss of power before that point leaves the old table and source intact. After that small critical write, the selected A copy of the bridge is the recovery path. The stock ESP32 bootloader reads one partition-table sector; rewriting that sector is not atomic. A power loss during its erase/write window can leave an unreadable table and require cable recovery. This recipe must not be called power-loss-proof on a device whose USB/serial flash port is inaccessible.

"},{"location":"esp32_wifi_partition_migration/#migrate-a-seeed-xiao-esp32-s3","title":"Migrate a Seeed XIAO ESP32-S3","text":"

Use --board xiao-s3-wio with the recipe above. Its Wi-Fi bridge is a legacy-slot application image; its Full image keeps the old repeater's exact LoRa OTA target ID. Upload wifi-bridge.bin from the ZIP through the legacy node's existing Wi-Fi browser updater. Then join MeshCore-Migrate using the node's saved admin password, following the shared credential and fallback rules, and wait for Expanded layout ready. Upload full-application.bin at /update. Do not upload an -merged.bin: merged images include bootloader and table offsets for cable flashing, not browser OTA.

xiao_s3_partition_legacy_seed is a disposable-hardware test image only. It models the legacy table and an identity file; it is not a deployable repeater.

"},{"location":"esp32_wifi_partition_migration/#lora-only-migration-of-a-listed-role","title":"LoRa-only migration of a listed role","text":"

This section describes the older identity-only LoRa handoff. A package whose LoRa first stage is a configuration-preserving Partition Expander, including the Waveshare observer packages, uses its package README's self-hosted route.

The exact old board/role must already support MeshCore LoRa mOTA and have a valid EndF image identity. The LoRa bridge checks the old app's target ID and body hash before any partition-table write; an unsupported or damaged receiver is refused. Keep a capable seeder on the old receiver's compiled default radio profile before starting: this route may reset saved radio settings. Verify that the seeder lists the Full image and has the manifest's required proof scratch before installing the first-stage bridge.

On 8/16 MiB flash the bridge can run from either legacy A or B. It copies legacy B to the future B address, boots the bridge once to restore the private key in expanded SPIFFS, then boots the preserved old receiver. That receiver can fetch the Full image into its now-expanded inactive slot.

On 4 MiB flash the expanded app1 overlaps old app1, so that copy cannot be used. A verified old same-target receiver must remain in old A and the bridge must be installed in old B; a bridge in A refuses migration. If the old node is running B, first install and boot a working same-target old receiver in A, then install the bridge in B. After expansion the old receiver in A may use a temporary identity. The original key stays staged in NVS until the Full image restores it on first boot. Complete the second update before treating that device as the migrated node.

The board-specific LoRa bridges are heltec_v4_partition_migrator_lora_repeater and xiao_s3_partition_migrator_lora_repeater; the other listed roles use their configured chip-family LoRa bridges. Build the final application with bash build.sh build-firmware <target> --full-exact so its mOTA target ID still matches the old role. The repeatable recipe already uses this selector. scripts/package_esp32_partition_migration.py checks the images, partition tables, hashes and mOTA containers and creates one Wi-Fi/LoRa migration ZIP for each selected exact board/role. The ZIP README gives the exact ota ls, ota pull <id> flash, ota install sequence.

An old image without LoRa mOTA cannot use the LoRa-only route. Use its working Wi-Fi updater with a listed package, or USB/serial if no supported updater or package is available.

"},{"location":"esp32_wifi_partition_migration/#heltec-v4-v43","title":"Heltec V4 / V4.3","text":"

Build heltec_v4_partition_migrator, upload it through the legacy Wi-Fi updater, then upload the Full V4 app-only image after the bridge reports ready. The 16 MiB target table creates two 6.25 MiB OTA slots. See the V4 build notes for its build command.

"},{"location":"espnow_bridge_setup/","title":"Espnow bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"espnow_bridge_setup/#esp-now-bridge-simple-setup","title":"ESP-NOW bridge: simple setup","text":"

Use two ESP32 LoRa boards to join two MeshCore networks with different radio settings. Each board repeats on its own LoRa network, and the boards exchange packets directly over 2.4 GHz ESP-NOW. No router, internet connection, or manual peer pairing is needed.

LoRa network A <--> Repeater A <-- ESP-NOW --> Repeater B <--> LoRa network B\n
"},{"location":"espnow_bridge_setup/#1-choose-the-firmware","title":"1. Choose the firmware","text":"

Install the exact board's Full repeater image on each board. ESP-NOW is included as a runtime transport on supported ESP32 Full repeaters. Newly combined ordinary repeaters leave it off until explicitly enabled; that choice is saved across reboot. Boards with measured capacity exceptions keep separate _repeater_bridge_espnow images. Historical dedicated target names remain available for direct builds with their original bridge-on defaults. Full Companion is a different role. See the firmware picker guide.

Open each board's USB console at 115200 baud and run:

get bridge.type\n

The answer can be espnow, mqtt+espnow, rs232+espnow, or mqtt+rs232+espnow, depending on the image. Confirm the independent ESP-NOW control is present with get espnow.enabled and get espnow.running. The espnow commands select this transport without changing another bridge in a combined image.

"},{"location":"espnow_bridge_setup/#2-set-each-boards-lora-network","title":"2. Set each board's LoRa network","text":"

Configure one board at a time over USB. Replace the placeholders below with the radio settings used by the network on that board's side. Use Bridge-A for the first board and Bridge-B for the second, or choose your own names.

set espnow.enabled off\nset name Bridge-A\nset radio <freq>,<bw>,<sf>,<cr>\nreboot\n

Do not type the angle-bracket placeholders literally.

Radio option Meaning freq LoRa frequency in MHz. Match the local network on this side. bw LoRa bandwidth in kHz. Match that network's bandwidth. sf Spreading factor, 5-12. Match that network's setting. cr Coding-rate value, 5-8. Match that network's setting. name A label to tell the two repeaters apart. It does not pair them.

set radio requires a reboot to apply. After reconnecting, get radio shows the saved settings. Use permanent radio settings for this setup; tempradio sessions expire.

"},{"location":"espnow_bridge_setup/#3-configure-the-esp-now-link-on-both-boards","title":"3. Configure the ESP-NOW link on both boards","text":"

Run this block on both boards. Replace YourSharedKey with the same case-sensitive secret on each board, using 1-15 characters. Channel 6 is an example; both boards must use the same channel.

set espnow.enabled off\nset bridge.channel 6\nset bridge.format wrapped\nset bridge.secret YourSharedKey\nset bridge.source tx\nset bridge.delay 500\nset repeat on\nset espnow.enabled on\n

These settings are saved across reboot. Bridge changes apply immediately; changing an enabled bridge's channel, format, or secret restarts the bridge. Stopping it first lets you finish configuring both sides before connecting them.

"},{"location":"espnow_bridge_setup/#what-each-bridge-option-does","title":"What each bridge option does","text":"Option Choices and purpose espnow.enabled on starts ESP-NOW; off stops it and keeps its saved settings. Independent of MQTT or RS-232. rs232.enabled On combined UART images, starts or stops RS-232 independently of MQTT and ESP-NOW. Use get rs232.running to check the live UART. bridge.channel The 2.4 GHz channel, 1-13. Match it on both boards. This is separate from the LoRa frequency in set radio. bridge.format wrapped is the default for this two-repeater setup and uses the shared secret. raw connects to primary-ESP-NOW nodes such as Generic_ESPNOW or SenseCapIndicator-ESPNow; it ignores the secret. Both peers must use compatible formats. bridge.secret A matching, case-sensitive value of 1-15 characters for wrapped mode. It separates bridge groups; it is not strong encryption. MeshCore's own message encryption remains in place. bridge.source tx crosses packets this repeater transmits on LoRa, after its forwarding decisions; use this for the setup above. rx crosses received packets before those decisions and can pass traffic the local repeater would not forward. bridge.delay Wait before processing a packet received from the other bridge, in milliseconds. Range: 0-10000. Start with the default 500. repeat on enables normal LoRa forwarding; off disables forwarding. Keep it on on both bridge repeaters. Normal routing, scope, and packet-filter rules still apply. bridge.type Read-only firmware capability, including combined transports. espnow.running Read-only ESP-NOW runtime state. on means the local bridge started; it does not prove the other board is reachable.

This fork uses set bridge.source tx or rx. Some upstream guides use logTx and logRx; those are not the command values used here.

For a LoRa gateway connected to primary-ESP-NOW Companions instead of another LoRa repeater, see raw ESP-NOW compatibility.

"},{"location":"espnow_bridge_setup/#4-check-the-link","title":"4. Check the link","text":"

Run on both boards:

get espnow.enabled\nget espnow.running\nget espnow.status\nget bridge.channel\nget bridge.format\nget bridge.source\nget bridge.delay\nget repeat\nget radio\n

For the example above, expect enabled and running on, matching channel and format, source tx, delay 500, repeat on, and the intended LoRa settings on each side. An existing bridge.source rx setup is also valid; diagnose the selected source rather than changing it just to match this example. Compare the shared secret privately; omit get bridge.secret from logs you share. get espnow.status distinguishes the configured channel from the actual 2.4 GHz channel. Its last error stage/code is historical and may remain after recovery. A running bridge is not proof that the other board received anything.

Send a message from a Companion on network A to one on network B, then reply from B to A. Check both directions; configuration readback alone does not test delivery.

If it does not work:

If those saved settings already match, use the bounded test below before changing them again.

"},{"location":"espnow_bridge_setup/#test-the-24-ghz-path-without-lora-forwarding","title":"Test the 2.4 GHz path without LoRa forwarding","text":"

Updated ESP-NOW bridge firmware includes runtime diagnostics in the ordinary image. No BRIDGE_DEBUG build flag or continuous USB logging is needed. Use the USB console so a WiFi change cannot disconnect your diagnostic session.

On both boards, start a fresh observation window:

espnow resetstats\nget espnow.status\nget espnow.stats test\n

On A, send one probe:

espnow test A1B2C3D4\n

OK - test queued id=A1B2C3D4; confirm on peer means queued locally. On B, run get espnow.stats test: rx must increase and rxid must be A1B2C3D4. Then send espnow test B1C2D3E4 on B and confirm that ID on A. The two commands are two one-way tests; there is no automatic reply or retry. Sender completion alone does not prove peer receipt. The optional ID is exactly eight hexadecimal digits, without 0x; omitting it uses the current uptime in milliseconds as a correlation ID.

These small, unencrypted CRC-checked probes test transport reachability only. They do not validate packet-format compatibility or the wrapped shared secret, authenticate the peer or prove MeshCore packet delivery. They are never injected into the mesh or forwarded onto LoRa. Older firmware ignores them and cannot report the received ID; use an updated peer for confirmation. Sending a probe and resetting statistics require a local connection; neither changes saved preferences.

"},{"location":"espnow_bridge_setup/#locate-a-mesh-forwarding-failure","title":"Locate a mesh forwarding failure","text":"

If both probe directions work, send one fresh identifiable message from a Companion on A's LoRa network and read these pages on both bridge boards:

get espnow.stats\nget espnow.stats rx\nget espnow.stats tx\nget espnow.stats drops\n

Repeat in the opposite direction with a new message. Counters accumulate until espnow resetstats or reboot and survive an ESP-NOW stop/start. Inspect changes across the one message, rather than assuming old totals describe the current attempt. Stopping or restarting clears pending queue work without classifying delivery; the TX drop counter records local rejection/final-failure decisions, not every undelivered packet.

An absent counter change narrows a stage; these checks do not assume a cause for a particular pair of boards.

If both bridge boards also hear the same message directly on LoRa, normal duplicate suppression can discard its later bridged copy. That alone does not show a broken ESP-NOW link. This is a possibility to check with the counters, not an established cause of a reported delivery failure.

"},{"location":"espnow_bridge_setup/#optional-short-usb-debug-capture","title":"Optional short USB debug capture","text":"

For a brief observation window, run get usb.logging and get usb.debug and record both original settings. Enable both gates with set usb.logging on and set usb.debug on. The bridge emits bounded ESPNOW: status/counter updates from its main loop, at most once per second when diagnostic state changes. No BRIDGE_DEBUG build flag is required, and WiFi callbacks do not print to USB. Send the two probes or one fresh message, then restore both original settings. The CLI counter pages and probes do not require either logging gate. Use the counter pages for exact values; a busy USB console can skip a debug update. Do not include secrets or WiFi credentials in a shared capture.

A running bridge keeps the device awake, even with device power saving enabled. ESP-NOW shares the WiFi radio: use the connected WiFi network's channel when WiFi is active. Browser OTA pauses ESP-NOW on ordinary non-MQTT repeaters, reports that pause, and resumes saved-enabled ESP-NOW when OTA is stopped. A failed OTA start restores the bridge. The RS-232 transport remains available on combined boards while ESP-NOW is paused.

For every command's full details, see the bridge CLI reference.

For a broader comparison with a Linux-based bridge, see the MeshCore Nexus bridging guide.

"},{"location":"espnow_bridge_setup/#keep-wardriving-traffic-off-the-bridge","title":"Keep wardriving traffic off the bridge","text":"

New generalized repeater filter tables include a third built-in rule that blocks authenticated #wardriving text and data in both bridge directions, at every hop count. The same row blocks crossover between radio profiles:

get flood.rule.3\nset flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\n

The same rule can be added with the shorter command:

set fr any c=#wardriving m=bc d\n

Both forms use the same table. m=b means bridge only, m=c means crossover only, and get fr.N returns a short command for copying a numbered rule.

The unnumbered command adds the rule safely on existing devices whose saved tables predate this default. It works with either bridge packet source and both ESP-NOW formats. Nearby same-profile LoRa forwarding retains the separate four-hop default. Use mode=bridge for a bridge-only block, or mode=cross for a crossover-only block. See the mode matrix and examples.

"},{"location":"faq/","title":"Faq","text":"

View this page on MeshCore Docs.

"},{"location":"faq/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

For this fork's 1.17.1.5 USA Cascade release, start with feature switches by role and the USB web console. For nRF52 OTAFIX use the exact board/storage build from OTAFIX 2.4.6; older bootloader links below provide upstream/background context.

A list of frequently-asked questions and answers for MeshCore

"},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"

A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)

MeshCore is free and open source:

Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.

These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.

Anyone is able to build anything they like on top of MeshCore without paying anything.

"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"

A: Everything you need for MeshCore is available at:

You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).

"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"

MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.

For an up-to-date list of supported devices, please go to https://flasher.meshcore.io

To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.

"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"

MeshCore firmware is organized by role and, for companions, by host transport. The main roles are Companion, Repeater, Room Server, and Sensor. The tree also contains specialized terminal-chat and KISS-modem builds. Availability depends on the board; use the flasher or that board's PlatformIO environments as the current source of truth.

"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"

Companion radios connect MeshCore client software to LoRa. Depending on the board and build profile, the companion protocol can be exposed over BLE, USB serial, Wi-Fi, or Ethernet. Some boards also provide a Full Companion profile with additional local administration features. Check the exact build name and transport before flashing. Pending messages are held in a volatile shared queue; capacity depends on the platform and memory profile. See the Companion offline message queue for the exact defaults and limitations. See https://meshcore.io and the web client at https://app.meshcore.nz.

"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"

Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.

A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"

A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.

Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.

A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

When a client logs into a room server, the client will receive the previously 32 unseen messages.

Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.

The recommendation is to run repeater and room server on separate devices for the best experience.

"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"

A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.

If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.

If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.

After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:

set freq {frequency}

The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands

If you have more supported devices, you can use your additional devices with the room server firmware.

"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"

A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.

The native Android and iOS client uses the freemium model and is developed by Liam Cottle.

The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.

"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"

A: Supported frequencies depend on the radio hardware and the rules for the country in which it operates. Common MeshCore hardware covers portions of the 433, 868, and 915 MHz ISM bands.

Use the current regional preset offered by the client or flasher, confirm it with the local MeshCore community, and comply with local frequency, bandwidth, duty-cycle, and power limits. Do not copy an old frequency from this FAQ. Narrow presets commonly use BW62.5 with a lower spreading factor; their exact frequency and SF are region-specific and can change as deployments coordinate.

If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.

"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"

A: An advert announces a node and its public information to the network.

MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".

MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:

set flood.advert.interval {hours}

The separate set advert.interval {minutes} command controls the local zero-hop advert timer.

"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"

A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.

"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"

A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.

Repeater or room server can be administered with one of the options below:

https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"

A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:

set lat <GPS Lat>

set lon <GPS Lon>

You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.

"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"

A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:

password {new-password}

"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"

A: The default guest password to a room server is hello. Use the following command to change the guest password:

set guest.password {guest-password}

"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"

A: You can issue these commands to get or set a repeater's private key using a USB serial connection.

get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console

Reboot the repeater after set prv.key <hex> command for the new private key to take effect.

"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"

A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen

Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.

Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.

"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"

A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.

set agc.reset.interval <number>

The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.

The reset is skipped while a packet is pending, being received, or being transmitted. When safe, RadioLibWrapper::resetAGC() warm-sleeps the radio, returns the wrapper to idle so receive mode is re-armed, reapplies the cached RX boost setting, and starts a fresh noise-floor calibration.

"},{"location":"faq/#371-q-can-firmware-recover-an-external-radio-whose-busy-pin-is-stuck-high","title":"3.7.1. Q: Can firmware recover an external radio whose BUSY pin is stuck high?","text":"

A: Current firmware first attempts a non-destructive RX/AGC re-arm and then uses a physical radio reset when the board exposes one. A normal RadioLib reinitialization is not reported as a hard reset unless firmware can actually pulse NRST or cycle the radio's power rail.

Most supported external-radio boards expose NRST directly. RAK3401 pulses its dedicated NRST while leaving the shared peripheral rail and SPI controller intact, and LilyGo T-Beam 1W can cycle the radio regulator. The generic Tenstar C3 target describes a user-wired radio and has no reset connection by default. For unattended recovery on that target, wire NRST to a free GPIO and override P_LORA_RESET with that GPIO in the build configuration.

The SenseCAP T1000-E LR1110 path was hardware-qualified with 50 successful radio-only resets: 25 in continuous receive and 25 with RX power saving active. The saved radio tuple, TX power, RX gain, and RX power-saving configuration were restored after every reset, bidirectional LoRa status traffic still passed, and the nRF52840 uptime remained continuous.

An integrated STM32WL radio has no independently resettable RF peripheral, so it intentionally remains on the bounded soft-recovery path. If its radio core cannot recover, resetting only the radio is electrically impossible; the MCU must be restarted.

"},{"location":"faq/#372-q-does-firmware-automatically-update-lr1110-transceiver-firmware","title":"3.7.2. Q: Does firmware automatically update LR1110 transceiver firmware?","text":"

A: No. A transceiver update erases and rewrites the LR1110 itself, so it is not performed as a side effect of installing ordinary device firmware. Use the hardware vendor's physical-access maintenance procedure and verify the exact LR1110 and bootloader versions before changing them.

Full remediation of Semtech SEM-PSA-2026-001 requires LR1110 BL2 0x1001 as well as the applicable transceiver firmware. It is a separate two-stage maintenance procedure and is not performed automatically by MeshCore.

"},{"location":"faq/#373-q-how-does-firmware-handle-lr2021-lowhigh-band-changes","title":"3.7.3. Q: How does firmware handle LR2021 low/high-band changes?","text":"

A: Firmware first selects the board's matching RF path, retunes the radio, and then reapplies output power so the LR2021 selects the correct low- or high-frequency PA. It also uses the matching RF-switch mode when RX or TX is launched. LR2021 high-band output power is limited to 12 dBm; a lower board-specific limit still takes precedence.

On Meshnology W12, the two external FEM supplies are mutually exclusive: GPIO4 powers the sub-GHz GC1109 path and GPIO3 powers the 2.4 GHz RFX2402E path. Both supplies are disabled before the selected one is enabled. Temporary radio settings preserve the saved TX power and safely clamp only the active band; returning to the saved settings restores its applicable power. A permanent frequency change clamps the saved TX power to the new band's limit.

The external SX1262, SX1268, LLCC68, LR1110, and LR2021 drivers also bound the post-SetTx BUSY wait. A failed PA ramp therefore returns an error and can enter the physical-reset recovery path instead of hanging the node indefinitely.

"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"

A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard

"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"

A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.

However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.

Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.

"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"

Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.

"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"

The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.

"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"

As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.

Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.

"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command path.hash.mode do on a repeater?","text":"

This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.

Usage: set path.hash.mode {0|1|2}:

+----------------+-----------------------+\n| path.hash.mode | Advert path hash size |\n+----------------+-----------------------+\n| 0              | 1 byte (default)      |\n+----------------+-----------------------+\n| 1              | 2 bytes               |\n+----------------+-----------------------+\n| 2              | 3 bytes               |\n+----------------+-----------------------+\n

It is safe to set your 1.14+ repeaters to mode 1 or 2.

"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"

A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.

"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"

You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.

"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"

A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users

"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"

A:

  1. Device off
  2. Connect USB cable to device
  3. Hold down trackball (keep holding)
  4. Turn on device
  5. Hear USB connection sound
  6. Release trackball
  7. T-Deck in DFU mode now
  8. At this point you can begin flashing using https://flasher.meshcore.io
"},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"

A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.

GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.

Source

"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"

A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.

"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"

A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.

"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"

A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==

There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0

The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72

Source

"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"

A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):

Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script

There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script

"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"

Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.

"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"

A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"

A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}

See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19

Value Payload 0x00 Request 0x01 Response 0x02 Plain text message 0x03 Acknowledgment 0x04 Node advertisement 0x05 Group text 0x06 Group datagram 0x07 Anonymous request 0x08 Returned path 0x09 Trace 0x0A Multipart 0x0B Control/discovery data 0x0C OTA-over-LoRa data 0x0D-0x0E Reserved 0x0F Raw custom data

See Packet Format and Payload Format for the maintained descriptions.

Source

"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"

A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:

"},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"

A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")

"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"

A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.

"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"

A:

BW is bandwidth - width of frequency spectrum that is used for transmission

SF is spreading factor - how much should the communication spread in time

CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate

TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.

Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.

The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.

There are 4 code rates used in LoRaWAN:

4/5 4/6 5/7 4/8

For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.

Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.

Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you're far from each other, you have to talk slow (SF10), but if you're close, you can talk faster (SF7)

So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project

"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"

A: Companion clients do not repeat by default. Supported builds can opt into bounded client repeating on permitted frequencies, and the emergency-channel path has a bounded delayed relay behavior. Dedicated repeaters remain the normal and recommended way to extend network coverage. Room servers can repeat when explicitly configured, but separating the roles is generally preferable.

"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"

A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.

In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.

"},{"location":"faq/#54-q-how-does-a-node-discover-and-reuse-a-path-to-its-destination","title":"5.4. Q: How does a node discover and reuse a path to its destination?","text":"

Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.

Source

"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"

A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.

Source

"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"

A: The smartphone app key is in hex:

8b3387e9c5cdea6ac9e5edbaa115cd72

T-Deck uses the same key but in base64:

izOH6cXN6mrJ5e26oRXNcg==

The third character is the capital letter O, not zero 0.

Source

"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"

A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.

Firmware repo: https://github.com/meshcore-dev/MeshCore

"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"

A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.

Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase

Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut

"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"

A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769

Build instructions for MeshCore:

For Windows, first install WSL and Python+pip via: https://plainenglish.io/python/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80

(Linux, Windows+WSL) In the terminal/shell:

sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n

Mac: python3 should be already installed.

Then it should be the same for all platforms:

python3 -m venv meshcore-venv\ncd meshcore-venv && source bin/activate\npip install -U platformio\ngit clone https://github.com/meshcore-dev/MeshCore.git\ncd MeshCore\n

Choose an exact environment listed by pio project config or in the target board's variants/*/platformio.ini. For example:

pio run -e RAK_4631_repeater\n

The output is under .pio/build/RAK_4631_repeater/. Set the radio parameters to your current regional preset after flashing; if changing build defaults, keep that local change out of commits unless it is intended for every user.

"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"

A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.

Web client: https://github.com/liamcottle/meshcore-web JavaScript: https://github.com/meshcore-dev/meshcore.js

"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"

A: This repository does not ship or document an official ATAK integration. An external integration must account for LoRa airtime, moving endpoints, stale direct paths, and the collision cost of frequent flood fallback. Optional, bounded Companion repeating does not turn every mobile client into a dedicated repeater and does not remove those capacity constraints. Check current community projects before designing a deployment.

Source

"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"

A:

To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map

To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.

You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.

"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"

A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.

Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions

For ESP-based devices (e.g. Heltec V3) you need:

  1. Download the firmware file from https://flasher.meshcore.io.
    • Select the exact board and role. Artifact names and release URLs include a changing version and commit, so copy the current download URL instead of using an example URL from a guide.
    • A non-merged application image is for an already compatible bootloader and partition table. A merged image also contains low-level flash data. Back up settings before either operation; do not assume a merged flash preserves configuration, pairing data, or a previous partition layout.
    • To download a copied URL from the shell, use curl -L '<copied-url>' -o <firmware>.bin.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
    • Run ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null before and after connecting the board. ESP devices can appear under either name.
  3. Install esptool in a virtual environment.
    • python3 -m venv .venv && . .venv/bin/activate
    • python -m pip install --upgrade esptool
  4. Flash the firmware.
    • For non-merged bin:
      • python -m esptool --port /dev/ttyUSB0 --chip auto write-flash 0x10000 <non-merged-firmware>.bin
    • For merged bin:
      • python -m esptool --port /dev/ttyUSB0 --chip auto write-flash 0x00000 <merged-firmware>.bin

Instructions for nRF devices:

For nRF devices (e.g. RAK, Heltec T114) you need the following:

  1. Download the firmware file from https://flasher.meshcore.io.
    • Select the exact board and role, and download its current ZIP package. Do not rename a package from another board or reuse a static versioned URL.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
    • Run ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null; nRF bootloaders commonly appear as /dev/ttyACM0, but the number can change.
  3. In a virtual environment, install adafruit-nrfutil:
    • python3 -m venv .venv && . .venv/bin/activate
    • python -m pip install --upgrade adafruit-nrfutil
  4. Flash the nRF device.
    • adafruit-nrfutil --verbose dfu serial --package <firmware-package>.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200

To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:

To start managing your USB serial-connected device using picocom, use the following command:

From here, reference repeater and room server command line commands in the MeshCore docs here:

"},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"

A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.

As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.

"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"

A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore

Both the Windows and Mac versions of the client app are fully unlocked and are free to use.

"},{"location":"faq/#516-q-where-can-i-learn-more-about-meshcore","title":"5.16. Q: Where can I learn more about MeshCore?","text":"

A: Start with the MeshCore documentation and QuickStart playlist. For this fork, see feature switches by role.

"},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"

A:

You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.

"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"

A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.

"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"

A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.

"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"

A: The default Bluetooth pairing code is 123456 on builds without a physical display. Display-equipped Companion builds generate a six-digit PIN at boot and show it when a client requests pairing. The display stays awake on the PIN page until Bluetooth connects or the two-minute pairing window expires.

"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"

A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.

"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52/... device seems to be corrupted, how do I wipe it clean to start fresh?","text":"

A: If you're able to connect to the device from your MeshCore app 1. Navigate to Gear icon(Settings) on the upper right corner 2. Click on Export Settings button, choose Select All and Confirm. This will save your Node configuration 3. Now choose Factory Reset option 4. Confirm that you want to reset your device 5. Go to Bluetooth System settings and remove pairing of the device, so you can connect again 6. Connect your device in the app and enter the PIN 7. Navigate to Gear icon(Settings) on the upper right corner 8. Click on Import Settings and choose the file from 2., click on Select All and Confirm 9. Find Reboot button on of the Settings list

B: You're not able to connect to the App 1. Connect USB cable to your device 2. Go to https://flasher.meshcore.io 3. Search for your device in the list 4. Choose Companion Bluetooth 5. Press Enter DFU mode button, choose your USB device 6. Press Erase Flash button, choose the device again and wait until it completes 7. Press Flash! button and choose the USB device last time 8. The device is erased and newest firmware is installed 9. You might need to remove the pairing in Bluetooth System Settings in order to re-pair the app again.

An erase application or ordinary application ZIP/UF2 does not repair a damaged or partially overwritten SoftDevice. This can happen after flashing an nRF52 image for a different board or SoftDevice layout. A characteristic failure is that the application can boot and read its filesystem, but reboots or hangs when it writes or formats internal flash. Repeating Erase Flash is not a recovery for that condition.

Recover it with a combined bootloader + SoftDevice package for the exact board (including XIAO Sense versus non-Sense), installed through serial/BLE DFU or SWD. With adafruit-nrfutil, the serial form is:

adafruit-nrfutil dfu serial -p <port> \\\n  -pkg <exact-board-bootloader-with-softdevice>.zip -sb -t 1200\n

Then install the exact-board MeshCore application. Do not use an application- only UF2, filesystem formatter, or a similarly named package from another nRF52 board as a substitute. See the confirmed XIAO recovery in issue #3284 and obtain the matching package from the OTAFIX releases.

Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io

"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"

A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:

NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.

Allow user access on your USB port:

sudo setfacl -m u:$USER:rw /dev/ttyUSB0

"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"

A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:

  1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update
  2. On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
  3. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges
  4. Go to the Command Line tab, type start ota and hit enter.
  5. You should see OK to confirm the repeater device is now in OTA mode
  6. Run the DFU app, then tap Settings in the top-right corner
  7. Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.
  8. Select the firmware zip file you downloaded
  9. Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again
  10. If the device is not found, enable Force Scanning in the DFU app
  11. Tap Upload to begin OTA update
  12. If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app.
  13. Wait for the update to complete. It can take a few minutes.
  14. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.
  15. A companion must be running a build that exposes start ota; consult the current flasher release notes rather than relying on a version threshold in this FAQ.
  16. Please see the MeshCore Blog for additional information on OTA firmware flashing:
    • https://blog.meshcore.io/2026/04/06/otafix-bootloader
    • https://blog.meshcore.io/2026/04/02/nrf-ota-update
"},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"

A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX

After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.

"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"

A: For ESP32-based devices (e.g. Heltec V3):

  1. On https://flasher.meshcore.io, download the current non-merged application image for the exact ESP32 board and role (no merged in the filename).
  2. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.
  3. Go to the Command Line tab, type start ota and hit enter.
  4. The reply gives the OTA upload URL, network to use, and HTTP login details.
  5. If the ESP32 is not already joined to Wi-Fi, start ota starts the MeshCore-OTA hotspot at 192.168.4.1. It uses the node's admin password for WPA2 when that password is 8\u201315 printable ASCII characters. Other accepted admin passwords leave the hotspot open; the HTTP login still applies. If the node is already joined, the command uses its router-assigned address.
  6. From your phone or computer, join MeshCore-OTA when one was started. Use the node's admin password if the WiFi connection asks for a password.
  7. Open the exact URL reported by start ota. Sign in with HTTP Basic username admin and the node's current admin password, then upload the non-merged application bin from the flasher. The URL normally uses port 80, or port 8080 on Full Companion builds with browser OTA support.

start ota automatically stops WebConfig and reports the change. Stop the OTA uploader before running start webconfig again. Temporary partition-migration and recovery applications retain their documented access policy. See the WiFi guide for the shared admin-password policy.

"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"

A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.

Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.

Currently, the following boards are supported:

"},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"

A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo

"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"

A:

Channel: meshcore://channel/add?name=<name>&secret=<secret>

Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>

Where &type is:

"},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"

A: Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If it has no saved Wi-Fi configuration, it starts a MeshCore-Setup-XXXX access point. The hotspot uses the node's admin password for WPA2 when it is 8\u201315 printable ASCII characters; standard builds initially use password. Other accepted admin passwords leave the hotspot open and require an HTTP admin login. Join it and browse to http://192.168.4.1/. Protected first-time setup continues directly to the wizard, which configures WiFi, node identity, radio, and the admin password. MQTT companion builds add an MQTT step and tab, while non-MQTT companions remove those controls. After saving, the page and device screen show the station IP assigned by your router.

If a saved network remains unreachable for two minutes, the setup AP starts as a fallback. Sign in with the existing admin password before changing settings on that recovery page. If the original network returns, the companion closes the recovery portal and resumes normal LAN operation.

The setup AP always uses 192.168.4.1; the companion's station IP is assigned by DHCP and can be different. A real WIFI_SSID and WIFI_PWD can still be supplied at compile time as initial defaults, but they are no longer required.

On the LAN, sign in to the WebUI at the companion's station IP with its saved admin password. ESP32 WiFi Companions share that password between WebConfig, protected setup WiFi, and runtime browser OTA; other Companion builds retain their existing no-admin-password behavior. Change it with password <new-password> on USB or another available administrative connection. An ESP32 WiFi Companion's password is stored in NVS and survives a filesystem erase; get password reports its availability without revealing it. If it reports an unavailable management password, set a new one over USB to repair it.

The Companion protocol on TCP port 5000 retains its trusted-network policy; the browser login does not protect that port. Use a trusted WiFi network. Router WiFi passwords, MQTT credentials, and Bluetooth PINs remain separate. See the WiFi guide for password-change and reconnect behavior.

ESP32 repeater and room-server builds use the same page, but keep it off by default. Run set webui on from the admin CLI, open the reported LAN URL, and sign in with the admin password; get webui reports its current address. Their MQTT observer variants expose the MQTT controls, while ordinary builds do not. The two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt builds retain CLI-only configuration so their flash can keep two app slots for LoRa OTA.

"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"

A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx

[WARN] WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.

Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"faq/#78-q-how-do-i-use-ethernet-with-a-rak4631","title":"7.8. Q: How do I use Ethernet with a RAK4631?","text":"

A: MeshCore supports Ethernet on RAK4631 boards using the RAK13800 WisBlock Ethernet module (based on the W5100S chip).

Hardware required: - RAK4631 WisBlock Core - RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) - RAK13800 WisBlock Ethernet module - Ethernet cable connected to a network with a DHCP server

Firmware: Flash one of the Ethernet-enabled firmware variants: - RAK_4631_repeater_ethernet - Repeater with Ethernet CLI access - RAK_4631_room_server_ethernet - Room server with Ethernet CLI access - RAK_4631_companion_radio_ethernet - Companion radio over Ethernet (replaces BLE)

Connecting: - The device obtains an IP address via DHCP automatically on boot. - For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. nc <ip> 23 or PuTTY in raw mode). This gives you the same CLI available over serial/USB. - For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). - Use the eth.status CLI command to check connection status and see the assigned IP address.

"},{"location":"filter_tool/","title":"Filter tool","text":"

View this page on MeshCore Docs.

"},{"location":"filter_tool/#filter-policy-playground","title":"Filter policy playground","text":"

Build a forwarding policy, see its readable definition, and simulate how a repeater handles a packet. Rules match received packet facts, then apply actions such as dropping, scoping, rate-limiting, or retrying a flood.

Everything runs locally in this browser. Channel keys, packet facts, and policy drafts are not uploaded anywhere.

Policy design preview

The phases and core conditions model current FPF7 behavior, including its forward rows, scope rewrites, and shared blacklist. The readable policy language, JSON, and Base64 bundle are still a prototype: current firmware is configured with set flood.* or set fr commands and cannot install a bundle from this page.

"},{"location":"filter_tool/#build-a-policy","title":"Build a policy","text":"

Start with an example or build a rule, then test the draft against packet facts in the simulator below. The examples draw from Flood Filtering and Moderation.

To match every 1-byte path, select Path hash width \u2192 1 byte, type \u2192 ANY, all hops, no channel/input-scope restriction, and no prefix condition (or use the 1-byte example below). This also matches zero-hop packets. The firmware command is set fr any pb=1 d (or set flood.rule type=any hashbytes=1 drop); it adds a forwarding-drop rule, including for relayed login/admin floods. Direct routes and local delivery remain unaffected. Width-only rules require updated firmware. The simulator's policy definitions remain design-preview text, not installable commands.

Select 2+ bytes (2 or 3) to match both larger widths in one rule. The firmware command is set fr any pb=2+ d. A rate-limit action on this rule uses one allowance shared by both widths, rather than a separate limit for each. Four-byte paths remain unsupported.

Remote login and direct routes

This policy only decides whether a relay retransmits a flood. Direct packets and local delivery are unaffected. Rules matching the login/admin family can still reduce multi-hop remote-login reach, so the analyzer flags them.

Example policies Choose one to load its full rules into the builder and draft.

Read each summary as when all conditions match, do the actions. A match alone does not stop forwarding.

Payload selectors type= valueMatches grp_dataOnly the named payload type; see every exact type below anyEvery payload type class:groupGRP_TXT and GRP_DATA class:loginREQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH class:otherEvery remaining payload type that is not group or login Other conditions and actions FieldMeaning hops=Received hop count: all, 3+, 2-6, or 3 channel=* means no channel condition; a name or key authenticates one group channel; hash:XX is an unauthenticated one-byte fallback rx.scope=Original incoming transport scope path=Path prefix, blacklist, bucket, or loop match hashbytes=Encoded path-hash width: 1, 2, 3, or 2+ (2 and 3 bytes), including zero-hop packets tempradio=Temporary-radio state do dropDo not retransmit do scope=Set the outgoing transport scope do rate=Apply a per-minute rate and burst do timing=Select fast, normal, or slow scheduling

There is no class:txt. Use type=grp_txt for channel text or type=txt_msg for peer text.

Drop all floods with 1-byte path hashes (including zero hops) when type=any hops=all hashbytes=1 do drop Drop all floods with 2- or 3-byte path hashes (including zero hops) when type=any hops=all hashbytes=2+ do drop Set #BlackHole86 scope on all #rgdata group traffic when type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast Add #BlackHole86 scope to unscoped #rgdata data when type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast Replace #usa with #BlackHole86 on #rgdata data when type=grp_data hops=all channel=#rgdata rx.scope=scope:usa do scope=#BlackHole86 timing=fast Rate-limit packets whose path starts with 860C when type=any hops=all path=prefix:860C do rate=10/min burst=10 Drop selected flood types at their hop limits when type=control hops=1+ do dropwhen type=req hops=3+ do drop; same for type=grp_datawhen type=response hops=9+ do drop; same for type=anon_req and type=path Rate-limit Public messages from \u201cNoisy User\u201d when type=grp_txt hops=all channel=public sender=\"Noisy User\" do rate=5/min burst=5 Stop forwarding traffic from blacklisted internet gateways when type=any hops=all path=blacklist do drop Three defaults: OTA, distant #wardriving, and bridge/crossover #wardriving when type=ota hops=all tempradio=inactive do dropwhen type=any channel=#wardriving hops=5+ do dropwhen type=any channel=#wardriving hops=all via=bridge,cross do drop Set #BlackHole86 scope on login and bucket-matched other traffic when type=class:login hops=all do scope=#BlackHole86 timing=fastwhen type=class:other hops=all path=bucket:2 do scope=#BlackHole86 timing=slow

Rule builder

Reset form

The factory example contains all three repeater defaults. The playground's via= traffic-path matcher corresponds to firmware mode=radio|bridge|cross|bridge,cross; the advanced Active/Shadow/Disabled mode is a separate playground setting. See the firmware mode guide. This simulator models flood packets; firmware bridge/cross rules also cover direct routes.

Common match settings Payload type or class Any payload type Class: group text and data Class: login/admin family Class: every other payload REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel (optional) Original incoming scope Path hash width Any width 1 byte 2 bytes 2+ bytes (2 or 3) 3 bytes Path matcher No path condition Ordered 1/2/3-byte pbyte prefix Passive blacklist Path bucket 1 Path bucket 2 Path bucket 3 Path bucket 4 Path bucket 5 Path bucket 6 Own-ID loop: strict Own-ID loop: moderate Own-ID loop: minimal Ordered pbyte prefix Decrypted sender (optional) Traffic path (firmware mode) Normal radio forwarding Bridge entry/exit Radio profile crossover Bridge and crossover Temporary-radio state Either state Inactive Active Common actions

Choosing a phase-specific action automatically selects a compatible phase and ACL owner. Open the advanced section to inspect or override those choices.

Forwarding verdict No verdict change Drop / do not retransmit Region-gate decision Leave global gate unchanged Require original allowed region Bypass global gate for this packet Scope rewrite target Do not rewrite scope Regionless public scope Configured region Target name Token rate per minute (optional) Burst tokens Rewrite timing Inherit normal behavior Fast-track rewrite Normal queue timing Slow receive/transmit timing Queue priority Inherit High Normal Low Retry path bucket No retry action Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket 5 Bucket 6 Retry attempts Decision tag (optional) Advanced execution, ownership, and flood route

Direct packets already carry a supplied path and are intentionally outside this engine. The route condition below only distinguishes unscoped floods from transport-scoped floods.

Stable rule ID Processing phase 1 - Incoming scope gate 2 - Scope rewrite 3 - Forwarding decision 4 - Decrypted content 5 - Scheduling and retry Rule owner / capability ACL 4 - Region/scope manager ACL 5 - Filter manager Administrator Firmware-managed system rule Priority Mode Active Shadow - observe only Disabled Rule processing after a match Continue to later rules Skip later rules in this phase Skip all later policy rules Flood route Either flood route Unscoped flood only Transport-scoped flood only Readable policy definition

Add rule to policy Copy definition

Policy draft

Clear policy Approximate target budget Compact STM32 - 2 KB nRF52 - 8 KB Classic ESP32 - 16 KB Roomy ESP32 - 64 KB

Rules match the same immutable packet facts. Ordering is phase, then descending priority, then authenticated channel, raw hash, or wildcard specificity, then stable rule ID. Drop decisions are sticky.

Add a rule or load an example to start exploring.

Immutable packet simulator

Reset packet Read a raw MeshCore packet

Paste an on-air packet as hexadecimal. The decoder follows the MeshCore wire format, displays its header, route, pbyte path, and clear payload envelope, then loads only facts actually present on the wire into the simulator.

Runs locally Raw packet hex 014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de Decode and load packet Packet traffic path Normal radio forwarding Bridge entry/exit Radio profile crossover Received route Unscoped flood Transport-scoped flood Payload type REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel Encoded path hash width Infer from path (empty defaults to 1 byte) 1 byte 2 bytes 3 bytes Received pbyte path Original scope status Unscoped Allowed region Unknown or denied scope Original scope name Resolved region name Decrypted sender Passive blacklist result No match Matched Matching path buckets Own-ID loop result No loop match Strict only Moderate and strict Minimal, moderate, and strict Temporary radio is active Run policy simulation

Import and explain

Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.

Explain input Load into builder

Export

Readable policy Policy JSON Playground Base64

Proposed human-readable definition. Current firmware does not accept it yet.

Structured draft used by this page.

Browser-playground interchange only. It is not the final packed firmware codec.

Copy visible export Download JSON"},{"location":"filter_tool/#filter-builder-title","title":"Create a policy rule","text":""},{"location":"filter_tool/#filter-policy-title","title":"Rules in execution order","text":""},{"location":"filter_tool/#filter-simulator-title","title":"Explain an evaluation","text":""},{"location":"filter_tool/#filter-import-title","title":"Paste readable policy or a saved draft","text":""},{"location":"filter_tool/#filter-export-title","title":"Move or save this design","text":""},{"location":"filter_tool/#payload-type-reference","title":"Payload type reference","text":"

Use an exact type= value when a rule should match only one payload type. The class column shows which broader selector also matches it.

type= value Payload Class req Request class:login response Response class:login txt_msg Peer text message class:login ack Acknowledgment class:other advert Node advertisement class:other grp_txt Group-channel text class:group grp_data Group-channel datagram class:group anon_req Anonymous request class:login path Returned path class:login trace Path trace class:other multipart One frame in a multipart sequence class:other control Control or discovery data class:other ota OTA-over-LoRa data class:other 13 Reserved payload type 13 class:other 14 Reserved payload type 14 class:other raw_custom Application-defined raw data class:other"},{"location":"filter_tool/#current-fpf7-command-mapping","title":"Current FPF7 command mapping","text":"Firmware command FPF7 role fr / flood.rule / flood.filter Forward-phase match and action rows flood.channel.data Compatibility view over one visible type=grp_data forward drop row flood.channel.scope Scope-rewrite phase rows flood.filter.blacklist One shared unordered path-ID set referenced by path=blacklist rows

Generalized repeaters expose 63 forward rows and commit those sections together. The blacklist is useful for refusing to retransmit floods associated with internet gateways dumping bulk traffic, but a path ID is truncated and unauthenticated; it identifies a routing pattern, not a person.

The firmware accepts both full and short CLI forms for the same forward row:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\n

Use get fr.3 on the device to obtain a short setter ready to copy. This CLI syntax is separate from the playground's proposed policy/bundle format. See the shorthand reference.

"},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"

The simulator uses these rules:

  1. Only flood retransmission enters the policy. Direct routing and local packet delivery remain outside it.
  2. Every matcher reads the same immutable receive-time packet facts.
  3. Rules run by phase and descending numeric priority. At an equal priority, authenticated channel matches run before raw one-byte hash matches, which run before wildcard channel matches; stable rule ID breaks the remaining tie. An explicitly higher numeric priority overrides channel specificity.
  4. A drop decision is sticky and cannot be undone by a later rule.
  5. The first matching scope, timing, queue, and retry action in execution order wins.
  6. All matching token-bucket rate constraints remain attached to the decision.
  7. stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety.
  8. Shadow rules report what they would do without changing the decision or stopping other rules.
  9. Expensive facts such as channel-key matching, decryption, and path-table lookup are resolved once per packet and reused by every matching rule.

The byte-budget display is deliberately approximate until the packed firmware codec exists. It demonstrates why simple mappings should not reserve a maximum- sized structure for every possible condition and action.

"},{"location":"firmware_memory_budget/","title":"Firmware memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"firmware_memory_budget/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"firmware_picker/","title":"Firmware picker","text":"

View this page on MeshCore Docs.

"},{"location":"firmware_picker/#firmware-picker","title":"Firmware picker","text":"View files Hardware Loading hardware... Firmware role Loading roles... Hardware variant Choose hardware first Optional: chip family

Skip this if you know your board. Picking hardware selects its chip family automatically. Choose Any to clear this filter.

Chip family Any chip family - skip this filter Install operation Loading install choices... Logging / MQTT Loading logging choices... Firmware profile

Each button combines OTA support with the feature, sensor, and storage choices for that image. Reduced optional sensors means fewer environmental/ranging drivers; I2C and supported board peripherals remain available.

Loading firmware profiles... Connection / bridge mode Loading modes... Clear all choices Copy link to settings Short link to these settings

Current release set

Loading release information...

nRF52 bootloader Loading the current firmware catalog...

Exact firmware match

Recommended download

No exact firmware matched

Browse all firmware releases Picker help

Pick in any order. Use Any to clear one choice, or Clear all choices to start over. Use Copy link to settings to share your selection.

Choices stay limited to compatible builds. Picking hardware selects its chip family automatically. Filters disappear when they select the same file; runtime options appear under Restore your settings after flashing instead. Installation instructions and board-specific guidance follow your download.

The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, targets, and download links come from the published firmware assets.

Advanced: search current release filenames

Use this for uncommon board suffixes or expert recovery. A filename match is not a board-identity check.

Filename contains"},{"location":"firmware_picker/#what-the-choices-mean","title":"What the choices mean","text":"

New Heltec T114 images detect the optional screen automatically, so the same firmware works with or without it. Choose the board and firmware role; there is no screen option for an automatic-detection image. See the T114 update guide when moving from an older screenless target.

Pick the choices in any order. The controls stay limited to compatible firmware combinations that were actually built in the current release set. The result count stays visible while you scroll through the filters on a phone. Once the exact board and role are selected, the picker skips logging, connection, and profile choices that all select the same firmware. Logging settings move into Restore your settings after flashing beneath the files. Choices that select different images remain visible. For qualified nRF52 sensor pairs, Full is the normal recommendation. Open Advanced: sensor compatibility \u2192 Show Reduced sensor images to select a Reduced image for an existing update route or its smaller optional-driver set. Shared links that explicitly select Reduced retain that selection. Both downloads remain available; this changes the recommendation, not their OTA identities. The optional chip-family filter (ESP32, nRF52, RP2040, or STM32) can narrow the hardware list first. You can skip it: picking hardware fills it in automatically. Chip family and hardware are the only dropdowns. All remaining choices use buttons. Firmware profile combines OTA support, Full/standard, and sensor or storage differences when a choice between different images is needed.

Use Any to clear one choice, or Clear all choices to start over. The address bar updates with your choices; use Copy link to settings for a shorter link to the same selection.

Each result includes Restore your settings after flashing. Its commands follow the selected logging mode and the exact image's verified role/hardware controls. Click On, Off, or Check to view and copy the commands; Companion MQTT and GPS show their app/WebConfig steps instead. See feature switches by role for the full reference. The USB web console works with the default ASCII terminal on USB Companion and infrastructure roles.

The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, target names, and download links come directly from the published firmware assets.

Choice Use Companion A phone, computer, or host application controls the radio Repeater Standalone mesh relay Room Server Hosts room conversations and history Sensor / telemetry Publishes supported sensor data Terminal Chat Standalone serial-terminal interface USB logging / USB-connected MQTT Node remains attached to a computer over a data-capable USB cable Wi-Fi MQTT observer Firmware connects directly to MQTT over Wi-Fi; this is not USB logging USB logging + Wi-Fi MQTT Unified FULL image sends to both paths; avoid two publishers aimed at the same broker unless messages are deduplicated No logging Normal standalone operation with external logging output disabled Receives LoRa OTA Repeater, room server, or sensor image that can stage an exact matching update received over LoRa; any sensor/storage tradeoff appears on the same Firmware profile button Receives LoRa OTA - Reduced optional sensors Compact OTA image that omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals Reduced sensors + LoRa OTA Qualified nRF52 repeater, room server, or sensor build with the reduced optional sensor recipe and LoRa OTA retained Full supported sensors + LoRa OTA Qualified nRF52 build with all sensor drivers supported by that board's full recipe and LoRa OTA retained; does not add support for every possible attached sensor LoRa OTA source only Full Companion serving a host-supplied update to another node without self-installing it

Connection and bridge choices depend on the selected role. Companion firmware may offer Full, combined USB + Bluetooth, Bluetooth, USB, Wi-Fi, serial, or Ethernet transports. Normal repeater firmware includes runtime-controlled RS-232 support where the board has room; use the selected image's runtime directions to configure it. Choose Wi-Fi MQTT in the result's saved logging settings when it shares the Full image; separate MQTT images retain a Logging / MQTT filter. Full ESP32 LoRa repeaters include runtime ESP-NOW when capacity permits; dedicated ESP-NOW images remain for measured capacity exceptions. Ethernet still needs its exact hardware recipe.

For commands and option explanations, follow the ESP-NOW bridge setup guide.

An ESP-NOW bridge target keeps LoRa as its primary mesh radio. Expanded ESP32 Full LoRa repeaters include ESP-NOW, including boards without an MQTT recipe. Existing MQTT-backed Full repeater and room-server images combine both transports. Use set espnow.enabled on|off for ESP-NOW and, where supported, set mqtt.enabled on|off for MQTT. Newly combined ordinary repeaters start with ESP-NOW off until explicitly enabled, including the first upgrade from unmarked legacy preferences. Later reboots preserve that setting. On combined RS-232 boards without MQTT, bridge.enabled controls the UART. Use the selected download's board notes and runtime controls for images with independent UART, MQTT, and ESP-NOW services. MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must match the connected WiFi access point's fixed channel. ESP-NOW-only mode does not require WiFi credentials. Its runtime bridge.format setting chooses the peer protocol: wrapped (the backward-compatible bridge-to-bridge default using bridge.secret) or raw (direct MeshCore ESP-NOW LR frames for Generic_ESPNOW, SenseCapIndicator-ESPNow, and other primary-ESP-NOW nodes). This is one firmware choice, not two board images. Match bridge.channel to the primary nodes' espnow.channel before selecting set bridge.format raw.

"},{"location":"firmware_picker/#share-a-selection","title":"Share a selection","text":"

Select any combination of choices, then use Copy link to settings for a short URL under https://mikecarper.github.io/MeshCore/p/. Opening the link restores those choices after the release catalog loads. Partial selections work too, so you can share a board and role while leaving other choices open. Clear all choices removes the picker parameters from the URL. Changes update the current browser-history entry without reloading the page or adding a Back-button entry for every click.

For example, RAK3401 unified Full repeater preselects that board, role, OTA capability, and Full sensor policy. Supported internal or external storage is detected by the unified image.

Short links encode only the selected choices in a versioned s parameter. Hardware identifiers stay readable, and the link omits redundant hardware-family and automatically selected chip-family values. It keeps profile and install choices explicitly rather than relying on the current catalog's defaults. The address bar also uses the compact selection format; copying it works too.

Existing long links still work. Their query parameters are chipFamily, hardwareFamily, hardware, role, logging, ota, mode, feature, variant, and install. Values use the picker's internal identifiers rather than the displayed labels. One Firmware profile button sets ota, feature, and variant together. Existing links with only some of those choices still work and show their partial selection. The selection also records when chip-family filtering was chosen manually so automatic chip-family selection or an explicit Any choice behaves the same after reopening. Existing section anchors and unrelated query parameters are preserved.

Links use the current release catalog. If a linked choice is no longer available or conflicts with another choice, the picker identifies it and asks you to review the remaining selections. The downloadable HTML's Copy link to settings button creates a short public website link that other people can open.

"},{"location":"firmware_picker/#full-versus-standard","title":"FULL versus standard","text":"

Current ESP32 ordinary releases use one Full / complete image per exact board and role. Full keeps the supported feature set and CLI; runtime switches select USB, Bluetooth, Wi-Fi, logging, and other qualified features. Separate portable, minimal, and single-transport ESP32 images are installation/recovery recipes, not additional ordinary release choices. Board-specific capacity exceptions are shown with the selected download. Other chip families retain their qualified capacity and sensor-profile choices.

Moving an older ESP32 installation to an expanded Full layout requires a partition migration. Use the exact-board merged image over USB, or a supported exact board and role staged migration ZIP from the utility release. Read that ZIP's README before choosing the Wi-Fi or LoRa route. Do not send a loose app-only Full .bin directly to an older layout: the running application cannot move its own active and inactive partitions. Run get storage.layout on the installed firmware if available. It reports the live partition table; a version-based guess cannot confirm the installed layout. See the partition lookup guide when the old firmware lacks that command.

Current full-usb-wifi profiles use one binary for no external output, USB packet logging/USB-connected MQTT, direct WiFi MQTT, or both. The picker skips the logging filter when these modes use the same file; select the saved runtime mode with set logging.output off|usb|wifi|both. A FULL logging-fallback profile is listed only when no WiFi MQTT sibling exists; it supports both the no-output and USB settings because set usb.logging off|on is persistent. On a fresh unified FULL install with no saved SSID, the setup AP and WiFi radio remain available for 30 minutes per boot, then turn off automatically until the next reboot or power cycle. An explicit administrator start webconfig remains available as an override. A saved SSID switches to the normal indefinite reconnect behavior instead.

Fresh ESP32 Full infrastructure defaults to USB logging off. Existing saved logging and device power-saving settings survive an update. Automatic unconfigured boot setup also pauses network-bridge retries until an explicit bridge start, allowing its radio to turn off when the AP closes. Manual WebConfig/WiFi starts preserve an explicitly enabled ESP-NOW bridge, including when the master radio switch restores the selected services. Compact OTA images scan before raising a new AP and verify live driver readiness before reporting the update URL.

Full Companion profiles use one binary for USB, BLE, ordinary Wi-Fi on ESP32, source-only LoRa OTA, Terminal Chat, optional USB packet logging, and any board-qualified serial or Ethernet Companion transport. Bulk builds therefore omit separate attached-transport, Terminal Chat, and USB-logging artifacts whenever the exact Full recipe exists. RAK4631 repeater and room-server Ethernet images remain separate roles. Fresh installs default to logging off. Heltec V3 and base OLED V4 Full images also contain the former direct Wi-Fi MQTT Companion capability, configured at runtime through WebConfig, so their separate companion_radio_wifi_mqtt artifacts are omitted from canonical builds as well.

When Full Companion does not fit but a matching USB Companion does, that USB artifact also supplies Terminal Chat and replaces its standalone release image. Heltec E290 and T190 now publish one Full USB + BLE + WiFi Companion; their old combined and single-transport names are explicit-build compatibility aliases. SSD1306 Full Companion builds use set display.rotation 90|180|270; 0 restores the board default, so a separate rotated release image is not recommended.

New display-capable RAK4631 and RAK3401 images with OLED controller detection also offer saved display.controller choices: auto, ssd1306 or sh1106. This applies after restart and avoids a custom image when a panel needs an explicit controller. See controller selection.

New Heltec V4 R8 TFT images also support saved rotation in Companion, Repeater, Room Server and Sensor roles. The observer dashboard follows the selected portrait or landscape orientation. Once the canonical image proves this capability, the picker replaces separate portrait recommendations with runtime rotation controls. The stock R8 kit has no accelerometer, so automatic orientation is not offered. See display rotation.

Other new canonical images can replace redundant hardware or transport choices: the Heltec V4 OLED covers its matching OLED expansion kit, Pico W Companion combines USB/Bluetooth/WiFi, and RC32 detects its optional T108 display. The picker requires verified capabilities from the actual downloadable application before replacing an older recommendation. Existing downloads remain available through the advanced filename search; changing target names still requires the matching local migration rather than a cross-target LoRa update.

Muzi Base Companion also includes the optional SuperIO accessory for its exact Uno or Duo radio, and T-Echo Lite Companion detects its e-paper panel. A new verified canonical image can therefore replace the matching SuperIO or non-shell recommendation. These accessory choices do not change the radio hardware selection. See optional screens.

The variant consolidation validation records the qualified package hashes, controls checks and pending hardware tests.

Ordinary non-OTA roles also use one artifact for normal operation and USB logging. On ESP32 1.17.1.5, run set powersaving off before set usb.logging on. Select the saved mode with set usb.logging off|on; no -logging- artifact is emitted. KISS, BLE-only Companion, and constrained LoRa OTA repeater images retain their protocol/partition contracts and do not inherit plaintext USB logging.

nRF52 Full Companion keeps the multi-role primary interface on 00; it starts as an ASCII terminal and automatically hands a complete < frame to Binary Companion. set usb.logging on reboot adds its plaintext interface 02.

Every ESP32 Full Companion instead exposes one USB TTY. Logging is off by default, so the TTY serves the ASCII/Binary Companion switcher. On 1.17.1.5, run these two text commands to enable USB logging:

set powersaving off\nset usb.logging on\n

The second command turns that same TTY into an input-capable plaintext CLI/logging stream; framed Binary Companion is unavailable on USB while logging owns it. set usb.logging off stops the logs and leaves the TTY in the normal ASCII terminal, matching a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch from there to Binary Companion. BLE and Wi-Fi Companion remain usable while the USB TTY is logging. ESP32 Full builds use the repository's Arduino-ESP32 2.x base where the board supports it; RC32 and ESP32-C6 retain their board-required Arduino 3.x platform but still expose only one USB TTY. A second ESP32 CDC interface is not part of the release profile.

The picker includes the power-saving workaround when selecting USB or USB + WiFi logging on ESP32 1.17.1.5. WiFi/MQTT-only logging does not need it while the Repeater/Room Server bridge is running; check get mqtt.running. The workaround is not added to nRF52 directions. See logging by role for the saved settings and the original firmware's USB sleep issue.

"},{"location":"firmware_picker/#companion-usb-terminal-on-linux","title":"Companion USB terminal on Linux","text":"

Full, USB and USB + Bluetooth Companion results include Companion USB terminal (Linux / Bash) under Restore your settings after flashing, with copyable connection and recovery commands.

Updated 1.17.1.6 USB Companions default to ASCII at boot and after an observable USB session reset. picocom -b 115200 /dev/ttyACM0 is sufficient for a fresh session. A valid framed app command automatically selects Binary Companion. The command below also works with older firmware or a port left in Binary mode.

Run this in Bash on the Linux computer connected to your radio:

picocom -b 115200 --imap spchex \\\n  --initstring $'\\r+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

Replace /dev/ttyACM0 with your radio's port, such as /dev/ttyACM1 or /dev/ttyUSB0. Use the primary USB data interface (00 when multiple interfaces appear), and close other apps using it. The init string requests the ASCII CLI; its leading carriage return clears an unfinished input line. --imap spchex displays binary control bytes safely during the transition.

If the terminal display is scrambled, exit picocom with Ctrl+A, then Ctrl+X, run reset in your Linux shell, then reconnect with the command above. At the radio prompt, run version and board to identify the node. See the Companion USB mode guide.

"},{"location":"firmware_picker/#companion-tempradio2-transmit-settings","title":"Companion tempradio2 transmit settings","text":"

LoRa Companion results for 1.17.1.6 and newer include Companion tempradio2: transmit on both under Restore your settings after flashing. After configuring tempradio2 in rxtx mode, choose Transmit on both to copy:

set radio2.cross on\n

This copies new messages to both radio and tempradio2. The setting is saved, outlasts the temporary session and reboot, and also allows OTA traffic to cross profiles. Default isolation copies set radio2.cross off; Check shows the temporary profile, crossing policy and per-profile transmit counters. See the two-profile setup guide.

"},{"location":"firmware_picker/#installation-methods","title":"Installation methods","text":"File Use -merged.bin Erase/fresh install, recovery, role migration, or partition-profile change on ESP32 over USB Non-merged .bin Update an existing same-board, same-role, same-partition installation *-migration.zip Exact ESP32 board and role staged partition migration; follow its included README for Wi-Fi or LoRa steps Other .zip firmware shown by the picker Native nRF52 Serial DFU update package; it is not an extra archive .uf2 UF2 bootloader drag-and-drop install or update .hex Erase/recovery flash with a supported wired programmer

Never send a merged ESP32 image through browser OTA or LoRa OTA. Back up the node identity, configuration, keys, and radio settings, and verify every filename suffix before flashing. A staged migration writes the partition table; power loss during that write can require cable recovery. The package README states what each route preserves and which older layouts it accepts.

"},{"location":"firmware_picker/#lora-ota-and-otafix","title":"LoRa OTA and OTAFIX","text":"

A LoRa OTA capable build can receive and stage updates for its role. A later LoRa update still needs an exact target identity, compatible partition signature, matching radio settings, and the correct update package.

Selecting an nRF52 board shows its direct Bootloader UF2, Bootloader DFU ZIP, and Bootloader HEX (SWD) downloads, when an exact published profile is available. ESP32 and other chip families do not show this section. The web picker checks the latest stable OTAFIX release; the downloadable picker includes the bootloader release named in its saved catalog. If a board has no verified mapping or a file is missing, the picker explains that instead of offering another board's bootloader. OTAFIX 2.4.11 publishes a combined MeshTower V2 SD/internal image, mapped only to Heltec_tower_v2_sdcard. The historical Heltec_tower_v2 internal-only profile is explicitly retired: move to the normal combined image with a one-time local USB/BLE DFU or SWD installation, not a cross-profile LoRa bootloader update. The picker does not silently substitute its bootloader. RAK3401 and RAK4631 each use their board-specific adaptive storage bootloader. Ikoka Stick, Nano and Handheld plus SolarXiao use the XIAO bootloader; Wio Tracker L1 1W and E-Ink use the L1 bootloader. See the board coverage audit for the source checks and boards still awaiting a dedicated, tested release.

Board mappings and file names, URLs, sizes and SHA-256 hashes come from bootloader-manifest.json in the OTAFIX release. Application UF2 and DFU files do not install a bootloader. Follow the bootloader release's migration instructions if the installed version needs a recovery bridge.

nRF52 LoRa OTA requires an OTAFIX bootloader built for the exact board and storage layout. Select the hardware-matched HEX, Serial DFU ZIP, or bootloader-update UF2 for the application image being installed.

"},{"location":"firmware_picker/#hardware-and-variant-names","title":"Hardware and variant names","text":"

Hardware families with multiple released targets get Hardware variant buttons. They separate revisions, display type, expansion kit, radio/PA layout, pin map, and other physical differences without crowding the Hardware dropdown. Firmware profile buttons combine OTA support, Full/standard, and choices that need a different image or wiring, such as serial port or external storage. The picker skips this step when all available profile choices lead to the same firmware file. The 1.17.1.9 nRF52 repeater, room-server and sensor releases publish two separately qualified choices: Reduced sensors + LoRa OTA and Full supported sensors + LoRa OTA, with -reduced-ota and -full-ota artifact suffixes. Each keeps its exact board/role OTA identity and storage layout; the suffix is not a new radio or partition identity. Labels come from the release's qualified sensor.profile.reduced / sensor.profile.full metadata and verified LoRa receiver, not from the filename alone. Both profiles remain separate in local and published releases, including boards whose optional sensor set is small enough that the images have little or no size difference. Packaging requires both profiles with matching packaged OTA identity and flash geometry, even when a matrix is resumed or partial publication is requested. A failed build is reported, never presented as a supported download. Companion is unchanged.

Normal SolarXiao, XIAO nRF52, Ikoka Nano/Stick/Handheld, T-Echo, T-Echo Lite, ThinkNode M1/M6, Wio Tracker L1 and SenseCAP Solar Room Server builds use onboard QSPI storage through their exact *_room_server_lora_ota targets, with both sensor profiles retained. T-Echo Lite Repeater similarly uses its exact LilyGo_T-Echo-Lite_repeater_lora_ota target. Each requires the board-matched QSPI-capable OTAFIX bootloader; a shared board-family name is not enough to choose a bootloader. Internal-only Room recipes remain available by exact name for explicit compatibility/recovery builds, but are excluded from ordinary build menus and future releases. For newly converted boards, the picker keeps an existing release's internal Room choice until that same release provides its qualified, source-matched QSPI Full/Reduced pair. This preserves working Room links during rollout; SolarXiao's already-retired internal choices stay hidden. Existing downloads remain available through exact filename searches. The legacy and QSPI images have different OTA identities and layouts: a QSPI package is not a cross-target LoRa update for an internal-only installation. Use the matching bootloader and documented local migration path when changing layouts.

T-Echo, SenseCAP Solar, ThinkNode M1/M6 and Wio Tracker L1 already use QSPI in their ordinary Repeater targets. Their older *_repeater_lora_ota_no_external_sensors identities remain explicit compatibility builds, not extra normal Full/Reduced choices. The picker hides these aliases only when a source-matched, qualified ordinary QSPI pair exists. XIAO nRF52 Repeater now also includes its runtime RS232 bridge, disabled by default (set rs232.enabled on|off). The separate bridge recommendation is hidden only after both ordinary sensor profiles prove the linked driver and packaged runtime controls. Existing bridge OTA identities remain unchanged.

Muzi Base Uno/Duo, T-Echo Card and Nano G2 Ultra have explicit *_repeater_lora_ota and *_room_server_lora_ota qualification recipes. Their dedicated bootloader ports still require physical qualification and publication. These experimental recipes reserve raw QSPI for OTA (no shared QSPI filesystem), are excluded from ordinary menus and bulk builds, and carry hardware.qualification.pending in their capability manifests so release packaging refuses them. Their current normal builds and picker choices remain available; do not install another board's bootloader to bypass this gate.

The legacy no_external_sensors profile is labeled Receives LoRa OTA - Reduced optional sensors on the same button. On RAK3401 and RAK4631 this compact profile uses internal storage without an external storage board. The reduction does not disable generic I2C or unrelated board-integrated peripherals. Reduced RAK3401 and RAK4631 targets retain INA219/INA226/INA260/INA3221 as voltage/current entries in the optional sensor table. They are not the only I2C users: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals in compatible recipes. RAK12501/L76K GPS uses Serial1 instead. The explicit RAK4631 Serial1 bridge omits the combined GPS provider because its bridge owns the RAK12501 UART, so that legacy image does not expose RAK12500 either. The firmware-configured INA3221 and RAK12500 addresses are both 0x42; to install both, keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43. Companion power saving, controllable FEM receive gain, and radio-chip receive gain are saved settings rather than separate recommended firmware files. Do not substitute a similarly named physical target.

The header-wired RAK19007 W25Q16 LoRa-OTA recipes are also exact hardware variants, labeled External storage board (W25Q16) in the picker. W25Q16 is the flash-memory part on the added storage board; RAK13302 identifies the radio module, not the storage board. Choose this variant for RAK4631 or for RAK3401 + RAK13302 only when that core/radio combination, the EF4015 flash wiring, and its matching OTAFIX bootloader are all present. The common base-board wiring does not make the two firmware or bootloader identities interchangeable.

The picker recommends one Full Companion image instead of separate USB, BLE, ordinary WiFi, and USB-logging images. On ESP32, logging is off by default so the one USB TTY starts in ASCII and automatically changes to framed Companion when a complete < frame arrives. Enabling logging gives that TTY to the plaintext CLI/logger and disables framed USB Companion until logging is turned off and the normal ASCII-to-binary mode switch occurs. nRF52 Full Companion retains its optional dedicated interface 02. Exact filename search finds compatibility images included in the selected release family. For older releases, open their GitHub release pages.

The current public matrix keeps one ESP32 Full image per board and role. Some richer images, including Station G2 Full, retain an observer-named OTA identity. Exact older identities needed by a partition migration are packaged inside that board and role's migration ZIP instead of adding a second ordinary Full download. A deliberate target-ID override does not make a different partition layout or physical board compatible.

Some normal ESP32 roles begin in the legacy 1.25 MiB dual-OTA layout but have a Full image with larger slots. The utility release lists the exact board and role migration ZIPs available for that release. Each package checks its source layout and target identity, contains the bridge and Full application files for its supported routes, and documents what saved data it preserves. Use the package's own Full application after the bridge; do not substitute a loose Full image with a different target ID. If no exact package exists, use the matching merged USB image. Once migrated, routine app-only updates still need the same target and partition signature.

"},{"location":"firmware_picker/#maintaining-the-runtime-directions","title":"Maintaining the runtime directions","text":"

See optional screens for manufacturer-confirmed screen options, automatic detection support, and integral-screen exclusions.

The online picker and downloadable HTML use the same command renderer. Hardware-specific controls are enabled only when _data/firmware_controls.json matches the selected release family and exact target. If that metadata is missing or belongs to another release, the picker retains basic role/logging directions and links the complete guide without inventing hardware support. For 1.17.1.6, that JSON also records the verified LoRa OTA role of each image and USB logging modes where the packaged application was checked. Logging metadata is tied to the source hash of its firmware file. The normal iKoka 30 dBm repeater has USB logging and LoRa OTA; its compact no-external-sensors image has LoRa OTA without logging. Capacity directions come from each qualified profile's recorded reductions and are tied to the source hash of the selected download. The older 1.17.1.5 USB sleep workaround stays limited to that release.

After qualifying a new release, resolve its PlatformIO configuration with no other PlatformIO process running, then generate the controls from that source revision and its staged manifests:

pio project config --json-output > /tmp/meshcore-picker-pio-config.json\npython3 scripts/generate_picker_controls.py \\\n  --stage /path/to/staged-release \\\n  --pio-config /tmp/meshcore-picker-pio-config.json\n

Refresh the data-controls-url version query when publishing the generated metadata so browsers fetch the new release's controls.

The generated partitionMigrations lookup comes from BOARDS in scripts/package_esp32_partition_migration.py, the same exact board/role table used to build the migration packages. Expanded ESP32 Full results use this lookup (including canonical board/role recipes for Observer profiles), then require one matching published ZIP in the same release family's utility page. Hardware variants and roles remain distinct. Unlisted targets, missing or ambiguous assets, and stale metadata never produce a guessed download link.

For the downloadable version, save the release family's public GitHub release objects as a JSON array, then package the same picker UI and controls:

python3 scripts/package_firmware_picker.py \\\n  --releases-json /path/to/releases.json \\\n  --output /path/to/FIRMWARE-PICKER.html\n

This HTML embeds its catalog and directions, so selections work without an internet connection. Firmware downloads and the USB web console still need network access. When replacing the downloadable picker on release pages, update its entry in each page's SHA256SUMS.txt as well.

"},{"location":"firmware_picker/#memory-corrected-11715-downloads","title":"Memory-corrected 1.17.1.5 downloads","text":"

Corrected downloads retain the 1.17.1.5 release page and use source suffix aa20e927, with 1e4d1e16 for Wireless Paper Full's 350-contact follow-up. The picker accepts replacement source hashes within that exact release version. Its installation directions identify the nRF52 queue-sharing behavior, Wireless Paper Full's 350 contacts with a shared 256/128-slot queue, and the 150-contact limit on the other six affected ESP32 Full profiles. Those notices apply only to the corrected files. See the memory correction details and each replacement's .memory.json report before updating.

"},{"location":"fleet_management/","title":"Private fleet management","text":"

View this page on MeshCore Docs.

"},{"location":"fleet_management/#private-fleet-management","title":"Private fleet management","text":"

Fleet control lets one publisher send filtering, radio schedules, secondary-radio changes, and clock commands to all enrolled infrastructure nodes, regions, a geographic radius, one node, or a list of targets over a private LoRa channel. It is disabled until configured. It is supported on repeaters and room servers; fixed-size STM32 images omit it because of their flash limit. Both the publishing Companion and receivers need firmware built with fleet-control support; the existing app can use its local Command Line screen.

"},{"location":"fleet_management/#read-only-receivers","title":"Read-only receivers","text":"

Commands use two separate credentials:

Giving a repeater the channel key and publisher public key does not give it the publisher's signing key. An ordinary channel message, a copied display name, or possession of the channel key cannot authorize a fleet command. Each command signature also binds the channel, target, sequence, expiry, and exact command.

This makes management commands read-only for receivers. It does not turn ordinary MeshCore chat into a signed, read-only announcement channel: stock apps do not verify signatures on ordinary channel text. Fleet acknowledgements are convenient channel text, not authenticated inventory evidence. Use existing individually authenticated administration for sensitive confirmation.

"},{"location":"fleet_management/#enroll-nodes","title":"Enroll nodes","text":"

Use an existing authenticated admin connection or the local console. Generate a random private channel key; for example, openssl rand -hex 16. Do not use Public or a predictable hashtag key. Add that same key as a private channel on the publishing Companion, and note its channel index.

Get the publishing Companion's full public identity key from its device information or existing identity command. On every managed repeater or room server, run:

set fleet.controller <publisher-public-key-64-hex>\nset fleet.channel <private-channel-key-32-hex>\nget fleet.channel\nget fleet.controller\nget fleet.stats\n

get fleet.channel reports enrollment, storage health, a channel hash, and the last accepted sequence. It does not reveal the channel key. set fleet.channel off disables reception; set fleet.controller off revokes the publisher. Key changes use the existing individually authenticated admin path, never fleet commands.

If a settings save reports unconfirmed; restart required, the durable result is unknown. Fleet reception and further fleet settings writes are disabled, and get fleet.channel reports store=error. Enrollment shown in RAM is not active authority while held. Restart to load and validate the public file, then check enrollment before retrying. A not saved; retry later error occurred before publication or after a confirmed rollback, and does not impose this hold. On ESP32/RP2040, rename return values alone are not sufficient: the transaction checks both names before accepting the move or rollback. Fleet also checks the exact public settings image before acknowledging new authority. A Fleet hold does not stop the node's normal radio, USB, or individually authenticated admin services; it prevents uncertain credentials or replay state from granting remote Fleet control.

get fleet.stats includes parts, the number of fragments currently buffered across the two bounded, incomplete two-packet command slots.

Nodes need a clock set to current UTC. Commands expire after ten minutes and may be at most one minute ahead of a receiver's clock. A bad clock fails closed; correct it through the normal administration path.

"},{"location":"fleet_management/#send-commands-from-the-app","title":"Send commands from the app","text":"

On the publishing Companion, use the existing local Command Line screen. The Companion signs with its own identity. No publisher private key is copied to a repeater, and no app protocol change is required.

Examples use private channel index 3:

fleet send 3 get radio2\nfleet send 3 set radio2 mediamesh\nfleet send 3 set tempradio2 910.5,500,8,5,rxtx,120\nfleet send 3 set radio2.cross auto\nfleet send 3 set flood.rule.4 type=grp_txt hops=6+ drop\nfleet send 3 del flood.rule.4\n

Targets are optional: omitting the target sends to all enrolled nodes on the private fleet channel. Explicit all still works, for example fleet send 3 all get radio2.

To address particular nodes, insert 8 or 12 hexadecimal characters from the start of a node's public key, or its complete 64-character public key, before the command. Separate multiple targets with semicolons, without spaces. Lengths can be mixed:

fleet send 3 1257aee5 get radio2\nfleet send 3 1257aee5a754;9abc0123 get radio2\nfleet send 3 1257aee5;9abc01234567 set radio2.cross auto\n

These are example public-key prefixes; substitute your nodes' actual keys. A prefix addresses every enrolled node whose public key starts with it. Use all 64 characters when you need one exact identity, especially in a large fleet. Names and short LoRa path IDs are not accepted as target identities. all must appear alone. A node that matches several entries still executes the command only once, and the signature protects the entire list.

"},{"location":"fleet_management/#target-existing-regions","title":"Target existing regions","text":"

Fleet commands read each receiver's existing region map. Two selectors are available, and they can be mixed with public-key targets:

Selector Which receivers execute sea or region:sea Receivers with an exact sea entry in their configured region list home:sea Receivers whose home region is sea, or whose home has sea as a parent or ancestor
fleet send 3 sea get radio2\nfleet send 3 sea;pdx clock sync\nfleet send 3 region:sea set radio2.cross auto\nfleet send 3 home:sea get tempradio2\nfleet send 3 home:sea;home:pdx clock sync\nfleet send 3 sea;home:pdx;1257aee5 get radio2\n

The semicolon-separated list is an OR: a receiver that matches any entry executes once, even if it matches several entries. Names match exactly and are case sensitive; sea does not match seattle. One leading # is an alias, as in the existing region commands. $ remains significant for private region names. Names may contain up to 30 printable ASCII bytes using region-name characters. Use region: or home: explicitly for names with 8 or 12 characters, names made entirely of hexadecimal characters, or reserved command words such as get, set, del, time and clock. Bare all still means the entire fleet; region:all selects a region actually named all.

Inspect the existing configuration with region and region home. To choose a home from regions already defined on that receiver, use:

region home sea\nregion save\n

List matching includes a configured region even when forwarding for it is denied. Home matching follows the selected home's ancestry; a wildcard or unset home matches no named home target. The default transmit scope and the incoming packet's scope do not determine target membership. Region selection is checked against the live map when the complete command is received.

Targets select which nodes execute. Delivery still follows the Companion's channel scope and the intervening forwarding rules. Unknown names match no receivers and never become a whole-fleet request.

"},{"location":"fleet_management/#target-a-gps-center-and-radius","title":"Target a GPS center and radius","text":"

Use gps:latitude,longitude:radius-km to select receivers within a circle. For example, this selects nodes within 25 km of the given Seattle coordinates:

fleet send 3 gps:47.6062,-122.3321:25 get radio2\nfleet send 3 gps:47.6062,-122.3321:25 set radio2.cross auto\nfleet send 3 sea;home:pdx;gps:47.6062,-122.3321:25;1257aee5 clock sync\n

GPS, region, home and key selectors can share the same semicolon-separated list. They use the same OR rule: any matching entry selects the node, and the command executes once. The center, radius and complete target list are signed. The comma is only for the coordinate pair; ; is the only target-list separator.

Coordinates use ordinary decimal degrees, with latitude from -90 to 90 and longitude from -180 to 180, and at most six decimal places. Radius is in kilometers, from 0.001 to 20050, with at most three decimal places. For example, 0.5 means 500 meters. Scientific notation, excess decimal places, missing fields and out-of-range values are rejected without sending. Distance uses the great-circle calculation, including across the date line and near the poles; a node on the radius boundary is included.

Each receiver prefers its configured lat and lon. A stationary radio can have a fixed position even without GPS hardware; configure it through its normal administration connection:

set lat 47.6062\nset lon -122.3321\nget lat\nget lon\n

When both configured coordinates are zero, the receiver can use an existing valid GPS cache no older than 12 hours. Matching never wakes GPS, takes a UART or requests telemetry. A missing, expired, invalid or default (0,0) position matches no GPS selector; it can still match another entry in the list. Invalid nonzero configured coordinates are rejected rather than falling back to GPS. For a stationary radio that lets GPS sleep for longer than 12 hours, configure its fixed coordinates.

Private fleet matching uses this position even when location is hidden from public adverts. It reads the position when the complete command arrives, including after the second fragment of a two-packet command, so a moving node is selected using its position at completion.

On air, full keys use a 128-bit SHA-256 digest; prefixes use their original public-key bytes. Whole-fleet commands use the compact FMC2 format with no target records, whether the CLI target is omitted or explicitly all. This reduces the header from 29 to 14 bytes. A single complete key uses the smaller fixed-target FMC1 format. Region, home and GPS selectors use separate signed record types in FMC2. Separate fleets use separate channel keys.

Only local commands on the publishing Companion can originate fleet sends. A remote CLI command cannot turn that Companion into a multicast signing proxy. An OK means the Companion queued the command, not that every receiver applied it. Wait for acknowledgements and confirm the result on important nodes.

Use at most one new command per second. The publishing Companion remembers its last queued sequence for the current boot; its clock supplies the next sequence. Each receiving node reserves the Unix-second sequence in durable storage before dispatch. That receiver never executes the same sequence twice, including after reboot or a lost reply. Retry an uncertain command with a fresh sequence; the operation should be chosen to be idempotent.

"},{"location":"fleet_management/#supported-controls","title":"Supported controls","text":"

The Companion automatically uses one or two LoRa packets, depending on the signed command's size. Whole-fleet commands fit 87 ASCII bytes in one packet or 230 bytes across two. Targets consume some of that allowance:

Targets One packet: command bytes Two packets: command bytes Omitted or all 87 230 One 8-character public-key prefix 82 225 One 12-character public-key prefix 80 223 One complete 64-character public key 72 215 One three-character region or home name, such as sea 82 225 Two three-character region or home names, such as sea;pdx 77 220 One GPS center and radius 74 217 Two 8-character public-key prefixes 77 220 Two complete public keys 53 196

Each prefix in a list uses 5 or 7 bytes; each complete key in a list uses 17. Each region or home selector uses two bytes plus its canonical name length. Each GPS selector uses 13 bytes: its type, latitude and longitude in millionths of a degree, and radius in meters. A single complete key uses the smaller fixed-target format with a 29-byte header, so its limits come from packet space. Lists are capped at 17 entries and 86 bytes of encoded target records; for example, at most six GPS selectors fit. Requests exceeding their target-specific budget are rejected before signing or sending, with no partial target list.

The existing app/console input limit also applies. The app's normal 176-byte command frame fits 161 command bytes after fleet send 3; a correlated request uses three more bytes, and explicit targets reduce the space further. The shorter USB rescue console has an 80-byte input buffer. The 230-byte limit is the fleet protocol's capacity, rather than a promise that every console can enter that much text. Oversized or invalid lists are rejected as a whole, without sending or truncating any targets.

"},{"location":"fleet_management/#two-packet-delivery","title":"Two-packet delivery","text":"

Longer commands keep one 64-byte signature over the entire command, including its channel, targets, sequence and expiry. The signed envelope is split into two FMP1 fragments. Each fragment has an 11-byte collection header and up to 154 envelope bytes, within the existing 165-byte group-data limit. Together they can carry a 308-byte signed envelope. No third packet is used.

The Companion reserves both packets before sending. If either cannot be queued, it cancels the queued part and any radio copy, reports an error, and leaves the sequence available for a retry. Successful queueing still does not guarantee delivery over LoRa.

A receiver accepts either arrival order and identical duplicates. It executes and acknowledges only after both parts arrive and the complete signature, target, clock and replay checks pass. Missing or altered parts cannot execute a partial command. Two bounded assemblies are kept for up to five minutes from their respective first parts; duplicates do not extend those deadlines. Unverified fragment sequences cannot grant priority over another transfer. A third transfer replaces the oldest collection. A complete, valid signed command clears the partial collections, and durable replay checks remain authoritative. Reboot or changing enrollment discards the partial transfers.

Control Fleet commands Saved second profile get radio2, set radio2 <tuple|mediamesh|mediamesh-range|off> Temporary second profile get tempradio2, set tempradio2 <tuple|off> Scheduled primary profile get/set/del radioat, get/set/del tempradioat Scheduled second profile get/set/del radioat2, get/set/del tempradioat2 UTC clock clock, clock sync, time <epoch-seconds> Profile diagnostics get radio2.status, get radio2.timing Crossover get radio2.cross, set radio2.cross <auto|on|off> Repeater guest login get guest, set guest on|off Repeater guest sessions get guests [page] Repeater login attempts get logins [page] Filter rules get/set/del flood.filter[.n], flood.rule[.n] Path blacklist get/set/del flood.filter.blacklist[.n] Channel moderation get/set/del flood.moderation[.n] Scope policy get/set/del flood.channel.scope[.n], flood.channel.scope.require[.n] Group-data forwarding get/set flood.channel.data, flood.channel.data.hops Flood hop limits get/set flood.max, flood.max.unscoped, flood.max.advert

Each role still checks whether it supports the requested setting. Immediate primary radio changes, identity export, passwords, ACL edits, filesystem operations, firmware installation, reboot, and unrestricted CLI are outside fleet capability.

Repeater guest login defaults to off. set guest off saves that choice and clears existing guest sessions immediately. Enabling it uses the existing guest password; this switch does not change any password or assigned ACL role.

get logins [page] returns recent repeater login attempts, two per page, showing only 12-character public-key prefixes, latest UTC epoch times, results, and attempt counts. The 16 most recently attempted full keys are kept in RAM; repeated attempts update the existing key's record. Results distinguish accepted logins, wrong passwords, disabled guests, replay/state rejection, and unavailable ACL capacity. The history clears on reboot and survives set guest off. It contains no passwords or shared secrets and is not an online-user list. Anonymous owner, region, and clock requests and packets that fail authentication are excluded. Ordinary channel messages cannot read this list.

set radio2 mediamesh saves 911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, with automatic scan-aware preamble. mediamesh-range uses 125 kHz instead. These presets preserve the primary profile and crossing policy; new configurations default to radio2 off and radio2.cross off.

"},{"location":"fleet_management/#schedule-radio-changes-and-set-clocks","title":"Schedule radio changes and set clocks","text":"

Use the normal schedule syntax, with UTC epoch seconds or +N whole minutes. Primary schedules are supported by repeaters; a role without a primary scheduler replies that the command is unsupported. Second-profile schedules use rx or rxtx after the coding rate:

fleet send 3 clock\nfleet send 3 clock sync\nfleet send 3 set radioat 910.5,500,8,5,+5\nfleet send 3 set tempradioat 910.5,500,8,5,+5,+15\nfleet send 3 set radioat2 910.5,500,8,5,rxtx,+5\nfleet send 3 set tempradioat2 910.5,500,8,5,rxtx,+5,+15\nfleet send 3 1257aee5 get tempradioat2 1\nfleet send 3 1257aee5 del tempradioat2 all\n

Both relative endpoints use the receiver's clock at command acceptance. For a coordinated fleet switch, use the same absolute UTC start/end timestamps on all nodes. An optional final preamble value or auto uses the existing radio syntax. Schedule capacity, radio-range validation, and overlap checks remain unchanged; pending schedules still do not survive reboot.

clock sync advances a receiver to the publishing Companion's signed timestamp plus one second. time <epoch-seconds> sets an explicit UTC timestamp. Both use the existing forward-only clock rule. Initial setup or a clock far out of sync still requires local or individually authenticated clock correction: fleet clock commands retain the signature, expiry, replay, and maximum-clock-lead checks.

Radio mutations and schedule edits keep the old state until their exact acknowledgement packets drain, with at least one successful physical transmission. Queue failure, failed transmissions, or an acknowledgement timeout cancels the staged mutation. Temporary-radio leases and scheduled windows retain their original monotonic end time; waiting for an acknowledgement does not lengthen them.

Exact single-node acknowledgements use 0.5-1.5 seconds of random delay; prefix, multiple-target, region, GPS and whole-fleet replies use 0.5-10 seconds, including radio mutations. Queuing, airtime and forwarding can add delivery time. This reduces simultaneous responses but is not a reliable delivery protocol for an arbitrarily large fleet. Use individual requests for confirmation and divide very large fleets into separately keyed channels.

"},{"location":"fleet_management/#filtering-and-recovery","title":"Filtering and recovery","text":"

Local fleet reception happens before a node's forwarding decision, so that node can still process an authenticated command when a filter or repeat off blocks forwarding. Intermediate nodes can still block delivery to downstream nodes. Management traffic has no automatic exemption from forwarding filters. Preserve an appropriate group-data/channel forwarding rule and an existing direct-admin recovery path before changing network-wide filters. Primary-radio schedules can move nodes off the normal network; use a temporary window or a planned recovery path for experiments. A forwarding policy can also partition the fleet.

The receiver is allocated lazily when fleet settings are present or queried, with one bounded mailbox and two 308-byte fragment assemblies, plus bounded metadata. Cryptographic work and storage writes occur after the receive stack unwinds. Signature-validation attempts are bounded after private-channel MAC verification, complete framing, replay, target, time and command checks. Keyless matching-hash junk, already-used requests, other nodes' requests and incomplete fragments do not spend that signature budget. A corrupt or unreadable fleet store denies commands rather than discarding its replay state.

"},{"location":"fleet_management/#ideas-for-packet-abuse-containment","title":"Ideas for packet-abuse containment","text":"

A packet-count chart can show a burst, but cannot establish intent, an authenticated source, or the airtime consumed. A large Trace spike deserves investigation; MeshCore already rejects flood-form Trace and Control packets. Valid direct Trace requests need different treatment from flood forwarding rules.

Useful next additions, rather than features already implemented here:

Start with observation, then a narrow temporary rule, then verify neighboring nodes still communicate. Software filtering can reduce forwarding and response amplification; it cannot recover airtime already occupied by an RF transmitter.

See flood filtering, radio profiles, and ACL administration.

"},{"location":"flood_filtering/","title":"Flood filtering","text":"

View this page on MeshCore Docs.

"},{"location":"flood_filtering/#flood-filtering-and-moderation","title":"Flood Filtering and Moderation","text":"

This guide explains the Keymind forwarding filters. Repeaters expose the full set of channel, rule, blacklist, and moderation controls described here. FULL-profile ESP32 room servers expose the generalized flood.rule table (and its flood.filter alias) with 31 forward-rule slots, but not the repeater's scope-rewrite, passive-blacklist, or text-moderation phases. Standard room-server profiles do not compile the rule table. Filters decide whether the node retransmits a packet and can assign a transport scope before that decision. They do not stop local reception, packet logging, or MQTT observation.

Radio rules default to the two flood routes:

Generalized rules can opt into relayed direct routes (0x02 and 0x03) with route=direct, or both route classes with route=any. Scope rewriting, source-path matching, and the legacy moderation table remain flood-only. Local delivery and requests addressed to this node are not relay decisions. The route and payload values follow the upstream packet-format reference and payload layouts, with this fork's LoRa OTA assignment noted below.

"},{"location":"flood_filtering/#automatic-flood-advert-limits","title":"Automatic flood-advert limits","text":"

Repeaters, room servers (including non-FULL builds), and forwarding sensors automatically limit relayed ADVERT packets. No CLI rule is needed. Companions and the repeater's receive-only MQTT observer do not allocate this history. This is separate from discovery-response limits, self-advert timers, hop caps, and the per-rule rate=N/min setting below.

The ordinary quota is shared by the first 12 public-key hex characters (six bytes of the advertised origin key, not a path hash). Each prefix has a fixed 180-minute window starting with its first tracked valid flood advert.

Shortest received hops in that window Distinct adverts forwarded per 180 minutes 0\u20131 10 2 9 3 8 4 6 5 5 6 4 7 3 8+ 2

The minimum received hop count, before adding this relay, sets the quota; one-, two-, and three-byte path hashes have the same policy. A shorter valid duplicate may increase the quota without restarting the window or clearing its count. Longer routes never decrease the allowance during that window. The payload signature must verify before any history changes. Path metadata itself is not signed, so this is a received-distance heuristic, not proof of physical distance.

"},{"location":"flood_filtering/#continuing-abuse-and-recovery","title":"Continuing abuse and recovery","text":""},{"location":"flood_filtering/#inspect-the-current-limits","title":"Inspect the current limits","text":"

Use get flood.advert to list rate-limited origins, three per page. Continue with get flood.advert 2, etc. Rows show a 12-hex public-key prefix, the reason (quota, history, or bad), and the wait before another new advert is eligible under the automatic limiter. get flood.advert key 1 shows the full key and counters for row 1, including the remaining bad-list recovery time. A bad-listed key remains visible with wait=0s when its next single forward is due. Prefix collisions appear as separate full-key entries.

This is a live, read-only view: expiry can change row indices between queries, but reading never resets quotas or extends recovery. Sources rejected solely because the protected-history table is full are not retained, so they cannot be listed. See the CLI reference.

"},{"location":"flood_filtering/#clear-history","title":"Clear history","text":"

All timers use rollover-safe uptime, so setting the clock forward or backward cannot clear the restriction. This history is currently RAM-only: rebooting the relay clears it, and time while powered off is not tracked. A manual clear also resets both the ordinary quota and abuse history for its selected target:

clear flood.advert all\nclear flood.advert <64-hex-full-public-key>\n

These use the ordinary local/admin CLI authorization, including authenticated admin LoRa CLI. A prefix is never accepted for a targeted clear; missing or invalid selectors do not clear anything. Clearing an exact key preserves other full keys even when their 12-hex prefixes match. No preferences, contacts, neighbours, replay timestamps, or manually configured flood rules are erased.

"},{"location":"flood_filtering/#scope-and-resource-bounds","title":"Scope and resource bounds","text":"

Only forwarding is suppressed. Valid adverts remain available to local contact/neighbour handling and can still appear in raw RX logs or MQTT. This cannot prevent the origin or upstream repeaters from transmitting. Direct adverts, discovery control responses, messages, OTA packets, and this node's own scheduled adverts are not charged. Existing physical retry attempts of an admitted advert are not additional distinct adverts.

The limiter retains up to eleven 64-bit payload hashes per full key per window: ten possible ordinary admissions plus evidence of exceeding the largest quota. Paths and transport scopes do not change these hashes. Retained duplicate hashes cannot consume additional quota even if the general packet-seen cache has evicted them. Once the eleven distinct receive slots are full, additional unretained payloads are suppressed until the next window. Only packets passing all ordinary forwarding gates consume forwarding quota; valid received traffic still supplies abuse evidence when another gate blocks it.

The default is 128 full-key slots (18,432 bytes of entry storage plus small bookkeeping), 96 on nRF52 (13,824 bytes, preserving the mOTA runtime RAM reserve), or eight on RAM-constrained STM32 builds (1,152 bytes). FLOOD_ADVERT_SOURCE_SLOTS overrides the capacity at compile time. When full, the least-recently-heard normal key is removed first. Valid retained duplicates update last-heard too. Evicting a normal key discards its ordinary quota history, so capacity should still be sized for the deployment's active origins. First-strike evidence and bad-list entries are protected from eviction: if every slot protects abuse history, untracked origins are not forwarded until a slot expires or an administrator clears history. Table churn cannot erase an existing bad-list penalty. No heap allocation or per-packet filesystem writes are used.

"},{"location":"flood_filtering/#before-making-changes","title":"Before making changes","text":"

On a repeater, show the current forwarding controls:

get repeat\nget flood.max\nget flood.max.unscoped\nget flood.max.advert\nget flood.channel.data\nget flood.channel.data.hops\nget flood.channel.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.rule\nget flood.filter.blacklist\nget flood.moderation\n

flood.rule is an alias for flood.filter, not another table. Generalized repeaters have 63 forward-rule slots plus scope-rewrite and shared-blacklist sections in the same atomic policy file. FULL room servers have 31 forward slots and empty repeater-only sections. Compact target profiles retain their separate FPF6-era controls. flood.moderation has 16 slots. A new repeater FPF7 table starts with ota all suspend=tempradio in slot 1 and an authenticated #wardriving hops=5+ radio drop in slot 2, and an authenticated #wardriving hops=all mode=bridge,cross drop in slot 3. FULL room servers seed only the OTA row; their generalized engine also accepts transport-mode rules. flood.moderation starts empty. A row can opt into suspend=tempradio; temporary radio is not synonymous with OTA and can carry normal packet types too. A corrupt or truncated table fails open, so corrupt storage does not silently enable blocking.

On a FULL ESP32 room server, use get flood.rule (or get flood.filter) for the available table. Remote rule changes require room-server administrator access. flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= condition on a room server.

"},{"location":"flood_filtering/#force-floods-into-a-transport-scope","title":"Force floods into a transport scope","text":"

flood.channel.scope can add a scope to a received unscoped flood or replace the scope of a transport-scoped flood before this repeater forwards it:

set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nget flood.channel.scope\nget flood.channel.scope.<slot>\ndel flood.channel.scope.<slot>\ndel flood.channel.scope all\n

Use fs in place of flood.channel.scope for any of these commands. Its optional fields also accept p=bl for path=blacklist, p=b1 through p=b6 for path=bucket:1 through path=bucket:6, and tx=s/tx=f for slow/fast.

The channel may be public, #channel, or a 128/256-bit hex key. A bare target names an existing region with a usable transport key. Use scope=<name> instead to derive a regionless public hashtag scope exactly as flood.filter scope=<name> does. The direct name is normalized with a leading #, may contain up to 30 characters, and does not need a region-list entry. Keyed rules first check the one-byte channel hash carried in the packet, then validate the MAC by decrypting with the configured channel key. A hash collision alone cannot force a scope.

For example, this authenticates only #rgdata and rewrites it to #BlackHole86 without creating a region:

set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope.1\n

If that channel arrives scoped to #usa, the rule replaces #usa with #BlackHole86. It also handles unscoped packets and replaces any other incoming scope; the source scope is not a condition on the rule.

Add path=blacklist to make a channel-scope row eligible only when the received path matches the passive flood.filter.blacklist ID table. It does not require an enabled flood.filter drop row. With 3-byte paths, one exact listed ID qualifies. With 2-byte paths, two received path entries must match the first two bytes of listed IDs. A 1-byte path never qualifies.

Use path=bucket:<1-6> to match one of the existing flood.retry.bucket tables instead. Each bridge bucket holds up to 17 three-byte IDs and remains usable by channel scoping while flood.retry.bridge is off. Bucket matching uses the same thresholds as the blacklist: one exact hit for 3-byte paths, two qualifying entries for 2-byte paths, and no matches for 1-byte paths. Channel scoping reads the configured IDs directly; recent.repeater freshness and flood.retry.ignore do not change this match.

There are three independent wildcard classes:

login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable targets always take precedence over txt:*, even if that wildcard has a lower slot number. Within the exact class, matching path-qualified rows are tried before ordinary fallback rows. The same qualified-then-fallback order applies within each wildcard class. A missing or unusable target is skipped, so later rows remain eligible. The lowest usable slot wins within each priority tier.

For example, this uses bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, and assigns west to all other authenticated public packets:

set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n

More 3-byte IDs can be added to bucket 1 later. Any one of them qualifies the east row. Replacing or clearing that bucket changes which paths qualify but leaves both channel-scope rows intact. Bridge retry does not need to be enabled.

On a successful match, an unscoped route changes from ROUTE_TYPE_FLOOD to ROUTE_TYPE_TRANSPORT_FLOOD; an already-scoped route remains transport-flood but receives replacement codes. Transport code 0 is calculated with the target region or direct hashtag key over the payload type and payload, and code 1 becomes zero. The change happens before region enforcement, forwarding filters, and deduplication. Consequently flood.max.unscoped no longer applies to a packet converted from unscoped, while flood.max, flood.filter, loop detection, and moderation still apply to every rewritten packet. A region target must be flood-allowed. A regionless target is trusted for this matched receive pass, but it neither creates a region nor changes how unrelated packets with the same transport code pass the region gate. By default, if the selected scope differs and the rewritten packet passes those checks, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal outbound queue priority, and force the maximum txdelay factor of 2.0. The actual transmit delay is still randomized, from zero through ten packet airtimes. It does not preempt an active radio transmission or bypass CAD and airtime-budget limits. Selecting the scope already present is a no-op and does not grant special treatment.

Direct routes are never rewritten. Standard traceroute is direct-routed and therefore remains outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and the normal region/unknown-code gates still apply. Scope assignment does not override normal payload validation or make an otherwise non-forwardable packet type forwardable.

LoRa OTA (0x0C) falls under other:*. A matching row adds the selected transport code or replaces the existing one, but OTA still operates normally during the temporary-radio window because the OTA handler accepts both unscoped and transport-scoped flood routes. A region target must allow flooding; a direct target uses the regionless trust behavior above. A new repeater also seeds ota all suspend=tempradio in flood-filter slot 1. That visible rule blocks OTA forwarding at every received hop outside temporary-radio operation and is skipped while temporary radio is active. Independently, the OTA core refuses OTA receive, relay, and transmit outside an actually active temporary-radio window, even if the seeded row is deleted or replaced.

Capacity is selected at build time:

Classic ESP32 uses the 31-slot default even on boards with PSRAM because these tables live in internal static DRAM. The separate channel-requirement table defaults to the same capacity and uses another 34 bytes per slot. Together, the 31-slot defaults save 15,712 bytes of static RAM compared with 255 slots. Custom builds can override FLOOD_CHANNEL_SCOPE_SLOTS and FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS with build flags.

Each rule retains its 36-byte record. A separate table holds 32-byte normalized names for up to the smaller of the rule count or 32 distinct regionless targets, except that very-tight STM32WL builds retain one reusable direct target. Both configured regions and regionless targets can be reused by any number of rules.

On generalized builds these records are the shared rewrite phase, and the file stores only through the highest occupied slot. Compact FPF6 builds retain the standalone FCS5 file and the file sizes described by their build profile.

"},{"location":"flood_filtering/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"

flood.channel.scope.require changes region enforcement for received flood GRP_TXT and GRP_DATA packets from a global policy to a channel opt-in policy:

set flood.channel.scope.require <public|#channel|128/256-bit-key>\nset flood.channel.scope.require.<slot> <public|#channel|128/256-bit-key>\nget flood.channel.scope.require\nget flood.channel.scope.require.<slot>\ndel flood.channel.scope.require.<slot>\ndel flood.channel.scope.require all\n

fsr is the short name for flood.channel.scope.require, with the same arguments, slots, and permissions.

An empty table preserves the normal global region behavior. Once at least one row exists, a group-channel packet that authenticates against a listed key must arrive as ROUTE_TYPE_TRANSPORT_FLOOD with a transport code matching a locally flood-allowed region. An unscoped packet, an unknown transport code, or a code for a denied region is not retransmitted. The check uses the original incoming scope before flood.channel.scope or flood.filter scope= can rewrite it. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.

Group-channel packets that do not authenticate against any listed key bypass the region/unknown-code forwarding gate. They still pass through repeat, flood.max*, flood.filter, loop detection, payload validation, and moderation. Non-channel flood payload types retain the normal global region behavior. A one-byte channel-hash collision is only a prefilter; the packet must also pass MAC validation/decryption with the configured key.

Without .slot, setting an existing key updates its row and a new key uses the first empty row. Numbered set replaces that slot. Detail output displays only the first four derived hash bytes and key size, never the secret. The table has the same build-dependent slot count as flood.channel.scope; each row consumes 34 bytes of RAM and storage, plus a five-byte file header. ACL permission 4 can manage it.

For example, this requires an allowed incoming scope on #bot, while every other group channel bypasses region enforcement:

set flood.channel.scope.require #bot\nget flood.channel.scope.require\n
"},{"location":"flood_filtering/#interaction-with-duplicate-detection","title":"Interaction with duplicate detection","text":"

The seen-packet hash contains the payload type and exact payload bytes. It does not contain the route type, either transport code, or the ordinary flood path. For TRACE only, the encoded path_len byte is also included. Therefore an unscoped packet and the same packet after this repeater adds a transport code are the same duplicate. A later copy with a different scope is also the same duplicate; changing or adding scope cannot evade the seen table.

When equivalent non-TRACE flood copies overlap in rxdelay, the normal receive-quality timing still chooses the packet to process, but that winner takes a scope from the queued copies whose transport code matches an allowed region in this repeater. Unknown and denied scopes are ignored. If eligible copies have different scopes, the shortest received path supplies the scope. Equal path lengths prefer the deepest matching child region (the narrowest configured scope). A remaining tie keeps queue order. The winner keeps its own path, SNR, and delay schedule; only its route and transport codes can change, including replacement of a less-preferred scope it already carried.

Scope selection happens at dequeue so the original scopes remain available for comparison. It applies only while copies are queued and cannot alter a copy already processed into the seen table. Flood-form TRACE participates in the same arbitration. Direct traceroute never enters this flood queue.

A packet that already matches a configured fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses this inbound rxdelay queue entirely. A tx=slow row remains in the queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.

"},{"location":"flood_filtering/#runtime-flood-rules","title":"Runtime flood rules","text":"

On repeaters with the rule engine enabled and on FULL-profile ESP32 room servers, fr, flood.rule, and flood.filter name the same persistent table. The evaluator is fixed firmware, but every row is data, so an authenticated operator can add, replace, inspect, or delete a row without an OTA or reboot. Existing flood.filter commands remain compatible. FPF6, FPF7, and FPF8 files are accepted; FPF1-FPF5 files are rejected and filtering fails open. New sender/text, payload-length, route, probability, enabled-state, or extended-path fields are stored in FPF8. Tables using only older fields retain FPF7. Firmware without FPF8 support cannot load an FPF8 table; remove the new fields before downgrading and keep a copy of the rules. Older firmware cannot preserve an FPF7 table containing the packed retry action, one-byte hash:XX matcher, or mode=bridge/cross flags; remove those rows before downgrading. Old firmware rejects such a table and fails open, which disables its other filter rows too. New firmware loads older rows as mode=radio without changing their behavior.

The former flood.channel.block table is now represented by ordinary FPF7 rows. On a generalized repeater, an existing FCB2 file is imported once into free FPF7 slots and then removed. For example, an old #wardriving h=4 row becomes type=any channel=#wardriving hops=5+ drop. The 63-row table can hold all 31 former general filter rows, all 15 legacy channel-block rows, and the migrated global flood.channel.data gate. Compact STM32WL FPF6 builds cannot match authenticated channels and retain the older separate gate.

On generalized repeaters, flood.channel.data* is a compatibility view over one ordinary visible FPF7 type=grp_data ... drop row. Turning it off creates or updates that row; turning it on removes the row. Its hop setting maps to hops=all or hops=N+1+. There is no hidden GRP_DATA forwarding check ahead of FPF7. Normal ordering applies, so a matching higher-priority stop row can exempt selected traffic. The compact rule list marks the managed row with ~data.

FPF7 and FPF8 bind in=region:<name> and region=<name> to canonical region names, not numeric region IDs. Removing, reordering, or reusing a region ID cannot silently redirect a rule. If the saved name is missing, an input-region match does not match and a target-region rewrite is skipped. Re-adding the same name reactivates the rule.

The extended form is:

set flood.rule[.<slot>] type=<type> [mode=radio|bridge|cross|bridge,cross]\n    [hops=<range>] [channel=<channel>] [hashbytes=any|1|2|2+|3]\n    [prefix=<ID[,ID...]>|path=[^]ID>ID...[$]] [in=<input-scope>]\n    [sender=<pattern>] [text=<pattern>] [len=<range>] [route=flood|direct|any]\n    [prob=<1-100>] [enabled=on|off] <action> [rate=<N>/min]\n    [priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]\nget flood.rule\nget flood.rule.<slot>\ndel flood.rule.<slot>\ndel flood.rule all\n

The command must be entered on one line. Match fields in one row are ANDed. Radio rows are matched against the same immutable packet state captured on receive, before any rule rewrites its scope. Bridge/cross rows match at their transport boundary. Only rows for the current mode participate in ordering and stop processing. Matching rows are then processed by descending numeric priority. At an equal numeric priority, authenticated channel matches run before raw hash:XX matches, raw hashes run before channel=*, and lower slot number breaks the remaining tie. Priority defaults to 0; an explicitly higher numeric priority overrides specificity.

Numbered examples replace the selected slots, including factory defaults. Omit the slot number to add a rule without replacing an occupied slot.

The first matching stop row ends the FPF7 forward phase after that row. Higher-order matches and the stop row still apply; lower-order matches do not. A stop-only row is therefore an exception to lower-priority FPF7 rows. It cannot undo a higher-priority drop and it does not bypass hard forwarding gates or the scope-rewrite and moderation phases. Without a stop row, matching drop and rate rows remain independent and the highest-order matching scope or region rewrite wins.

Match fields:

Saved older prefix rules keep their start-anchored meaning. When retyping an older path=A1,B2 command, use prefix=A1,B2 or pa=^A1>B2 to retain it. - sender= (n=) and text= (x=) match decrypted group text. An explicit authenticated channel (public, #name, or a channel key) is required; a raw channel hash is insufficient. See content patterns. - len= (l=) matches payload bytes, using N, N+, or inclusive N-M. It excludes the route/path envelope; encrypted padding is part of the payload. - route= (rt=) selects flood, direct, or any; f, d, and a are short values. Omitted radio rules remain flood-only. Direct/any rows may drop, rate-limit, or stop; scope/region rewriting, retry, and TX timing require flood-only radio rows. - prob= (pr=) applies this rule to approximately that percentage of otherwise-matching packets. Values are 1-100; omitted means 100. A failed roll lets later rules run. This is sampling, not a delivery quota. The decision stays the same for the same packet and row during one boot, including repeated timing checks and scope rewrites. - enabled=off (e=off) retains a row without evaluating it. on/1 enables and off/0 disables. Change only this state with set fr.<slot> e=off and set fr.<slot> e=on; the rest of the row stays saved. - in=any|none|scoped|allowed|unknown|scope:<name>|region:<name> tests the original incoming route before any rewrite. none means an unscoped flood; scoped means any transport flood; scope:name compares the exact public hashtag-derived scope; and region:name compares an allowed configured region. allowed is the legacy require=region test and includes an unscoped packet when the wildcard region allows it. unknown means a scoped packet that does not resolve to an allowed local region.

Actions:

"},{"location":"flood_filtering/#content-patterns","title":"Content patterns","text":"

Sender/text patterns are case-sensitive byte matches. They search anywhere unless anchored with ^ and $. Supported forms include ., [A-Z], [^0-9], *, +, ?, grouping, and | alternatives. Escape punctuation with \\ to match it literally; use [0-9] for digits. Each pattern is at most 63 bytes. Quote values containing spaces.

For Alice: BEACON 123, n=^Alice$ matches the display name and x=^BEACON matches the body. They can be combined in one row. The parser uses the complete fields, including names longer than 31 bytes, and removes AES zero padding before matching. A message without : has no sender field; a text rule can still inspect its body. Wrong keys and non-text payloads do not match content rules. Pattern matching shares a 131,072-operation budget across the rules in one evaluation. Exhausting it rejects the packet rather than granting an unverified allow, rewrite, or retry action. Keep patterns short and specific.

# Block BEACON messages on this channel.\nset fr grp_txt c=#local x=\"^BEACON\" d\n\n# Exempt Alice from lower-order general rules, then block everyone else.\nset fr grp_txt c=#local n=\"^Alice$\" pri=200 a\nset fr grp_txt c=#local d\n\n# Temporarily disable an occupied row without deleting its settings.\nset fr.4 e=off\nset fr.4 e=on\n\n# Block floods whose most recent relay has this 3-byte ID.\nset fr any pa=A1B2C3$ d\n

The allow exception remains subject to separate moderation and normal forwarding gates. A sender name is freely chosen message text, not a verified public-key identity. Legacy flood.moderation keeps its existing exact, case-insensitive name matching.

Rows using the same channel key reuse the initial authentication verdict. Content patterns also decrypt into temporary scratch for matching. Neither the verdict cache nor plaintext survives the receive evaluation. A hash:XX row skips authentication and uses only the one visible byte.

The global retry controls remain hard gates. A retry row does not override a zero flood.retry.count, path/type attempt caps, disabled forwarding, flood.retry.advert, a drop verdict, or any other forwarding rejection. flood.retry.bridge independently chooses ordinary retry or bridge-bucket completion for an allowed packet. The selector applies to received floods; locally originated floods retain the normal global behavior.

The exact requested examples are:

# If #rgdata arrives unscoped with more than 3 received hops, add\n# the regionless #BlackHole86 scope.\nset flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\n\n# Rewrite the exact incoming #usa scope to #BlackHole86 for #rgdata.\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\n\n# Match a two-byte source-path prefix and cap forwarding at 10 per minute.\nset flood.rule.4 type=any prefix=860C rate=10/min\n\n# Keep authenticated #rgdata at two hops or less out of lower-priority FPF7\n# rules. Hard gates and separate tables still apply.\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\n\n# Retry received Public and #hamradio floods, using bridge completion.\n# Once these rows exist, other received floods are not retried.\nset flood.retry.bridge on\nset flood.rule.6 type=any channel=public retry\nset flood.rule.7 type=any channel=#hamradio retry\n\n# Fall back to an unauthenticated one-byte match if only the visible hash is known.\nset flood.rule.8 type=any channel=hash:A7 retry\n\nget flood.rule.2\nget flood.rule.3\nget flood.rule.4\n

Public's visible channel hash is 0x11, so a hash:11 drop row also matches Public unless an earlier exact-channel rule stops it. This standalone example preserves authenticated Public while dropping other packets that use the same visible byte:

set flood.rule.2 type=any channel=public retry stop\nset flood.rule.3 type=any channel=hash:11 drop\n

The Public rule's MAC/decrypt check must succeed before its stop applies. A colliding channel misses that rule and reaches the hash drop. With equal numeric priorities, authenticated-channel specificity automatically puts the Public rule first regardless of slot order. Without stop, both rows match Public and the sticky drop wins. Remove retry from the Public row if the exemption should not also opt Public into flood retry. An explicitly higher numeric priority on the hash rule remains an operator override.

The 240 KB STM32WL profiles keep MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the persistent compact FPF6 flood.filter and blacklist syntax below. They still perform filtering, but omit the generalized flood.rule parser and extended fields. No partition size is changed by this feature.

The compatible filter and blacklist commands are:

set flood.filter.blacklist <ID[,ID...]>\nset flood.filter.blacklist.<slot> <ID[,ID...]>\nget flood.filter.blacklist\nget flood.filter.blacklist.<slot>\ndel flood.filter.blacklist\ndel flood.filter.blacklist.<slot>\nset flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nset flood.filter.<slot> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nget flood.filter\nget flood.filter.<slot>\ndel flood.filter.<slot>\ndel flood.filter all\n

The blacklist holds up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds. Each is written as six hexadecimal digits. For example:

set flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter any all path=blacklist\n

This is intended for abuse containment, such as refusing to retransmit floods that repeatedly enter the mesh through known internet gateways dumping bulk traffic. The list is shared by every FPF7 row and scope-rewrite row that uses path=blacklist; it is not copied into each rule.

An unnumbered set replaces the list with up to 18 IDs, the largest command that fits every CLI transport. A numbered set writes a batch of up to 18 IDs beginning at an existing slot or the next consecutive slot. This is how an ESP32 list grows beyond 18. Deleting a numbered entry compacts the entries after it. Unnumbered get reports the total and prints the leading IDs that fit; numbered get retrieves one specific entry.

path=blacklist is an unordered precondition on that row. With 3-byte path hashes, one or more exact blacklist hits qualifies the packet. With 2-byte path hashes, two or more received path entries must match the first two bytes of listed IDs. Each received entry is counted at most once. A 1-byte path never qualifies. The IDs may occur anywhere in the received path; neither their list order nor their path order matters.

Without a slot number, set reuses an identical row or selects the first empty slot. Use a slot number to replace a row whose match or action is changing. Omitting the hop expression means all (0-63).

Numbered get flood.rule.N and get flood.filter.N normally use descriptive field names. On generalized builds, a reply that would be too long uses the complete set fr.N ... spelling below. get fr.N always requests that spelling. If even the short command exceeds one reply, it returns Err - compact command exceeds reply size instead of a partial command. Saving such a rule still succeeds and reports that its details exceed the reply size.

"},{"location":"flood_filtering/#shorthand-for-copying-rules","title":"Shorthand for copying rules","text":"

On generalized repeaters and FULL ESP32 room servers, fr is a shorter name for flood.rule. Both forms use the same parser, validation, permissions, and saved rows. Short and full field names can be mixed in either command. fr requires an explicit action, just like flood.rule.

These commands set exactly the same rule:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\n
Full form Short form Meaning flood.rule[.N] fr[.N] Same rule table for get, set, and del. type=any any or * First token: any payload type. Named/numeric types also work, with optional t=. hops=N, N+, N-M, or all h=N, h=N+, h=N-M, or h=* Hop condition; a bare range also works. Omitted means all hops. mode=radio, bridge, cross, bridge,cross m=r, m=b, m=c, m=bc Select the admission path. Omitted means radio; cb, b,c, and c,b also mean both transport paths. channel=... c=... Same authenticated name/key or explicit hash:XX matcher. prefix=... p=... Ordered path prefix. path=[^]ID>ID...[$] pa=... Ordered chain anywhere, or anchored at either end. sender=..., text=... n=..., x=... Case-sensitive patterns on an authenticated group-text channel. len=N, N+, or N-M l=... Payload-byte range. route=flood, direct, any rt=f, rt=d, rt=a Route class. prob=N, enabled=on/off pr=N, e=1/0 Sample matches or retain a disabled row. hashbytes=any, 1, 2, 2+, or 3 pb=*, 1, 2, 2+, or 3 Encoded path-hash width, even with zero hops; 2+ matches 2 and 3. path=blacklist p=bl Shared unordered blacklist; repeater only. in=any, none, scoped, allowed, unknown i=*, i=n, i=s, i=a, i=u Incoming scope condition. in=scope:name, in=region:name i=s:name, i=r:name Match a specific incoming scope or region. drop d Drop matching traffic. scope=name, region=name s=name, r=name Rewrite to a public scope or configured region. rate=N/min q=N Per-row forwarding allowance. priority=N pri=N Higher values run first. stop, retry s, r Stop later rules or allow the configured flood retry. allow, forward a Same action as stop; other forwarding gates still apply. tx=slow, suspend=tempradio, retry f=s, f=t, f=r Flags can combine, for example f=str.

The = distinguishes s=name (scope) from s (stop), and r=name (region) from r (retry). Short forms keep the same action restrictions; for example, m=bc r is rejected because transport rules cannot enable retries.

"},{"location":"flood_filtering/#filter-every-1-byte-flood-path","title":"Filter every 1-byte flood path","text":"
set flood.rule type=any hashbytes=1 drop\n

The equivalent short command is set fr any pb=1 d. This adds a rule in a free slot without replacing the default rules. It stops this node forwarding all 1-byte flood packets, including advertisements and relayed login/admin traffic; direct routes and local delivery are unchanged. Use a narrower type= or channel= if those floods must remain reachable. Use hashbytes=2 or 3 for an exact width, or set fr any pb=2+ d to drop both larger supported widths with one rule. Use get fr.<slot> to copy the saved command.

Width-only rules require updated firmware. Existing rules remain unchanged and the table uses no additional RAM or storage. Before downgrading to firmware without this selector, remove width-only rules: its older reader considers them invalid and can reset the rule table to defaults.

get fr.3\n

Returns a complete command ready to copy:

set fr.3 any m=bc c=#wardriving d\n

The compact reply omits default values and prints specific payload types as numbers. get fr lists the table. set fr ... without a slot reuses an identical rule or adds it to the first empty slot. del fr.3 removes slot 3; del fr all clears the forward rules.

Private keys are still hidden in replies. A displayed c=key:XXXXXXXX is a local reference to the exact key in an active rule on that device, so it can be pasted to edit or copy a rule there. On another device, or after deleting the last rule holding that key, supply the original key instead. An unknown or ambiguous reference is rejected; it never becomes an unauthenticated channel-hash match.

Other repeater families have short names too, with identical arguments, slot numbers, validation, persistence, and permissions:

Full family Short family Commands flood.moderation fm get, set, del flood.channel.scope fs get, set, del flood.channel.scope.require fsr get, set, del flood.filter.blacklist fb get, set, del flood.advert fa get, clear

For example, get fa key 1 inspects a full advert key and clear fa all clears the advert limiter history. Companions do not expose these repeater families. Compact FPF6 profiles retain their compiled legacy commands and their family aliases, but do not expose fr or the new generalized fields.

"},{"location":"flood_filtering/#scope-actions-and-legacy-rules","title":"Scope actions and legacy rules","text":"

A legacy row without scope= is the existing drop action. On extended builds, an explicit drop has the same result, while rate= by itself creates a rate-only row. A row with scope= adds transport scope to an unscoped packet or replaces the codes on an already-scoped packet. The scope name is normalized with a leading #, and the 128-bit transport key is derived directly from that hashtag. The name does not need to exist in the region list and is not added to it. Public names up to 30 characters are accepted; private $ scopes are not.

require=region is the legacy spelling of in=allowed. The repeater evaluates the packet's original route before any rewrite in that receive pass. An incoming transport scope must match a locally allowed region; an unscoped flood must be allowed by the wildcard region. If the check fails, that row is skipped, the filter does not grant a region bypass, and the unchanged packet is allowed to fail normal region enforcement. Other independently configured scope rows still apply in their normal order.

When multiple scope or region rows match, the highest numeric-priority row wins. Equal priorities use authenticated channel, raw hash, then wildcard specificity before the slot-number tie-break. Rewrite rows do not approve a packet: any matching drop row and every remaining forwarding gate can still reject it. A filter-assigned scope is trusted without local region-list validation, but repeat, flood.max, loop detection, and moderation still apply. By default, a changed scope bypasses inbound rxdelay, then is retransmitted with zero txdelay and the highest outbound queue priority. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal queue priority, and force the maximum txdelay factor of 2.0; the randomized transmit delay ranges from zero through ten packet airtimes. tx=fast explicitly restores the default. Selecting the scope already present does not grant special treatment. Active radio transmission, CAD, and airtime-budget limits are unchanged.

On generalized repeaters, forward rules, flood.channel.scope rewrite rows, the shared blacklist, and the flood.channel.data compatibility state are one atomic FPF7/FPF8 policy image. Existing /flood_ch_scope, /flood_filter_bl, FPF6, and FCB2 data is imported once; the old files are removed only after the new image verifies and commits. Compact FPF6 repeaters retain separate files. Deleting the blacklist leaves path=blacklist rows in place but dormant until IDs are configured again. Path hashes are truncated routing identifiers and are not authenticated proof that a particular repeater\u2014or a particular person\u2014handled a packet. FULL room servers reject blacklist commands.

On first initialization, flood-filter slot 1 is seeded with:

set flood.filter.1 0x0C all suspend=tempradio\n

This is a normal editable row. After the table has been saved, deleting it remains persistent across reboot; the firmware does not recreate it. Run the same command to restore the exact seeded row, or omit .1 to preserve existing slot assignments and use the first empty slot. Operators may add suspend=tempradio to any other row that should be skipped while either tempradio or tempradio2 is active. This includes scheduled temporary profiles. The row resumes once both temporary sessions have ended.

Suspension does not approve a packet or bypass the rest of the filter table. It skips that row, then evaluation continues with the next row and the remaining forwarding gates. An ordinary drop any row therefore still applies during the temporary-radio window unless an earlier matching stop row ends FPF7 processing. repeat, flood.max*, region handling, loop detection, and the OTA subsystem's own hop limit also remain in force.

Standard traceroute uses direct routing and bypasses mode=radio filters. Explicit bridge/cross filters can also match direct traffic at those transport boundaries; the built-in channel rule does not match TRACE packets. For a custom flood-form trace, type=any, explicit trace, scope, region, rate, and drop rows all behave normally. The stock core does not normally flood-forward TRACE packets.

"},{"location":"flood_filtering/#remote-administration-lockout-warning","title":"Remote-administration lockout warning","text":"

There are no hidden payload-type or short-hop exemptions in FPF7. Drop and rate rules can block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so.

A flooded login starts as ANON_REQ; its reply is commonly a PATH packet carrying an encrypted RESPONSE. Before a direct return path is established, administrative replies and CLI text can also be flooded. Transit repeaters do not have the session key and cannot distinguish those encrypted admin exchanges from ordinary peer packets with the same outer type. A rule therefore affects the complete outer packet class, not only packets that ultimately authenticate as administrators. Keep a serial or other recovery path and stage broad deny/rate rules carefully.

Hop expressions are based on the path count when this repeater receives the packet:

Examples:

# Stop forwarding group data once it arrives with four or more path entries.\nset flood.filter grp_data 4+\n\n# Stop long adverts, while still allowing shorter adverts.\nset flood.filter.2 advert 6+\n\n# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.\nset flood.filter.3 ota 2-4\n\n# Assign #local scope to group text without requiring #local in the region map.\nset flood.filter grp_txt all scope=local\n\n# Rewrite only packets whose incoming region was already acceptable.\nset flood.filter grp_data all scope=local require=region\n\n# Rewrite matching blacklisted paths without fast-tracking their retransmission.\nset flood.filter grp_data all path=blacklist scope=local tx=slow\n\n# Drop matching flood types after the unordered path blacklist qualifies.\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter any all path=blacklist\n\n# Apply a hard ceiling to flood payload types at 12 or more received hops.\nset flood.filter any 12+\n
"},{"location":"flood_filtering/#high-traffic-mesh-example","title":"High-traffic mesh example","text":"

This preset limits request and group-data propagation early while allowing the login-capable response, anonymous-request, and path types to travel farther:

set flood.filter req 3+\nset flood.filter response 9+\nset flood.filter 0x06 3+\nset flood.filter 0x07 9+\nset flood.filter path 9+\nset flood.filter control 1+\nget flood.filter\n
Rule Stops retransmission when received with req 3+ 3 or more path entries response 9+ 9 or more path entries 0x06 3+ (grp_data) 3 or more path entries 0x07 9+ (anon_req) 9 or more path entries path 9+ 9 or more path entries control 1+ 1 or more path entries

On a new generalized repeater table, the three factory rules occupy slots 1 through 3, so these unnumbered commands normally fill slots 4 through 9. FULL room servers and compact tables seed only OTA and use slots 2 through 7. Existing tables may choose different free slots. All six rules take effect at the thresholds shown; there are no hidden short-hop exceptions. The Control rule allows a flood received with path count 0 to be forwarded once, then stops it at the next repeater. Normal node-discovery Control packets are direct zero-hop packets and bypass these mode=radio rules. These rules affect only retransmission by the repeater; local reception and logging remain unchanged.

Accepted payload names are:

Value Short name Full name 0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA in this fork 0x0D 13 reserved 0x0E 14 reserved 0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM

Decimal values 0 through 15, hexadecimal values 0x00 through 0x0F, the full PAYLOAD_TYPE_* names, and any are also accepted. Upstream currently reserves 0x0C; this fork assigns it to LoRa OTA. Rows are suspended during temporary-radio operation only when explicitly configured that way.

"},{"location":"flood_filtering/#moderate-group-text-by-channel-and-username","title":"Moderate group text by channel and username","text":"

Use flood.moderation for flood GRP_TXT messages. The repeater validates and decrypts the selected channel, extracts the display name before the first :, then applies the rule:

set flood.moderation <channel> <sender> <action> [action...]\nset flood.moderation.<slot> <channel> <sender> <action> [action...]\nget flood.moderation\nget flood.moderation.<slot>\ndel flood.moderation.<slot>\ndel flood.moderation all\n

Channels can be specified as:

The key is stored locally so packets can be authenticated and decrypted. It is not included in get flood.moderation output.

Available actions are:

fm is the short family name. Its option aliases are d for drop, q=N for rate=N/min, h=N for hops=N, and p=... for path=.... q=N/min and q=N/m are also accepted; mixed duplicate rate/drop options are rejected just as in the full form.

At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. rate=0/min is equivalent to drop.

"},{"location":"flood_filtering/#per-user-per-channel-rate-limits","title":"Per-user, per-channel rate limits","text":"

Rate limits require an exact username; * is not accepted for a rate rule. Username comparison is ASCII case-insensitive, and names containing spaces must be quoted. A rule's counter is independent from rules for the same name on other channels, so this directly supports \"X messages per minute from user X on channel Y.\" Counters are local to this repeater and reset on reboot.

# At most five Public-channel messages per minute from this display name.\nset flood.moderation public \"Noisy User\" rate=5/min\n\n# A separate limit for the same name on #local.\nset flood.moderation #local \"Noisy User\" rate=10/min\n\n# Combine a rate limit with a maximum forwarding distance.\nset flood.moderation public alice rate=4/min hops=5\n\n# Same rule using short names.\nset fm public alice q=4 h=5\n

The wildcard sender * works with a hop restriction, including for names longer than 31 bytes. Exact configured names remain limited to 31 bytes. Moderation settings retain the FGM1 format and use staged writes, readback, and backup recovery. A failed save is reported as unconfirmed because a failure after publication can leave the new settings available on reboot.

"},{"location":"flood_filtering/#match-the-start-of-a-path","title":"Match the start of a path","text":"

Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule must use the same width, and matching always starts at the beginning of the received path:

set flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation public alice rate=3/min path=71\n

A path-qualified rule cannot match a zero-hop packet and does not match until the packet contains all path entries listed by the rule.

"},{"location":"flood_filtering/#how-the-forwarding-controls-combine","title":"How the forwarding controls combine","text":"

A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:

  1. flood.channel.scope.require evaluates a listed group channel against the original incoming scope; unlisted group channels bypass the later region gate while the table is active.
  2. flood.channel.scope tries a path-qualified channel row before that channel's ordinary fallback, then adds or replaces the scope from either a configured region or a direct scope=<name> target.
  3. All extended flood.rule match fields are evaluated against the same original incoming packet. Matches are ordered by descending numeric priority, then authenticated/raw-hash/wildcard channel specificity, then ascending slot. The first matching stop row removes every later FPF7 match. The highest-order remaining scope= or region= row may replace the channel-scope result; a direct scope does not require a region-list entry.
  4. repeat and flood.max* are checked.
  5. The FPF7 forward phase, including any row managed through flood.channel.data*, applies drop and rate decisions using that saved match result. No packet type or short-hop range is silently exempted.
  6. Region and loop-detection rules are checked; a regionless scope assigned by either table is already trusted when it has no region-list match, except that it cannot rescue a channel rejected by flood.channel.scope.require.
  7. flood.moderation checks decrypted group text, username, rate, hops, and path. If it accepts the packet, matching general-rule rate counters are charged immediately before retransmission is approved.

The first denial is enough to prevent retransmission. A packet that is denied can still appear in local logs or MQTT output, unless a separate mode=bridge rule blocks MQTT packet publication. Moderation runs last because its rate counters are charged only for packets that pass every other forwarding control and will actually be retransmitted.

"},{"location":"flood_filtering/#delegate-filter-management","title":"Delegate filter management","text":"

On repeaters, ACL permission 5 is the filter-manager role:

setperm <companion-public-key-hex> 5\n

A filter manager can read non-secret operational status and manage repeat, loop.detect, flood.max*, flood.channel.data*, flood.filter*, flood.rule*, fr[.N], and flood.moderation*, including the fm, fb, and fa aliases. Delegated get access uses an explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or unrelated administrator settings. Because flood.filter scope= derives a public hashtag key directly, a filter manager can configure that action without region-manager permission; it still cannot edit the region hierarchy.

FULL ESP32 room servers use their existing administrator check for remote fr, flood.rule, and flood.filter commands; they do not grant this table through permission 5.

ACL permission 4, the region/scope-manager role, can read, add, replace, and delete flood.channel.scope and flood.channel.scope.require rows and manage regions. This lets the same delegate create target regions, assign forced scopes from regions or direct hashtag names, and select the channels that require valid incoming scopes. The fs and fsr aliases retain this role.

Admin CLI sessions and these delegated roles use the same setters and validation as the serial console. On enrolled builds, the aliases also work inside signed fleet commands, for example:

fleet send 3 set fr.4 grp_txt c=public x=\"^BEACON\" d\nfleet send 3 set fr.4 e=off\n

Fleet enrollment, signature, expiry, replay, and command-size checks still apply. An ordinary channel message cannot authorize a filter change.

"},{"location":"flood_filtering/#security-limitations","title":"Security limitations","text":"

Public and hashtag channels use shared, well-known keys. A valid channel MAC proves that the sender knew the channel key; it does not identify a person. The <sender> value is an unverified display name and can be spoofed. Path hashes are truncated routing hints and can collide or be manipulated; they are not authenticated user identities.

Use username and path rules as traffic moderation, not as an authorization boundary. For a strict network boundary, combine these tools with region ACLs, private transport/channel keys, and controlled device access.

"},{"location":"flood_filtering/#filter-bridge-and-radio-crossover-traffic","title":"Filter bridge and radio crossover traffic","text":"

The mode= matcher selects a separate admission phase within the same flood.rule / flood.filter table:

Mode Where the rule applies radio Ordinary LoRa forwarding; flood-only by default, or opt into direct routes with route=. This is the default mode. bridge Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. cross Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. bridge,cross Both transport paths using one rule. cross,bridge is equivalent.

The third built-in repeater filter is:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nget flood.rule.3\n

The equivalent short command is set fr.3 any c=#wardriving m=bc d. Use m=b for bridge only or m=c for crossover only.

It authenticates both GRP_TXT and GRP_DATA against the public #wardriving channel key. A different channel with the same one-byte hash is not blocked. It covers every hop count, including zero-hop/local messages, flood routes and direct routes. It remains active during temporary-radio sessions and with radio2.cross on: enabling crossover makes a copy eligible, but does not override the filter. The second built-in rule still permits ordinary #wardriving forwarding through four received hops; the third rule keeps those messages on their originating radio profile.

Bridge filtering runs in both directions before bridge deduplication, inbound queueing, serial writes, ESP-NOW queueing, or MQTT packet publication. It applies with either bridge.source rx or bridge.source tx and with wrapped or raw ESP-NOW framing. MQTT here is the observer packet output; this does not add MQTT-to-LoRa injection or filter MQTT status/telemetry. Local reception and local packet logs remain available. Switching one radio between profiles is covered by cross; the bridge's WiFi channel is a separate setting.

Transport modes support drop, rate=X/min, priority=, and stop, with the existing type, channel, hops, prefix, blacklist, incoming-scope and suspend=tempradio matchers. Scope/region rewrites, retry actions, and TX timing actions are rejected in these modes. Ordering and stop apply only to rules in the current mode: a radio exception cannot bypass a bridge/cross drop. A combined rule shares one rate counter across bridge and cross admission attempts; retries count too. A rejected queue insertion can consume an admission allowance. Queued packets already admitted are not recalled by editing a rule. Same-profile transmissions use the ordinary radio policy.

get flood.rule.N shows mode=; the overview marks each row with its mode. Modes are persistent, case-insensitive, and apply immediately to new admission attempts. radio cannot be combined with bridge or cross in a single row. For transport rules, hop and scope matchers see the packet at that boundary: a TX/crossover packet can already contain an appended hop or rewritten scope. The built-in rule uses all hops and any scope, so these changes cannot bypass it. On FULL room servers, add a transport rule explicitly; their factory table still has only the OTA row. Companions and compact FPF6 builds do not expose this generalized filter table.

Saved tables, occupied slots, and deleted defaults are preserved on upgrade. To add the new default to an existing table without replacing another rule, omit the slot number (repeating the identical command reuses its row):

set flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\nget flood.rule\n

To allow only bridge traffic while retaining the crossover block, replace the third rule with mode=cross. Use mode=bridge to retain only the bridge block. Use del flood.rule.3 to remove both, after checking that slot 3 is still the built-in rule. Removing it does not change the separate LoRa hop-limit rule.

"},{"location":"flood_filtering/#restore-the-factory-seeded-rows","title":"Restore the factory-seeded rows","text":"

The repeater's factory-seeded forwarding rows can be restored through the CLI:

set flood.rule.1 type=ota hops=all drop suspend=tempradio\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n

Or use the equivalent short forms:

set fr.1 ota d f=t\nset fr.2 any 5+ c=#wardriving d\nset fr.3 any c=#wardriving m=bc d\n

These commands explicitly replace the three seeded generalized-repeater slots. Inspect them first if they may now contain other rules. Compact FPF6 builds use only the first command's flood.filter.1 0x0C all suspend=tempradio form.

"},{"location":"flood_filtering/#remove-the-custom-rules","title":"Remove the custom rules","text":"

To save both tables in an empty state:

del flood.filter all\ndel flood.moderation all\nget flood.filter\nget flood.moderation\n

This does not change the older flood.max*, loop-detection, or region settings; inspect or reset those separately when troubleshooting.

"},{"location":"four_fixed_tx_dwell_validation/","title":"Four fixed tx dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"four_fixed_tx_dwell_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"four_fixed_tx_single_pass_validation/","title":"Four fixed tx single pass validation","text":"

View this page on MeshCore Docs.

"},{"location":"four_fixed_tx_single_pass_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"full_companion_features/","title":"Full companion features","text":"

View this page on MeshCore Docs.

"},{"location":"full_companion_features/#full-companion-turn-features-on-and-off","title":"Full Companion: turn features on and off","text":"

For side-by-side Companion and infrastructure commands, see feature switches by role. MQTT and logging controls differ between these roles.

Use the exact companion_radio_full image for your board. Full includes its supported transports and MOTA sending in one firmware; ordinary settings do not require rebuilding it. USB remains the normal way to update the Companion itself. WiFi/Bluetooth updates are optional, and Full does not install LoRa MOTA packages on itself.

New Heltec T114 Full images automatically detect the optional screen and also run without it. Choose Heltec T114 in the firmware picker for either board configuration; see the T114 update guide.

The Wireless Paper Full memory-sharing update keeps 350 contacts and 256 queued messages normally. During an mOTA session, 128 message slots are available while the other half holds its workspace. If more than 128 unread messages are queued, sync them with an app before starting mOTA. Disconnecting the source or turning it off returns all 256 slots. Idle WiFi mOTA listening does not borrow the queue. See the 1.17.1.5 release guide for the corrected download; the earlier aa20e927 Wireless Paper image has 150 contacts.

"},{"location":"full_companion_features/#open-the-text-terminal","title":"Open the text terminal","text":"

Close the app or serial monitor using the USB port, then open a terminal at 115200 baud, or use the USB web console. A fresh Full Companion boot or observable USB session reset starts in ASCII text mode. If it is already in Binary Companion mode, send this line once and wait for the terminal banner:

+++MESHCORE-TERM-START\n

Run help, board, and version to inspect the device. On ESP32 you can alternatively open the node's LAN address and select CLI, or connect with nc DEVICE_IP 5002. Both use the same terminal commands as USB, including import meshcore://..., list, to, and send. Use disconnect to release a browser/TCP session. See the terminal guide. When finished, send +++MESHCORE-TERM-STOP, close the terminal, and reconnect your Companion app. These are local text commands; an app's custom-variable editor does not necessarily forward them.

"},{"location":"full_companion_features/#common-switches","title":"Common switches","text":"Feature Turn on Turn off When it takes effect Device power saving set powersaving on set powersaving off Immediately; saved LoRa receive power saving set radio.rxps on set radio.rxps off Saved; radio applies when available Radio chip RX boost set radio.rxgain on set radio.rxgain off Immediately; saved; supported radios only External FEM RX boost set radio.fem.rxgain on set radio.fem.rxgain off Immediately; saved; controllable FEM only External FEM TX gain set radio.fem.txgain on set radio.fem.txgain off Immediately; saved; controllable PA only ESP32 USB packet/debug logging For 1.17.1.5: set powersaving off, then set usb.logging on set usb.logging off Immediately; saved; logging owns the USB terminal nRF52 separate USB logging port set usb.logging on reboot set usb.logging off reboot Saves and reboots to add/remove the second USB port nRF52 one-port packet stream set usb.logging stream reboot set usb.logging off Saved; CLI and packets share the primary port for stock serial bridges ESP32 browser settings set webui on set webui off Saved; starts/stops WebConfig ESP32 temporary setup portal start webconfig ap stop webconfig This session; opens a setup network/QR where available ESP32 WiFi firmware uploader start ota or start ota ap stop ota This session; only usable with two application slots Temporary MOTA radio window tempradio 910.525,250,5,5,120 normalradio Bounded window; saved normal settings return afterward

Check a saved switch with the corresponding get command, for example get usb.logging, get radio.rxps, or get webui. Use get powersaving to inspect device power saving. Fresh installations enable device power saving and leave USB logging off. Existing saved preferences take precedence.

For an ESP32 1.17.1.5 USB logging session, use this sequence in the text terminal:

set powersaving off\nset usb.logging on\n

The power-saving step is the documented USB-session workaround for this release. It is saved separately; set usb.logging off does not restore power saving. nRF52 does not need the ESP32 workaround. WiFi-only MQTT logging does not need this USB step; see logging by role.

ESP32 logging and Binary Companion share one USB port. After turning logging off, return to Binary Companion with +++MESHCORE-TERM-STOP. nRF52 keeps its optional logging port separate from its primary USB port and BLE connection. See the USB switching guide.

"},{"location":"full_companion_features/#wifi-bluetooth-gps-and-mqtt","title":"WiFi, Bluetooth, GPS, and MQTT","text":"

On ESP32, configure WiFi through the setup portal or these commands:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nget wifi.status\n

Credentials are saved. A credential change schedules a reconnect; a TCP terminal will disconnect. The WiFi switch on supported device displays or their assigned WiFi button controls the Companion WiFi services. Stopping WebConfig alone stops its browser service, while the Companion's TCP and MOTA services can remain available.

WiFi modem sleep is independent of device power saving. Use set wifi.powersave min for normal BLE/WiFi coexistence; inspect the effective state with get wifi.powersave. max is available only where the firmware's radio coexistence policy permits it.

Most Full Companions provide BLE alongside their other transports. On Bluetooth-capable ESP32 and nRF52 Companions, get bluetooth reports the running state and set bluetooth on|off saves the service preference. Plain off requires another active management connection; set bluetooth off force explicitly permits disconnecting the only client. Reboot does not undo a saved-off preference. See Bluetooth controls for reply draining, connection checks, and unavailable boot modes.

SenseCAP Indicator selects one secondary wireless transport per boot:

set companion.transport wifi\nreboot\n

Use set companion.transport ble followed by reboot to select Bluetooth instead. USB stays available. The ESP-NOW layout keeps its primary ESP-NOW mesh active in either mode.

For GPS-equipped boards, use get gps, set gps on, and set gps off. The Companion app's gps=1 / gps=0 custom setting controls the same GPS. Only boards with a compiled GPS provider expose this setting. Sharing location with contacts is a separate setting.

Use set gps.sync.interval <hours> to save an automatic clock-sync interval of 1-336 hours (two weeks); get gps.sync.interval reports it. Larger inputs are capped at 336 and the reply shows the applied value. Existing GPS timings are unchanged until configured. This controls clock synchronization, not the position-update gps_interval or location-sharing policy.

When the exact Full image includes MQTT, use WebConfig's MQTT cards or the same CLI settings used by infrastructure, such as set mqtt1.preset custom and set mqtt1.server broker.example.com. set mqtt.enabled on|off controls MQTT without erasing the configured slots; check get mqtt.enabled, get mqtt.running, and get mqtt.status. The MQTT tab's Enable MQTT checkbox controls the same saved switch. set logging.output off|usb|wifi|both selects USB and MQTT outputs together. Status, packets, raw, receive, and transmit switches control their individual publications. Builds without MQTT omit these controls. There is no need for a separate logging or WiFi-MQTT Companion image when that feature is included in Full.

"},{"location":"full_companion_features/#send-mota-from-any-full-companion","title":"Send MOTA from any Full Companion","text":"

Place valid, destination-specific .mota files in a host directory. Set the source, destination, controller, and required relays to the same bounded temporary radio tuple. For USA Cascade on LoRa hardware:

tempradio 910.525,250,5,5,120\n

On the computer connected to the Full Companion, run:

motatool serve --serial /dev/ttyACM0 --dir ./motas -v\n

This works on both ESP32 and nRF52 Full. Close the text terminal first. motatool automatically sends ota folder on and owns USB while serving. Stop the host tool to detach the source; use normalradio afterward to return early, or let the window expire. A .bin, .uf2, or DFU .zip is not itself a .mota; use motatool to prepare the package for the exact destination firmware and hardware.

ESP32 also accepts a host over WiFi:

motatool serve --tcp DEVICE_IP:5001 --dir ./motas -v\n

nRF52 also accepts an authenticated Bluetooth host:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion --dir ./motas \\\n  --local 'tempradio 910.525,250,5,5,120'\n

Use one MOTA source transport at a time. See the complete MOTA instructions for pairing, remote destination commands, and automation. ESP-NOW hardware uses its ESP-NOW primary radio rather than transmitting LoRa.

"},{"location":"full_companion_features/#update-the-companion-itself","title":"Update the Companion itself","text":"

USB is supported for every Full Companion. Use the exact board's UF2/DFU package or ESP32 image and the normal flasher. Preserve the target's existing partition layout unless deliberately performing a documented USB migration.

For ESP32 with two application slots, start ota returns the WiFi uploader URL, normally http://DEVICE_IP:8080/update. start ota ap explicitly opens MeshCore-OTA; join it and use the returned URL. Upload the exact board's application .bin. The device reboots when the upload succeeds. Use stop ota to close an unused uploader. Starting OTA enables WiFi as needed, stops WebConfig automatically, and reports the network and URL to use. Use a trusted local network or a temporary setup network.

The 4 MB Full layouts and T-Beam 1W Full use a single application slot and require USB; the command reports that limitation. The artifact's .capabilities.json lists verified ota_update_methods when a wireless self-update path is usable.

nRF52 Full exposes Bluetooth DFU to paired clients. Use the matching application DFU .zip with a compatible Nordic/Adafruit Legacy DFU client and the board's matching BLE-DFU-capable bootloader/SoftDevice. USB remains the fallback when the installed bootloader does not support that procedure. Bluetooth DFU updates the Companion; the separate Bluetooth MOTA source service feeds packages to other mesh nodes.

For OTAFIX installations, use the exact board/storage profile from OTAFIX 2.4.6. It includes the Bluetooth reconnect fix from 2.4.5. Its release notes cover upgrades from older bootloaders, including the special UF2-drive precautions for version 2.4.3. The new 64 KiB retained-RAM staging feature applies to qualified internal-flash nRF52840 receivers; Full Companions continue to serve host-supplied packages without needing that receiver storage layout.

"},{"location":"full_companion_usb_switcher/","title":"Full companion usb switcher","text":"

View this page on MeshCore Docs.

"},{"location":"full_companion_usb_switcher/#companion-usb-cli-and-binary-switcher","title":"Companion USB CLI and binary switcher","text":"

USB Companion uses one primary USB serial interface for two incompatible wire formats:

The primary interface starts in the ASCII terminal after each boot and returns to ASCII after a USB session reset that the hardware can observe. A Binary Companion client does not need to send a special mode command: its first valid frame automatically hands the interface to the binary parser.

Current source keeps the default terminal quiet until the host sends ASCII input. Press Enter or type a command to see the banner. This prevents the ASCII > prompt from being mistaken for the beginning of an app's first binary response. See USB client compatibility validation for the tested firmware and release status.

These mode controls apply to Full and ordinary USB Companion builds. The explicit +++MESHCORE-TERM-START and +++MESHCORE-TERM-STOP controls remain available, including for older firmware. See the Terminal Chat CLI guide.

"},{"location":"full_companion_usb_switcher/#wire-formats","title":"Wire formats","text":"

Host-to-device Binary Companion frames use this layout:

'<'  length-low  length-high  payload[length]\n

Device-to-host frames use the same little-endian length with a different marker:

'>'  length-low  length-high  payload[length]\n

For example, a representative two-byte device query is:

3C 02 00 16 03\n

The ASCII terminal is line-oriented and accepts commands such as:

get radio.cad\nset display.rotation 90\nreboot\n

Changing the configured baud rate does not select a mode. Use 115200 for compatibility even though native USB CDC hardware does not use UART timing.

"},{"location":"full_companion_usb_switcher/#state-transitions","title":"State transitions","text":"
                         complete '<' frame\n                 +--------------------------------+\n                 |                                v\nboot ------> ASCII terminal                  Binary Companion\n                 ^                                |\n                 |                                | +++MESHCORE-TERM-START\n                 | incomplete '<' probe           |\n                 | (one-second timeout)            |\n                 +--------------------------------+\n\nASCII terminal -- +++MESHCORE-TERM-STOP ------> Binary Companion\nany mode ------- observable USB session reset ---> ASCII terminal\nESP32 logging terminal -- usb.logging off -----> ASCII terminal\nany mode ------- reboot ------------------------> ASCII terminal\n

Serial mOTA and single-TTY logging add exclusive ownership states described below. BLE, WiFi, Ethernet, and hardware-serial Companion transports are not switched; they remain binary.

"},{"location":"full_companion_usb_switcher/#how-automatic-detection-works","title":"How automatic detection works","text":"
  1. Companion initializes the normal USB Binary Companion interface, then gives its primary stream to the ASCII terminal before normal loop service begins.
  2. While the prompt has no buffered input, the terminal skips blank CR/LF delimiters and peeks at the next byte without removing it. A queued binary frame after a blank line is detected before any ASCII banner is emitted.
  3. If the byte is <, the terminal temporarily releases the stream and enables the existing ArduinoSerialInterface frame parser.
  4. The parser consumes the original <, the two-byte length, and the payload. There is no second parser and no copied or synthetic frame.
  5. A monotonically increasing completed-frame counter confirms that the parser received a complete frame. The interface then remains in Binary Companion mode.
  6. If no complete frame arrives within one second, the parser state is reset and the ASCII terminal prints a new banner and prompt.

The switcher checks framing, not client identity. A complete Binary Companion frame with 1-176 payload bytes confirms binary mode; it does not require the first command to be CMD_APP_START or CMD_DEVICE_QUERY. Normal command validation still occurs after the frame parser returns the payload.

Oversized frames are consumed and discarded, never truncated into executable commands or counted as proof of a Binary client. ASCII, framed Binary, and mOTA input handlers process at most the initial queued-byte count, so a continuously sending host cannot keep the radio loop trapped in one input call.

ASCII and mOTA reject an entire line containing an embedded NUL or exceeding its line buffer, discarding through the next CR/LF. A valid-looking suffix is not executed. Binary payloads may still contain NUL bytes. The immediate manual terminal-stop token remains an exception to ordinary line-oriented handling.

The empty-prompt requirement prevents a literal < in the middle of a command from silently changing modes. A literal < typed as the first character does start a probe, but the terminal returns after the one-second timeout if no binary header and payload follow.

"},{"location":"full_companion_usb_switcher/#manual-controls","title":"Manual controls","text":"

The original controls remain available.

From Binary Companion, send this exact unframed line while the parser is idle:

+++MESHCORE-TERM-START\n

Terminate it with CR or LF. The binary parser accepts control tokens only as complete delimiter-bounded lines; prefixes, suffixes, and partial tokens are ignored.

From the ASCII terminal, send this exact sequence to return to binary mode:

+++MESHCORE-TERM-STOP\n

The stop sequence takes effect as soon as its last byte arrives in ASCII mode. The start sequence is recognized only as a completed line while the binary parser is idle and is not examined inside a length-prefixed frame.

meshcli can normally connect directly after boot:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

An explicit terminal start token is still useful when the device is already in binary mode:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n
"},{"location":"full_companion_usb_switcher/#logging-and-mota-ownership","title":"Logging and mOTA ownership","text":"

The switcher never attempts to mix ASCII, framed Companion traffic, or binary mOTA traffic on the same stream.

Situation Primary USB behavior USB Companion after boot or observable session reset ASCII; a complete < frame switches to Binary Companion nRF52 logging enabled Primary interface still follows the switcher; logs use the optional second interface ESP32 logging enabled at boot Logging terminal owns the only USB TTY; automatic < detection is disabled nRF52 USB serial mOTA active mOTA owns primary USB; ASCII and Binary Companion are unavailable there BLE/WiFi/Ethernet/hardware serial Always Binary Companion and unaffected by the USB mode

On RAK4631 and other nRF52 Full Companions, ASCII and Binary Companion both use the primary interface (*-if00 on Linux). The optional *-if02 interface is for logging. OK - Binary mode acknowledges a mode change on the same primary port; it does not move the connection to the logging port.

On every ESP32 Full Companion, use set usb.logging off in its logging terminal before trying to use USB with an app. That command stops logging and returns to the ordinary ASCII terminal; it does not select Binary Companion. The app's first valid framed probe can perform the normal automatic switch, or a human can send +++MESHCORE-TERM-STOP first. The framed/binary Companion parser is disabled while logging owns that TTY. BLE and WiFi Companion transports remain available while USB is logging. ESP32 Full builds use the Arduino-ESP32 2.x base where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but follow the same one-TTY policy. No ESP32 Full build enumerates a second CDC interface.

usb.logging is the saved master for USB packet output. usb.debug separately saves verbose diagnostic intent and defaults off, including when older preferences do not contain the new field. RAW/RX/TX packet records need only the master; diagnostics need both switches and compiled debug support. Turning the master off preserves debug intent. get usb.debug reports that saved choice, not whether USB currently owns a plaintext logging stream.

Full Companion now emits complete received frames as uppercase ASCII RAW: hex, separately from decoded RX summaries and TX summaries. Leave debug off for USB-to-MQTT capture. These are bounded best-effort records; drops and host disconnects can separate a RAW/summary pair. The dedicated nRF52 log CDC cannot answer CLI/identity queries: programs expecting one bidirectional CLI/log port need separate control-port support, not just a change to the serial pathname. Companion supports get public.key in the primary terminal or framed CLI; the reply is > followed by 64 uppercase hex characters. This query does not make the dedicated log CDC input-capable or establish full bridge compatibility. Send get/set usb.debug to the primary ASCII terminal; changing verbosity needs no USB-interface reboot and does not relax Binary/mOTA ownership rules.

Current source adds get usb.watchdog and saved set usb.watchdog off|on|auto, defaulting to Auto until 14 continuous healthy logging-client days qualify it for On. Existing USB stats polls renew a 15-minute client lease; a cable or DTR alone does not count. A lost confirmed client can trigger USB-only recovery after lease expiry plus five minutes. MCU resets additionally require On and a full physical-fault interval, a verified tier save, and clear protocol/update ownership. Active Binary USB and mOTA sessions veto recovery. On dual CDC, only logging CDC1 accepts the bounded stats markers; primary CDC0 stats do not renew that lease. CDC1 still provides no CLI/statistics replies. See USB logging watchdog for status and limitations.

With logging enabled on a shared port, the stop token is rejected with ERROR: set usb.logging off before Binary mode. Turn logging off first; a successful binary-mode acknowledgement must not immediately revert to logging.

On nRF52, serial motatool is also text-first. Its exact initial ota folder on line is recognized in either startup ASCII or Binary Companion mode. From ASCII, the firmware leaves terminal mode and directly enters exclusive mOTA ownership; from binary, the idle frame parser recognizes the same control sequence. The following mOTA request/reply frames therefore cannot be consumed by the ASCII line editor. motatool serve --serial can be the first client after boot and does not require a terminal token or disconnect workaround.

Only the exact completed line selects mOTA from ASCII. Extra arguments, leading/trailing whitespace, or a partial line remain ordinary terminal input.

"},{"location":"full_companion_usb_switcher/#shortcomings-and-edge-cases","title":"Shortcomings and edge cases","text":"

This mechanism is deliberately small and deterministic, but it is not a full protocol negotiation layer.

"},{"location":"full_companion_usb_switcher/#session-detection-depends-on-the-usb-hardware","title":"Session detection depends on the USB hardware","text":"

After the first complete binary frame, the device stays in Binary Companion mode for that session. Native USB with DTR/session events restores ASCII when the host closes or resets the connection. Old input, queued output and mOTA ownership are cleared before selecting the default for the next session.

Some clients, including meshcore 2.3.10 used by stock MeshCLI, deliberately deassert DTR. The nRF52 primary interface also accepts actual incoming USB data as evidence of a client in the current session. Closing, changing line coding, or resetting USB invalidates that evidence. The dedicated logging interface continues to require its own connected reader.

ESP32 hardware USB Serial/JTAG reports bus reset and physical host loss, but cannot reliably report a terminal program closing its handle. A USB-to-UART bridge also usually cannot report that close. On those ports, use the terminal start token to return from Binary mode without rebooting. Idle time alone never changes the selected protocol.

An active network terminal temporarily delays restoring the USB ASCII prompt. If a fresh USB app sends a complete binary frame during that delay, its Binary mode remains selected when the network terminal closes.

"},{"location":"full_companion_usb_switcher/#detection-works-only-at-an-empty-prompt","title":"Detection works only at an empty prompt","text":"

If part of an ASCII command is already buffered, an incoming < is treated as ordinary terminal input. Clear or submit the line before starting a Binary Companion client. Only one process should have the serial port open.

"},{"location":"full_companion_usb_switcher/#the-first-frame-has-a-one-second-deadline","title":"The first frame has a one-second deadline","text":"

The complete marker, length, and payload must arrive within the probe window. This is generous for local USB but may reject a heavily buffered serial proxy, a debugger that pauses the MCU, or a tool that writes the header and body with a long delay. A timed-out client can retry after the ASCII banner appears.

"},{"location":"full_companion_usb_switcher/#framing-confirmation-is-not-authentication","title":"Framing confirmation is not authentication","text":"

Any complete length-prefixed frame selects binary mode, even if its command is unknown or malformed at the application layer. This is safe for stream separation but means a random complete frame can leave the device in binary mode until an observable USB session reset or a manual switch back.

"},{"location":"full_companion_usb_switcher/#the-default-terminal-waits-for-input","title":"The default terminal waits for input","text":"

The firmware enters ASCII mode during boot but suppresses the banner and unsolicited terminal events until ASCII input identifies a terminal client. Send Enter or a harmless get command to display the banner. A binary client instead gets a clean first response. Older builds that immediately printed > could consume the first binary response's marker as part of a bogus frame length; scanning past leading text alone did not reliably recover that reply.

"},{"location":"full_companion_usb_switcher/#text-and-binary-output-cannot-be-interleaved","title":"Text and binary output cannot be interleaved","text":"

Primary USB suppresses Binary Companion output while the terminal owns the stream. Packet/debug logging must use its dedicated CDC interface or the exclusive single-TTY logging mode. Writing diagnostic text directly to the primary binary stream will corrupt clients regardless of the switcher.

"},{"location":"full_companion_usb_switcher/#a-literal-leading-briefly-hides-the-prompt","title":"A literal leading < briefly hides the prompt","text":"

Typing < as the first terminal character begins a binary probe. With no complete frame, the prompt returns after one second and the banner is printed again. There is currently no escape syntax for entering a literal leading <; prefix it with another character if it is needed as command text.

"},{"location":"full_companion_usb_switcher/#troubleshooting","title":"Troubleshooting","text":"

If meshcli cannot connect:

  1. close every terminal or logging reader using the primary interface;
  2. on ESP32, confirm that saved USB logging is off;
  3. reboot and let meshcli be the first process to open the data interface;
  4. use the stable /dev/serial/by-id/*-if00 path on Linux when available;
  5. if a prompt appears after one second, the client's first frame was not completed inside the probe window.

If an ASCII terminal shows no banner, press Enter and issue a harmless query such as get radio.cad. If binary bytes appear, send the exact terminal start token or reboot.

The switch policy is implemented in UsbAsciiBinarySwitch.h, with stream ownership in main.cpp and framing in ArduinoSerialInterface.cpp.

"},{"location":"full_profiles/","title":"Full profiles","text":"

View this page on MeshCore Docs.

"},{"location":"full_profiles/#full-firmware-profiles","title":"Full firmware profiles","text":"

Full combines the features supported by an exact board and firmware role in one image. Use runtime settings to choose the services you need instead of flashing a separate image for each transport. Hardware, memory, and the installed flash layout still determine which combinations can run.

Full is not a runtime role switch. Choose Companion, Repeater, Room Server, or Sensor in the firmware picker. Changing roles requires the matching firmware; enabling a bridge does not turn a Companion into a repeater.

"},{"location":"full_profiles/#choose-the-board-role-and-profile","title":"Choose the board, role, and profile","text":"Choice What Full combines Important limits Full Companion The board's supported USB, Bluetooth, ESP32 WiFi, and qualified serial/Ethernet Companion transports, plus terminal chat and mOTA serving The primary radio and board interfaces remain those of the selected target. Full Companion serves updates to other nodes; it does not install LoRa mOTA on itself. ESP32 Full Repeater or Room Server Standalone operation, supported USB/WiFi services, and the qualified MQTT, ESP-NOW, or RS-232 combination Not every image contains all three bridges. Read its capabilities and check the compiled controls. ESP32 Full Sensor Sensor operation, supported CLI/logging, and qualified WiFi firmware-update and TCP 5001 seeding services Current Sensor profiles do not gain the repeater's MQTT/ESP-NOW/RS-232 bridges. nRF52 Full supported sensors or Reduced sensors Two separately qualified Repeater, Room Server, or Sensor recipes with LoRa OTA These are sensor-driver choices, not ESP32 network features. Full Companion remains a separate profile.

Select the exact hardware variant and storage choice. Read the selected image's capability manifest for update methods and requirements; a similar filename is not evidence that another board's image will work.

"},{"location":"full_profiles/#full-companion-choose-the-active-transports","title":"Full Companion: choose the active transports","text":"

Close any application using the USB port, then open the USB web console at 115200 baud. A fresh Full Companion starts in the text terminal. If an app already selected Binary Companion mode, send this line and wait for the terminal banner:

+++MESHCORE-TERM-START\n

Run help, board, and version to inspect the device. The same image can serve an app through its compiled transports or provide terminal chat. USB remains the normal installation and recovery path.

On a Bluetooth-capable Companion, get bluetooth shows the running state; set bluetooth on enables the saved Bluetooth preference. On an ESP32 profile with WiFi, start webconfig ap opens a temporary setup portal where you can save network credentials. MQTT-capable Companions also expose broker settings there; MQTT is not included on every Companion board.

Most Full Companions can use multiple transports together. SenseCAP Indicator selects Bluetooth or infrastructure WiFi per boot using set companion.transport ble or set companion.transport wifi, followed by reboot; USB remains available. Generic ESP-NOW and Indicator ESP-NOW profiles use ESP-NOW as their primary mesh radio. Full does not convert those profiles into LoRa radios.

USB logging shares the ESP32 Companion's single USB port. Use set usb.logging off, then +++MESHCORE-TERM-STOP and close the terminal before reconnecting a Binary Companion app. nRF52 Full Companion can instead add a separate logging port with set usb.logging on reboot; use its primary port for Companion commands and mOTA serving. A one-port host bridge can use set usb.logging stream reboot, which occupies the primary port until logging is disabled. See the USB mode guide.

"},{"location":"full_profiles/#esp32-infrastructure-enable-the-compiled-services","title":"ESP32 infrastructure: enable the compiled services","text":"

Full Repeater and Room Server images retain their normal role while their supported bridges run. A LoRa/ESP-NOW bridge keeps LoRa as its primary radio and exchanges packets with its ESP-NOW peers over 2.4 GHz. MQTT publishes observed traffic to brokers; it does not subscribe to broker messages or inject them into LoRa. RS-232 uses the board's configured UART and pins.

Check get bridge.type on bridge-capable images. Depending on the recipe, it can report combinations such as mqtt+espnow, rs232+espnow, or mqtt+rs232+espnow. Use only the controls compiled into your image:

Service Saved enable/disable control Check actual operation MQTT set mqtt.enabled on / off get mqtt.running, get mqtt.status ESP-NOW set espnow.enabled on / off get espnow.running RS-232 set rs232.enabled on / off get rs232.running USB packet logs set usb.logging on / off get usb.logging

An enabled preference is not proof that a service started. Missing credentials, a channel mismatch, or a UART conflict can prevent operation. bridge.enabled is a historical alias with different meanings between recipes; use the explicit transport controls on combined images.

For an MQTT-capable image, first follow its role's WiFi/MQTT setup and verify the actual connection with get wifi.status. Once connected, this example selects a custom broker:

set mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.status\n

Replace the sample broker value. Add the broker's username and password if needed. Use the WiFi/MQTT guide for TLS, publication settings, broker-slot limits, and reconnect behavior. Where MQTT and USB logging coexist, set logging.output off|usb|wifi|both selects those two outputs together; it does not change LoRa repeating.

For ESP-NOW, configure both peers with the same channel and compatible bridge.format. wrapped uses the shared bridge.secret for bridge peers; raw connects to primary-ESP-NOW MeshCore nodes. When MQTT WiFi and ESP-NOW run together, the ESP-NOW channel must match the access point's fixed WiFi channel. ESP-NOW alone needs no WiFi credentials. Follow the two-board setup for the complete commands.

For RS-232, stop the UART bridge before changing its configuration:

set rs232.enabled off\nget bridge.uart\nset bridge.baud 115200\nset rs232.enabled on\nget rs232.running\n

Use the UART and pin assignment supported by your board. GPS can reserve or share a UART; enabling the bridge may be rejected or pause a yieldable GPS provider. Check the board's recipe before changing bridge.uart.

"},{"location":"full_profiles/#capacity-and-role-exceptions","title":"Capacity and role exceptions","text":"

MKE S3 combines UART and ESP-NOW in its normal Full repeater. Full Heltec V3, WSL3, and RAK3112 MQTT repeaters also retain independent RS-232 alongside MQTT and ESP-NOW. T-LoRa V2.1 retains two Full repeater choices because of internal RAM limits: UART plus ESP-NOW, or MQTT plus ESP-NOW. Other measured capacity exceptions retain dedicated images. Non-ESP32 boards do not inherit these ESP32 combinations; Wio-E5, for example, keeps a separate RS-232 image.

"},{"location":"full_profiles/#nrf52-full-and-reduced-supported-sensors","title":"nRF52: Full and Reduced supported sensors","text":"

Qualified nRF52 Repeater, Room Server, and Sensor builds offer Full supported sensors + LoRa OTA and Reduced sensors + LoRa OTA. Full includes the drivers supported by that board's full recipe; it does not add support for every possible attached sensor. Reduced omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals. Reduced RAK3401/RAK4631 recipes also retain their supported INA219/INA226/INA260/INA3221 voltage/current drivers.

The picker recommends Full when both profiles are qualified from the same source. Reduced remains available through its Advanced sensor option and explicit shared links, with both downloads and their update identities retained. An absent sensor does not require a Reduced image; supported sensor discovery already handles missing peripherals.

Both profiles retain their exact board/role OTA identity and storage layout. Choose the matching storage variant and meet its staging/bootloader requirements. These profiles are LoRa OTA receivers; nRF52 Full Companion is an update source and does not acquire WiFi or on-device MQTT. An attached Pi can publish its USB packets using a separate host bridge. See sensor/storage choices.

"},{"location":"full_profiles/#install-or-update-the-matching-layout","title":"Install or update the matching layout","text":"

Expanded ESP32 Full images need application slots large enough for the image. Use get storage.layout on firmware that provides it to inspect the installed partition table. Firmware version alone does not establish the layout; use the partition lookup guide for older firmware.

For a compatible layout, use the exact board/role application-only image through a supported updater. To change an ESP32 layout, use the matching merged image over USB or an exact board/role staged migration ZIP whose README supports your installed source layout and update route. An app-only image cannot change the partition table, and a merged image is not a browser-OTA application. The picker links the matching migration package where available.

ESP32 Full Companion WiFi self-update requires two application slots; the single-slot profiles require USB. Infrastructure WiFi/LoRa self-update also depends on the exact image's declared capabilities and installed layout. nRF52 updates use the matching application UF2/DFU package or a qualified LoRa route; they do not use the ESP32 browser procedure. See the ESP32 WiFi update and partition guide for supported staged routes and the WiFi guide for role-specific network setup.

"},{"location":"full_usb_logging_validation/","title":"Full usb logging validation","text":"

View this page on MeshCore Docs.

"},{"location":"full_usb_logging_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"gps_tracking/","title":"Gps tracking","text":"

View this page on MeshCore Docs.

"},{"location":"gps_tracking/#gps-tracking","title":"GPS Tracking","text":"

This document describes how GPS telemetry works for companion/client nodes and sensor nodes.

"},{"location":"gps_tracking/#scope","title":"Scope","text":"

GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a new phone app protocol field.

The GPS telemetry value contains latitude, longitude, and altitude. Speed, heading, and freshness are not sent as separate telemetry fields.

If the firmware does not have a valid fresh GPS cache, it omits the GPS field from telemetry. This is intentional: stale or missing fixes are not reported as zero coordinates.

"},{"location":"gps_tracking/#freshness","title":"Freshness","text":"

GPS telemetry is cached separately from advert location.

The cache behavior is:

When a telemetry request asks for location, GPS is normally kept on for 2 hours after the latest location request. During that hold window, later location telemetry requests can use fresh GPS data as soon as valid fixes are available.

If GPS is manually enabled, it stays on and valid fixes continue to update the cache.

If no contact or ACL client can receive location telemetry, the scheduled 2-hour refresh does not run. A real location telemetry request can still turn GPS on, and manual GPS-on still keeps the cache updated.

"},{"location":"gps_tracking/#repeater-nodes","title":"Repeater Nodes","text":"

Repeaters use a shorter on-demand policy when GPS is manually off. An authorized location-telemetry request wakes the receiver only when its cached position is stale. The first valid fix is cached and GPS processing immediately stops; the next request can return that fix without another wake. gps off also cancels an acquisition already in progress. An acquisition that never obtains a fix retains the normal 15-minute safety timeout. Physical supply switching depends on the board and its shared peripheral policy.

When GPS and device power saving are both enabled, a repeater wakes GPS for up to 10 minutes to obtain a valid time and then sleeps it for seven days. A valid GPS time may correct the RTC either forward or backward. With device power saving off, an enabled GPS remains active and refreshes the RTC every 30 minutes.

"},{"location":"gps_tracking/#rak-wisblock-power-supply","title":"RAK WisBlock Power Supply","text":"

RAK4631 defaults to saved peripheral mode gps-only. With no protected power owner, stopping or sleeping GPS can turn off the shared 3V3_S rail. If other attached modules need that rail, explicitly select shared mode:

get peripherals.mode\nset peripherals.mode shared\nreboot\n

Use set peripherals.mode gps-only and reboot to return to the default. Changes are saved for the next boot, not applied live; the query reports the saved choice. Other peripheral requirements are not guessed from an I2C scan. Boot and discovery keep the rail on until the power policy is established.

This saved setting is available in Companion and Common CLI roles (repeater, room server, and sensor). Raw KISS and legacy Terminal Chat do not configure it and retain the powered rail. Shared mode controls the supply; it does not add Ethernet support to a non-Ethernet firmware target.

Ethernet/PoE builds and detected external NOR storage automatically retain power regardless of this setting. RAK3401 always retains the rail for its radio frontend and does not offer this override.

In shared mode, GPS-off does not mean the supply is off. In particular, the RAK12501/L76K module has no independent power switch: stopping MCU parsing leaves the receiver powered. Internal sleep support on other GNSS modules is receiver-dependent. This setting does not remove the UART1 bridge conflict or authorize another transmitter on the GPS wiring.

"},{"location":"gps_tracking/#companionclient-nodes","title":"Companion/Client Nodes","text":"

Companion/client telemetry uses the existing companion telemetry permission system:

Location telemetry is sent only when the requester's effective telemetry permissions include location. Those permissions are derived from the companion telemetry mode settings and contact flags.

No new phone app behavior is required. Existing clients see the existing GPS telemetry field when it is present.

The scheduled GPS cache refresh runs only when at least one stored contact has effective location telemetry access:

"},{"location":"gps_tracking/#sensor-nodes","title":"Sensor Nodes","text":"

Sensor telemetry access is controlled by:

get telemetry.access\nset telemetry.access all\nset telemetry.access acl\n

all is the default and matches the previous sensor telemetry behavior. A request that reaches the normal sensor telemetry request path can receive the requested telemetry fields, including GPS, subject to the request mask and GPS freshness rules.

acl gates telemetry through the sensor ACL. A requester with read-only or higher permissions receives the existing telemetry set, including GPS. Guest or unknown requesters receive no telemetry fields.

The scheduled GPS cache refresh follows the same access setting:

Use the existing ACL command to grant access:

setperm <pubkey> 1\n

Permission values:

"},{"location":"gps_tracking/#advert-location","title":"Advert Location","text":"

Advert location is separate from telemetry GPS. The advert policy is controlled with:

gps advert\ngps advert none\ngps advert share\ngps advert prefs\n

Policies:

prefs is the first-boot default for every repeater, room-server, and sensor build. Firmware updates retain an explicitly saved policy from the existing preferences filesystem.

Telemetry GPS can be fresh while advert location is fixed or disabled, depending on this policy.

"},{"location":"gps_tracking/#recommended-setup","title":"Recommended Setup","text":"

For private sensor tracking:

set telemetry.access acl\nsetperm <owner_pubkey> 3\nsetperm <trusted_pubkey> 1\n

For compatibility with the older sensor behavior:

set telemetry.access all\n

For GPS telemetry behavior, leave the phone app unchanged. The firmware omits GPS when it is stale or missing and sends the existing GPS telemetry field when fresh data is available.

"},{"location":"halo_keymind_settings/","title":"Halo keymind settings","text":"

View this page on MeshCore Docs.

"},{"location":"halo_keymind_settings/#halo-and-keymind-branch-settings","title":"Halo and Keymind Branch Settings","text":"

This file covers only CLI settings and helper commands added by the Halo or Keymind branches. Use CLI commands for the general infrastructure CLI. For MQTT, logging, and former variant controls, use feature switches by role; Full Companion uses different controls for some of these settings. See Repeater Flood Filtering and Moderation for a focused filter setup and troubleshooting guide.

"},{"location":"halo_keymind_settings/#quick-start","title":"Quick Start","text":"
set retry.preset rooftop\nset direct.retry.heard on\nset flood.retry.advert off\nset flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n

Then verify:

get retry.preset\nget direct.retry.heard\nget flood.retry.advert\nget flood.retry.prefixes\nget flood.retry.ignore\n

Use prefixes from the analyzer or neighbors list or get recent.repeater after the repeater has been online for a few hours.

"},{"location":"halo_keymind_settings/#common-examples","title":"Common Examples","text":"

Disable retrying advert packets:

set flood.retry.advert off\nget flood.retry.advert\n

Ignore a repeater as a successful flood retry echo: Use this if you have a car repeater and a house repeater; have the house ignore the car.

set flood.retry.ignore 71CE82,C7618C\nget flood.retry.ignore\n

Only accept specific downstream relays as flood retry success: You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.

set flood.retry.prefixes A58296,860CCA,425E5C\nget flood.retry.prefixes\n

Bridge two groups of repeaters:

set flood.retry.bridge on\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 BEEBB0,425E5C\nget flood.retry.bucket.1\nget flood.retry.bucket.2\n

Return to simple non-bridge flood retry:

set flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n
"},{"location":"halo_keymind_settings/#added-settings","title":"Added Settings","text":"Setting What it does How to use Example telemetry.temp, telemetry.volt, telemetry.volt.i2c, telemetry.gps Repeater and room builds record 30-minute MCU temperature and battery samples for seven days. Detected INA voltage channels retain four days at 0.02 V resolution through 655.34 V; all-zero channels are omitted as disconnected. GPS-capable builds retain GPS separately. Repeater telemetry.tx sends temperature, battery, and per-channel I2C RAW_CUSTOM packets over the shared data.tx path on a persistent 1-30 day schedule or immediately for testing; automatic transmission is off by default and its retained cadence starts at two days. data.tx defaults to zero-hop direct plus automatic region selection without enabling any producer. GPS is never included. History resets on reboot. get/set data.tx path|region, get telemetry.temp [page], get telemetry.volt [page], get telemetry.volt.i2c [channel [page]], get telemetry.gps [page], set telemetry.gps <1-30>, get telemetry.tx, set telemetry.tx schedule <off|1-30d>, send telemetry.tx now get data.tx battery.alert Sends opt-in, region-scoped low-battery warnings to #repeaters after 30 minutes of uptime. get battery.alert, get battery.alert.region, set battery.alert on [region], set battery.alert off set battery.alert on sea battery.alert.low Warning threshold percentage. Must be greater than battery.alert.critical. get battery.alert.low, set battery.alert.low <1-100> set battery.alert.low 20 battery.alert.critical Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. get battery.alert.critical, set battery.alert.critical <0-99> set battery.alert.critical 10 recent.repeater Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep. get recent.repeater [page], get recent.repeaters search <2|4|6 hex> [page], set recent.repeater <prefix> <snr_db>, clear recent.repeater get recent.repeaters search A1B2 flood.channel.data Compatibility view for a visible FPF7 type=grp_data drop row on generalized repeaters. off creates the row and on removes it; compact FPF6 builds retain the legacy gate. Use an authenticated channel= row when only selected channels should be blocked. get flood.channel.data, set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Sets the compatibility row's hop range: all maps to hops=all; 1-7 repeats through that hop count and maps to hops=N+1+. get flood.channel.data.hops, set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.scope FPF7 rewrite-phase rows that add a transport scope to received unscoped floods or replace the scope of already-scoped floods. A bare target uses an existing flood-allowed region; scope=<name> derives a public hashtag target directly without creating a region, exactly like flood.filter scope=. By default, a changed packet bypasses inbound rxdelay and is forwarded at the highest outbound queue priority with zero initial txdelay, so the selected scope can win at the next hop. tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), retains normal queue priority, and forces the maximum txdelay factor of 2.0; its actual randomized transmit delay ranges from zero through ten packet airtimes. path=blacklist and path=bucket:<1-6> make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat txt:*; path-qualified rows beat the ordinary channel fallback. login:* covers the remote-login family, and other:* covers every remaining flood type, including flood-form TRACE and OTA. Direct traceroute remains outside the flood table. Generalized builds commit these rows with the forward phase and blacklist; compact FPF6 builds retain separate storage. ACL permission 4 can manage the table. get flood.channel.scope[.n], set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow], del flood.channel.scope.<n>|all set flood.channel.scope #rgdata scope=BlackHole86 flood.channel.scope.require Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated GRP_TXT/GRP_DATA channels must arrive already scoped to a locally allowed region; unscoped, unknown, or denied incoming scopes are dropped before any rewrite can rescue them. Unlisted group channels bypass only the region gate and retain all other forwarding controls. An empty table preserves global region behavior; non-channel payloads are unchanged. ACL permission 4 can manage the table. get flood.channel.scope.require[.n], set flood.channel.scope.require[.n] <public|#channel|key>, del flood.channel.scope.require.<n>|all set flood.channel.scope.require #bot flood.filter Persistent flood-route rules selected by payload type and optional hop range. Generalized repeaters have 63 FPF7 forward slots; FULL ESP32 room servers have 31. Repeaters store their scope-rewrite phase, shared unordered blacklist, and channel-data compatibility state in the same atomic FPF7 image. path=blacklist is intended for forwarding abuse containment, including bulk internet-to-mesh dumping, but truncated path IDs are not authenticated identities. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and separate blacklist storage. New generalized repeater tables seed slot 1 with ota all suspend=tempradio slot 2 with an authenticated #wardriving hops=5+ radio drop, and slot 3 with #wardriving hops=all mode=bridge,cross drop. mode=radio is the default; bridge, cross, and bridge,cross select separate transport admission paths, including direct packets. Local receive/logging is unchanged. See the mode guide. get flood.filter[.n], set flood.filter[.n] <type> [N|N+|N-M|all] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio], del flood.filter.<n>|all; repeater only: get/set/del flood.filter.blacklist[.n] set flood.filter grp_txt all scope=local tx=slow flood.rule Live alias for extended flood.filter on rule-engine repeaters and FULL ESP32 room servers. A row can AND packet type, hop range, optional channel match, ordered 1/2/3-byte pbyte source prefix, and original scope/region conditions, then drop, rewrite to a direct scope or configured region, enforce a per-row rate, allow flood retry, and/or stop lower-priority FPF7 rules. channel=* means no channel condition; public, #name, and keys authenticate; hash:XX is an unauthenticated one-byte fallback with collision/spoof risk. With no retry rows, global retry behavior is unchanged; once any exists, matching retry rows allow-list received floods while the global retry gates and flood.retry.bridge algorithm still apply. scope=BlackHole86 directly derives a regionless sink scope; region=BlackHole86 would require a configured flood-allowed region. All rows match the original receive-time packet. Higher numeric priority runs first; equal values automatically order authenticated channels before raw hashes before channel=*, then use lower slot as the final tie-break. Repeated rows with one channel key share a per-packet authentication result. Persistent FPF7 stores canonical region names, so region ID reorder or reuse cannot retarget a rule. A missing saved region makes its region= rewrite and paired stop inert until the name returns, allowing lower safety rows to run. Fixed 240 KB STM32WL profiles keep FPF6 and do not expose this alias; partition sizes are unchanged. get flood.rule[.n], set flood.rule[.n] type=<type> [hops=...] [channel=...] [prefix=...] [in=...] <drop|scope=...|region=...|rate=N/min|retry|stop> [priority=0-255], del flood.rule.<n>|all set flood.rule.2 type=any channel=#hamradio retry flood.moderation Decrypts keyed GRP_TXT channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports public, #channel, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. get flood.moderation[.n], set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...], del flood.moderation.<n>|all set flood.moderation public \"Noisy User\" rate=5/min hops=4 clock.sync.mesh Defaults on for all repeater, sensor, and room-server builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result. clock.sync.mesh now queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its .table and .1 through .16 forms inspect collected samples. get clock.sync.mesh, set clock.sync.mesh <on|off>, clock.sync.mesh now, get clock.sync.status[.table|.1-.16] set clock.sync.mesh on clock.sync.mesh.edge Defaults on so edge infrastructure nodes can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so disabled forwarding and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples. get clock.sync.mesh.edge, set clock.sync.mesh.edge <on|off> set clock.sync.mesh.edge on clock.sync.internet Adds a read-only internet/NTP estimate at the same initial and seven-day checks on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. get clock.sync.internet, set clock.sync.internet <on|off> set clock.sync.internet on clock.sync.drift Absolute correction threshold in seconds; the default is 600 seconds (10 minutes). The clock is moved forward or backward only when the estimate differs by more than this value. get clock.sync.drift, set clock.sync.drift <30-86400> set clock.sync.drift 600 clock.sync.samples Minimum fresh evidence count before mesh time can be used: distinct receive paths in normal mode or distinct advert keys/Public-channel display names in edge mode. A strict majority of all fresh samples is also required. Range 3-16; default 9. get clock.sync.samples, set clock.sync.samples <3-16> set clock.sync.samples 9 outpath Overrides the primary direct route used for replies to the current remote client. get outpath, set outpath <hops>, set outpath direct, set outpath clear, set outpath flood set outpath A1B2C3,D4E5F6 altpath Adds a secondary direct route for repeater replies to the current remote client. get altpath, set altpath <hops>, set altpath direct, set altpath clear, set altpath flood set altpath 71CE82,BA09F0

The generalized filter table also accepts fr as a short alias for flood.rule: set fr.3 any c=#wardriving m=bc d is the full third built-in rule. Modes can be m=r (radio), m=b (bridge), m=c (crossover), or m=bc (both transport paths). get fr.N returns a short setter ready to copy; del fr.N deletes that row. Full names and short tokens can be mixed, with the same validation and permissions. See the shorthand reference.

"},{"location":"halo_keymind_settings/#other-keymind-commands","title":"Other Keymind Commands","text":"Command What it does How to use Example send text.flood Sends a #repeaters flood text message formatted as <node_name>: <message>, with : in the node name sent as ;. send text.flood <message> send text.flood checking ridge link"},{"location":"halo_keymind_settings/#battery-alerts","title":"Battery Alerts","text":"

Battery alerts are off by default. Enabling requires a named region. With no region argument, the repeater selects the single deepest (most narrow) region in the hierarchy; if multiple regions tie, the command asks for an explicit region. For example, after region def west pnw wa w-wa sea, set battery.alert on selects sea, while set battery.alert on w-wa overrides the default. Alerts are never sent as unscoped floods, and removing the selected region stops alerts until a valid scope is selected again.

The repeater suppresses alerts for its first 30 minutes of uptime. It then checks every 30 minutes and sends a flood text warning to #repeaters when voltage is above 1 V and the estimated battery percent is below battery.alert.low.

Warnings and critical alerts both use a 12-hour resend cooldown, beginning only after the radio reports that the alert transmission completed.

Defaults:

Setting Default battery.alert off battery.alert.region <unset> battery.alert.low 20 battery.alert.critical 10

Example:

set battery.alert.low 20\nset battery.alert.critical 10\nset battery.alert on\nget battery.alert\nget battery.alert.region\n

CPU power saving remains compatible with the check. The battery timer never requests a wake earlier than its 30-minute deadline; when the normal loop is already awake after that deadline, no additional wake is needed. Sleeping time counts toward the startup delay, and an outbound warning prevents another sleep until the queued packet has been handled. This is separate from RX duty-cycle power saving, which only cycles the LoRa receiver and does not stop the main loop's battery timer.

"},{"location":"halo_keymind_settings/#recent-repeater-table","title":"Recent Repeater Table","text":"

Direct retry uses the recent repeater table when direct.retry.heard is on. Bridge buckets also use this table: a configured bucket prefix is active only when it was heard within the last hour.

Show learned rows:

get recent.repeater\nget recent.repeater 2\nget recent.repeaters 2\nget recent.repeater page 3\nget recent.repeaters search A1\nget recent.repeaters search A1B2 page 2\n

Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row shows its stored SNR and a whole s, m, or h age measured from the most recent recording. Search pages contain up to six rows.

Seed or correct a prefix:

set recent.repeater A1B2C3 8.5\n

Clear learned and manually seeded rows:

clear recent.repeater\n

Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by 0.25 dB. Unfiltered paged replies contain up to 10 rows; an unpaged local serial query prints the full table.

"},{"location":"halo_keymind_settings/#direct-path-overrides","title":"Direct Path Overrides","text":"

outpath and altpath apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.

Room Server also supports these commands for readers and writers, restricted to the caller's own routes. See room return routes for session persistence, alternate chat delivery and client requirements.

Set paths with comma-separated hop hashes. Each hop must be 2, 4, or 6 hex characters, and all hops in one path must use the same width. Hex input is case-insensitive. Replies use uppercase hex and retain the commas, so the value can be copied directly into another set outpath or set altpath command.

get outpath\nget outpath path\nset outpath A1B2C3,D4E5F6\nset outpath path\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath clear\n

For example, both set altpath 600000,0d2784,f8dada and Set altpath 600000,0d2784,f8dada store the same path and reply with:

> 600000,0D2784,F8DADA\n

The first CLI word is case-insensitive (set, Set, and SET are the same, as are get, Get, and the other command verbs). Argument case is preserved.

get outpath path reports the reciprocal path received after the latest flood login without changing outpath. That packet arrives asynchronously, so retry shortly if the first query reports > path pending; the observation window expires after one minute. The login path is not automatically selected. set outpath path copies the observed route into outpath and saves it; it leaves the current route unchanged when no observed route is available.

set outpath direct sets a zero-hop direct route for a client reachable without repeaters. set outpath clear forgets the override, replies > outpath cleared, and lets normal path discovery fill it again. set outpath flood forces replies to use flood packets until the client logs in again.

When outpath is a valid direct path and altpath is also a valid, different direct path, repeater DM replies send two packets: one on outpath and one on altpath. The secondary altpath copy does not create its own direct-retry state, so retry tracking stays attached to the primary outpath packet. altpath clear disables the secondary direct reply. altpath flood is accepted for command symmetry, but it does not create a second flood reply; only a valid direct altpath sends the second packet.

"},{"location":"halo_keymind_settings/#direct-retry-settings","title":"Direct Retry Settings","text":"

Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. Repeaters expose the settings below; non-repeater firmware uses the same packet-type timing rules with fixed shared base/step timing.

Setting What it does How to use Example retry.preset Applies shared direct and flood retry defaults. Values: infra, rooftop, mobile or 0, 1, 2. get retry.preset, set retry.preset <value> set retry.preset rooftop direct.retry.heard Uses the recent repeater table as the direct retry eligibility gate. get direct.retry.heard, set direct.retry.heard on/off set direct.retry.heard on direct.retry.margin SNR margin in dB above the SF-specific receive floor. get direct.retry.margin, set direct.retry.margin <0-40> set direct.retry.margin 5 direct.retry.count Maximum direct retry attempts after initial TX. Direct-routed type 2 text packets always use 21 attempts regardless of this setting or the short-path cap. get direct.retry.count, set direct.retry.count <1-15> set direct.retry.count 15 direct.retry.base Base wait in milliseconds before retry; packet-length add-on is 3x for TRACE and ANON_REQ/type 7, 7x for TXT_MSG/type 2, and 6x for other direct retry packets. get direct.retry.base, set direct.retry.base <10-5000> set direct.retry.base 175 direct.retry.step Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter. get direct.retry.step, set direct.retry.step <0-5000> set direct.retry.step 100 direct.retry.cr Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use CR4, CR5, CR7, or CR8, then escalate by attempt: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters start at the current radio CR and follow the same escalation pattern, clamped at CR8. get direct.retry.cr, set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>, set direct.retry.cr off set direct.retry.cr 10.0,7.5,2.5,0

The default adaptive coding-rate profile is 10.0,7.5,2.5,2.5. SNR 10.0 dB and up uses CR4, 7.5 dB and up uses CR5, 2.5 dB and down uses CR8, and the middle band uses CR7. If no recent repeater table entry is available, retry packets use CR5. Use set direct.retry.cr off to disable adaptive coding-rate overrides. Repeater attempts escalate from the adaptive starting CR: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters use the current radio CR as the first retry CR and follow the same pattern up to CR8.

Preset details:

Preset Base Count Step SNR gate infra 275 ms 4 150 ms SF floor + 15 dB rooftop 175 ms 15 100 ms SF floor + 5 dB mobile 175 ms 15 50 ms SF floor

Example for a quiet fixed repeater:

set retry.preset rooftop\nset direct.retry.heard on\nset direct.retry.margin 5\n

Example for a moving or weak-link node:

set retry.preset mobile\nset direct.retry.margin 0\n
"},{"location":"halo_keymind_settings/#flood-and-advert-settings","title":"Flood And Advert Settings","text":"

Flood retry applies to flood-routed packets. A queued retry is canceled when the same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses the bucket rules below instead.

Setting What it does How to use Example flood.retry.count Base flood retry attempts after initial TX. Role path scaling happens first, then all builds apply payload caps: REQ is 0; GRP_TXT keeps up to 15; remote-login RESPONSE, TXT_MSG, ANON_REQ, and PATH keep up to 15 at path count 0 and cap at 2 in transit; every other flood type caps at 1. A lower calculated value is preserved, and 0 disables flood retry. get flood.retry.count, set flood.retry.count <0-15> set flood.retry.count 7 flood.retry.path Maximum path hash count eligible for flood retry, or off to disable the gate. get flood.retry.path, set flood.retry.path <0-63/off> set flood.retry.path 1 flood.retry.group.path Additional path gate for group data (type=6) flood retries. The stricter of this and flood.retry.path applies; off disables only this additional gate. Setting the general path gate to 0 forces this setting to off; a named preset restores the default of 1. get flood.retry.group.path, set flood.retry.group.path <0-63/off> set flood.retry.group.path 1 flood.retry.advert Allows or blocks retry for node advert packets (type=4). Default is off. get flood.retry.advert, set flood.retry.advert on/off set flood.retry.advert off flood.retry.prefixes Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry. get flood.retry.prefixes, set flood.retry.prefixes <prefixes/none/off> set flood.retry.prefixes BEEBB0,425E5C flood.retry.ignore Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. get flood.retry.ignore, set flood.retry.ignore <prefixes/none/off> set flood.retry.ignore 71CE82,C7618C flood.retry.bridge Enables bucket-based bridge retry logic. get flood.retry.bridge, set flood.retry.bridge on/off set flood.retry.bridge on flood.retry.bucket.<n> Shows one bridge bucket. Buckets are numbered 1-6. get flood.retry.bucket.<n> get flood.retry.bucket.1 flood.retry.bucket Sets bridge bucket prefixes. set flood.retry.bucket <1-6> <prefixes/none/off> set flood.retry.bucket 1 71CE82,C7618C

Extended flood.rule rows can select which received floods may use these settings. For example, enable bridge completion only for Public and #hamradio with set flood.rule type=any channel=public retry and a second row using channel=#hamradio. Adding the first active retry row changes received retry selection into an allow-list; deleting the last restores the legacy global selection. channel=hash:XX is available when only the visible one-byte hash is known, but it is not authenticated and is collision-prone. For example, hash:11 includes Public and any other packet using 0x11; channel=public alone performs the Public-key MAC/decrypt check.

Bridge buckets may also be reused as passive flood.channel.scope path selectors with path=bucket:<1-6>. This does not require flood.retry.bridge to be enabled and does not depend on recent-repeater freshness or the retry ignore list.

Forwarded neighbor adverts also use an automatic echo guard in every build. If this node hears a downstream, longer-path echo after transmitting an advert whose signed timestamp is less than six hours old, it will not forward that exact advert again during the six-hour age window. This guard works even when flood.retry.advert is off and does not affect self-originated adverts.

Self-originated advert retries are deliberately slow: the first retry waits at least one extra minute beyond the normal airtime-aware delay. Queueing a newer self advert retires queued and future retry attempts for older self adverts without disturbing other flood retry sequences. Companion firmware allows the one slow retry for its own adverts but not for neighbor adverts it relays.

The shared retry preset sets these flood defaults:

Preset Retry count Path gate Group-data path gate infra 1 1 1 rooftop 3 2 1 mobile 15 1 1

Hop-zero flood retries use the direct-retry CR ladder starting at the active radio CR. With CR5, infra allows CR5, CR7; rooftop allows CR5, CR7, CR7, CR8, CR8, CR8; mobile allows CR5, CR7, CR7, then CR8 through retry 15. These lists exclude the initial send and remain subject to payload caps and echo cancellation. Floods with recorded hops keep the active CR. flood.retry.count 0 still disables every automatic flood retry, including hop zero, without changing direct-retry settings or their CR schedule. See hop-zero flood retry coding rate for every starting CR and the payload limits.

Example for path-gated retry:

set retry.preset rooftop\nset flood.retry.path 1\nset flood.retry.group.path 1\nset flood.retry.advert off\nset flood.retry.ignore 71CE82,C7618C\n
"},{"location":"halo_keymind_settings/#north-south-buckets","title":"North South Buckets","text":"

Buckets describe groups of repeaters on different sides of this relay. Bucket numbers do not have built-in meanings; this example uses bucket 1 for North and bucket 2 for South.

              North bucket 1\n        +-----------------------+\n        | A1B2C3       D4E5F6   |\n        | North A      North B  |\n        +-----------+-----------+\n                    |\n                    v\n              +-----------+\n              | This node |\n              +-----------+\n                    ^\n                    |\n        +-----------+-----------+\n        | 71CE82       C7618C   |\n        | South A      South B  |\n        +-----------------------+\n              South bucket 2\n

Configure the buckets:

set flood.retry.bridge on\nset flood.retry.bucket 1 A1B2C3,D4E5F6\nset flood.retry.bucket 2 71CE82,C7618C\nset flood.retry.ignore none\n

Packet heard from the North:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | North bucket | -----> South bucket\n  | bucket 1     | -----> Other fresh/unbucketed relays\n  +--------------+\n

Packet heard from the South:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | South bucket | -----> North bucket\n  | bucket 2     | -----> Other fresh/unbucketed relays\n  +--------------+\n

Packet heard from an unbucketed or pathless source:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | Other bucket | -----> North bucket\n  | implicit     | -----> South bucket\n  +--------------+\n

Bridge retry stays eligible until every target bucket has been heard or flood.retry.count is exhausted. A configured bucket is a target only when at least one of its prefixes is fresh in recent.repeater. Prefixes in flood.retry.ignore never count as bucket hits.

Configuration reports a warning when prefixes in different buckets, including bucket 7 (flood.retry.prefixes), share the same first byte. A 1-byte path cannot distinguish those buckets. Bridge mode therefore excludes every matching bucket when that short prefix is the source, and credits every matching target bucket when it is heard as an echo. This prevents an ambiguous short prefix from keeping an impossible target outstanding through every retry.

Each flood retry wait retains the fixed maximum-frame plus 20 packet-airtime delay, then adds random jitter from zero to 200 percent of one additional packet airtime. This keeps nearby repeaters from repeating a collision on fixed timing while capping the added wait at two frames.

Only one enhanced retry sequence can be active for the same logical flood packet. Identical floods still receive their normal transmission, but do not multiply the extra attempts. Evicted queued retries release their bridge state, and the final echo wait does not reserve a packet-pool entry.

Earlier path hops from a successful bridge echo refresh a separate per-bucket reachability cache without an SNR value. Only the final hop, which actually sent the received RF frame, updates recent.repeater and its SNR. Indirect path hops therefore cannot change direct-retry SNR gating or coding-rate selection.

"},{"location":"halo_keymind_settings/#troubleshooting","title":"Troubleshooting","text":"

If advert packets are still retrying:

get flood.retry.advert\nset flood.retry.advert off\n

If ignored prefixes still appear in flood retry good logs:

get flood.retry.ignore\nset flood.retry.ignore <prefix>\n

The ignored prefix must match the last hop shown as heard=<prefix>. For example, this log needs C7618C in the ignore list:

flood retry good (... path=7773D0>C7618C, heard=C7618C ...)\n

If retries are too aggressive:

set flood.retry.count 1\nset flood.retry.path 1\nset direct.retry.count 4\n

If retries are too sparse:

set flood.retry.count 7\nset flood.retry.path 2\n
"},{"location":"hardware_validation_bluetooth_stealth_2026-09-07/","title":"Hardware validation bluetooth stealth 2026 09 07","text":"

View this page on MeshCore Docs.

"},{"location":"hardware_validation_bluetooth_stealth_2026-09-07/#report-moved","title":"Report moved","text":"

This report is now in Test Results.

"},{"location":"hardware_validation_checklist/","title":"Hardware validation checklist","text":"

View this page on MeshCore Docs.

"},{"location":"hardware_validation_checklist/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"heltec_t114/","title":"Heltec t114","text":"

View this page on MeshCore Docs.

"},{"location":"heltec_t114/#heltec-t114-screen-detection","title":"Heltec T114 screen detection","text":"

Choose Heltec T114 and your firmware role in the firmware picker. New T114 images with automatic screen detection work on boards with or without the optional stock 1.14-inch screen. There is no separate screen selection. For Companion, use the Full image for USB and Bluetooth support together. The KISS modem role remains headless.

The picker currently supplies 1.18.0.1 preview1 for T114. Automatic screen detection is the default for future T114 builds. The preview includes Full Companion and Full/Reduced optional-sensor profiles for Repeater, Room Server and Sensor, plus the headless KISS utility.

At startup, the firmware checks whether the screen responds before starting the display. The backlight stays off during the check. If no screen responds, the display stays powered off and the node continues without a screen. The selected role's supported connections and radio functions continue to work normally.

A detected screen uses the existing display settings and button wake behavior. Detection does not reset saved screen timeouts, power-saving preferences, identity, or radio settings. To add or remove the screen, power the board off first, then restart it after changing the hardware.

"},{"location":"heltec_t114/#updating-an-older-t114","title":"Updating an older T114","text":"

Use USB when moving from an older Heltec_t114_without_display firmware target to the canonical Heltec_t114 target. The old and new target names have different LoRa update identities; a package built for one is not an update for the other. Follow the normal update procedure and preserve your settings; screen detection does not require a factory reset or a new bootloader.

Previously published firmware remains unchanged. The picker identifies automatic detection from the selected release's verified firmware metadata; an old display-enabled image does not gain detection from its filename.

See the implementation and validation record for the startup checks and hardware-testing boundary.

"},{"location":"heltec_v4_wifi_partition_migration/","title":"Heltec v4 wifi partition migration","text":"

View this page on MeshCore Docs.

"},{"location":"heltec_v4_wifi_partition_migration/#heltec-v4-legacy-partition-migration-over-wi-fi","title":"Heltec V4 legacy-partition migration over Wi-Fi","text":"

Heltec V4/V4.3 OLED repeaters with their normal 16 MiB flash use the shared ESP32 partition migration bridge. It changes Arduino's old default.csv layout, with two 1.25 MiB OTA slots, to default_16MB.csv, with two 6.25 MiB OTA slots.

Build the complete V4 package with the repeatable ESP32 recipe in WSL/Linux:

python3 -B scripts/build_esp32_partition_migration.py \\\n  --board heltec-v4 --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

The ZIP contains the Wi-Fi bridge, LoRa bridge, and exact-target Full image. Upload its wifi-bridge.bin through the legacy browser Wi-Fi updater. Rejoin MeshCore-Migrate using the node's saved admin password as the Wi-Fi password (see the shared credential and fallback rules); when it reports Expanded layout ready, upload the ZIP's full-application.bin at /update.

Do not use an -merged.bin in either browser uploader. Merged images contain bootloader and table offsets for cable flashing. The bridge preserves the private identity and supported saved configuration, including node name, radio profiles, ACL and login replay state. Files outside the supported preservation set may need to be recreated after the Full image starts.

For an existing heltec_v4_repeater that already supports LoRa mOTA, use the ZIP's lora-bridge.mota with the identity-only LoRa handoff described in the shared guide. It preserves the old repeater across the partition change so the Full image can also be installed over LoRa without reaching the board's Wi-Fi network. That route does not restore saved settings; its post-expansion Wi-Fi recovery AP normally uses the published fallback. The optional Partition Expander route instead restores supported settings and retains the saved admin password.

"},{"location":"home_text_spacing/","title":"Home text spacing","text":"

View this page on MeshCore Docs.

"},{"location":"home_text_spacing/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"host_cli_service/","title":"Host cli service","text":"

View this page on MeshCore Docs.

"},{"location":"host_cli_service/#lora-cli-host-service","title":"LoRa CLI Host Service","text":"

The repeater host <text> command lets an authenticated LoRa administrator request a small, explicitly allowed operation from a USB-connected computer. The included Raspberry Pi endpoint supports these exact requests:

cmd host help\ncmd host cpu-temp\ncmd host hostname\ncmd host uptime\ncmd host load\ncmd host memory\ncmd host disk-free\ncmd host clock status\ncmd host clock sync\ncmd host clock set <unix_epoch>\ncmd host network restart\ncmd host reboot\ncmd host action status <operation_id>\ncmd host run <alias> [arguments]\n

The first seven actions after help are read-only. Clock changes, network restart, and reboot are opt-in actions and are disabled unless explicitly enabled. Text such as reboot now, cpu-temp; reboot, and embedded newlines is not a command: the endpoint accepts only an exact allowlist match.

The bridge is included in normal repeater firmware except on Wio-E5. Its specialized RS232 bridge image omits the host service because the combined application exceeds the fixed 240 KiB partition; use the normal Wio-E5 repeater image when the USB/MQTT host service is needed.

"},{"location":"host_cli_service/#run-alongside-meshcoretomqtt","title":"Run alongside meshcoretomqtt","text":"

meshcoretomqtt must remain the only process that opens the repeater USB serial port. The endpoint communicates with it through the same MQTT broker, which can be local to the Raspberry Pi:

LoRa -> repeater -> USB -> meshcoretomqtt -> MQTT -> host endpoint\nLoRa <- repeater <- USB <- meshcoretomqtt <- MQTT <- host endpoint\n

Clock-recovery deployments must run the loopback broker, meshcoretomqtt, and endpoint on the same Pi. A wrong Pi clock can prevent TLS validation against a remote broker, while split host clocks can reject the signed live claim before the correction arrives. Remote brokers remain suitable for ordinary commands when the connection and both clocks are already healthy.

Launch meshcoretomqtt with its existing arguments plus --debug; its current debug-topic parser needs that flag to publish the repeater request record. Do not open the serial TTY from the host endpoint too.

This feature does not compare the repeater wall clock with the Pi clock. Minutes of drift, an unset repeater clock, and later clock corrections are safe: request freshness comes from a live one-time challenge. The normal meshcoretomqtt sync_time setting may remain enabled for its other uses, but host-command authorization does not depend on it.

Generate a dedicated service key using the Python environment installed by meshcoretomqtt:

sudo /opt/mctomqtt/venv/bin/python3 host_cli_service.py \\\n  --generate-key /etc/mctomqtt/host-cli-key.json\nsudo chown mctomqtt:mctomqtt /etc/mctomqtt/host-cli-key.json\nsudo chmod 600 /etc/mctomqtt/host-cli-key.json\n

Add the printed public key to the existing meshcoretomqtt configuration:

[remote_serial]\nenabled = true\nallowed_companions = [\n  \"SERVICE_PUBLIC_KEY_PRINTED_ABOVE\"\n]\nnonce_ttl = 120\ncommand_timeout = 10\n

Restart meshcoretomqtt, then run the endpoint. Replace USA with the exact three-character IATA namespace configured in the broker and supply the repeater's complete 64-character public key:

sudo -u mctomqtt /opt/mctomqtt/venv/bin/python3 host_cli_service.py \\\n  --broker 127.0.0.1 \\\n  --iata USA \\\n  --repeater-key REPEATER_PUBLIC_KEY \\\n  --service-key /etc/mctomqtt/host-cli-key.json\n

Add --username NAME --password-file FILE for broker authentication. Add --tls, and optionally --ca-cert FILE, for TLS. Custom installations can use --request-topic and --command-topic to match their broker namespace. The endpoint source and its systemd/configuration details are in examples/host_cli_service.

"},{"location":"host_cli_service/#allowlisted-programs-and-arguments","title":"Allowlisted programs and arguments","text":"

Add --programs-file FILE to expose locally selected programs as host run <alias> [arguments]. The example JSON file fixes each executable, fixed leading arguments, maximum 1-5 second runtime, and the exact remote argument schema. For example:

cmd host run fan on 15\n

The example maps that request to the fixed process argument vector:

/usr/local/bin/mesh-fan-control --source lora on 15\n

The supported argument rules are an explicit non-option choice, a bounded nonnegative integer, or a short token using a restricted ASCII character set. Alias lookup and every argument validation happen before process creation. Execution uses an absolute configured path, no stdin, shell=False, a minimal environment, and / as its working directory. Extra arguments, leading-option injection, shell syntax, control characters, and invalid quoting are rejected. The allowlist file and executable must not be group- or world-writable.

Copy and edit programs.example.json, then give the service account only the operating-system permissions needed by those trusted programs.

"},{"location":"host_cli_service/#opt-in-network-and-reboot-recovery","title":"Opt-in network and reboot recovery","text":"

These actions use their own socket-activated root broker, independently of the clock-control broker. Installing or enabling clock control does not grant host recovery actions. Install the following as root-owned files:

sudo install -o root -g root -m 0755 meshcore_host_actions.py \\\n  /usr/local/sbin/meshcore-host-actions\nsudo install -o root -g root -m 0644 meshcore-host-actions.socket \\\n  /etc/systemd/system/meshcore-host-actions.socket\nsudo install -o root -g root -m 0644 meshcore-host-actions.service \\\n  /etc/systemd/system/meshcore-host-actions.service\nsudo install -o root -g root -m 0644 meshcore-networkmanager-restart.service \\\n  /etc/systemd/system/meshcore-networkmanager-restart.service\nsudo install -o root -g root -m 0644 meshcore-host-reboot.timer \\\n  /etc/systemd/system/meshcore-host-reboot.timer\nsudo install -o root -g root -m 0644 meshcore-host-reboot.service \\\n  /etc/systemd/system/meshcore-host-reboot.service\n

The root-owned broker policy enables nothing by default. Create a systemd drop-in and select network-restart, reboot, or both as an exact comma-separated list:

sudo systemctl edit meshcore-host-actions.service\n
[Service]\nEnvironment=MESHCORE_HOST_ACTIONS=network-restart,reboot\n

Reload systemd and enable only the broker socket:

sudo systemctl daemon-reload\nsudo systemctl enable --now meshcore-host-actions.socket\n

After changing the policy on an already active installation, restart meshcore-host-actions.service so the broker reads the new value.

Finally add --allow-network-restart, --allow-reboot, or both to the unprivileged endpoint. An action must pass both gates: the endpoint flag and the root-owned broker policy. The endpoint never invokes sudo; remove legacy wifi-restart program aliases and host-action sudoers entries because sudo aliases are not a fallback for this hardened service. Use the exact host network restart action instead.

Systemd creates /run/meshcore-host-actions.sock as root:mctomqtt mode 0660. The root broker authenticates both the endpoint UID and primary GID with SO_PEERCRED, while the endpoint authenticates the root-created listener. The protocol is one bounded ASCII line. Neither the caller nor request text can choose an executable, unit, path, or argument. The broker can start only the fixed static NetworkManager restart service or the fixed reboot timer; the broker and action units have empty capability sets and systemd sandboxing. The configured mctomqtt UID and its effective primary GID are the delegated local trust boundary; supplementary group membership alone is rejected.

Side effects follow a fail-closed two-phase sequence. The endpoint reserves a canonical 128-bit operation ID with PREPARE, publishes its reply with MQTT QoS 1, and requires wait_for_publish plus is_published confirmation before sending one COMMIT. It does not block the Paho network callback while waiting. No confirmation means no commit. Reboot uses a fixed approximately 10-second timer that starts only after commit. The MQTT confirmation proves acceptance by the local broker, not delivery over USB and LoRa; there is no correlated serial delivery acknowledgement, so the physical reply is best effort.

network restart deliberately drops Wi-Fi and Tailscale management while NetworkManager restarts. Use a loopback MQTT broker and keep the broker, meshcoretomqtt, host endpoint, and USB device services independent of NetworkManager: they must not have Requires=, BindsTo=, or ordering dependencies on it. This preserves the local reply/commit path while remote management temporarily disappears.

The operation ID is derived from the authenticated repeater key, request ID, and nonce. The reply displays it, and its state can be queried later:

cmd host action status 0123456789ABCDEF0123456789ABCDEF\n

The root broker retains at most 64 records for the current boot. It persists prepared before replying and in-progress before scheduling, schedules a committed operation at most once, and never automatically retries an ambiguous outcome. A broker restart converts recovered in-progress to ambiguous. Only an old uncommitted prepared record may be evicted; otherwise a full store rejects new work. Reusing an operation ID for a different action is rejected. scheduled means PID 1 accepted the fixed unit job; it does not claim that the subsequent network restart or reboot completed successfully.

A distinct reboot is rejected while another reboot is committed or ambiguous, so a second request cannot falsely promise a fresh 10-second timer while the first timer is already running. A new reboot reservation may supersede only an older, uncommitted reboot reservation; the superseded operation can no longer be committed.

"},{"location":"host_cli_service/#opt-in-clock-recovery","title":"Opt-in clock recovery","text":"

clock status reports the Pi epoch and NTP synchronization state without root access. To enable the exact clock sync and clock set <unix_epoch> actions, install meshcore_clock_control.py as /usr/local/sbin/meshcore-clock-control, owned by root and mode 0755, then install the accompanying meshcore-clock-control.socket and meshcore-clock-control.service and static meshcore-chrony-step.service files in /etc/systemd/system as root-owned mode-0644 files:

sudo install -o root -g root -m 0755 meshcore_clock_control.py \\\n  /usr/local/sbin/meshcore-clock-control\nsudo install -o root -g root -m 0644 meshcore-clock-control.socket \\\n  /etc/systemd/system/meshcore-clock-control.socket\nsudo install -o root -g root -m 0644 meshcore-clock-control.service \\\n  /etc/systemd/system/meshcore-clock-control.service\nsudo install -o root -g root -m 0644 meshcore-chrony-step.service \\\n  /etc/systemd/system/meshcore-chrony-step.service\nsudo systemctl daemon-reload\nsudo systemctl enable --now meshcore-clock-control.socket\n

The socket is fixed at /run/meshcore-clock-control.sock; systemd creates it as root:mctomqtt with mode 0660. Run the endpoint as user and primary group mctomqtt, then add --allow-clock-control. The endpoint validates the socket metadata and authenticates the connected server as root with Linux SO_PEERCRED. The root service independently requires the peer's UID and primary GID to match mctomqtt:mctomqtt before it reads or executes a request. No clock-control sudo rule is used; remove old clock-helper sudoers lines when upgrading.

Both layers require one canonical unsigned decimal argument with no sign, leading zero, whitespace, or trailing text. The accepted epoch range is 2020 through 2099. The private protocol accepts only the complete bounded ASCII lines sync and set <epoch>. Every child process uses a fixed absolute argv, shell=False, and a timeout no longer than 1.5 seconds.

clock set changes CLOCK_REALTIME without disabling NTP, then requests an NTP step. If the NTP request fails, the response honestly reports that the clock changed and which synchronization step remains incomplete. If clock sync enables NTP but the explicit step/restart fails, that partial outcome is also reported. When chrony is available, the root broker starts only the fixed static meshcore-chrony-step.service. That hardened one-shot runs /usr/bin/chronyc -a makestep as _chrony:_chrony, with no capabilities, AF_UNIX only, and a one-second start timeout; it can therefore reach chrony's private runtime socket without broadening the root broker. The _chrony account comes from the Debian/Raspberry Pi chrony package. Without chrony, the broker restarts systemd-timesyncd. Clock drift cannot authorize a captured command: the signed request still needs the repeater's live one-time claim, whose deadline is monotonic rather than wall-clock based.

"},{"location":"host_cli_service/#trust-and-injection-controls","title":"Trust and injection controls","text":"

The MQTT broker transports records but does not establish their authenticity. The endpoint checks the configured repeater identity and verifies its Ed25519 signature over the complete request ID, random nonce, and Base64URL request. It also validates framing, UTF-8, and byte limits. It does not execute that first record.

Instead, the endpoint stores the request in memory, creates a random 64-bit challenge, and sends @claim=<random> through meshcoretomqtt's signed serial channel. The repeater accepts it only from physical USB while the exact ID and nonce are pending, then signs a CLAIMED proof containing the challenge. Only a matching live proof lets the endpoint perform allowlist matching and execute the action. It consumes the proof before execution, so MQTT redelivery cannot execute an action twice.

A captured request only causes a new challenge that an idle repeater refuses. A captured proof does not match a new challenge, and restarting the endpoint forgets pending challenges. Repeater/Pi clock drift therefore cannot turn an old reboot or run record into a valid action. The reply and claim commands still use short-lived JWTs; meshcoretomqtt checks their signer allowlist, target, expiration, signature, and separate replay nonce. Wall-clock expiry and its in-memory nonce cache are not sufficient across a backward clock jump plus restart; the firmware's live one-time claim is what protects these host actions. Keep recovery-mode allowed_companions limited to the dedicated service key, with the endpoint and meshcoretomqtt on the same Pi.

Newlines and other reply control characters are converted to spaces and are also independently rejected by the firmware parser.

Protect the service private key and use broker ACLs that allow it to subscribe only to the selected repeater debug topic and publish only to that repeater serial-command topic. A private key listed in allowed_companions is trusted to sign serial requests.

"},{"location":"host_cli_service/#limits","title":"Limits","text":"

Use cmd get host to report the bridge state and limits.

"},{"location":"kiss_modem_protocol/","title":"Kiss modem protocol","text":"

View this page on MeshCore Docs.

"},{"location":"kiss_modem_protocol/#meshcore-kiss-modem-protocol","title":"MeshCore KISS Modem Protocol","text":"

Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.

KISS protocol version 2 adds an opt-in logical port 1 for MeshCore's second time-shared LoRa profile. It is still one physical transceiver: the firmware fast-switches between radio and radio2; it does not create a second radio.

"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"

115200 baud, 8N1, no flow control.

"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"

Standard KISS framing per the KA9Q/K3MC specification.

Byte Name Description 0xC0 FEND Frame delimiter 0xDB FESC Escape character 0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0) 0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
+------+-----------+--------------+------+\n| FEND | Type Byte | Data (escaped)| FEND |\n| 0xC0 |  1 byte   | 0-510 bytes  | 0xC0 |\n+------+-----------+--------------+------+\n
"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"

The type byte is split into two nibbles:

Bits Field Description 7-4 Port 0 primary radio; 1 secondary radio2 for Data only in protocol v2; controls stay on port 0 3-0 Command Command number

Maximum unescaped frame size: 512 bytes.

"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data 0x00 Raw packet Queue on primary radio; use type 0x10 to queue on radio2 in v2 (one pending packet total) TXDELAY 0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence 0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime 0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail 0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex 0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware 0x06 Sub-command + data MeshCore extensions (see below) Return 0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data 0x00 Raw packet Received packet from primary radio Data 0x10 Raw packet Received packet from secondary radio2 (v2)

Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. A v2 port-1 Data packet reaches RF only while radio2 is configured rxtx; with off or rx, the modem returns TxDone(0) and never falls back to primary.

Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).

"},{"location":"kiss_modem_protocol/#fast-dual-profile-receive-switching","title":"Fast dual-profile receive switching","text":"

After OpenHop (or another KISS client) configures radio2 or tempradio2, the board's radio_driver.loop() performs the RF hops locally. KISS does not send a command for every hop: that would be slower than the serial link alone. The shared scanner uses a 600 \u00b5s nominal retune allowance per hop, with the profile timing/preamble calculation accounting for it. It is a measured target rather than a hard maximum; packet handling and a slow board loop can extend an individual hop. The KISS parser services at most 32 incoming bytes per outer loop so a busy OpenHop serial peer cannot starve the scanner.

Thus \u201caround 0.6 ms\u201d refers to the on-board RF setting transition, not a full host-to-board request/response round trip. The returned Data port (0x00 or 0x10) identifies which profile actually received each packet.

SetRadio2 and SetTempRadio2 acknowledge that their tuple was accepted and scheduled; they do not acknowledge that a receiver has already entered that tuple. The next local scanner service performs the retune. KISS clients should configure the profiles once and let the board perform automatic time-sharing; a 115200-baud serial round trip cannot be a 0.6 ms receive-hop control plane. Clients that need temporary-lease state can poll GetTempRadio2.

"},{"location":"kiss_modem_protocol/#host-output-backpressure","title":"Host Output Backpressure","text":"

Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; loop() never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.

"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"

The TNC implements p-persistent CSMA for half-duplex operation:

  1. When a packet is queued, monitor carrier detect
  2. When the channel clears, generate a random value 0-255
  3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
  4. Otherwise, wait SlotTime and repeat from step 1

In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.

"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"

MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.

"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
+------+------+-------------+--------------+------+\n| FEND | 0x06 | Sub-command | Data (escaped)| FEND |\n| 0xC0 |      |   1 byte    |   variable   | 0xC0 |\n+------+------+-------------+--------------+------+\n
"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity 0x01 - GetRandom 0x02 Length (1 byte, 1-64) VerifySignature 0x03 PubKey (32) + Signature (64) + Data SignData 0x04 Data to sign EncryptData 0x05 Key (32) + Plaintext DecryptData 0x06 Key (32) + MAC (2) + Ciphertext KeyExchange 0x07 Remote PubKey (32) Hash 0x08 Data to hash SetRadio 0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower 0x0A Power dBm (1) GetRadio 0x0B - GetTxPower 0x0C - GetCurrentRssi 0x0D - IsChannelBusy 0x0E - GetAirtime 0x0F Packet length (1) GetNoiseFloor 0x10 - GetVersion 0x11 - GetStats 0x12 - GetBattery 0x13 - GetMCUTemp 0x14 - GetSensors 0x15 Permissions (1) GetDeviceName 0x16 - Ping 0x17 - Reboot 0x18 - SetSignalReport 0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport 0x1A - SetRadio2 0x1B Radio2 parameters (13; see below) GetRadio2 0x1C - SetTempRadio2 0x1D Radio2 parameters + duration (15) GetTempRadio2 0x1E - SetRadio2Ext 0x1F Radio2 parameters + protocol (14) GetRadio2Ext 0x20 - GetRadioSlot 0x22 - (Station G3 only) SetRadioSlot 0x23 Mode (1): 0=auto, 1=slot 1, 2=slot 2 (Station G3 only)"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"

Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.

Sub-command Value Data Identity 0x81 PubKey (32) Random 0x82 Random bytes (1-64) Verify 0x83 Result (1): 0x00=invalid, 0x01=valid Signature 0x84 Signature (64) Encrypted 0x85 MAC (2) + Ciphertext Decrypted 0x86 Plaintext SharedSecret 0x87 Shared secret (32) Hash 0x88 SHA-256 hash (32) Radio 0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower 0x8C Power dBm (1) CurrentRssi 0x8D RSSI dBm (1, signed) ChannelBusy 0x8E Result (1): 0x00=clear, 0x01=busy Airtime 0x8F Milliseconds (4) NoiseFloor 0x90 dBm (2, signed) Version 0x91 Version (1) + Reserved (1) Stats 0x92 RX (4) + TX (4) + Errors (4) Battery 0x93 Millivolts (2) MCUTemp 0x94 Temperature (2, signed) Sensors 0x95 CayenneLPP payload DeviceName 0x96 Name (variable, UTF-8) Pong 0x97 - SignalReport 0x9A Status (1): 0x00=disabled, 0x01=enabled Radio2 0x9C Saved radio2 parameters (13) TempRadio2 0x9E Active temporary radio2 + remaining minutes (15) Radio2Ext 0xA0 Saved radio2 parameters + protocol (14) RadioSlot 0xA2 Saved mode, active slot, detected mask, reboot needed (4 bytes) OK 0xF0 - Error 0xF1 Error code (1) TxDone 0xF8 Result (1): 0x00=failed, 0x01=success RxMeta 0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength 0x01 Request data too short InvalidParam 0x02 Invalid parameter value NoCallback 0x03 Feature not available MacFailed 0x04 MAC verification failed UnknownCmd 0x05 Unknown sub-command EncryptFailed 0x06 Encryption failed TxBusy 0x07 Radio TX busy, or host output queue full SaveFailed 0x08 Setting could not be saved; running slot unchanged"},{"location":"kiss_modem_protocol/#station-g3-physical-radio-slot","title":"Station G3 physical radio slot","text":"

GetRadioSlot takes no data and replies with four bytes: saved mode (0 auto, 1 or 2 forced socket), active socket (0 unavailable, 1 or 2), detected socket mask (bit 0 = socket 1, bit 1 = socket 2), and reboot-needed flag (0 or 1). This selects the physical SX1262 daughterboard socket, not the logical radio2 PHY profile. Other boards return UnknownCmd.

SetRadioSlot requires exactly one mode byte (0\u20132). An OK means the mode was saved; the running socket stays unchanged until a separate Reboot command or power cycle. Radio settings and identity are untouched. A failed save returns SaveFailed, with no change to the running socket.

If no usable socket is found during boot, the normal KISS modem has not started. Use the separate USB recovery console: !g3 get radio.slot, !g3 set radio.slot auto, then !g3 reboot. Recovery accepts only these prefixed management commands (and !g3 help), never KISS packet payloads.

"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"

The TNC sends these SetHardware frames without a preceding request:

TxDone (0xF8): Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.

RxMeta (0xF9): Sent after each Data frame (type 0x00 or v2 type 0x10) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). It remains exactly two bytes; the Data port identifies the receiving profile. Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.

"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"

All values little-endian.

Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#secondary-radio-parameters-setradio2-radio2-response","title":"Secondary Radio Parameters (SetRadio2 / Radio2 response)","text":"

SetRadio2 is the binary KISS equivalent of radio2. It changes only RAM state for this KISS session; it does not write the Mesh CLI profile file. All values are little-endian. The mode values are 0=off, 1=rx, and 2=rxtx. An off tuple may contain zero radio parameters, which are ignored.

Field Size Description Frequency 4 bytes Hz Bandwidth 4 bytes Hz SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8) Mode 1 byte off, rx, or rxtx as above Preamble 2 bytes Symbols; 0 chooses automatic

GetRadio2 returns this exact 13-byte tuple. The modem preserves the exact frequency and bandwidth values sent by the host even though its radio driver uses floating-point MHz/kHz internally.

"},{"location":"kiss_modem_protocol/#optional-extended-secondary-radio-setradio2ext-radio2ext-response","title":"Optional Extended Secondary Radio (SetRadio2Ext / Radio2Ext response)","text":"

These additive commands retain protocol version 2 and all existing OpenHop commands unchanged. SetRadio2Ext takes the exact 13-byte secondary tuple above followed by one protocol byte: 0 = MeshCore (LoRa sync word 0x12), or 2 = raw Ethernet (sync word 0x16). Other values, unavailable wire support, invalid modem tuples and lengths other than 14 bytes are rejected without changing the saved or active profile. GetRadio2Ext takes no data and returns the saved tuple plus that protocol byte, exactly 14 bytes.

Both modes use explicit-header, CRC-on, normal-IQ LoRa. Ethernet frames remain raw Data on KISS port 1 (0x10); the modem adds no IP stack, ARP implementation or WiFi server. Hosts provide Ethernet/IPv4/TCP and must respect the one-pending- TX limit by waiting for TxDone before submitting another Data frame. The standard KISS TXDELAY default is 500 ms; hosts may explicitly change it for a low-delay link. Signal reports retain their existing two-byte format.

The extended setting is RAM-only and has the same saved/temporary semantics as SetRadio2: an active SetTempRadio2 lease is not interrupted, and its expiry or cancellation restores the saved tuple including its protocol. The legacy 13-byte SetRadio2 and 15-byte SetTempRadio2 always select MeshCore; the legacy GetRadio2 remains exactly 13 bytes and does not report the protocol. Hosts that need Ethernet must first probe GetRadio2Ext; older firmware replies UnknownCmd. Do not silently fall back to MeshCore when this probe fails.

Builders can opt a compatible raw KISS modem into Ethernet wire support with MESH_ENABLE_ETHERNET_WIRE=1, leaving MESH_ENABLE_ETHERNET_TUNNEL=0. The wire flag defaults to the tunnel flag, so existing Companion builds are unchanged. The radio chip must independently support the requested profile; for example, SX1276 cannot use SF5 even when Ethernet wire support is compiled in.

Bench images compiled with MESH_HIL_TX_FREQ_MHZ additionally support the read-only SetHardware GetLabTxGuard (0x21, no request data). Its response 0xA1 is the compiled allowed frequency as u32 little-endian Hz; the standard 909.5 MHz bench lock returns 909500000. It proves the same frequency boundary used by the radio TX guard, not merely a requested setting. Production images without this explicit build flag return UnknownCmd; no setter exists.

"},{"location":"kiss_modem_protocol/#temporary-secondary-radio-settempradio2-tempradio2-response","title":"Temporary Secondary Radio (SetTempRadio2 / TempRadio2 response)","text":"

SetTempRadio2 is the binary KISS equivalent of tempradio2: the 13-byte secondary tuple followed by duration_minutes:u16, from 1 to 10080. It is RAM-only and automatically restores the saved radio2 tuple on expiry or reboot. A mode off with duration zero cancels the temporary lease and immediately restores saved radio2; all other off/zero-duration combinations are invalid. GetTempRadio2 returns the active 13-byte tuple plus remaining minutes, rounded up. It returns a zero/off tuple with zero duration when no temporary lease is active.

Changing saved radio2 during an active temporary lease does not interrupt the active tuple; the replacement becomes active when the lease ends. Pending KISS Data is bound to a profile generation at queue time. If that profile is replaced before RF transmission begins, the modem sends normal TxDone(0) and never falls back to the other profile.

"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"

All values little-endian.

Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"

All values little-endian.

Field Size Description Noise floor 2 bytes int16_t, dBm (signed)

The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.

"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"

All values little-endian.

Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"

All values little-endian.

Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"

All values little-endian.

Field Size Description Temperature 2 bytes int16_t, tenths of degC (e.g., 253 = 25.3degC)

Returns NoCallback error if the board does not support temperature readings.

"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"

Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.

"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0 0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure)

Use 0x07 for all permissions.

"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"

Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.

"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":""},{"location":"lora_ota_automation/","title":"Lora ota automation","text":"

View this page on MeshCore Docs.

"},{"location":"lora_ota_automation/#scripted-lora-ota-from-start-to-finish","title":"Scripted LoRa OTA from start to finish","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

The dedicated RAK3401 chain report records the physical failures of withdrawn migrations and the physically qualified compact nine-step replacement. Its runner blocks the withdrawn chains and pins every accepted bridge image by SHA-256.

tools/lora_ota/lora_ota.sh and tools/lora_ota/lora_ota.ps1 automate a MeshCore LoRa firmware update from a release .zip or ready .mota. They identify the destination, validate the hardware and running firmware, prepare the right container, move the participating nodes to a temporary radio channel, serve and monitor the download, request installation, restore the controller, and check the rebooted node.

The script cannot install the destination's first OTA-capable firmware. It can stage signed v3 bootloader containers with --no-install, but never sends the privileged bootloader install command. Invalid v2 containers carrying the bootloader flag remain rejected. Do the first nRF52 bootloader installation over USB. An already-capable, explicitly supported internal-flash, XIAO QSPI, or exact MeshTower V2 microSD repeater may later use the separate manual ota bootloader install workflow.

"},{"location":"lora_ota_automation/#required-topology","title":"Required topology","text":"

The reliable serial topology uses two local radios:

                                      authenticated admin commands\ncomputer -- MeshCore binary API --> controller Companion -------------------+\n   |                                                                         |\n   +-- raw text CLI + mOTA seeder --> OTA source ---- LoRa OTA blocks ----> target\n                                                        |                    ^\n                                                        +-- relay(s) --------+\n

One serial port cannot serve both controller roles: meshcli must keep reopening the controller while motatool owns the source port. The script rejects an attempt to use the same port for both.

The USB ASCII switch (+++MESHCORE-TERM-START) is the local control path, not the mOTA data framing. On an nRF52 dual-CDC Full Companion, the script uses that mode briefly for ota status and TempRadio commands. It then closes the CLI and starts motatool; the exact ota folder on preamble switches the same USB interface 00 into exclusive mOTA mode from either startup ASCII or Binary Companion mode. If USB logging was enabled and the Full Companion rebooted, its separate interface 02 continues to provide plaintext logging and is not a controller or source port. With default logging off, interface 02 is not enumerated. BLE remains available.

ESP32 Full Companion normally uses its dedicated TCP seeder on port 5001. ESP32 builds that also retain serial folder support use the same exclusive USB mOTA ownership state as nRF52. The exact ota folder on line transfers the USB port from startup ASCII or idle Binary Companion mode before binary mOTA frames begin; neither console mode shares that port until the folder detaches.

"},{"location":"lora_ota_automation/#bootloader-packages-and-station-identifiers","title":"Bootloader packages and station identifiers","text":"

Both wrappers use the same package-aware runner. It identifies application versus bootloader updates from container metadata, not filenames. A ZIP mixing application inputs and bootloader mOTAs requires --zip-member; their versions and target IDs are independent and must not be ranked together.

Signed format-3 nRF52 bootloader packages can be staged with --no-install. Use the same controller/source/TempRadio arguments as an application transfer, but provide the bootloader .mota (or its ZIP member) and --no-install. Without that flag, even --yes stops before opening a radio. Bootloader staging requires Python cryptography in the interpreter running the wrapper and a current motatool supporting format 3. The runner reuses tools/mota/motalib.py for strict geometry, embedded identity/CRC, capability, hash and signature validation, and still runs motatool verify including any --public-key pin.

If Python cannot load cryptography, an interactive run offers:

Install cryptography privately and retry signature verification? [y/N]\n

After approval, it uses the exact running Python interpreter to install cryptography==50.0.1 and its dependencies from PyPI into a separate per-user, per-interpreter cache. Binary wheels only are accepted; the repair does not compile native code, overwrite system/virtual-environment packages, change PATH, use sudo, or bypass externally-managed Python protections. It checks Ed25519 against a known valid signature and a tampered message, then retries the original full package verification. A later run can reuse a working cache without another download. This works for direct .mota files and bootloader members inside ZIPs; ordinary application updates and raw --tcp-cli commands do not need this library.

The interpreter must have working pip. If pip is missing, the script gives an exact-interpreter ensurepip command and operating-system pip/venv guidance; it does not silently install system tools. No compatible binary wheel, an old pip, network/download failures, an unwritable cache, or a failed signature check stop the run before radio access. Pip output is shown and installation is bounded at ten minutes. The radio admin-password environment variable is not passed to pip. Partial installation files may remain in the displayed private directory. The cache is under meshcore-lora-ota/python/<interpreter-id>/cryptography==50.0.1 in the same per-user cache base described below for motatool.

Only y or yes approves this repair; --yes is not installation consent. Noninteractive runs print a copyable private-install command and stop. After running that command manually, rerun the original OTA command; no Python path changes are needed. Declining never skips signature verification.

It probes actual motatool inspect support before radio access: older and newer builds may advertise the same version. If the tool is missing or incompatible, an interactive run offers an automatic repair:

Install this bootloader-capable motatool and continue after verification? [y/N]\n

Only y or yes approves installation. --yes does not approve host software installation. Noninteractive runs stop and print a copyable manual installation command plus the --motatool argument to use afterward.

The motatool repair builds the mikecarper/motatool fork at pinned revision 8c38369e7d35ad50cf74261869676d52dd24adf7, with Cargo's --locked dependencies, in a separate per-user cache directory. It needs matching stable Cargo and rustc 1.86.0 or newer and a native linker already installed; otherwise it points to Rust installation and stops without installing anything. The pinned dependency set needs more than the older rust-version in motatool's own manifest: zeroize 1.9.0 needs edition 2024, and ICU/idna dependencies need Rust 1.86. Cargo 1.75 is a version problem, not a root-permission problem; no nightly toolchain is necessary.

Before asking to build or downloading crates, the runner checks both executable versions. It keeps a compatible default pair; otherwise it looks for versioned executables on PATH (such as Ubuntu's cargo-1.91 and rustc-1.91) and already installed stable rustup toolchains. It displays the selected paths and pins both for the build process, so a newer Cargo cannot silently invoke an old rustc. Rustup proxies are resolved to their actual toolchain binaries before changing to the build directory. No default toolchain, symlink, system PATH, or package installation is changed, and probes disable rustup's automatic toolchain install. Tool version probes have ten-second timeouts. A working cached motatool does not need Cargo or Rust checks at all.

To choose a pair explicitly, append:

--cargo cargo-1.91 --rustc rustc-1.91\n

These options also accept executable paths, not shell aliases or a command with embedded arguments. Explicit selections and RUSTC/CARGO_BUILD_RUSTC compiler overrides are honored; if incompatible, they fail instead of silently selecting another compiler. --rustc takes precedence over those environment variables. The manual build command includes the chosen compiler as well as Cargo. Native linker, SDK, and system-library availability are still checked by the real build; a passed version check is not a guarantee that every host prerequisite is present. Source/dependency downloads, build time and disk use are disclosed before confirmation. Cargo progress is displayed, with a one-hour build timeout. The radio admin-password environment variable is not passed to the build.

The installer does not overwrite the selected/system motatool or alter PATH. It may replace only a previous copy in its displayed private install directory. After installation it repeats the bootloader-package capability check and uses the new binary for this run only if that check passes. A later run using the default motatool selection can reuse the checked cache without downloading again; switching from an explicit --motatool path still asks permission. Refusal, a failed build, or a failed recheck stops before opening any radio. Failed build files may remain in the displayed cache directory for diagnostics.

The install root is meshcore-lora-ota/motatool/<revision> under %LOCALAPPDATA% on Windows, ~/Library/Caches on macOS, or $XDG_CACHE_HOME (default ~/.cache) on Linux. Cargo uses an explicit --root, --rev, and --locked instead of replacing a global installation or following a moving branch.

Before transfer it queries the destination's ota bootloader status, checks its bootloader-specific target/hardware identity, installed ABI/codecs and matching internal/QSPI/SD storage profile. It does not compare an application version or application target ID to a bootloader package. After transfer it checks the destination's exact staged MID and image-hash confirmation, then prints the manual command; it never sends a bootloader install command:

ota bootloader status\nota bootloader install <MID8> <HASH16>\n

Review and send those commands on the destination, not the source. The device independently enforces signer authorization, safe live storage, continuity and upgrade-only policy at installation. --base, --allow-non-upgrade, and --prepare-only remain application-only. See the bootloader prerequisites and recovery limits.

TARGET_NODE, --relay, and --source-contact accept a contact name, full public key, or unique hexadecimal key prefix. The runner reads the controller's existing contact table and binds each selection to a full key before remote commands, so emoji names need not be typed and later name changes cannot redirect the run. Duplicate names, ambiguous prefixes, or the same radio under different participant aliases stop with an actionable error. Missing destination and relay contacts must first be imported or discovered. A missing connected source can be imported with the confirmation prompt below; a key alone does not create a contact.

For a separate source with a managed USB/TCP console, automatic source selection reads the physical source's full public key and matches it in the controller's contacts. Repeaters use get public.key. Full Companion does not implement that repeater command: on both USB and TCP, its key is read from a fresh terminal welcome banner, with a supported ver reply on the same connection proving the terminal is live. USB briefly uses the existing STOP/START/STOP terminal wrapper to obtain that banner, even for an ASCII-first Full Companion; TCP reads the greeting from a new connection. Identity rechecks repeat this exchange rather than trusting a key cached from an earlier connection.

ota status identifies the Full Companion seeder role before this probe. For unrecognized firmware, only an explicit unsupported get public.key response can trigger the Companion fallback. Timeouts, permission errors, malformed keys, missing/ambiguous banners, and failed ver replies still stop the run. Firmware that exposes neither identity mechanism needs a supported terminal firmware; the runner does not guess a key from the node name or skip the identity check.

Source selection does not require the source's current name to match an old saved advert, and duplicate/emoji names cannot redirect this lookup. An explicit --source-contact must identify that same physical source. If its key is missing, an interactive run offers:

Add the connected OTA source to this controller's contacts? [y/N]\n

This works through both the shell and PowerShell launchers. The runner reads the source's existing card command over USB/TCP, checks the full key in its contact card against the connected source, and shows its name/key before asking. On approval it rechecks the source/controller identities and live contact table, then uses MeshCore's existing import_contact API. Signature validation remains with the controller's normal advert importer. For manual-add controllers it can approve only that exact source through add_pending; it does not enable general auto-add. A fresh reload_contacts must confirm the full source key before OTA continues. No LoRa advertisement or firmware update is required for this import.

The default answer is No; --yes does not approve adding a contact. A noninteractive run stops with instructions to rerun in a terminal. Unsupported card firmware, malformed/mismatched cards, and unverified imports stop with manual-import/discovery guidance. To protect existing contacts, the controller's overwrite-oldest policy must be verifiably disabled; otherwise the runner stops and shows how to disable that bit without changing other auto-add flags. It does not change this policy itself. A full table or rejected import cannot be worked around by deleting another contact; an uncertain import is not automatically retried. An approved contact remains on the controller for subsequent runs.

The normal/TempRadio on-air ACK checks still run; adding a contact does not prove radio reachability or skip those checks. When the controller and source are the same TCP Full Companion, use the verified --source-shares-controller topology; it does not need a contact for itself.

"},{"location":"lora_ota_automation/#destination-requirements","title":"Destination requirements","text":"Destination Package installed One-time prerequisite Raw ZIP handling ESP32 Full application image OTA-enabled image with an A/B partition table Builds a full mOTA from the matching non-merged application .bin nRF52, internal flash In-place delta Exact-board OTAFIX bootloader with mOTA apply support Requires --base with the exact image currently running nRF52 repeater, external QSPI Full image or in-place delta QSPI-aware exact-board OTAFIX bootloader and matched repeater build Builds a full mOTA; adding --base requests a delta MeshTower V2 nRF52, microSD Full image or in-place delta SD-aware exact-board OTAFIX bootloader and compatible card Builds a full mOTA; adding --base requests a delta

MeshTower V2 microSD application and bootloader containers require an Ed25519 signature from a key in the device allowlist. Supply --sign-key when the runner must build a container, and install the BLM2-capable SD-aware bootloader locally before attempting application or bootloader OTA. Preview.12 requires USB/BLE DFU or SWD; see MeshTower V2 microSD LoRa OTA.

The firmware inside a raw ZIP must have a valid MeshCore EndF trailer. An ESP32 merged/factory image is not an application image and is rejected. A generic vendor DFU ZIP may also be unusable if it does not contain the raw EndF-bearing .hex or .bin.

For an internal-staging nRF52, the exact base image is irreducible information. The node reports its eight-byte body hash, but that hash cannot reconstruct the firmware bytes needed to create a delta. Keep the .pio/build/ENV/firmware.hex that was actually flashed. A matching filename or version alone is not enough.

"},{"location":"lora_ota_automation/#1-install-the-host-tools","title":"1. Install the host tools","text":"

Install Python 3.10 or newer, Rust, Git, the official meshcore-cli 1.6.0 or newer, and the official motatool.

For signed bootloader staging, see the automatic cryptography and bootloader-capable motatool repairs above. Pip is needed only when installing the missing Python dependency; Rust/Cargo and a native linker are needed only when building motatool, not when using an already compatible binary. The runner checks the selected meshcli version before opening radios. Keep its serial/BLE dependencies in meshcli's own environment (pipx handles these); installing cryptography in meshcli's pipx environment does not make it available to the separate Python interpreter running this wrapper. A ready bootloader .mota does not need the Python detools or intelhex build dependencies.

On Bash:

python3 -m pip install --user pipx\npython3 -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n\nmeshcli -v\nmotatool --version\n

On PowerShell:

py -m pip install --user pipx\npy -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path .\\motatool\n\nmeshcli -v\nmotatool --version\n

Restart the shell if pipx or Cargo reports that it changed PATH.

"},{"location":"lora_ota_automation/#2-identify-and-test-both-local-links","title":"2. Identify and test both local links","text":"

List serial devices:

meshcli -l\n

The examples below assume /dev/ttyACM0 is the controller and /dev/ttyACM1 is the OTA source. On Windows they might be COM7 and COM8. Close picocom, a serial monitor, the phone app, and any other program holding either link.

Test the controller's binary API:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

For an ordinary raw-text source, test its OTA support:

meshcli -r -s /dev/ttyACM1 -b 115200 \"ota status\"\n

The command must print an OTA | ... target:XXXXXXXX status.

For an nRF52 full Companion, open the source port with terminal mode selected automatically:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM1\n

Run ota status. It must report OTA seeder, install:disabled, and target 00000000; send +++MESHCORE-TERM-STOP before closing the terminal. The automation detects and performs this token-wrapped preflight itself, so no extra command-line option is needed.

For an ESP32 full Companion, test its separate WiFi control console instead:

./tools/lora_ota/lora_ota.sh --tcp-cli 192.168.1.50 \"ota status\"\n

It must report OTA seeder, install:disabled, and target 00000000.

This standalone fallback uses Python's raw TCP socket directly. It works with meshcli versions whose -r mode rejects -t; it does not need meshcli, motatool, nc, or a firmware package. It defaults to text port 5002; port 5000 speaks the binary Companion protocol and port 5001 serves mOTA data. The firmware must expose the text console; this option does not enable one.

Specify a port or adjust the total connection/command timeout (10 seconds by default):

./tools/lora_ota/lora_ota.sh --tcp-cli 192.168.1.50:5002 --timeout 15 \"ver\"\n

PowerShell uses the same option:

.\\tools\\lora_ota\\lora_ota.ps1 --tcp-cli 192.168.1.50 \"ota status\"\n

Place --tcp-cli first. Each invocation sends one command and waits for the complete reply and terminal prompt. Connection failures, timeouts, missing replies, and rejected commands exit nonzero. If USB owns the terminal, close the active USB terminal and try again. Commands are never automatically resent in this standalone mode: a timeout after sending may mean the command executed but its reply was lost. Inspect the device state before repeating a change.

For an actual OTA run, continue using --source-tcp HOST:5001 with --source-cli-tcp HOST:5002; that existing control path also bypasses meshcli raw mode. The standalone fallback does not transfer a firmware image itself.

Changing a terminal to 57600 baud does not select ASCII mode. USB Companion builds and the normal raw management CLI use 115200 unless a particular build was explicitly configured otherwise.

"},{"location":"lora_ota_automation/#3-check-the-destination-once","title":"3. Check the destination once","text":"

The destination must be in the controller's contacts and remotely reachable on the normal channel. The script runs these authenticated checks itself:

ota status\nget bootloader.ver\nota self\nota stats\n

The script uses get bootloader.ver as an optional platform/version hint, not as an install gate. It makes one attempt without an interactive retry loop. unknown, an unsupported/missing command, an unrecognized reply, or a lost reply produces a warning and does not by itself block the run. In particular, older MeshCore firmware can report unknown for the installed signed OTAFIX 2.4.6 MeshTower V2 SD image because it omits the legacy UF2 version string. Both the shell and PowerShell launchers use this same behavior; no application or Companion firmware update is required for this host-side workaround. The runner also accepts the new OTAFIX2.4.6 and explicitly labelled 0.11.0 (base) formats.

The runner still requires ota self to report bootloader: apply OK, bootloader: QSPI apply OK, or bootloader: SD apply OK and checks the reported bootloader ABI and codec mask against the selected package. If the version command is unavailable on older firmware, the script warns and falls back to the legacy ota self platform marker. If an nRF52 bootloader lacks the required capabilities, install the exact-board OTAFIX bootloader first. Positive nRF52 capability markers take precedence over a generic unsupported getter response, so it cannot silently skip nRF52 safety checks. Bootloader packages still require the independently checked ota bootloader status board, target, ABI, codec and storage identity; missing version text never bypasses package signature checks or the explicit bootloader-install workflow. The separate hardware-qualified rak3401_mota_chain.py retains its exact deployed version restriction; it is not a general OTAFIX 2.4.6 update runner. Current firmware also reports maxblk:2048 near the front of both OTA replies. The runner treats a missing marker as the deployed 1 KiB receive limit, rejects a ready 2 KiB package for such a target before transfer, and passes the live limit to motatool --block-size when it builds from raw firmware. Thus updating an older target from a release ZIP produces a compatible 1 KiB package while a current target retains the 2 KiB application default. ota stats is only an optional EndF version probe. It uses one bounded retry cycle; unsupported firmware or a lost reply falls back directly to the required ver command instead of entering an operator continuation loop.

The default TempRadio tuple is:

909.5,500,5,5,120\n

The lab test default is 909.5 MHz, 500 kHz bandwidth, SF5, and CR5. The frequency is only a North American example: choose a legal frequency supported by every participating radio and appropriate to your location. Older radios that do not support SF5 require a complete replacement tuple passed with --temp-radio. The fifth value is the TempRadio window in minutes. To change only that value, pass --temp-radio-minutes 90; this works with --relay and keeps the chosen frequency, bandwidth, SF, and CR. The hard minimum is 20 minutes. The 120-minute default and the runner's worst-case timeout estimate are guidance. A window below the estimate prints a warning and continues; the radio lease can expire before a slow OTA finishes. For an interactive run configured at 250 kHz, the runner offers a 125-kHz path rehearsal at SF5 before the final update confirmation. If a 250-kHz rehearsal cannot prove the temporary path but every short lease expires and the exact normal path returns without active recovery, it offers the same 125-kHz SF5 start. If the 125-kHz SF5 path fails in that same cleanly returned way, the operator can approve a fresh SF6 rehearsal, then SF7 if SF6 fails. Each change prints the revised plan and requires confirmation before its rehearsal; the long transfer uses only the setting that passes. The selected long-window duration is unchanged, and each attempt repeats the full short rehearsal. --yes and non-interactive runs never change bandwidth or spreading factor automatically. To start directly at 125 kHz SF5 without a prompt, pass --temp-radio 909.5,125,5,5,150 (choose a legal frequency and an appropriate window for the actual route). The separate three-minute path rehearsal and its identity checks still run.

Before that long window is allowed, the live runner performs a mandatory independent three-minute rehearsal. Its exact normal-channel ota status and ota self identity proof gets one shared four-minute, read-only budget before any schedule, radio override, seeder, or transfer is armed; this tolerates a marginal link without consuming or extending a live lease. It advances a stale managed controller with exact readback and treats a source terminal's whole-minute display as an uncertainty window, never as an exact epoch. A source minute overlapping the host is preserved; a completely stale minute is pinned to a guarded future value and read back once. Neither clock is moved backward, and the runner stops if a managed clock cannot be proven within the ten-minute limit. Each remote participant must expose an empty tempradioat schedule. The runner projects one fixed start/end interval into each participant's independently sampled RTC, sends each mutation once, proves every exact identity on the temporary tuple, waits for natural expiry, and proves the complete normal path again. A lost schedule reply is resolved by those on-air proofs; it is never blindly replayed with a fresh duration.

The published RAK3401 v1.16.7-c1caa5ad LoRa-OTA image includes get/set/del tempradioat and can take this safe first step. A truly older or reduced build without fixed TempRadio scheduling is rejected before mutation; bootstrap it locally or with another explicitly controlled maintenance path. The automation does not substitute an immediate tempradio command whose first delivery could remain queued after cleanup.

"},{"location":"lora_ota_automation/#rxps-handling-during-tempradio","title":"RXPS handling during TempRadio","text":"

RXPS improves receive performance per unit of radio-on time, so the runner keeps the destination's saved RXPS preference enabled whenever the selected fast tuple has a qualified timing window. Before changing any radio, it reads ver from the destination, controller, source, and every named relay. It treats v1.17.1.5 as the first forward contract in which every SF5-SF8 transmission, including a retry, uses the same tuple-selected physical preamble: normally 32 symbols, 64 only when 32 cannot enable RXPS, and 128 only when neither 32 nor 64 can. A saved RXPS level is also safely retuned after a radio change. An older or unparseable destination version fails closed: the runner temporarily sends set radio.rxps off instead of assuming an ad-hoc build contains the timing fixes. Automation deliberately treats the version as a wire-format contract.

For the default SF5/BW250 tuple, the runner keeps destination RXPS on only when the destination and every possible sender are v1.17.1.5 or newer. It does not overwrite an existing level-based preference. Current firmware retunes that preference against the tuple-selected 64-symbol wire preamble; the qualified boundary is effective level 8, preamble 64 (1252 / 6424 us). A manually configured node may save set radio.rxps level 8 preamble 32 before entering TempRadio: when 32 symbols cannot cover the TCXO transition, firmware safely selects the real 64-symbol preamble. If the saved setting uses fixed manual timings, or even one participant is older or unknown, RXPS is temporarily off. This avoids the receive gap that a 32-symbol sender or an unretuned manual window would create.

The complete qualified SX1262+TCXO policy is:

TempRadio SF/BW Saved reference setting Qualified boundary Automation SF7/BW500 level 7 preamble 32 7 / 32 RXPS on for a current destination SF6/BW250 level 7 preamble 32 7 / 32 RXPS on for a current destination SF5/BW125 level 7 preamble 32 7 / 32 RXPS on for a current destination SF5/BW250 level 8 preamble 32 8 / 64 RXPS on only when every participant follows the 64-symbol contract SF6/BW500 level 8 preamble 32 8 / 64 RXPS on only when every participant follows the adaptive-preamble contract SF5/BW500 level 8 preamble 32 8 / 128 RXPS on only when every participant follows the adaptive-preamble contract SF5/BW62.5 level 10 preamble 16 10 / 16 RXPS on for a current destination Unqualified tuple none continuous RX RXPS temporarily off

If RXPS was already off, the runner leaves it off. Otherwise it writes the original level, preamble assumption, receive/sleep periods, and temporary decision to protected target-rxps-settings.json in the run's working directory, verifies the temporary state, and restores the exact original setting after the target is back on its normal radio. Current firmware exposes this complete state through get radio.rxps.config; a legacy reply has only on/off and periods, so the runner can restore those periods but cannot reconstruct an unreported saved level. A radio change later recalculates from a preserved saved minimum, so moving back to a slower tuple returns to the operator's saved level.

The OTA source has a stricter policy than the destination. For every source with a managed serial or TCP CLI, the runner reads and retains its exact RXPS preference, using the legacy fixed-period query only when the detailed query is unavailable. It builds and verifies the package, completes the read-only target checks, and obtains confirmation while that preference remains unchanged. Immediately before the first radio mutation, it reads the source RXPS state again, disables RXPS, and verifies the readback. Source RXPS stays off through catalog serving, download, installation, and post-install identity verification. Cleanup first proves that the source has returned to its normal radio, then restores and verifies the exact saved level/preamble or fixed-period state once. A source whose RXPS state cannot be read, disabled, or restored safely fails closed. If current firmware explicitly rejects an RXPS disable or restore with radio busy; retry, the runner retries that idempotent mutation at staggered 210\u2013378 ms intervals. All 32 delays are distinct and contribute about 9.4 seconds of waits; source-CLI command round-trip time is additional. This avoids repeatedly sampling one radio phase while retaining a strict attempt cap. Other rejections are not replayed.

"},{"location":"lora_ota_automation/#4-run-an-esp32-update","title":"4. Run an ESP32 update","text":"

The ZIP can contain a compatible ready .mota or the exact board-and-role non-merged application .bin:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof ESP32\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n

The script shows the detected target, hardware, running hash, chosen package, version, manifest ID, and action before asking for confirmation. For an unattended job, add --yes:

./tools/lora_ota/lora_ota.sh ./release.mota \"Roof ESP32\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1 \\\n  --yes\n

PowerShell equivalents:

$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof ESP32' `\n  --controller-serial COM7 `\n  --source-serial COM8\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Roof ESP32' `\n  --controller-serial COM7 `\n  --source-serial COM8 `\n  --yes\n

Prefer the environment variable or the interactive password prompt. Passing --password works, but the wrapper's own command line may be visible to other local processes. The runner keeps the password out of child meshcli command lines and removes its protected temporary command file after each call.

"},{"location":"lora_ota_automation/#5-run-an-nrf52-update","title":"5. Run an nRF52 update","text":"

If the input ZIP already contains a compatible in-place delta .mota, no base argument is needed: its embedded base hash is compared with the live node. If the ZIP contains raw new firmware, supply the exact running image:

./tools/lora_ota/lora_ota.sh ./nrf52-new-release.zip \"Hill nRF52\" \\\n  --base ./firmware-that-is-running.hex \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n
& .\\tools\\lora_ota\\lora_ota.ps1 '.\\nrf52-new-release.zip' 'Hill nRF52' `\n  --base '.\\firmware-that-is-running.hex' `\n  --controller-serial COM7 `\n  --source-serial COM8\n

Before building a delta, the runner proves that the base's target ID, hardware identity, firmware version when available, and EndF body hash match the live destination. It then asks motatool for codec 2, the nRF52 in-place format. The normal workspace is 0x98000.

For a QSPI-backed repeater or the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without --base. Supplying an exact base requests a smaller in-place delta and automatically selects the conservative external workspace 0xC6000, which is safe for S140 v6 and v7 application layouts. An explicit --inplace-memory overrides the automatic value.

The live runner detects QSPI from ota self (QSPI apply OK) or ota status (bl:QSPI) and refuses an explicit QSPI store:ERR 0K report even when the bootloader itself advertises QSPI apply support. Offline --prepare-only runs must supply --nrf-qspi; do not use that switch for a board that only exposes QSPI pins or uses the chip as a Companion filesystem. The application and bootloader must both be from the matched repeater list in the nRF52 QSPI guide.

"},{"location":"lora_ota_automation/#6-add-intermediate-relays","title":"6. Add intermediate relays","text":"

The runner automatically checks ota config on the destination, managed source, and each OTA-enabled relay before confirmation. The declared --relay count is the minimum required OTA reach: one relay requires at least one hop, not the direct-only hops=0. After the radio rehearsal passes, it rechecks the original policies and temporarily raises only limits below that minimum. Higher existing limits remain unchanged. Every change needs exact readback; a lost setter reply is reconciled by reading the policy, not blindly repeating the write.

The original limits are restored after the download, before installation can reboot the destination, and cleanup also attempts restoration after an error or Ctrl-C. A serial seeder is stopped before the source text CLI is used. The private, password-free ota-hop-settings.json recovery file is flushed before any hop write. Hop limits are persisted by firmware: if the host loses power or a radio becomes unreachable, use that file's original values once the correct radio channel is reachable. Unexpected external policy changes are not overwritten.

For a route containing additional, undeclared repeaters, add --ota-hops N (0..8, at least the declared relay count). This is a minimum, not a command to reduce larger limits. It does not discover or switch undeclared repeaters; all participating radios still need overlapping TempRadio windows. Direct runs without relays or a positive explicit minimum leave hop policies untouched. Explicitly non-OTA relays remain opaque forwarders; unsupported/ambiguous policy replies, authentication failures and timeouts on OTA participants stop the run. No signer, install-security or forwarding-filter checks are bypassed.

List relays from farthest to nearest so each command is sent before its route moves to TempRadio. A bare relay name uses the destination password; use NAME=PASSWORD when it differs:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1 \\\n  --relay \"Far Relay=far-password\" \\\n  --relay \"Near Relay=near-password\"\n

PowerShell uses the same arguments:

& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Remote Target' `\n  --controller-serial COM7 `\n  --source-serial COM8 `\n  --relay 'Far Relay=far-password' `\n  --relay 'Near Relay=near-password'\n
"},{"location":"lora_ota_automation/#other-connection-choices","title":"Other connection choices","text":"

The controller can use any one of:

--controller-serial PORT\n--controller-tcp HOST[:PORT]       # default port 5000\n--controller-ble ADDRESS_OR_NAME\n

An ESP32 FULL/repeater source can serve over its dedicated WiFi seeder port while its raw USB CLI is used to start TempRadio:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-serial /dev/ttyACM1\n

An ESP32 companion_radio_full uses WiFi for both dedicated source links:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-tcp 192.168.1.50:5002\n

Port 5002 defaults automatically when it is omitted from --source-cli-tcp. Current Full Companion firmware exposes its complete role-specific text terminal there; the runner uses only ota, tempradio, and normalradio, and remains compatible with older bounded port-5002 consoles. The source-only Companion never stages or installs the image itself; it streams the host folder to other nodes over LoRa. See the full Companion guide for manual operation and interface details.

An nRF52 companion_radio_full uses one USB source port sequentially. The runner automatically wraps local control commands in the terminal tokens, and unmodified motatool switches that port into mOTA mode when seeding starts:

The wrapper sends STOP, then START, before each command. This makes fallback independent of whether a prior raw probe left an unobservable USB-UART connection in ASCII or Binary mode. Seeder startup is reported only after the verbose device log contains its COUNT acknowledgement; an immediate device ERR or a missing acknowledgement fails during startup instead of surfacing later as a catalog timeout.

Newer motatool builds prepare raw-DEFLATE blocks with Zopfli before opening the source link. This host-only work has a separate 30-minute preparation limit (--seeder-prepare-wait SECONDS) and reports progress while waiting. The five-second device acknowledgement timer (--seeder-start-wait SECONDS) starts after the final compressed N/N blocks message, not when compression starts. Neither compression progress nor a host \"serving\" message proves device readiness: the runner still requires the device's COUNT reply. Older motatool builds without a preparation phase keep the original device startup timeout. Process exits and device errors still fail immediately. Preparation happens after TempRadio is armed, so leave enough lease time for both compression and transfer, especially on a slow host. The suggested TempRadio budget includes the preparation limit; it remains a warning, not a hard requirement to select that entire worst-case budget.

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n

If the source is already on the exact TempRadio tuple through a scheduled or manual operation, --source-already-temp lets a TCP source run without a raw CLI link. The script cannot verify, extend, or shorten that source window, so leave a comfortable time margin. It also cannot inspect or change that unmanaged source's RXPS state; disable source RXPS yourself before starting the run and restore it only after returning the source to its normal radio.

Use --controller-baud or --source-baud only for a build whose corresponding interface is genuinely configured to another speed.

"},{"location":"lora_ota_automation/#package-selection-and-safety-gates","title":"Package selection and safety gates","text":"

For a ZIP, the runner first examines every .mota without extracting paths. It keeps only packages matching the live target, hardware, base, platform, codec, and bootloader capabilities. It chooses the newest compatible version and prefers a delta over a full image at the same version. If equally suitable files differ, select one explicitly:

--zip-member path/inside/archive/update.mota\n

If no ready mOTA is usable, it searches .bin and .hex members for a valid, matching EndF, then builds the platform-appropriate container. Every result is structurally checked by the runner and independently passed through motatool verify before any radio changes. Direct firmware and mOTA inputs, as well as individual ZIP members, are rejected above 64 MiB before being loaded.

Useful controls:

For offline package preparation only:

./tools/lora_ota/lora_ota.sh ./release.zip offline \\\n  --prepare-only \\\n  --platform nrf52 \\\n  --target-id 1234ABCD \\\n  --target-base-hash 0011223344556677 \\\n  --target-hw Heltec_T114 \\\n  --base ./firmware-that-is-running.hex\n

Live operation is safer because the script obtains these values directly from the destination.

"},{"location":"lora_ota_automation/#what-happens-during-a-run","title":"What happens during a run","text":"
  1. Validate the input paths and host tools, then prove the source is either an OTA-enabled raw CLI or a source-only full Companion control interface. Read and durably save a managed source's exact RXPS preference without changing it.
  2. Before sending any remote packet, prove the managed source has no active, pending, or fixed TempRadio work, then gate the source and controller clocks. Advance a stale exact clock to host time; for a minute-resolution source, preserve an overlapping minute or use one guarded forward value with readback. Preserve a small future lead and fail closed above the ten-minute drift limit. Then authenticate to the target, query its target ID, hardware, running body hash, firmware version, bootloader version, and nRF52 bootloader capabilities, and save the controller's normal radio tuple.
  3. Select or build one compatible v2 application mOTA and verify all block hashes, Merkle root, full-image hash where applicable, identity fields, signature, codec, base, and the live firmware's reported maximum block size (1 KiB for legacy replies without maxblk, currently 2 KiB otherwise). Version-3 bootloader packages are refused before any target state changes.
  4. Read every participant's version, save the destination's RXPS state, select the qualified destination policy, and show the confirmation prompt.
  5. Run the fixed-window three-minute rehearsal described above. This phase does not disable RXPS, start a seeder, request a pull, or install anything; the long transfer remains completely unarmed until temporary and normal reachability both pass.
  6. Fresh-read, disable, and verify source RXPS; apply and verify the destination RXPS policy; then start TempRadio on the target, far-to-near relays, and source. A separate controller is moved and read back through Binary; a shared Full Companion instead schedules its bounded local override while Binary remains the transport. The runner rejects TempRadio windows under 20 minutes and warns when the selected window does not cover worst-case setup, seeder preparation and startup, discovery, the transfer timeout, final polling, and install checks.
  7. Start motatool serve, discover the exact eight-hex manifest ID, request ota pull <id> flash, and poll until that same ID reports ready. A seeder process exit stops the run immediately. For --no-install, schedule all script-controlled nodes back to their normal radios before restoring the controller, unless --leave-controller-radio was requested.
  8. Recheck that exact ID, give the target a short final TempRadio safety window, and request ota install. Then shorten each relay's TempRadio window so the normal multi-hop route returns, stop the seeder, shorten the source window, restore the controller, and probe ota self every 10 seconds through the configured readiness window (five minutes by default). The exact new body hash is the readiness signal; only then does the runner require the exact package version. A relayed run continues the 10-second probes through the mandatory relay-return window. A source supplied with --source-already-temp is never modified. --leave-controller-radio moves the controller back to TempRadio only after this normal-channel verification. Restore the destination's exact original RXPS setting after normal-channel identity is proven. A managed source stays RXPS-off through that verification and its exact setting is restored only after its own TempRadio state is proven inactive.

Remote replies are matched only after queued messages have been drained and only when they come from the intended contact and fit the command. A ready status for another manifest ID is an error, never permission to install it.

"},{"location":"lora_ota_automation/#transmission-loss-and-retries","title":"Transmission loss and retries","text":""},{"location":"lora_ota_automation/#legacy-relay-manifest-workaround","title":"Legacy relay manifest workaround","text":"

The deployed 2d03e098 firmware uses the ordinary 160-entry, non-expiring packet hash FIFO for OTA forwarding. Identical manifest requests and replies can therefore be suppressed after packet loss, even while CLI commands work. The firmware fix replaces that forwarding decision with a small timed OTA cache, suppresses downstream echoes and already-queued copies, and keeps TempRadio, hop limits and forwarding filters authoritative. It applies to discovery, manifest and block traffic, including opaque OTA relays.

Until the relay is updated, add --legacy-relay-dedup-workaround to the same runner command. This is opt-in because it adds airtime. It requires --source-shares-controller, exactly one --relay, a saved direct (zero-hop) route to that relay, and no extra --ota-hops beyond one. After an exact download: failed (manifest timeout) 0/0 for the requested MID, it sends up to 192 unique 13-byte direct, non-forwarded OTA no-operation/unknown-opcode frames on the verified TempRadio channel. These replace legacy FIFO entries; 192 gives some loss headroom over 160 entries, not proof of reception. Then it rechecks the destination and requests the same signed package once. A lost pull reply is reconciled using status, not blindly replayed.

There are at most two recovery rounds within the original transfer deadline. Each round adds 2,496 origin Mesh bytes plus LoRa overhead, normally roughly a minute at 125 kHz/SF7 before command/status latency. The companion's local queue acceptance is not an RF acknowledgement. Recovery stops on insufficient verified lease time, wrong radio settings, command errors, or changed session. It never reboots a relay, cancels a partial image, changes package bytes, or bypasses signature/install checks. It does not fix later block-retry stalls on old firmware or relays beyond the controller's direct radio reach; the firmware update is the permanent fix.

For a short path, --legacy-relay-reboot-recovery offers a lower-airtime alternative after that same exact 0/0 timeout. It requires one managed relay whose saved normal-channel and active temporary-channel routes are each zero or one hop, plus --source-shares-controller. Before any reboot, the runner checks the relay and destination identities, the exact failed manifest, and at least eight minutes of remaining transfer and target TempRadio time. It asks for a separate confirmation at the failure (or requires both this flag and --yes in unattended use). The relay's reboot CLI intentionally sends no reply: the runner returns the shared source/controller to normal radio, authenticates the relay's exact key there, re-arms only the remaining portion of the target's TempRadio lease, then proves the relay, destination and same manifest on the temporary channel before one pull retry. If the normal path cannot be proved after reboot, it stops instead of assuming the reboot worked; the other temporary leases still expire. A reboot also clears the relay's temporary radio setting, so manually rebooting it during a transfer without this re-arming sequence will break the temporary path. When both legacy recovery options are enabled, the reboot choice is offered first; declining it leaves the direct-cache workaround available on a zero-hop route.

Read-only and replay-safe transmissions retry up to three times. Three retries or 90 seconds, whichever comes first, opens a 10-second stop-or-continue prompt when stdin is an interactive terminal. Continue remains the default on timeout or Enter, so an operator can persist through a temporary outage; enter s or stop to end the run, and Ctrl-C remains immediate. With non-interactive stdin, the runner stops after that finite automatic retry cycle instead of silently starting another cycle forever. Optional participant-version probes always use a bounded cycle and degrade to unknown; optional destination ota stats falls back directly to ver. The mandatory normal-channel destination baseline is the narrow exception: it may make nine total read-only attempts but is stopped by one hard four-minute deadline before any TempRadio lease or OTA mutation exists. Proofs inside the live rehearsal retain the smaller four-attempt limit.

Commands that change OTA state are reconciled before replay:

Retries and operator-selected continuation can outlast the original TempRadio budget. If a bounded window expires, rerun the same package after the nodes return to their normal channel; the manifest-ID check resumes its partial download without replacing it.

The working directory is created before a managed source can be changed, then retained and printed at exit. It contains the exact served mOTA, motatool-serve.log, extracted build inputs when needed, and controller-radio.txt, plus ota-hop-settings.json when OTA reach needed a temporary increase. A managed source also gets a protected source-rxps-settings.json containing its exact original preference and idempotent restore command. Its contents and directory entry are flushed before RXPS is disabled. When the destination started with RXPS enabled, protected target-rxps-settings.json records its manual recovery state. The RAK3401 chain points every nested step at one chain-root source record, so a rerun after host power loss does not adopt the temporary RXPS-off state as the original. A retained record is accepted only for the same managed CLI endpoint. These files contain no saved admin password. After exact source restoration, a standalone run atomically retires its record; the chain retains its shared record between steps and retires it only after the verified endpoint restoration completes.

"},{"location":"lora_ota_automation/#interruption-and-recovery","title":"Interruption and recovery","text":"

Ctrl-C stops the seeder, detaches its serial folder, makes a bounded attempt to shorten a source TempRadio window started by the script, and attempts to restore the controller. For a managed source it leaves RXPS off until the source is proven back on its normal radio, then restores and verifies the saved source preference. A transient success-path restore failure remains armed for one more idempotent cleanup attempt. The target and relays remain on TempRadio only until their bounded windows end; rebooting also restores their saved radio settings. A normal cleanup restores the destination's exact RXPS periods. If that remote restore cannot be confirmed, use target-rxps-settings.json after the target returns to its normal channel. A partial download remains safe. Once the target is reachable again (after its TempRadio window ends, or after putting the controller back on that tuple), rerunning the same package recognizes its manifest ID and resumes the existing session instead of clearing it.

The destination and each controlled relay also receive separate protected target-tempradio-cleanup.json or relay-tempradio-cleanup-<full-key>.json records. Each record is flushed before its single one-minute shortening attempt. A lost acknowledgement remains unconfirmed; a reply on the temporary channel does not establish that the shortening arrived. Restoring the controller or reaching the destination through another path does not establish that every relay returned either.

Default cleanup succeeds only after the controller is back on its normal radio and every owned participant independently returns its bound full public key and an inactive get tempradio result. Shared Full Companions must also report their live TempRadio override inactive; Binary radio settings alone show the saved preference. Unresolved participants retain their own record and the run reports failure. A deliberately staged --leave-controller-radio run preserves its windows and records. After an install, that option proves all participants during the normal-radio verification interval before returning the controller to the requested temporary tuple.

After a process interruption, retain these records and use a new work directory. Existing records cannot be overwritten to reset a shortening attempt. Never repeat a relative tempradio command whose attempt is recorded. On the normal route, use the recorded full key and a separately supplied admin password to read get public.key and get tempradio. Both must confirm that same participant and an inactive override; a timeout, changed key, active profile, or malformed reply leaves its obligation pending. Wait for its original finite lease and repeat only these read-only checks. Keep the original record and a separate private restoration receipt binding its file hash and successful proofs. These records contain no admin passwords and do not authorize reboot or installation.

When a chained run has already proved the newly running body hash, its retained previous package can briefly report verifying staged blocks after TempRadio reactivates the OTA manager. The runner waits only through the configured discovery timeout, keeps checking source liveness, and accepts only the same manifest becoming ready to install or the manager becoming idle. A changed ID, failed or incomplete state, or timeout stops the chain. It then proves the exact installed body again. If that same session is still attached and ready, the runner detaches it with ota cancel; if the manager has become idle, it sends no cancel. An ordinary-channel no download status proves only that the manager is idle, not that persistent staging was erased. The runner therefore does not issue or describe an IDLE cancel as durable cleanup. The next chain transition explicitly re-adopts and proves the expected previous MID before detaching it; after the final install OTAFIX has consumed the approval word, so any retained container is inert and is replaced by the next valid pull.

A hard process kill or host power loss cannot run cleanup. Recover a serial controller using the tuple saved in the printed work directory:

radio=$(tr -d '\\r\\n' < ./meshcore-lora-ota-20260807-123456-1234/controller-radio.txt)\nmeshcli -s /dev/ttyACM0 set radio \"$radio\"\n
$radio = (Get-Content '.\\meshcore-lora-ota-...\\controller-radio.txt' -Raw).Trim()\nmeshcli -s COM7 set radio $radio\n

For a managed source, first return it to its ordinary radio, then inspect source-rxps-settings.json and issue its exact restore_command through the same serial or TCP-console endpoint recorded in that file. The command is idempotent; confirm the full setting with get radio.rxps.config before resuming an update.

If you stop during final confirmation, reconnect on the node's normal channel and run ota self and ver. A completed run returns success only when ota self reports a valid new body hash and ver exactly matches the package; an unverified install returns status 2. Do not immediately replace a staged image: the default active-download guard preserves it until you explicitly use --replace-active-download or run ota cancel.

Exit status is 0 for success, 2 for a validation or operational error, and 130 for Ctrl-C.

"},{"location":"management_decoder/","title":"Management decoder","text":"

View this page on MeshCore Docs.

"},{"location":"management_decoder/#management-report-decoder","title":"Management report decoder","text":"

Paste MGR1 or MGR2 management-report payloads or complete GroupData packets from a packet analyzer to read a radio's public management information. Everything, including password authentication and ACL decryption, happens locally in this browser. The report, password, and candidate public key are never uploaded.

The public portion of a report is deliberately plaintext, but it is not authenticated until a management password is supplied. A password-authenticated page proves that its public fields and encrypted ACL bytes have not been altered by someone who does not know that password.

"},{"location":"management_decoder/#decode-a-management-report","title":"Decode a management reportEncrypted ACL entries","text":"MGR1/MGR2 payload or complete GroupData packet hex

Spaces, line breaks, colons, dashes, 0x, and MQTT JSON fields named raw or data are accepted. Paste all pages from one report together to view a complete multi-page ACL. Press Ctrl/Command+Enter to decode.

Management password (optional for public fields; required for ACLs)

The password remains in this page only. The decoder derives the management AES-SIV key in your browser and does not send it anywhere.

Candidate administrator public key (optional)

ACL encryption reveals per-radio 12-byte fingerprints, not recoverable public keys. Supplying a complete candidate key checks whether its fingerprint appears in this report.

Decode management report Clear local data Load authenticated example Decode notes"},{"location":"management_decoder/#what-the-decoder-accepts","title":"What the decoder accepts","text":"

Use the same password configured by set mgmt.password. A decoded ACL lists the report-specific fingerprints and the administrator and/or OTA-signer flags. It cannot turn a fingerprint back into a full key. Use the optional candidate field to test a specific full public key.

MGR2 also shows USB logging, host/reader connection, logger-client activity, stalled/recovering/deferred state, recovery stage, saved backoff, retry interval/inactive seconds, Auto qualification seconds, and persistence readiness. Unsupported observation is shown explicitly; legacy MGR1 has no USB block and shows it as unavailable. These values are the report-start snapshot, not proof that a Pi or host application is healthy. Older decoder copies require an update for MGR2.

MGR2 also shows the latest USB watchdog action, reason mask and names, advisory node-clock timestamp, recorded-boot uptime seconds, event sequence, and whether the record was durably saved. Software recovery and re-enumeration mean attempted actions, not successful repairs. A reboot request is not proof that a reboot completed; a later cancellation can replace it. The node clock can be wrong, even when it resembles a valid date. Event uptime and sequence remain useful for ordering. MGR1 does not carry this event field and shows it as unavailable. All 36 ACL entries still fit, using up to nine MGR2 pages.

The earlier USB-only MGR2 development layout was never released. This decoder accepts the finalized MGR2 format with its 111-byte public header and latest watchdog event.

Reports contain a frozen latest-event snapshot, not immediate watchdog alerts or a complete event history. See USB logging watchdog for local review and persistence limitations.

Host/reader connection only describes the USB link or open reader. Logger-client activity means recent USB stats polling within a 15-minute lease, renewed by the existing five-minute polls without a new registration or heartbeat command. The flag also requires logging enabled and a connected, unstalled USB link. It does not identify the client or prove MQTT delivery, disk writes, or Pi health.

For the report schedule, public-field layout, cryptographic design, and the offline Python capture tool, see Management reports.

"},{"location":"management_reports/","title":"Management reports","text":"

View this page on MeshCore Docs.

"},{"location":"management_reports/#management-reports-mgr1mgr2","title":"Management reports (MGR1/MGR2)","text":"

Available on repeater (including observer), room-server and sensor firmware. Exception: the constrained wio-e5-mini_repeater omits management reporting and shared data.tx routing to fit its existing application region. Normal authenticated administration, statistics, live USB/MQTT logging and the host-service bridge remain available. Companions/terminal-chat nodes and KISS modems do not originate these reports. Off by default. A management password and explicit enable are both required. No radio settings, existing preferences layout, or OTA authorization policy is changed by enabling reporting. This protocol does not authorize updates.

Current firmware emits MGR2, including a public USB logging/recovery snapshot and its latest watchdog event. Updated decoders still accept released MGR1; older decoder copies need an update to read MGR2. The earlier USB-only MGR2 development layout was never released and was replaced before release. Reporting USB status does not enable logging or change retry policy.

"},{"location":"management_reports/#cli","title":"CLI","text":"

Run through the existing local CLI or an authenticated administrator session:

get data.tx\nset data.tx path 1:12ab77\nset data.tx region auto\nset mgmt.password <12-to-96-byte password>\nset mgmt.direct 5\nset mgmt.flood 21\nset mgmt.enabled on\nget mgmt\nset mgmt.enabled off\n

Use a long randomly generated password. There is no password getter. Firmware stores the derived 32-byte key, not the plaintext password. That stored key is equivalent authority to decrypt reports and must also be protected. Passwords entered into terminal programs may still be recorded by those programs. The recognized firmware password command wipes its value from the input buffer on success and error, including unavailable management or allocation failure.

data.tx is the single shared route for management reports, telemetry history, and future scheduled data producers. Its fresh-install defaults are path=direct (zero hops) and region=auto; configuring it never enables a producer. Paths use 1:, 2: or 3: followed by complete hop hashes without separators, or the comma-separated form accepted by set outpath. Empty hops, including a trailing comma, are rejected. none removes the path. get/set mgmt.path remain compatibility aliases for get/set data.tx path. Alias expansion that exceeds the command buffer is rejected, not truncated.

region=auto uses the radio's configured default region when it is usable; otherwise it resolves the unique deepest flood-enabled entry in the region hierarchy. If equally deep candidates make that choice ambiguous, it resolves to nothing and no fallback is transmitted. default requires and follows region default; a region name pins that named scope. none disables scoped fallback. The resolved transport key is looked up when a report starts, so edits to the region definition take effect without rewriting /data_tx.

The direct path leads to the receiver/uplink's vicinity; this broadcast-radio datagram has no private destination identity. The radio ID in the payload identifies the reporter.

Direct and flood schedules are independently configurable and may each be turned off. Fresh settings are direct=5d and flood=21d; global reporting is still off until mgmt.enabled on. Direct accepts 5-90 days and requires the shared path. Flood accepts 21-90 days and requires a resolvable shared region. At least one route must remain active while reporting is enabled. The legacy set mgmt.interval N shorthand enables both, setting direct to N and flood to max(21,N). No transmission is sent merely by configuring a password. Initial reporting waits for the configured schedules. Invalid settings leave saved configuration, countdowns, and any report pages already in progress unchanged. Day counts must be unsigned decimal integers; signs and trailing text are rejected. Reports have deterministic per-radio/per-sequence jitter of up to an hour; pages are spaced at least a minute apart. Watchdog events appear in the next scheduled snapshot, not as immediate radio alerts. Multiple events between snapshots can replace the latest-event record; this is not a complete off-node event history.

The radio cannot know that an observer uploaded a packet to MQTT, so the flood schedule is deliberately independent of direct transmission. A region-scoped TRANSPORT_FLOOD is sent at its independently configured interval. An unresolved or ambiguous data region blocks that transmission rather than sending an unscoped flood. If direct and flood become due together, the flood is sent and replaces the redundant direct copy. Ordinary reports are never retried in a tight loop; a partial report gives up after an hour. Existing relay filters, hop limits and duty constraints still apply.

Schedule state is atomically reserved before transmission, and checkpointed hourly. Timers use elapsed powered-on time rather than the RTC: clock corrections cannot create floods, and reboot does not clear the budget. Downtime is not credited; each reboot can delay a report by up to an additional hour. This is a deliberately conservative tradeoff for nodes with unreliable clocks. Off/on and password changes do not reset the flood limit. Corrupt/unreadable state or failed writes stop reporting; get mgmt shows FAULT(no TX) until storage is repaired and the radio restarted. /management and the shared /data_tx are versioned, CRC-protected, and replaced transactionally.

On upgrade, an existing saved data.tx route takes precedence over the legacy management-only path. Otherwise that path must be saved successfully to /data_tx before reporting resumes. A failed migration leaves the old /management file intact for retry after storage is repaired and the radio restarted.

Weekly history is collected once a minute while enabled. Hour-bucket extrema cover 7 days to 7 days + 1 hour (conservative boundary bucket). The first report after enable/reboot is marked partial where appropriate. Since-report extrema reset after all pages have been queued, retaining readings taken since the snapshot for the next report. These statistics are not durable; reboot loses the history, not the flood countdown. Temperature is MCU temperature, not ambient. Reporting does not wake GPS, start Wi-Fi, or initialize external OTA media. Unknown capabilities/readiness are explicitly distinguishable from false. History/snapshot working memory is allocated only when reporting is enabled and is bounded to 1.5 KiB, plus a small configuration object and temporary stack use.

"},{"location":"management_reports/#routing-and-mqtt","title":"Routing and MQTT","text":"

Both direct/path and flood reports use PAYLOAD_TYPE_GRP_DATA (0x06). The route bits independently select direct or flood. MGR1/MGR2 are application extensions with literally plaintext public fields, not a call to the ordinary encrypted createGroupDatagram() builder. A fixed public marker is not an owner or password-derived channel ID. There is no outer channel encryption.

The body is padded with zeroes to a group-compatible length 3 + 16*n (maximum 179 bytes). Existing repeaters in the checked upstream implementation route group data without requiring a successful channel decryption. This fork recognizes the management envelope before ordinary channel processing. Reception of a management packet never exempts it from forwarding policy. Some third-party firmware may apply additional channel/layout policies; interoperability with every fork is not guaranteed. Packet logging/uplinks can capture it without the password.

RAW_CUSTOM (0x0F) is not used for reports because stock upstream does not flood-route it.

The observer's existing MQTT PACKET JSON supplies the complete frame in raw. No broker configuration or password changes are necessary to capture a report. The decoder understands all four route forms, 1-3-byte hashes, scope transport codes, and duplicate copies heard by several uplinks:

python -m pip install -r tools/management/requirements.txt\npython tools/management/report.py --mqtt capture.jsonl\n

The password is prompted, not supplied as a process argument. Input may be JSONL or a JSON array of MQTT messages. Alternatively omit --mqtt for a JSON array of canonical payload hex strings. --match-admin FULL_PUBLIC_KEY matches a known administrator against the encrypted fingerprints in either input mode. MQTT captures match each radio's fingerprints separately. Invalid files or keys are rejected before the password prompt; an unavailable password input gives a command-line error instead of a traceback. This is an offline capture decoder, not a broker subscriber or downlink service.

For a browser-local decoder that also decrypts and authenticates ACL entries, use the Management report decoder. It does not upload the captured packet or password.

"},{"location":"management_reports/#canonical-mgr2-payload-little-endian","title":"Canonical MGR2 payload (little endian)","text":"Offset Bytes Field 0 4 MGR2 4 16 First 16 bytes of reporter public key 20 4 Persisted report sequence (never wraps; exhaustion stops TX) 24 4 RTC Unix timestamp; advisory, may be wrong 28 4 Firmware major/minor/patch/pre packed as mOTA version 32 4 Bootloader packed version, or unknown 36 4 EndF target ID 40 8 Complete mOTA delta-base body hash 48 4 EndF image length 52 4 Staging capacity; planning still checks actual package geometry 56 4 OTA capability bits 60 2 Uptime hours, saturated at 65535 62 4 Weekly minimum mV, minimum C, maximum C 66 4 Since-report extrema in the same format 70 1 History coverage hours, capped at 168 71 1 Interval days for this report's direct or flood schedule 72 1 Role: 1 repeater, 2 room server, 3 sensor 73 1 Compiled/detected capability bits 74 1 Active status bits 75 1 Mask of status bits whose state is known 76 2 Validity/partial-history flags 78 1 Zero-based page index 79 1 Total pages (1-9) 80 1 Unique ACL count (0-36) 81 1 First ACL index in this page 82 1 ACL entries on this page (0-4) 83 2 USB status flags 85 1 Packed USB recovery stage and retry backoff step 86 4 USB full-reboot retry interval seconds 90 4 USB inactive seconds 94 4 USB continuous Auto qualification seconds 98 1 Latest watchdog reason/action/durability code 99 4 Latest watchdog event RTC Unix epoch; advisory, zero if unavailable 103 4 Uptime seconds at that event's boot 107 4 Latest watchdog event sequence, saturated at 4294967295 111 13 * count AES-SIV encrypted ACL entries after ACL 16 Full AES-SIV authentication tag after tag 0-15 Zero padding to group-compatible length; not part of canonical payload

MGR2 retains all 36 ACL entries, using four per page and up to nine pages. The largest canonical page is 179 bytes and already group-compatible; the radio payload budget is unchanged. The USB snapshot is frozen with the other public fields at report start and repeated on each page. It is not a live host-health monitor.

USB flag bits 0-10 are, in order: supported, logging enabled, watchdog enabled, host connected, reader connected, stalled, recovering, recovery deferred, persistence ready, watchdog Auto, and logger active. Bits 11-15 must be zero. Byte 85 bits 0-1 hold stage: 0 idle, 1 soft-recovery wait, 2 re-enumeration wait, and 3 reboot pending. Bits 2-5 hold the saved backoff step; bits 6-7 must be zero. Backoff is not a count of successful recoveries. The three timers are unsigned seconds; local recovery/reboot counters are not sent. The retry field is the saved backoff interval, not a countdown to an action; inactive age is reported separately. USB-only cleanup can precede that interval. Unsupported USB observation/recovery is explicitly represented by a false supported bit, not omitted or mistaken for a connected healthy reader. USB status describes the radio's observed transport and policy, not proof that the host process, MQTT uplink, SSH service, or Pi is healthy. Host/reader connection alone does not mean a logging client is running. The logger-active bit means recent USB stats polling within a 15-minute lease while logging is enabled and the observed USB host/reader link is not stalled, separate from USB attachment or an open reader. The existing five-minute stats polls renew this lease; no new client-registration or heartbeat command is required. This does not identify the client, prove logs reached MQTT or disk, or show that the Pi remains otherwise healthy.

Missing watchdog state on new or upgraded installations defaults to Auto. Auto promotes to saved On after 14 continuous healthy logging-USB days with renewed stats polling; its qualification age resets on reboot, detach, stall, stats-lease expiry, or USB logging off. Explicit On and Off remain available. Nonlogging/no-reader nodes remain Auto without watchdog board reboots. The Auto flag and age report this qualification separately from an enabled On policy. Use the node's get usb.watchdog and set usb.watchdog off|on|auto CLI to inspect or change policy; receiving a management snapshot changes nothing.

The 13-byte latest-event block is all zero when no event is available. Otherwise byte 98 bits 0-3 are the reason mask: host absent, reader absent, TX stalled, and confirmed stats client inactive. Bits 4-6 hold the action: 1 software recovery attempted, 2 re-enumeration attempted, 3 reboot requested, or 4 reboot cancelled after a final health/ownership check. Bit 7 means the event was saved and verified on durable storage. Sequence is nonzero for a present event. Physical faults may combine reason bits; stats-only expiry cannot authorize a board reboot. Epoch zero is allowed when the clock source is unavailable.

Event time is the node's advisory clock, not verified UTC. Default clocks can start at a plausible fixed date. Clock corrections do not rewrite old events; recorded uptime and sequence help review them independently. A saved reboot request is intent, not proof of a completed reset. The latest event survives normal reboot and mode changes, but is not a log of hardware hangs or sudden power failures. See USB logging watchdog for storage failure, cancellation, and review limitations.

Legacy MGR1 retains the same offsets through byte 82, followed immediately by encrypted ACL entries at byte 83 and the authentication tag. It carries six entries per page and up to six pages, with no USB block. Its absent USB status is decoded as unavailable, not as all switches off.

Each private entry is a per-radio 12-byte keyed fingerprint followed by flags: bit 0 administrator, bit 1 trusted OTA signer. The firmware combines permissions for the same fingerprint before paging, so every transmitted entry is unique. Decoders reject authenticated duplicate fingerprints within or across pages; repeated identical packet copies from multiple uplinks are still deduplicated. An oversized ACL fails closed rather than silently truncating. The reported allowlist includes all current full administrators and all four possible trusted OTA signing keys; region/filter managers and ordinary clients are excluded.

Feature bits: 0 Wi-Fi, 1 GPS, 2 NTP, 3 USB data, 4 LoRa OTA. Wi-Fi active means connected, GPS means receiver enabled (not necessarily a fix), NTP means an actual accepted NTP response this boot, USB means observable native USB data connection (not power or an unobservable external UART bridge). OTA capability means compiled support; active means an established usable apply/store path.

Validity bits: 0 firmware version, 1 bootloader version, 2 EndF/base identity, 3 staging capacity, 4 partial week, 5 partial since-report period, 6 MCU temperature. Voltage is unsigned millivolts, zero missing. Temperatures: zero missing, 1-251 represent -50..200 C, 252 below range, 253 above range, 254-255 reserved. OTA bits: 0 protocol compiled, 1 transfer DEFLATE, 2 2-KiB app transfer blocks; bits 8-23 are apply codec bits (full/sequential/in-place). Transfer DEFLATE is not compressed bootloader apply. There is no manifest ID. Exact old binaries are still needed on the computer to generate a differential update; a hash alone cannot reconstruct them. Unknown metadata must not be treated as OTA readiness.

"},{"location":"management_reports/#cryptography","title":"Cryptography","text":"

Password root: SHA256(\"#\" || literal UTF-8 password) (hashtag-style derivation). Subkeys: HMAC-SHA256(root, ASCII_domain || radio_id16). Domains: MeshCore-MGR1-SIV, MeshCore-MGR1-ACL. Fingerprint: HMAC-SHA256(ACL_subkey, radio_id16 || full_admin_key32)[0:12]. The same owner has different fingerprints on different radios. A collector needs a candidate administrator's full key to identify a fingerprint.

Encryption is RFC 5297 AES-SIV-CMAC-256, using the existing rweather AES primitive. The single associated-data string is the complete clear header: 111 bytes for MGR2, including USB status and its event, or 83 bytes for MGR1. Both retain the existing derivation domains; the authenticated magic and layout distinguish their wire versions. Ciphertext is exactly the ACL byte length. Full 16-byte tag, no truncation. This deterministic misuse-resistant mode avoids reliance on the firmware's noncryptographic general-purpose RNG or RTC nonces. An identical restored snapshot may repeat ciphertext, but does not expose XORs of different ACL plaintexts as nonce-reused stream encryption would.

All public fields are readable without a password but are authenticated only to password holders. A shared password authenticates knowledge of that password, not a unique individual radio identity; another holder can forge reports. There is no per-device signature. Collectors must enforce their own persisted sequence/replay policy and explicitly handle factory resets/restored backups. The decoder verifies snapshot consistency but does not maintain a database. This fast password derivation permits offline guessing, so twelve characters is only a minimum length, not a guarantee of password strength. Packet timing, public metadata and entry/page counts remain visible.

"},{"location":"mbedtls-tls-footprint/","title":"Mbedtls tls footprint","text":"

View this page on MeshCore Docs.

"},{"location":"mbedtls-tls-footprint/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"mesh_pocket_gui/","title":"Mesh pocket gui","text":"

View this page on MeshCore Docs.

"},{"location":"mesh_pocket_gui/#meshpocket-full-companion-display","title":"MeshPocket Full Companion display","text":"

Full Companion on MeshPocket includes a channel message board, a worldwide coastline map and display settings. Other boards keep their existing home UI. USB, Bluetooth and notifications remain available.

Use the Pocket's user button:

Page One click Two clicks Hold Normal pages Next page Previous page Page action Message board, browsing Older message Newer message Finish browsing Map, zooming Zoom in Zoom out Save and finish Settings, editing Change value Next setting Save and finish

Hold on the message board, map or settings page to enter its mode. Three or four clicks on the board select the next or previous configured channel. The inbox still opens individual messages, including direct messages.

The board shares the existing 32-message history, showing the selected channel with sender names and local timestamps. Successfully sent app messages appear there too. History is held in RAM; it is not a permanent message archive.

Settings cover channel, text size, sender name length, screen orientation, light/dark theme, return-to-board delay, full refresh interval and map zoom. They use a separate checked, atomically replaced record in primary storage. Existing node, channel and contact formats are unchanged. Timezone comes from the existing node preferences.

Changing pages or map zoom uses a full clean refresh to clear the previous image. Updates within the same page keep the faster partial refresh, with a clean refresh after at most ten partial updates. Settings allow shorter intervals of one, three or five updates.

The idle screen-off timer defaults to 15 minutes on battery and USB. Existing saved timeouts are retained; set display.timeout and display.usb.timeout to 900 to use 15 minutes. The return-to-board delay is a separate setting.

The map centres on this node's location, or the first contact with a location. With no locations it shows a world overview, keeping the selected zoom saved for when a location becomes available. Zoom controls require a location. Auto zoom uses a 200 km view when a location is available. Contacts appear as dots, with nearby names at closer zooms. The view spans 20,000 km down to 5 km vertically; coastline detail stays coarse at close zooms. It contains no streets, terrain, regional downloads or ocean tiles. See the map data notes for resolution, storage and reproducible generation.

The UI is adapted from MeshCore-Mesh-Pocket-MessageBoard. Include PocketGui-NOTICE.txt alongside distributed firmware for the UI and font notices. The application DFU ZIP keeps its required manifest, binary and metadata entries; the notice is a sidecar.

Build with Mesh_pocket_companion_radio_full. Defining UI_POCKET_GUI=0 retains the standard Pocket display.

"},{"location":"mixed_scan_validation/","title":"Mixed scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"mixed_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"mixed_sf_bw_validation/","title":"Mixed sf bw validation","text":"

View this page on MeshCore Docs.

"},{"location":"mixed_sf_bw_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nimble_companion_trial/","title":"Nimble companion trial","text":"

View this page on MeshCore Docs.

"},{"location":"nimble_companion_trial/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nimble_companion_trial_results/","title":"Nimble companion trial results","text":"

View this page on MeshCore Docs.

"},{"location":"nimble_companion_trial_results/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"notifications/","title":"Notifications","text":"

View this page on MeshCore Docs.

"},{"location":"notifications/#companion-notification-builder","title":"Companion notification builder","text":"

Make a rule, preview it here, then copy the CLI commands or apply and test it over USB. The device needs firmware containing the new notification commands. The programmable alert engine is enabled on ESP32, nRF52 and RP2040 builds. Compact STM32WL Companions retain their existing alerts because the engine does not fit their 224 KiB application partition. Browser previews show one cycle; device tests use the configured repeat and stop policy. USB testing uses Binary Companion mode at 115200 baud.

Find my node Food order ready VIP chat Only channel 9 Who should trigger the alert? Messages fromAll contacts and channelsOne contactOne room serverOne channel Contact/room key or channel slot Companion client statusConnected or disconnectedConnectedDisconnected Silence other message alerts (channel exception)

This saves a silent default plus the selected channel's rule. It also silences ordinary DMs and room-message alerts. Existing specific/state rules and authorized !notify strings can still alert; review the channel 9 example and caveats. Copy commands and USB Save include both rules; preview only plays the exception.

Connected means an active phone/app, USB, WiFi, Ethernet or serial companion client. It selects a message rule; connecting or disconnecting alone does not create an alert. Contact matching uses the full public key. Channel slots resolve to the channel's key when the rule is saved.

What should the device do? Vibration patternMillisecond on/off durations, or off / inherit LED patternStarts on, alternates off/on, finishes off Sound melodyRTTTL: note letters a-g, # for sharp, p for rest; off / inherit also work ScreenUse normal screen policyOn during alertOff during alert GPIO outputoff / inherit, or an approved pin and pattern: 22:50,300,50

50,300,40,20,500 means on for 50 ms, off for 300 ms, on for 40 ms, off for 20 ms, then on for 500 ms. A rule controls only outputs physically supported by its firmware. The USB connection reports the supported outputs and approved GPIO pins before applying anything.

Repeating and stopping Repeat countA number, or forever Gap between cycles (ms) Stop alertButtonNode connectedNever automatically dismiss

Node connected stops an active alert when a companion client connects. Never disables button/connection dismissal; a finite repeat count still finishes. notify.stop always stops playback. A new matching message replaces the current alert.

Allow notification strings from this contact or room PermissionLeave permission unchangedAllow this contact or roomRevoke this contact or room

Only applies to a contact or room server's full public key, without a connection condition. An allowed contact can DM the notification string below, or it can be posted in an allowed room. Trusting a room allows its posters to send alerts through that server. GPIO, stop-policy and permission changes are forbidden in DMs or room posts. The device stops playback after at most 15 seconds, including infinite repeats. Incoming strings do not save settings and obey the recipient's master switches.

Preview one cycle VibrationLEDGPIOScreen

Ready

Ready to play the melody

Preview with sound Stop preview

The melody plays on this device after a one-second audio warmup, then the timer and output indicators start together. No contact or room key is needed to preview an example. Sound set to off or inherit plays no melody here.

Timing here is a visual approximation. Firmware schedules outputs using millisecond timestamps without blocking radio work; loop and hardware latency can delay an edge. A haptic motor also takes time to start/stop.

Device commands
Copy commands Notification DM or room post

The recipient must first grant your contact or room permission. Send the string as an ordinary DM or room post; it contains no GPIO action. Long patterns may need shortening to fit 159 characters for a DM, or 150 for a room post.

Try it on your device

No device connected

Connect USB Disconnect USB Save rule Save and test Test saved rule Stop device alert Delete selected rule

Save writes the generated commands in order and stops at the first device error; earlier successful settings remain saved. A test with a connection suffix selects that profile even over USB. Stop-on-connect still watches real subsequent connections. The silence-other-alerts option saves both the silent default and the selected channel rule. Deleting the selected rule does not delete that default. USB testing requires Chrome or Edge.

"},{"location":"notifications/#cli-reference","title":"CLI reference","text":"

Settings apply immediately and survive reboot and filesystem-preserving updates. They live in a separate checked file, /notify_prefs, with an atomic replacement and backup. There are 12 rule slots. Existing alerts stay in use until a rule or master control enables the new system. Unmatched messages keep their existing alert behavior, subject to the master switches.

"},{"location":"notifications/#master-controls","title":"Master controls","text":"
get notify\nset notify.enabled on\nset notify.sound on\nset notify.vibration on\nset notify.led on\nset notify.screen on\nset notify.gpio off\nget notify.gpio.pins\n

Each output has get notify.<output> and set notify.<output> on|off. Master off cannot be overridden by a contact, channel or incoming notification DM. set notify.enabled off disables custom rules and DM actions and returns to the existing alert behavior. Supported-output bits in get notify are vibration=1, sound=2, LED=4, screen=8 and GPIO=16. GPIO uses the same board pin approval as repeater GPIO control and is inactive by default.

"},{"location":"notifications/#rules","title":"Rules","text":"

Targets are all, contact:<64 hexadecimal public-key digits>, room:<64 hexadecimal room server public-key digits>, and channel:<slot>. A channel's 32-digit key also works. Add @connected or @disconnected to select the client state; no suffix matches either state.

set notify.vibration channel:3 50,300,40,20,500\nset notify.sound channel:3 order:d=8,o=5,b=180:c,e,g,4c6\nset notify.led channel:3 100,100,100,500\nset notify.screen channel:3 on\nset notify.gpio channel:3 22:50,300,50\nset notify.repeat channel:3 forever\nset notify.gap channel:3 2000\nset notify.stop channel:3 button\nget notify.vibration channel:3\nnotify.test channel:3\nnotify.stop\nnotify.delete channel:3\nget notify.rules\nget notify.rules 0\n

The GPIO example works only if pin 22 appears in get notify.gpio.pins. Pulse lists have up to 12 positive durations of 1-60000 ms each. GPIO output is active-high, inactive-low. Sound is one note at a time, not chords. RTTTL requires name:d=<1|2|4|8|16|32>,o=<4..7>,b=<25..900>:notes; names use up to 12 ASCII letters/digits/underscores. Melodies are at most 63 characters and 60 seconds. Notes use a through g, p for rest, optional duration/octave, # for sharps on a/c/d/f/g, and one . for a dotted duration. For example 16c#6 and 4g.5 are valid. Invalid melodies are rejected before the buzzer library sees them. This ASCII format matches the installed NonBlockingRTTTL driver's pitch table; musical staff symbols and polyphony are not supported.

For any rule output, off explicitly disables it and inherit removes that field's override. Rules combine per field: shared all, shared state rule, contact/channel rule, then contact/channel state rule. A matching rule replaces the old message sound/vibration; unspecified outputs are off unless inherited from a shared rule. An unspecified screen keeps its normal display policy. screen on|off temporarily overrides that policy and restores it when the alert ends. A selected LED pattern temporarily takes ownership from the usual status heartbeat; the master LED switch also controls that heartbeat.

Finite repeats are 1..65535; forever loops until dismissed. Gap is 1..60000 ms after the longest output sequence. Stop modes are button, connected, and never. Triple-press sound/vibration mute controls also update their notification master switch where those buttons are supported. Continuous alerts prevent MCU sleep so their timers can run.

"},{"location":"notifications/#contact-and-room-permission-for-notification-strings","title":"Contact and room permission for notification strings","text":"

On the recipient, replace PUBLIC_KEY with the sender's complete public key:

set notify.remote contact:PUBLIC_KEY on\nget notify.remote contact:PUBLIC_KEY\nset notify.remote contact:PUBLIC_KEY off\nset notify.remote room:ROOM_PUBLIC_KEY on\n

Permission defaults to off, is saved per contact or room key, and cannot be granted to all, channels or connection-state variants. The identity comes from the authenticated received DM, not a key written inside its text. The permission alone does not replace the contact's normal message alerts. A trusted room's forwarded !notify posts use the room server's complete key for permission, not the post author's four-byte prefix. Trusting a room allows its posters to send notifications; trusting a chat contact alone does not authorize their posts forwarded by an untrusted room. Both plain DMs and signed room posts pass through the same validation and 15-second playback cap.

The allowed sender can send:

!notify vibration=50,300,40,20,500 led=100,100,100,500 sound=order:d=8,o=5,b=180:c,e,g screen=on repeat=forever gap=2000\n

Allowed fields are vibration, sound, led, screen, repeat and gap. The whole notification string is validated before any output changes. Unknown/duplicate fields, GPIO, stop modes, permission changes and inherit are rejected. The command cannot change saved rules. Button dismissal is always allowed, notify.stop works, and playback stops at 15000 ms even when the requested pattern or repeat count is longer. Unapproved or invalid notification strings remain ordinary message text. Repeated delivery of the same recently received post or DM does not restart its playback deadline. Channel messages cannot execute notification strings. Receiver master switches apply even while connected.

"},{"location":"notifications/#examples","title":"Examples","text":""},{"location":"notifications/#only-channel-9-alerts-all-other-channels-silent","title":"Only channel 9 alerts; all other channels silent","text":"

This example makes message alerts silent by default, then plays one short melody for channel 9. It works both with and without a connected companion client and requires a device with a buzzer. Messages still arrive normally; this changes the node's alerts, not the phone app's notification settings. Use the Only channel 9 button above to load this setup into the builder. You can change the channel slot or alert pattern before copying or saving. Uncheck Silence other message alerts to generate only the selected rule; this does not remove a silent default already saved on the node.

channel:9 means the firmware's zero-based slot 9 (the tenth slot), not a channel named ch9. Configure that slot before running these commands. The rule follows its saved channel key; recreate it if you replace the channel in that slot.

Start with no conflicting custom rules. Use get notify.rules, then get notify.rules <rule-slot> to inspect existing selectors. Remove only unwanted rules with notify.delete <selector>: existing state-specific and contact/channel rules can override the silent default below.

set notify.enabled on\nset notify.vibration all off\nset notify.sound all off\nset notify.led all off\nset notify.screen all off\nset notify.gpio all off\nset notify.repeat all 1\nset notify.gap all 500\nset notify.stop all button\nset notify.sound on\nset notify.sound channel:9 ch9:d=8,o=5,b=180:c,e,g\n

Keep the sound master on: set notify.sound all off silences the default rule, while set notify.sound off would also block the channel 9 exception. The other outputs remain silent, including screen wake-ups for messages. To add vibration for channel 9 on a device with a vibration motor:

set notify.vibration on\nset notify.vibration channel:9 100,100,300\n

Test the saved exception with notify.test channel:9; stop it with notify.stop. Send a message on another configured channel to verify that it stays quiet.

Scope: there is no channels-only wildcard: all also silences ordinary DM and room-message alerts unless a more specific rule enables them. Previously authorized contact/room !notify strings can still request alerts. If those should stay quiet too, revoke their permission with set notify.remote contact:PUBLIC_KEY off or set notify.remote room:ROOM_PUBLIC_KEY off, using the full sender/server key.

"},{"location":"nrf52-usb-ready-fix/","title":"Nrf52 usb ready fix","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52-usb-ready-fix/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nrf52_bluetooth_debug/","title":"Nrf52 bluetooth debug","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_bluetooth_debug/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"nrf52_companion_storage_recovery/","title":"Nrf52 companion storage recovery","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_companion_storage_recovery/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nrf52_power_management/","title":"Nrf52 power management","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_power_management/#nrf52-power-management","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"

The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.

"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":""},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":""},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":""},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"

Shutdown reason codes (stored in GPREGRET2):

Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board family Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (xiao_nrf52) Yes Yes Yes SenseCAP Solar Yes Yes Yes RAK4631 / RAK3401 Yes Yes Yes GAT562 30S / EVB Pro / Tracker Pro / Watch13 Yes Yes Yes Heltec T096 / T114 / T1 / Tower V2 Yes Yes Yes Muzi Works R1 Neo Partial Inactive Inactive Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite / T-Impulse Plus No No No WIO Tracker L1 / L1 E-Ink / WM1110 No No No Mesh Pocket / Meshtiny / Nano G2 Ultra No No No ThinkNode M1 / M3 / M6 No No No T1000-E No No No Ikoka Nano / Stick / Handheld (nRF) No No No Keepteen LT1 / Minewsemi ME25LS01 No No No

Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - R1 Neo has the integration compiled, but its board configuration deliberately sets PWRMGT_VOLTAGE_BOOTLOCK to 0; automatic protective shutdown and its voltage/VBUS recovery wake are therefore inactive. - User power-off does not enable LPCOMP wake; voltage recovery is armed only for boot protection and automated low-voltage shutdown. - VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.

"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#rak-peripheral-shutdown","title":"RAK peripheral shutdown","text":"

Explicit power-off and timed deep sleep on RAK4631 and RAK3401 stop peripheral owners before switching off the shared 3V3_S supply. On RAK4631, Ethernet must quiesce first; if its worker cannot stop within the bounded shutdown interval, keep its rail and bus intact instead of removing power during an SPI operation. Detected external-storage holds also retain the supply, including after an unresolved flash-operation timeout. Before storage discovery and configuration finish, rail removal is refused. Shutdown does not probe again, cancel a staged update, or float a storage owner's select pin. This does not change ordinary interrupt-idle sleep, boot/low-voltage protection, or the configured WisBlock GPS/shared-rail policy during normal operation.

The RAK19007 power schematic and RAK13800 connector schematic show that IO2 switches 3V3_S, not the separate upstream PoE VCC_IN supply. Merely compiling Ethernet support therefore does not require this rail to stay on after terminal shutdown. The RAK4631 radio-enable GPIO controls the RF-switch supply, not the SX1262 core supply.

RAK3401's external RAK13302 radio and RAK13800 Ethernet use the same IO slot, SPI signals, select pin and IO3 control. Their simultaneous operation is not supported; Ethernet owner registration on RAK3401 is rejected rather than allowing those electrically conflicting controls to run together.

Software shutdown cannot remove every hardware leakage path. The baseboard has I2C pull-ups to unswitched VDD. RAK13302 VD's boost enable follows IO2, but the legacy VC default resistor configuration connects it directly to VBAT; that boost cannot be switched off by firmware alone.

"},{"location":"nrf52_power_management/#xiao-battery-divider-safety","title":"XIAO battery-divider safety","text":"

XIAO and XIAO-based Ikoka startup preload READ_BAT_ENABLE LOW before enabling its output. XIAO protective and user shutdown also keep it LOW. Seeed warns that HIGH can overvoltage the ADC input during charging, so this is not a safe pin to switch HIGH as a power optimization. The schematic's 1 MOhm + 510 kOhm divider draws about 3 microamps at 4.2 V, not 3 milliamps. Charging current settings are unchanged. See Seeed's battery guidance.

"},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"

The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).

"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"

A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before: - SystemInit() (priority 102) - which clears RESETREAS - Static C++ constructors (default priority 65535)

This ensures we capture the true reset reason before any initialisation code runs.

"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"

To enable power management on a board variant:

  1. Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT

  2. Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)

  3. Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };

void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);

 if (enable_lpcomp) {\n   configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n

} #endif

void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...

#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```

For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).

  1. Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif
"},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"

The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered

VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).

LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):

REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V

Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold ~ (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).

"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"

The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)

This ensures compatibility regardless of BLE stack state.

"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"

Power management status can be queried via the CLI:

Command Description get pwrmgt.support Returns \"supported\" or \"unsupported\" get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power) get pwrmgt.bootreason Returns reset and shutdown reason strings get pwrmgt.bootmv Returns boot voltage in millivolts

Boards with nRF52 power management and a text CLI also support saved pwrmgt.bootlock, pwrmgt.cutoff, battery.profile, battery.empty, battery.full, and relative adc.multiplier settings. See the CLI reference for their ranges, board-specific defaults, and wake behavior. KISS builds honor a saved policy if one exists, but have no text CLI for changing it.

On boards without power management enabled, get pwrmgt.source and get pwrmgt.bootmv return:

ERROR: Power management not supported\n

get pwrmgt.support returns unsupported. get pwrmgt.bootreason is not compiled out at all and answers on every board: getResetReason() and getShutdownReason() are virtuals on the base board class, so a board that does not override them reports Not available rather than an error. ESP32 boards override the reset half with esp_reset_reason(), so they return a real reset reason and Not available for the shutdown reason.

"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"

When MESH_DEBUG=1 is enabled, the power management module outputs:

DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n
"},{"location":"nrf52_power_management/#further-power-management-work-planned","title":"Further power-management work (Planned)","text":"

For the opt-in Sensor check-in workflow using System ON idle sleep and bounded GPS acquisition, see Sensor dog tracker. It is separate from the battery-protection SYSTEMOFF shutdown described here.

"},{"location":"nrf52_power_management/#references","title":"References","text":""},{"location":"nrf52_sleep_event_validation/","title":"Nrf52 sleep event validation","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_sleep_event_validation/#nrf52-idle-sleep-regression","title":"nRF52 idle-sleep regression","text":""},{"location":"nrf52_sleep_event_validation/#cause","title":"Cause","text":"

The MCU could repeatedly enter the board's sleep function without actually sleeping. Three operations could leave the Cortex-M event register set:

  1. The pinned Adafruit core yields after every Arduino loop(). Its FreeRTOS yield issues SEV.
  2. The old board sleep function queried SoftDevice state immediately before waiting. In the measured SoftDevice-off case this SVC recreated the event. Moving only the scheduler-event clear was therefore insufficient.
  3. Adafruit CDC available() yields when its receive FIFO is empty. A USB client that never asserted DTR but established an RX session could therefore recreate an event on every idle console poll.

With SoftDevice enabled there is an additional, separate application-interrupt history. The core's PendSV yield updates it. Clearing the ARM event register does not acknowledge that history, so the next sd_app_evt_wait() still returns immediately. This follows from the public SDK contract and the pinned S140 6.1.1/7.3.0 interrupt-forwarder/wait code, not just the ARM event model. The failed SoftDevice-on captures below had another blocker as well.

There was also a disabled but pending CryptoCell IRQ42 after BLE startup. SoftDevice's documented disabled-pending-IRQ rule prevents sleep in this state, regardless of the event-register/history fixes. The pinned CC310 library polls rather than enabling that interrupt; the pinned BSP already clears its pending bit in its own idle wait. The application-owned wait had missed that cleanup.

This is distinct from the existing sleep-policy guards. A large eligible time or a high number of wait calls does not establish CPU sleep residency.

"},{"location":"nrf52_sleep_event_validation/#fix-and-ordering","title":"Fix and ordering","text":"

All four nRF52 role loops now begin with prepareSleepCycle(). It queries the actual SDK SoftDevice state and USB power status. When SoftDevice is active, preparation forces a real application PendSV with taskYIELD(), checks that it was serviced in an unmasked application thread, and acknowledges its SDK history with one early sd_app_evt_wait(). A bare SEV cannot guarantee that this SDK call returns: the SDK can internally wait again without an application interrupt. The real PendSV is required. This ordering depends on the pinned core: the other SDK wait caller is the lower-priority idle task, which cannot consume that acknowledgement while the sketch task is ready. Preparation rejects exception context, masked interrupts, a suspended/stopped scheduler, a still-pending PendSV, or an invalidated cycle instead of trying that early SDK acknowledgement without proof.

Preparation then clears the stale ARM event with SEV; WFE, before servicing work or checking sleep predicates. A current loop's USB-power snapshot also avoids another state-query SVC during UI work.

On chips with CryptoCell, preparation also clears that specific pending IRQ only when its interrupt is disabled and the SDK is not in a critical region. It never sweeps other pending bits or clears an enabled CryptoCell owner's interrupt. Clearing NVIC pending does not discard the polling crypto result.

The snapshot is valid for one loop and at most one sleep attempt. Both application-task Bluefruit.begin() call sites invalidate it before startup, including partial or failed startup. Invalid, missing or failed preparation declines sleep. The existing SDK/direct-register USB access policy is retained. SoftDevice startup remains owned by the application task; the cache is not permission to add an asynchronous SDK enable/disable path racing a wait.

Primary USB RX polling now uses the native CDC0 FIFO query, with the existing session checks both before and after that query. It does not change DTR policy, CDC1 logging ownership, or the facade's blocking-read/write protections.

The late wait remains a single sd_app_evt_wait() when SoftDevice is enabled, or the existing WFE; SEV; WFE sequence when disabled. There is no late SEV; WFE; WFE double-wait: that could discard the only radio IRQ arriving after the caller checked for work. No blanket IRQ clearing or SEVONPEND change was added.

"},{"location":"nrf52_sleep_event_validation/#controlled-hardware-evidence","title":"Controlled hardware evidence","text":"

The test device was a RAK3401/nRF52840 with an external RAK13302 SX1262 on Mercerwood. Tests used 908.5 MHz, BW250, SF9, CR5, power saving on, manual RXPS 30000/10000 us, AGC 0, TX power 2, no forwarding/adverts and USB watchdog off. Runtime switches in private benchmark binaries allowed same-binary paired controls without reflashing. Benchmark controls are not production features.

Control Capture Time-weighted sleeping observations Original behavior 30 s 0% Early clear only 30 s 0% Non-yielding USB poll only 30 s 0% Early clear plus USB poll, still with late state SVC 30 s 0% No late SVC, non-yielding USB poll, without early clear 30 s 0% Early clear, no late SVC, non-yielding USB poll 90 s 76.7% Never-DTR client held open, old empty USB poll 30 s 0% Same client held open, non-yielding USB poll 30 s 82.9%

The first production candidate restored sleeping observations with SoftDevice off (6522/8331 samples, about 78% over 90 s), but not with SoftDevice on (0/8344 samples with a never-DTR client over 90 s; 0/2776 with the client closed over 30 s). This negative result exposed the separate SDK history and is why the final preparation also acknowledges a real PendSV before servicing work. The second candidate added that acknowledgement but still did not sleep with SoftDevice on. A breakpoint then confirmed the SDK branch, the actual S140 6.1.1 vector, and disabled IRQ42 pending (ISER1=0x81, ISPR1=0x400). Clearing only that bit, with the same second binary and no configuration change, restored 1911/2775 sleeping observations over 30 s. That control justified the specific disabled-CryptoCell cleanup, rather than a speculative IRQ change.

The final, normal Full repeater artifact was then tested without benchmark controls or a DTR bypass. Its application readback matched the packaged image byte-for-byte (SHA-256 8f6a1654f95d0179dad358d1fc68887eaacc41f4c60281a23099482e1f44af94).

Final production test Capture Sleeping samples Weighted estimate SoftDevice off, USB terminal closed 90 s 6526/8310 78.3% SoftDevice on after OTA start/stop, never-DTR client held open 90 s 6077/8341 73.0% SoftDevice on, client closed 30 s 1943/2776 70.0%

The client received valid CLI replies both before and after the capture. The final frozen SDK-on diagnostics had 281272 completed late waits, only 12 short returns, and zero vetoes/errors. Their accounting window was longer than the capture; those counters are not its sleep-residency measurement. All final samples had the same valid debugger/clock state described below.

In the controlled old/new USB-poll pair, the never-DTR client successfully issued get powersaving and held the same file descriptor open across both captures. Early clearing and removal of the late state SVC were held constant in that pair, isolating the USB poll change. A separate unsafe late-double-wait control also slept, but was rejected because of the lost-work race above.

Samples read DHCSR S_SLEEP at jittered 7-13 ms intervals. All causal captures had C_DEBUGEN set, HFCLKSTAT 0x10001 and DCDC baseline 1, without sampled halt, step, mask, lockup or reset flags. All timestamps were monotonic with no gap over 20 ms. Percentages weight each sampled state until the next timestamp; they are point-observation estimates, not exact sleep-residency accounting. Zero sleeping observations does not exclude extremely short sleeps. Different calibration phases can contribute to variation between otherwise fixed captures.

"},{"location":"nrf52_sleep_event_validation/#limits","title":"Limits","text":"

There was no current meter. These results establish the CPU-sleep regression, not a measured mA reduction or a complete explanation of the T096/Wio L1 screenshots. SWD and USB alter the power/clock environment. Radio RXPS, FEM/LNA, GPS, screen, regulator and USB clock loads require matched electrical tests. In particular the T096 FEM/LNA default differs from EasySkyMesh; this RAK cannot measure that board-specific load. No RF delivery or long battery-life claim follows from these captures.

"},{"location":"nrf52_sleep_event_validation/#sensor-tracker-wake-timer","title":"Sensor tracker wake timer","text":"

A separate tracker-mode issue posted xTimerChangePeriod() on every eligible loop. Waking the higher-priority timer daemon recreates a scheduler event just before the late wait. The helper now reuses an active timer whose remaining time is nonzero and no later than the requested wake. Its active/expiry/tick snapshot is protected by a short FreeRTOS critical section. Dormant, expired or too-late timers are rearmed outside that section; allocation/queue failure still keeps the node polling. No late event clearing was added.

Host tests execute the actual helper, including the original repeated-rearm negative control, deadline changes, one-shot expiration, queue failures, tick wrap and late IRQs. T1000e and RAK3401 Sensor builds pass. This timer follow-up has not been physically qualified in GPS tracker mode; the RAK captures above are repeater tests.

See power-saving diagnostics for the permanent on-demand CLI counters. Counters remain off by default. Wait-call counters cover the late eligible wait, not the guaranteed-nonblocking early SDK acknowledgement. Preparation failures are reported through the board error or veto path instead.

"},{"location":"number_allocations/","title":"Number allocations","text":"

View this page on MeshCore Docs.

"},{"location":"number_allocations/#number-allocations","title":"Number Allocations","text":"

This document lists unique numbers/identifiers used in various MeshCore protocol payloads.

"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"

The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.

To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.

NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.

Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.

Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com \u2014 https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com \u2014 https://buymeacoffee.com/ripplebiz 0120 MCO Advanced most.original.address@gmail.com \u2014 https://hdden.ru/MCOa/ 0130 - 013F StreamSensor william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion 0140 Nelos support@nelos.app \u2014 https://nelos.app FF00 - FFFF -reserved for testing/dev-

(add rows, inside the range 0100 - FEFF for custom apps)

"},{"location":"optional_screens/","title":"Optional screens","text":"

View this page on MeshCore Docs.

"},{"location":"optional_screens/#optional-screens","title":"Optional screens","text":"

Use the normal display-capable firmware for your exact board and role. When automatic detection is included, that image starts the supported screen if it responds, or continues headless if it is absent. There is no screen-presence setting. Power the board off before attaching or removing an optional screen.

The firmware picker describes automatic screen detection only when the selected application has been verified to contain it. Previously published firmware retains its original behavior. These additions become available as new firmware is built and qualified; changing the picker does not add detection to an older download.

"},{"location":"optional_screens/#manufacturer-confirmed-optional-screens","title":"Manufacturer-confirmed optional screens","text":"Hardware Manufacturer evidence Firmware behavior Heltec T114 Heltec offers the optional stock screen. Automatic ST7789 detection; see the T114 guide. Heltec RC32 with T108 The T108 is a separate pin-header display module. The manufacturer example documents its pins and power controls. New display-capable images detect the NV3001B panel. Legacy without_display targets remain headless compatibility builds. Heltec Wireless Tracker V1/V1.1 The manufacturer listing explicitly offers Screen Yes/No. New display-capable images detect the ST7735 panel. Its absence releases only the display's power claim; GPS retains its own claim on the shared supply. Heltec V4 R8 Expansion Kit The manufacturer kit offers front-panel configurations; its V2 expansion schematic connects the display's separate return line. New R8 TFT images detect the panel through the documented MISO pin. Existing touch reset and backlight-only sleep/wake behavior are retained. Heltec V4/V4 R8 OLED The manufacturer listing offers OLED and No Display configurations. Existing SSD1306 images already check whether the OLED responds. RAK WisBlock The RAK4631 kit guide labels the RAK1921 OLED optional. Display-capable RAK4631 and RAK3401 images check presence and detect SSD1306 or SH1106 controllers. New images also provide a saved controller choice; see below. iKoka Stick and Handheld The designer documents an optional Stick OLED and optional Handheld OLED. Existing display-capable images already check presence. Handheld Companion supports the screen; currently headless infrastructure recipes remain headless. Original LilyGo T-Beam LilyGo offers soldered/non-soldered OLED configurations. Existing display-capable SSD1306 images already check presence. XIAO ESP32-S3 with Wio-SX1262 Seeed lists the expansion board as an accessory for adding screens and sensors. Existing display-capable Companion images already check presence. Muzi Base Uno/Duo with SuperIO SuperIO adds the OLED, GPS, buttons and buzzer to the matching Base radio. New Companion images probe the SH1107 display, leave its 12 V supply off when absent, and discover GPS independently. Uno and Duo retain their different radio drivers. LilyGo T-Echo Lite core/display assembly LilyGo documents the split core/base design. New Companion images check SSD1681 status readback before e-paper initialization and continue headless when the panel does not respond. The old non-shell target names remain compatibility recipes.

The older V4 TFT expansion configuration is also optional, but its current recipe has no verified panel readback connection. Detection is not enabled for that wiring. A touchscreen acknowledgement alone does not establish that the LCD responds. The V4 R8 TFT and older V4 TFT pin mappings are distinct.

RAK4631 and RAK3401 images with OLED controller detection support set display.controller auto, ssd1306 or sh1106 in all display-enabled roles. Keep auto for normal use; choose the panel's controller explicitly if detection gives shifted or wrapped pixels. The choice is saved and applies after restarting the node. get display.controller shows the configured and active choices and any pending restart. With no saved choice, automatic detection or an existing build-time override supplies the default; saved auto takes precedence over that override. Normal controller selection does not require custom firmware. See the controller commands.

New R8 TFT images support saved display.rotation values of 0, 90, 180 and 270, including portrait/landscape dashboard changes without reflashing. The stock R8 expansion board has no accelerometer for automatic orientation. See the rotation commands.

The variant consolidation validation records the ten qualified packages, software checks and remaining physical tests for R8 rotation, Muzi SuperIO and T-Echo Lite detection.

Full Wireless Tracker repeaters retain the board's 50-neighbor capacity, as the V1.1 and V2 Full profiles already do. This preserves RAM for the screen, GPS, MQTT and update services; the RAM admission limits remain unchanged.

The R8 readback route is documented in Heltec's Expansion Kit V2.03 schematic. Tracker V1 wiring is documented in the V1.1 schematic.

Screen presence does not establish controller type, dimensions, physical orientation, or board revision. Continue choosing the exact hardware profile. Use USB when changing an old firmware target name; a different target's LoRa update package is not interchangeable. Saved identity and settings do not need to be erased for screen detection.

"},{"location":"optional_screens/#integral-screens","title":"Integral screens","text":"

Manufacturer documentation identifies built-in screens on the LilyGo T-Echo Card, Heltec T190, T1, T096, Wireless Tracker V2, ThinkNode M9 and T-Deck. No new optional-screen detection is enabled for these boards. In particular, the T190's optional module is its LoRa radio, not its screen.

"},{"location":"optional_screens/#validation","title":"Validation","text":"

The new probes run before display SPI initialization and framebuffer allocation, with the backlight off. Bounded repeated reads under opposite pull directions reject floating, stuck and unstable return lines. An absent screen stays disabled until reboot; later drawing or wake requests cannot power it back up. Shared supplies retain other peripherals' ownership.

Native tests cover controller replies, timing, pin turnaround, power ownership, headless behavior, rendering, sleep/wake, and exclusions for integral screens. Release checks require the detection marker in the uploadable application, the exact resolved board/driver flags and the matching source commit before the picker can advertise detection.

Check Result Native detection, rendering, power and headless regressions PASS, 31 test methods, including 12 new detection methods with sanitizers and negative controls Recipe, release-proof, controls and offline-picker regressions PASS, 42 test methods Resolved PlatformIO configuration PASS, 31 optional-screen environments across the three hardware families; integral-screen recipes excluded Picker JavaScript regressions PASS, 4 suites Complete build-profile selection suite PASS Hosted documentation links PASS, 7 test methods Packaged ESP32 firmware smoke builds PASS, RC32 Full Companion, Tracker V1 Full Repeater/MQTT, Tracker V1.1 Full Repeater/MQTT, and V4 R8 TFT Full Companion

The qualified smoke packages use firmware source 8268befbd. All four uploadable applications contain their board-specific detection marker and matching firmware/source version. Their bound package hashes and generated picker controls pass verification. RAM headroom after the complete modeled allocation and transient margin is 68,672 bytes for RC32, 1,942 for Tracker V1, 4,054 for Tracker V1.1, and 66,528 for V4 R8 TFT. These are build-time admission results, not measurements of physical runtime load.

Physical verification on RC32, Tracker V1 and V4 R8 with and without their manufacturer-supported screen has not been performed. Hardware qualification requires cold USB/battery boots, screen sleep/wake, headless input, normal radio transmit/receive, and GPS or touch operation where fitted.

"},{"location":"ota_easy/","title":"Ota easy","text":"

View this page on MeshCore Docs.

"},{"location":"ota_easy/#easy-firmware-updates-over-lora","title":"Easy firmware updates over LoRa","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

This guide shows the shortest manual path for sending firmware from a computer to a MeshCore node over LoRa. Choose the package type for the destination node:

For an end-to-end controller that accepts a release ZIP or ready mOTA, see Scripted LoRa OTA from start to finish.

Destination Update type Files needed to build the .mota Installer ESP32 Full firmware New non-merged application .bin ESP32 A/B firmware slots nRF52, internal staging In-place delta Exact running firmware.hex and new firmware.hex Exact-board OTAFIX bootloader Supported nRF52 QSPI repeater Full firmware or in-place delta New firmware.hex; a delta also needs the exact running firmware.hex Matching QSPI-aware OTAFIX bootloader MeshTower V2 SD target Full firmware or in-place delta New firmware.hex; a delta also needs the exact running firmware.hex Matching SD-aware OTAFIX bootloader

An internal-staging nRF52 target accepts only an in-place delta built against its exact running firmware. Matched QSPI repeater targets and the MeshTower V2 microSD target can also install a full image because the complete container stays off-chip. See nRF52 repeater OTA with external QSPI and MeshTower V2 microSD LoRa OTA.

MeshTower V2 microSD application and bootloader containers must be Ed25519-signed by a key in the device allowlist; pass --sign to motatool build (or --sign-key to the end-to-end runner) when building one. Its BLM2-capable SD-aware bootloader is also required before ordinary application or bootloader OTA can use the reset-retained authorization record. Upgrade preview.12 locally over USB/BLE DFU or SWD first; there is no raw-card compatibility handoff.

"},{"location":"ota_easy/#temporary-ota-channel-used-in-this-guide","title":"Temporary OTA channel used in this guide","text":"Setting Value Center frequency 910.525 MHz Bandwidth 250 kHz Spreading factor SF5 Coding rate used in this guide CR5 Example window 120 minutes

The copy/paste command is:

tempradio 910.525,250,5,5,120\n

The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates do not need to match.

tempradio is not saved and the node returns to its normal radio settings when the window ends or the node reboots. Current full-parser firmware also accepts normalradio, which cancels the temporary window and restores the saved tuple after replying on the current channel. This frequency is intended for North American configurations. Confirm that it is permitted in your location and change it when necessary.

At SF5/BW250, an SX1262+TCXO destination can keep RXPS enabled only when it and every sender that can reach it (controller, source, and relays) follow the v1.17.1.5-or-newer adaptive-preamble contract. The qualified destination boundary is effective level 8, preamble 64 and 1252 / 6424 us. For manual setup, save set radio.rxps level 8 preamble 32 before entering TempRadio; the 32 is a conservative saved timing assumption and current firmware selects the real 64-symbol wire preamble when it retunes the temporary radio. For an older, unknown, or mixed-version path, use set radio.rxps off for the maintenance window. First record get radio.rxps.config; after normalradio, restore a reported level with set radio.rxps level N preamble P (omit preamble P when P is 0). A legacy node returns only on/off and periods, which can be restored with set radio.rxps RX_US SLEEP_US. The scripted runner performs the version checks, temporary change, verification, and restoration automatically while preserving automatic retuning. SF5/BW500 is too fast for RXPS even with a 64-symbol preamble, so current firmware selects 128 and uses the qualified level-8 626 / 6398 us timing when every participant supports the adaptive contract.

"},{"location":"ota_easy/#before-you-start","title":"Before you start","text":"

Both paths require:

LoRa OTA packets are generated, consumed, and relayed only while tempradio is active. Intermediate repeaters apply their normal forwarding filters, duplicate checks, and flood limits; they do not interpret the OTA payload. If any required window closes, the transfer stops making progress and can resume during a later overlapping window.

build.sh provides a *_repeater_lora_ota_no_external_sensors or *_room_server_lora_ota_no_external_sensors build for standalone infrastructure targets that need a smaller internal update workspace. nRF52 sensors also have *_sensor_lora_ota_no_external_sensors builds. ESP32 room servers and sensors use their expanded Full image for LoRa OTA. These compact siblings omit selected optional environmental and ranging drivers while retaining board-native features such as displays, buttons, battery monitoring, and GPS where the target uses the GPS-preserving lean profile. RAK3401 and RAK4631 reduced builds also retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors; together these drivers cost less than 5 KiB in the measured profile. Those four monitor drivers are not the only retained I2C support: the board's SSD1306 OLED, supported autodiscovered RTCs, and the RAK12500 GPS remain separate I2C peripherals in GPS-compatible RAK recipes. RAK12501/L76K GPS instead uses UART Serial1. The RAK3401 OTA repeater supports either GPS module in sensor slot A; slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The RAK4631 OTA repeater defaults its runtime RS-232 bridge to Serial2 so the UART RAK12501 can retain Serial1. The runtime guard always reserves Serial1 because a bounded silent probe cannot prove a cold RAK12501 is absent; the shared WB_IO2/3V3_S rail keeps a fitted module powered and driving the UART. Use Serial2. Serial1 requires an explicit no-GPS/dedicated image. The merged image remains fail-closed even when it detects an I2C RAK12500. The explicit legacy Serial1 bridge omits the combined GPS provider, including the otherwise non-UART RAK12500 path.

The firmware-configured INA3221 address and the RAK12500 address are both 0x42; they cannot coexist on one bus at those addresses. Keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

Selected nRF52 boards with matched external QSPI application and bootloader support can instead make the normal full-sensor repeater or room-server install-capable; those targets do not need to reserve internal flash for the downloaded container. SolarXiao 30S and 33S use this matched external-QSPI path: repeaters retain their existing OTA target, and room servers use the explicit *_room_server_lora_ota target with the matching OTAFIX QSPI bootloader. A RAK19007 can use the same separately wired W25Q16 with either RAK4631 or RAK3401 + RAK13302: the flash consumes no WisBlock slot and GPS remains supported in slot A. This is not the RAK15001 arrangement; RAK15001 and RAK13302 share a chip-select and cannot be used together. Other normal repeaters can still serve as intermediate relays but cannot necessarily install an update themselves. ESP32 -ota- siblings also retain the lightweight browser WiFi uploader (start ota), the complete CLI, and up to 254 neighbors; constrained targets use 50 as recorded in their capability manifests. RP2040 and STM32 repeaters do not currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely during TempRadio.

For one explicitly selected internal-flash nRF52 repeater or room server, the default --build-profile auto run emits two install-capable choices. The *-full-ota-* artifact retains external sensors, while the *_lora_ota_no_external_sensors-reduced-ota-* artifact trades only those optional drivers not explicitly retained by the board profile for more delta-staging space. They retain the same OTA target identity so update tooling does not confuse the optimization choice with a different board. QSPI- and SD-backed repeater and room-server roles emit only the complete choice unless a measured size failure requires a fallback.

nRF52 -ota- siblings are compiled with size optimization instead of the Adafruit platform's default speed optimization. This prevents the retained software Ed25519 fallback from expanding beyond the fixed in-place workspace; CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features remain enabled.

ESP32 *-full-usb-wifi-ota-* artifacts retain all compiled features and enable LoRa OTA for every FULL role, including room servers, sensors, observers, and bridges. A FULL image requires its expanded partition table: install the matching merged image over USB once before installing later non-merged FULL updates over LoRa. ESP32 MQTT observers and ESP-NOW bridges are always emitted as FULL artifacts; compact-CLI variants are no longer built. The *-full-usb-wifi-ota-* profile compiles USB packet logging and direct WiFi MQTT into one image. Use set logging.output off|usb|wifi|both to persist the desired path. A *-full-logging-ota-* artifact is now only a fallback for a hardware/role combination without a WiFi MQTT target and can produce substantial serial output.

"},{"location":"ota_easy/#choose-the-source-radio","title":"Choose the source radio","text":"

Use an OTA-enabled MeshCore node as the source. It receives the update folder from the computer, then advertises it over LoRa. ESP32 USB/WiFi companions and FULL ESP32 roles include the required transport. A *_companion_radio_full target keeps only the source half of LoRa OTA: it serves host images but cannot stage or install one for itself. ESP32 full combines USB, BLE, and WiFi; nRF52 full combines USB and BLE because nRF52840 has no WiFi. A small set of high-capacity, non-PSRAM classic ESP32 companions keep their normal image and provide a separate -full-logging-ota- fallback with 100 contacts, 8 group channels, a 16-frame offline queue, and persistent USB output selection. Install that variant's merged image over USB once before using it. Connect the source by USB serial or, when supported, by WiFi. For an ordinary raw-text USB source, confirm that its USB CLI accepts:

ota folder on\n

If an older build reports that OTA_FOLDER_SERIAL is not compiled in, install a current -ota-, -full-usb-wifi-ota-, or applicable -full-logging-ota- build first. Do not use a KISS modem: KISS firmware is a TNC/KISS frame interface and does not provide the MeshCore CLI or the OTA-folder transport that motatool serve requires.

An nRF52 companion_radio_full starts in its USB ASCII terminal and automatically changes to Binary Companion when it receives a complete < frame. It also recognizes motatool's exact initial ota folder on line and enters exclusive mOTA mode directly, so motatool serve --serial can be the first client after boot. No terminal token or preliminary disconnect is required. Stopping the tool or disconnecting resets USB to Binary. BLE remains available throughout. See the Full Companion USB switcher guide for the complete ownership transitions. Protocol v14 can instead take the catalog from a paired phone or Linux host over a separate encrypted BLE mOTA service while Binary Companion remains active. USB and BLE catalog sources are mutually exclusive. See the Full Companion Bluetooth source guide.

For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:

motatool serve --dir ./motas --tcp <source-host>:5001 -v\n

Port 5001 is separate from the companion application port (5000) and the HTTP configuration/browser-OTA port (80, depending on the role). An ESP32 companion_radio_full also has its complete role-specific text terminal on port 5002; OTA automation uses its ota and TempRadio subset. See the full Companion guide. On a FULL repeater or room server, start webconfig can bring up the saved WiFi connection. Other FULL roles with browser OTA support can raise MeshCore-OTA with start ota and use 192.168.4.1:5001. The TCP seeder auto-attaches; do not also run ota folder on for USB serial.

"},{"location":"ota_easy/#install-motatool","title":"Install motatool","text":"

Install Rust if necessary, then install the standalone packaging and serving tool:

git clone https://github.com/vk496/motatool\ncargo install --path ./motatool\n
"},{"location":"ota_easy/#esp32-package-a-full-firmware-image","title":"ESP32: package a full firmware image","text":"

The destination must be an OTA-capable ESP32 with an A/B partition table. Download or build the new non-merged .bin application for the destination's exact board and role. Do not use an ESP32 -merged.bin factory image.

Put the application firmware in a working directory, then build a full .mota container. For example:

mkdir -p ./motas\nmotatool build --fw ./Heltec_v3_repeater-ota-v1.16.05.bin --out-dir ./motas\nmotatool verify ./motas/*.mota\n

Replace the example filename with the firmware for the destination's exact target. With no --base argument, motatool build creates a full-image update. The firmware must contain its MeshCore EndF identity trailer so motatool and the destination can verify the target, hardware, and version.

Do not continue if motatool verify reports a failure.

"},{"location":"ota_easy/#nrf52-package-a-full-image-or-in-place-delta","title":"nRF52: package a full image or in-place delta","text":""},{"location":"ota_easy/#1-install-and-check-the-otafix-bootloader","title":"1. Install and check the OTAFIX bootloader","text":"

This is a one-time prerequisite. Install the OTAFIX bootloader built for the destination's exact board from the OTAFIX nRF52 bootloader releases. Internal-staging delta updates require OTAFIX 2.4 or newer. External QSPI and SD staging require OTAFIX 2.4.1 or newer and release notes that explicitly list the exact board and storage mode. Follow the release's board-specific installation and erase instructions. If it does not contain the destination's exact board, this LoRa install path is not yet available for that board; never substitute a similar board's bootloader.

Before preparing or downloading a LoRa update, run this on the destination:

get bootloader.ver\nota self\n

The first command identifies the installed nRF52 bootloader. Continue only if the ota self reply includes the apply mode required by this target:

bootloader: apply OK\nbootloader: QSPI apply OK\nbootloader: SD apply OK\n

The reply also contains the running firmware's base_hash. Save it for the package check below. A stock, legacy, or older OTAFIX bootloader without .mota in-place-apply support will report that apply support is missing, and ota install will refuse to reboot into it.

"},{"location":"ota_easy/#2-choose-full-or-delta-packaging","title":"2. Choose full or delta packaging","text":"

For a QSPI- or SD-backed target, a full update needs only the new raw firmware.hex:

mkdir -p ./motas\nmotatool build --fw ./LilyGo_T-Echo_repeater-new.hex --out-dir ./motas\nmotatool verify ./motas/*.mota\n

Use a full package when an exact base image is unavailable. Use the delta path below when reducing airtime is worth retaining the exact running image.

"},{"location":"ota_easy/#3-keep-the-exact-current-and-new-application-images-for-a-delta","title":"3. Keep the exact current and new application images for a delta","text":"

You need the raw .pio/build/<environment>/firmware.hex from the build that is actually running, plus the corresponding firmware.hex from the new build. Save the current file before building the new version, because PlatformIO reuses that path. For example:

# Save this immediately after building/flashing the version now running on the node.\ncp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \\\n  ./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex\n\n# After checking out and building the new version, save its image separately.\ncp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \\\n  ./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex\n

Replace Heltec_t114_repeater_lora_ota_no_external_sensors with the destination's exact PlatformIO environment. The two images must be for the same board and role, and both must contain their EndF trailers. Do not pass a release .uf2 or BLE-DFU .zip to motatool; those are installation containers rather than raw application images.

Keeping a file with the same version label is not enough: the base must be byte-for-byte identical to the running application. The hash check in the next step proves that it is the right file.

On RAK4631 repeaters, use the RAK_4631_repeater_lora_ota_no_external_sensors environment. It retains built-in battery monitoring but omits selected optional environmental/ranging drivers so the delta fits the safe in-place workspace. INA219, INA226, INA260, and INA3221 I2C voltage/current monitors remain supported despite the legacy target name. That trim does not remove the SSD1306 OLED, autodiscovered I2C RTCs, or GPS-compatible RAK12500 I2C path. The 0x42 RAK12500/INA3221 addressing rule above still applies. If the device has a RAK15001 installed in sensor slot C and the matching RAK15001 OTAFIX bootloader, use RAK_4631_repeater_rak15001_slot_c_lora_ota instead. That target retains the full sensor/GPS set and can install either a full image or a delta from the external 2 MiB store.

With the separately wired Winbond breakout, use RAK_4631_repeater_w25q16_lora_ota for RAK4631 or RAK_3401_repeater_rak13302_w25q16_lora_ota for RAK3401 + RAK13302. Both require an EF4015 2 MiB device. As a one-time prerequisite, install wiscore_rak4631_w25q16 for RAK4631 or wiscore_rak3401_rak13302_w25q16 for RAK3401 + RAK13302. Wire CLK/MISO/MOSI to the RAK19007 IO connector pins 26/27/28, CS to J11 AIN1, and VCC/GND to J12 VDD/GND. Fit an approximately 10 kOhm CS-to-VDD pull-up; never use VBAT. The SPI pins require underside taps or an interposer, but no sensor slot is occupied. See the nRF52 QSPI guide before installing either image.

For a build that uses only the 2.54 mm headers, use the unified RAK_4631_repeater_unified_lora_ota or RAK_3401_repeater_unified_lora_ota image with its matching compatible OTAFIX 2.4.10 bootloader. Wire CLK to J10 TX1, DO to J10 RX1, DI to J11 IO1, and CS to J11 AIN1; keep the CS pull-up and J12 power wiring. This gives up UART1 GPS/PPS but leaves I2C sensors and the RAK3401 radio available. See the header-only wiring table.

"},{"location":"ota_easy/#4-build-and-check-the-in-place-delta","title":"4. Build and check the in-place delta","text":"
mkdir -p ./motas\nmotatool build \\\n  --base ./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex \\\n  --fw ./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex \\\n  --patch-type in-place \\\n  --out-dir ./motas\nmotatool verify ./motas/*.mota\n

motatool prints the generated filename. Inspect that file:

motatool inspect ./motas/GENERATED_FILENAME.mota\n

Check all three of these before serving it:

Current layout-aware motatool derives the safe workspace from the new firmware's authenticated EndF layout record, so the normal recipe deliberately omits --inplace-memory. If older tooling requires an explicit override, use 0xC6000 only for matched external SD/QSPI staging and 0x98000 for an internal-staging nRF52 such as the RAK4631 lean OTA build. Do not substitute the external value on an internal target, and do not override either value unless you have verified the exact app base, bootloader, and package geometry. Do not continue if verification or any identity check fails.

"},{"location":"ota_easy/#transfer-and-install-either-package","title":"Transfer and install either package","text":""},{"location":"ota_easy/#1-start-the-temporary-ota-channel","title":"1. Start the temporary OTA channel","text":"

First save the current ota config output. For a direct source-to-destination link, set both OTA-enabled endpoints to direct-only mode before starting the transfer:

ota config hops 0\n

Do not use hops 1 as extra margin on a direct link. It tells a receiving repeater to retransmit each direct DATA/PROOF packet; that unnecessary half-duplex transmission can make it miss the source's next fragment, especially with older single-block receivers. Use hops 1 only when one real intermediate relay is required, hops 2 for two, and so on. This setting is saved, so restore the original value after the maintenance window.

On the source node, destination node, and every intermediate repeater, then run:

tempradio 910.525,250,5,5,120\n

All participating nodes must use the same frequency, bandwidth, and spreading factor. Their time windows must overlap. Start with the farthest hop (the destination) and work back toward the source when using tempradio.

If you administer the destination over LoRa, the controller used to send later ota commands must also be able to communicate on this temporary channel. For unattended nodes, use synchronized tempradioat entries instead of manually starting the windows. Ensure the nodes' clocks are set before using tempradioat.

"},{"location":"ota_easy/#2-serve-the-update-from-the-computer","title":"2. Serve the update from the computer","text":"

Close any serial terminal using the source node's USB port, find its device name, and start the server:

motatool serve --dir ./motas --serial /dev/ttyACM0 -v\n

Replace /dev/ttyACM0 with the USB serial device of the source companion selected above. motatool attaches the folder to the source, which advertises the update over LoRa while its temporary-radio window is active. KISS modem serial ports cannot be used here.

For an nRF52 Full Companion and a Bluetooth-capable host, the equivalent cable-free source is:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion \\\n  --dir ./motas \\\n  --local 'tempradio 910.525,250,5,5,120'\n

Leave this command running until the destination finishes downloading.

"},{"location":"ota_easy/#3-find-and-download-the-update","title":"3. Find and download the update","text":"

On the destination node, check its state and ask for nearby updates:

ota status\nota ls\n

Discovery is asynchronous. ota ls says refreshing; wait a few seconds and run it again even if it first shows an older row. Select [same target]: it can say full for ESP32 and external SD/QSPI nRF52 targets, or delta for any supported nRF52 target. Do not select [unsupported] (for example, a source's self-served full image on an internal-staging nRF52). Rows labelled bootloader are also outside this ordinary application flow: they are never automatic and ota install refuses them. Only an already-capable, allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2 microSD nRF52 target can use the separate explicit bootloader workflow. Use the row's stable eight-hex manifest ID rather than its changing list position:

ota pull 838B8169 flash\n

If a legacy internal-flash nRF52 reports no EndF, only a row marked [rescue] is eligible. Current rescue-capable firmware requires ota pull <mid8> flash rescue, followed after completion by ota rescue install <base_hash16>. Older running firmware without those commands must be recovered over USB. Shared-internal bootloader-update builds are stricter: without a valid live EndF, every internal pull is refused before erase because the normal application may extend through 0xED000. Recover those builds over USB/BLE DFU or SWD instead of relying on the older 608 KiB rescue estimate.

Monitor the transfer:

ota status\n

To adjust LoRa OTA pacing, use get ota.speed and set ota.speed <0.05..3> on the source, destination, and OTA relays. 1 preserves the existing timing; lower values slow OTA down, and higher values shorten adjustable delays. This saved setting covers both profiles and OTA adverts as well as the transfer. It does not change radio modulation or the configured airtime budget. See LoRa OTA speed for details.

For a powered bench update where restart-resume is not needed, ota config checkpoint 0 on the destination removes periodic progress writes. It is a smaller optimization than selecting the correct hop count and it trades away persisted mid-download resume; restore the previous checkpoint cadence afterward.

Current repeater firmware automatically uses the full transmit budget during a bounded TempRadio window. Older receivers do not: if get af reports a nonzero value on such a node, record it, use set af 0 for the maintenance window, and restore it afterward. This affects how promptly the legacy receiver can send its next block/proof request; it does not increase LoRa transmit power. Use a full duty budget only where the selected frequency and local rules permit it.

The update is ready when the status says ready to install. Discovery is background traffic, while an active OTA download is primary mesh traffic. At the temporary-radio settings in this guide, allow roughly one hour for a typical ESP32 full image over a quiet, direct link. That is a planning estimate, not an upper bound: repeaters, retries, weak links, and normal mesh traffic can extend it well past an hour. The 120-minute example window is intentional. If necessary, start another overlapping tempradio window; the download resumes rather than starting over.

"},{"location":"ota_easy/#4-verify-and-install","title":"4. Verify and install","text":"

Once the destination reports ready to install, run:

ota install\n

The destination verifies the complete package again before approving it. ESP32 installs the full image into its inactive A/B slot. nRF52 checks the bootloader and storage capabilities and, for a delta, the base hash, then reboots into OTAFIX. The bootloader independently rechecks the package, installs the external full image or applies the delta in place, and verifies the resulting image. Pre-install failures leave the running firmware unchanged and report the reason. If power is lost after an nRF52 in-place apply has begun, OTAFIX will not boot a partial image; it enters recovery DFU so a known-good application can be restored.

After the node returns, reconnect on its normal radio channel and confirm:

ota status\n
"},{"location":"ota_easy/#quick-troubleshooting","title":"Quick troubleshooting","text":"

For additional commands and safety details, see the full OTA user guide. For protocol and container internals, see the OTA protocol specification.

"},{"location":"ota_meshtower_v2_sdcard/","title":"Ota meshtower v2 sdcard","text":"

View this page on MeshCore Docs.

"},{"location":"ota_meshtower_v2_sdcard/#meshtower-v2-microsd-lora-ota","title":"MeshTower V2 microSD LoRa OTA","text":"

Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors is the one canonical MeshTower V2 repeater target. Its SD OTA target ID 0A9DBBF0, hardware ID Heltec_tower_v2, application layout, and artifact names remain unchanged. The required Full and Reduced sensor choices use that same target. Existing SD users need no configuration change.

The build aliases Heltec_tower_v2_repeater and Heltec_tower_v2_repeater_lora_ota_no_external_sensors resolve to this primary target. An installed internal-only image has a different OTA identity and bootloader/layout contract: migrate it once with the matching combined OTAFIX profile and application over local USB/BLE DFU or SWD. An alias does not make that migration a same-target LoRa update.

"},{"location":"ota_meshtower_v2_sdcard/#automatic-storage-selection","title":"Automatic storage selection","text":"

The onboard microSD socket remains primary. A mountable card accepts full application images, in-place application deltas, and explicitly selected signed v3 bootloader packages. After verification, the SD-aware OTAFIX bootloader reads the staged file and programs the application or bootloader.

With a missing or unmountable card, the same application can select internal flash for signed application in-place deltas only. The signer must be trusted by the node, as on the SD application path. This fallback requires the published combined MeshTower SD/internal profile in OTAFIX 2.4.11: the privileged storage profile must remain exactly 09 (SD|BOOT_UPDATE), with a separate positive MOTASTOR optional-application storage record of 02 (STAGE_CEILING). A missing record or an internal-only bootloader does not authorize fallback. Full images, bootloader packages, SD archiving, and automatic own-image offering remain unavailable on internal storage.

Selection is deferred until OTA use so card probing does not delay USB setup. The selected backend stays pinned through the transfer, including a paused or failed transfer. Removing a card produces a storage failure; it does not move the partial download into internal flash. Use ota cancel before selecting storage again after changing the card. The fallback uses internal flash only; it does not reserve or use the 64 KiB APPHYBRID retained-RAM arena.

The pin assignment follows the Heltec MeshTower V2 partial reference circuit:

Signal nRF52840 pin Arduino pin number SD CS P1.00 32 SD MOSI P1.01 33 SD SCK P0.06 6 SD MISO P0.26 26

The SD socket uses its own SPI peripheral, so card traffic does not change the LoRa radio pinout.

"},{"location":"ota_meshtower_v2_sdcard/#card-requirements","title":"Card requirements","text":"

Use a FAT16, FAT32, or exFAT card accepted by the bundled SdFat version. The normal update path imposes no additional MBR/sector-1 layout requirement; filesystem layouts SdFat cannot mount are still rejected. MeshCore creates /meshcore-ota.mota as a contiguous file and passes its exact sector range to OTAFIX in reset-retained MCU RAM.

MeshCore never reads or writes raw sector 1 and does not infer ownership from blank card sectors. Both application and bootloader OTA require a locally provisioned BLM2-capable bootloader that understands the retained-RAM record. Preview.12 must first be upgraded over USB/BLE DFU or SWD.

"},{"location":"ota_meshtower_v2_sdcard/#capacity-and-update-types","title":"Capacity and update types","text":"

When SD is selected, the card removes the internal-flash staging limit. The .mota container may be much larger than the old internal staging gap, and either a full image or an in-place delta may be downloaded. The installed firmware itself must still fit the nRF52840 application region below InternalFS (ending at 0xED000); SD storage does not increase the MCU's executable flash.

For this S140 v6 target, the maximum application image including its EndF trailer is 0xC7000 bytes (815,104 bytes). To package a full self-update:

motatool build --fw ./Heltec_tower_v2_sdcard-new.hex \\\n  --sign ./trusted-signer.key --out-dir ./motas\nmotatool verify ./motas/*.mota\n

A delta uses the exact installed image as its base. The normal 0x98000 workspace remains compatible. If either image is larger than that legacy limit, build the patch with the SD target's larger workspace:

motatool build \\\n  --base ./Heltec_tower_v2_sdcard-running.hex \\\n  --fw ./Heltec_tower_v2_sdcard-new.hex \\\n  --patch-type in-place \\\n  --inplace-memory 0xC7000 \\\n  --sign ./trusted-signer.key \\\n  --out-dir ./motas\nmotatool verify ./motas/*.mota\n

The internal fallback has a smaller flash staging gap and in-place workspace. A delta generated for the larger SD workspace can be rejected even though it updates the same application and target. Keep a usable card inserted for that package, or generate an exact-base delta that fits the fallback's available workspace and staging capacity. The firmware refuses an oversized package; it does not switch backends during its download.

An SD download is resumable because the partial .mota stays on the card. Once it reaches ready, ota install performs the final verification, publishes a one-reset authorization record, and reboots. SD application installation requires a valid signature from a key in the node's allowlist. Keep the card inserted through the reboot and installation.

The BLM2 retained-auth SD-aware bootloader is mandatory for application OTA. ota install refuses to reboot if its continuity metadata does not match the running S140 family/FWID/application layout or if its capability marker does not advertise SD staging and the selected codec. Earlier internal-only MeshTower firmware retains its old identity until the deliberate local migration described above.

"},{"location":"ota_meshtower_v2_sdcard/#signed-bootloader-update","title":"Signed bootloader update","text":"

Only the canonical SD target with SD selected exposes a usable privileged LoRa bootloader-update path. Internal application fallback does not stage bootloader packages. It requires an already installed exact-board ABI-3 OTAFIX bootloader whose one unambiguous capability marker is exactly 0x09 (SD|BOOT_UPDATE) and whose codec mask is 0x0005 (FULL|INPLACE). Requiring both application codecs prevents a bootloader self-update from disabling either normal SD application path. A stock bootloader still requires USB/BLE DFU or SWD. The sole remote bootstrap is intentionally absent: preview.12 and any other legacy-v1 image must be upgraded locally before either SD application or bootloader OTA.

The signed v3 container is exactly 41,330 bytes and carries a 40 KiB candidate for the installed 239A0071 / TOWER_V2_OTA identity (boot target 1150F50E). It uses the same contiguous /meshcore-ota.mota file as an application update. GPREGRET 0x6B plus the distinct SD source marker 0x53 selects the bootloader path; LoRa transport remains payload type 0x0C.

The application linker still ends at 0xED000, but OTAFIX needs 0xE0000..0xEA000 as temporary scratch while replacing itself. Before a boot package can be downloaded or approved, MeshCore requires a hash-valid live EndF proving the complete running image ends by 0xE0000; OTAFIX repeats that no-overlap check before its first erase. An application extending above that boundary can still receive normal FULL or delta application updates from SD, but its bootloader must be updated locally.

MeshCore also checks the live boot settings before touching that scratch page. Erased settings and a valid-app record with CRC disabled are allowed. When a nonzero bank CRC is active, its recorded bank_0_size must cover the complete EndF-inclusive running image and must end by 0xE0000; an undersized or oversized record refuses the operation and requires local DFU/SWD.

Because the card is removable, approval is bound to the exact bytes that were authenticated. For both fmt2 application and fmt3 bootloader packages, MeshCore authenticates one exact signed manifest, requires every streamed manifest byte to match it, and verifies the leaves, payload, and image. During that same pass it hashes the entire container with only the mutable four-byte APRV field normalized to zero. After writing and syncing APRV, MeshCore publishes a 72-byte MOTASDA2 record at reset-retained RAM address 0x20006008. The record binds package purpose/format, exact LBA range, card size, container length, and that normalized SHA-256. OTAFIX consumes and clears the record before reading the card; changing the card or file can only fail. A power loss erases the authorization and also fails closed.

For fmt3, MeshCore additionally writes and readback-verifies the temporary 64-byte MOTASDBL token at 0xE0000, binding the exact total and signed manifest image_hash. OTAFIX requires the parsed manifest, streamed payload, and final scratch image to match it. The token page becomes scratch during a successful boot update and is not permanently reserved.

Every new candidate also carries a backward-compatible BLM2/SOFT continuity extension next to its legacy embedded manifest. The complete 76-byte envelope is fixed at final raw-image offset 0x9FB4; a relocated copy is not a valid candidate. Its embedded bootloader version must equal the outer package version, match the runtime SoftDevice family/FWID, application base, and layout ABI, and be strictly newer than an installed BLM2 version. Preview values use low bytes 1..254 and a stable release uses 0xFF; zero-preview and all-ones versions are invalid. Remote bootloader rollback is not supported; use local DFU/SWD when rollback or migration is intentional.

Bootloader packages are never autofetched or autoinstalled. Select and confirm one exact package manually:

ota ls\nota pull <MID8> flash\n# wait for ota status to report the bootloader download ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy the MID and first 16 image-hash hex digits from ota bootloader. Ordinary ota install rejects this package, and the bootloader command rejects an application package. Keep the SD card inserted through the reboot. A later ota status value of blup:C8 reports a successful bootloader replacement.

After reboot, ota bootloader displays the installed board identity, CRC and capabilities. Its mid and hash fields describe a currently staged download, not the installed bootloader, so staged:none mid=- hash=- is not an install failure. blrc:00 means no retained apply result is available; it does not by itself prove either success or failure.

Older MeshCore application firmware can report get bootloader.ver as unknown for OTAFIX 2.4.6 because this bootloader image omits the legacy UF2 Bootloader text. The updated getter also reads the CRC-validated BLM2 version. For the official signed MeshTower V2 SD 2.4.6 .mota, the installed identity is TOWER_V2_OTA, CRC 5DACDB3D, ABI 3, and capabilities 09. Compare the exact board/storage package when interpreting these values; the non-SD image differs.

"},{"location":"ota_meshtower_v2_sdcard/#sd-card-cli","title":"SD card CLI","text":"

The SD-backed target provides these CLI commands:

set sdcard format [--force]\nset sdcard erase [--force]\nget sdcard\nget sdcard *\nget sdcard format\nget sdcard erase\nget sdcard free\nget sdcard ls\nget sdcard ls 2\nget sdcard dir 3\n

format creates a new FAT16, FAT32, or exFAT filesystem according to card size. erase first uses the card's raw media erase command and then formats it, so a successful erase finishes with a usable filesystem. Both operations destroy all data on the card and cancel a staged SD OTA download. If the internal fallback slot is occupied, card maintenance is refused until an explicit ota cancel; it does not abandon an internal download to format SD.

The firmware records successful format and erase completion times in RAM. Repeating the same operation within five minutes is rejected unless --force is present. Format and erase have independent cooldowns. Because erase also formats the card, a successful erase updates both timestamps. The timestamps reset when the device reboots. The get sdcard age queries report how long ago each operation completed. get sdcard free reports used and free filesystem space in human-readable binary units.

get sdcard ls and get sdcard dir recursively list files on the card, two files per page. A bare command shows page 1; append a positive page number to move through the remaining results. Each row includes the path and a compact file size. The header reports the selected page, total pages, and total files.

"},{"location":"ota_meshtower_v2_sdcard/#persistent-ota-archive-and-seeder","title":"Persistent OTA archive and seeder","text":"

With SD selected, automatic OTA archiving is on by default. While the temporary OTA radio is active, the node requests full catalogs from seeders and saves every complete mOTA it discovers, including firmware for other hardware targets and codecs that this node cannot install. Archive downloads use the same per-block Merkle proof checks as an install download, but archived images are never selected for local installation.

Completed containers are stored as /mota/<manifest-id>.mota. An interrupted download remains /mota/<manifest-id>.part and resumes when that mOTA is seen again. Completed files survive reboot, are enumerated on the first archive access or TempRadio window, and are advertised and served directly from SD. The SD target supports the protocol maximum served set: its own running firmware plus up to 254 archived mOTAs.

Automatic capture preserves an 8 MiB free-space reserve for manual /meshcore-ota.mota installation staging. It stops starting new archive files when the next file would cross that reserve. Archive allocation first tries the fast contiguous path and then falls back to an ordinary fragmented FAT file; only the bootloader staging file requires contiguous sectors.

"},{"location":"ota_meshtower_v2_sdcard/#preload-many-motas-from-a-computer","title":"Preload many mOTAs from a computer","text":"

You can populate the archive much faster on a computer than over LoRa. Use only complete, verified .mota containers. Do not copy firmware .bin, .hex, .zip, or .part files into the archive.

Install the standalone motatool first if it is not already available:

git clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n

The on-card filename is part of the archive index and has a strict format:

/mota/<merkle_root>.mota\n

<merkle_root> is the eight-hex-digit value printed by motatool inspect. It is also the mOTA's four-byte manifest ID. The extension must be lowercase, the file must be directly inside /mota, and descriptive release filenames are not indexed. For example, if inspection reports:

merkle_root    : ABCD1234\n

copy that container to:

/mota/abcd1234.mota\n

If two containers have the same Merkle root, they have the same protocol ID and cannot both be present. Keep only the intended one. The current SD seeder can index up to 254 archived files. A served file must use a logical block size of at most 1024 bytes and contain at most 2048 blocks; check block_size and block_count in motatool inspect when importing unusually large images. Files outside that serve geometry are not counted or advertised. If a malformed /mota/<id>.mota conflicts with a newly discovered valid image, the repeater preserves the malformed file as <id>.bad through <id>.bad9 and downloads a clean replacement instead of treating path existence as a valid cache hit.

To prepare and load the card:

  1. Format it as described under Card requirements. The easiest route is to insert it in the repeater, run set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents.
  2. Mount the card on the computer and create a directory named mota at the filesystem root. Do not use a nested directory such as /firmware/mota.
  3. Run motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL.
  4. Run motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card.
  5. Flush pending writes, safely eject the card, power the repeater off, insert the card, and boot Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors.

On Linux or macOS, this Bash example verifies and imports every .mota from ./motas. Replace the example mount path before running it:

card_mount=/media/YOU/MESHCORE\nmkdir -p \"$card_mount/mota\"\n\nshopt -s nullglob\nfor image in ./motas/*.mota; do\n  motatool verify \"$image\" || exit 1\n  mid=$(motatool inspect \"$image\" |\n    awk '$1 == \"merkle_root\" { print tolower($3) }')\n  if [[ ! $mid =~ ^[0-9a-f]{8}$ ]]; then\n    echo \"Could not read the Merkle root from: $image\" >&2\n    exit 1\n  fi\n\n  destination=\"$card_mount/mota/$mid.mota\"\n  if [[ -e $destination ]]; then\n    cmp -s \"$image\" \"$destination\" || {\n      echo \"Different containers have the same ID: $mid\" >&2\n      exit 1\n    }\n  else\n    cp \"$image\" \"$destination\"\n  fi\ndone\nsync\n

On Windows, create E:\\mota, inspect each source with motatool inspect, and rename it to the reported lowercase root in the same way. Safely eject the drive after all copies finish.

After boot, scan the archive and confirm the card contents from the repeater console:

get sdcard ls\nget sdcard ls 2\nota cache\nota folder\n

ota cache should report the imported count. ota folder reports the current served count; once OTA serving starts in TempRadio, that set also includes the repeater's running firmware. If the files appear in get sdcard ls but not in the served count, check their exact names, run motatool verify again, and inspect their block geometry. It is fine to run ota cache off for a curated, read-mostly archive: that disables capture of new mOTAs but continues serving every valid file already on the card.

"},{"location":"ota_meshtower_v2_sdcard/#serve-the-preloaded-archive-over-tempradio","title":"Serve the preloaded archive over TempRadio","text":"

LoRa OTA traffic exists only during an active temporary-radio window. The SD repeater, each receiving node, and every intermediate repeater in the path need overlapping windows on the same temporary channel. Use a frequency permitted for the node's configured region. This North American example uses the recommended fast OTA settings and a 120-minute window:

tempradio 909.950,250,5,5,120\n

Start the farthest receiving node first, then intermediate repeaters, and the SD source last so their windows overlap for as long as possible. Before or during the source window, ota cache makes the source scan and attach the SD archive. Entry into TempRadio automatically triggers an OTA advertisement burst; ota announce can send another advertisement immediately.

On a receiving OTA-capable node, allow a few seconds for catalog exchange, then run:

ota ls\nota ls 2\nota get <mid8> flash\nota status\n

Use ota ls 2, ota ls 3, and so on when the source advertises more than the two rows that fit in one remote CLI reply. Each row includes a stable eight-hex-digit manifest ID. Use that ID instead of a list number, because asynchronous catalog refreshes can reorder rows between the list and pull commands. A receiver retains the complete protocol catalog and verifies every transferred block. It still applies its normal target, hardware, codec, signature, and installation checks. The SD repeater may advertise images for many hardware families; it never installs those archive files merely because it serves them.

When a temporary window expires or a node reboots, it returns to its saved radio settings and OTA transfer stops. The archive remains on the card. Start another set of overlapping tempradio windows to resume an interrupted download, or use synchronized tempradioat entries for a scheduled window.

Archive capture is lower priority than operator work. An explicit ota pull to install storage or a host folder immediately takes the single receive slot; the archive partial is checkpointed and resumes later. Existing cached files can still answer peer requests while another archive file is downloading.

Use these commands to inspect or control automatic capture:

ota cache\nota cache on\nota cache off\nota config cache on\nota config cache off\n

The on/off choice is saved on the SD card. Turning capture off stops new downloads but keeps serving files already cached. Formatting or erasing the card removes both the archive and its off marker, so a newly formatted card returns to the default-on setting. ota config sdseed on|off is also accepted as an alias.

"},{"location":"ota_nrf52_bootloader_update/","title":"Ota nrf52 bootloader update","text":"

View this page on MeshCore Docs.

"},{"location":"ota_nrf52_bootloader_update/#nrf52-bootloader-updates-over-lora","title":"nRF52 bootloader updates over LoRa","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Selected nRF52840 repeater LoRa-OTA builds can replace their matching OTAFIX bootloader without replacing the running application. This is a privileged maintenance path, not a normal firmware update. A stock bootloader must first be replaced with the exact ABI-3 self-update-capable OTAFIX build over USB/BLE DFU or SWD. MeshTower SD also requires the BLM2 retained-auth version to be provisioned locally before either application or bootloader OTA.

The released adaptive RAK3401 and RAK4631 OTAFIX 2.4.8/2.4.9 profiles support application full and delta updates, but not bootloader self-update over LoRa. USB UF2 and local Bluetooth/serial Legacy DFU bootloader updates still work. Their running bootloader protects 0xF4000..0x100000 with the nRF52840 ACL before starting MeshCore. This includes the MBR parameter page at 0xFE000, so an application cannot initiate COPY_BL even after verifying a signed bootloader package. Hardware testing on a RAK3401 confirmed the MBR SVC enters a bus fault and leaves the installed bootloader unchanged. For new RAK installations use the normal OTAFIX 2.4.10 compatible *_auto bootloader. Existing standard 3401_DFU / 4631_DFU devices can receive its signed bootloader .mota over LoRa. Devices already running 3401_AUTO_DFU / 4631_AUTO_DFU need the matching 2.4.10 recovery bridge installed locally before switching to the normal image.

"},{"location":"ota_nrf52_bootloader_update/#storage-layouts","title":"Storage layouts","text":"Layout Application limit Staged boot package Work area Handoff / capability XIAO-module raw QSPI below 0xE0000 external QSPI offset 0 dedicated internal 0xE0000..0xEA000 scratch source 0x51, flags 0x0E Qualified internal-flash target normal 0xED000 flash limit; top 64 KiB SRAM reserved for application-delta staging shared internal slot, exact start 0xE2000 the same eleven-page slot; no second flash reservation source 0xED, flags 0x0A MeshTower V2 microSD normal 0xED000 limit contiguous /meshcore-ota.mota dynamic internal 0xE0000..0xEA000 scratch; live image must end by 0xE0000 source 0x53, flags 0x09

The exact SD target is Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors. Its normal application FULL and delta updates continue to use the SD file without the bootloader-update scratch restriction. For a bootloader package only, both MeshCore and OTAFIX require a hash-valid live EndF proving the complete running image ends at or below 0xE0000. If boot settings carry a nonzero app bank CRC, the recorded bank size must also cover that full EndF-inclusive image and stop by 0xE0000; erased or explicitly CRC-disabled settings remain valid. OTAFIX then copies the verified 40 KiB payload from SD into 0xE0000..0xEA000 and uses the MBR copy operation to replace 0xF4000..0xFE000. The application linker remains at 0xED000; a future application extending above 0xE0000 can still use application mOTA but must update its bootloader through local DFU/SWD.

The removable SD authorization is fail-closed for both application and bootloader packages. MeshCore directly authenticates one exact signed manifest, requires the streamed copy to remain byte-identical, verifies all leaves/payload/image bytes, and computes a normalized full-container SHA-256 in that same pass (APRV bytes are treated as zero). After syncing APRV, it publishes a 72-byte MOTASDA2 record in reset-retained RAM at 0x20006008. That record binds purpose/format, first LBA, sector count, total length, card sector count, and the normalized digest. OTAFIX copies, validates, and clears the record before SD access. A card swap/mutation or a power cycle therefore fails closed; there is no general sector-1 ownership inference.

For fmt3, MeshCore also writes and readback-verifies a 64-byte MOTASDBL token at 0xE0000. That token contains the exact container length and the exact authenticated signed manifest image_hash. OTAFIX requires the same hash in the parsed manifest, streamed payload, and final scratch image. The page is consumed as scratch during success and is not permanently reserved.

The first BLM2-capable MeshTower bootloader cannot be delivered to preview.12 through the retained-RAM protocol that preview.12 predates. MeshCore does not write a raw-sector compatibility record because a blank sector is not proof of ownership. Upgrade preview.12 through USB/BLE DFU or SWD. Both fmt2 application and fmt3 bootloader OTA then require the installed BLM2 metadata to match the live SoftDevice/application layout; neither MeshCore nor OTAFIX uses sector 1.

The internal path does not change the application's 0xED000 flash limit or permanently set aside separate app-OTA, boot-package, and flash-scratch regions. Qualified applications do use a dedicated linker that reserves the top 64 KiB of SRAM for hybrid application-delta staging and a retained authorization record. That SRAM is never part of a bootloader-update package. The ordinary bottom-aligned internal flash store holds one container prefix at a time: either an application delta or the complete bootloader package.

The exact bootloader container is 41,330 bytes: 365 bytes of signed mOTA metadata, a 40 KiB payload, and the five-byte trailer. Below the normal 0xED000 store ceiling it bottom-aligns at 0xE2000. Admission requires a hash-valid live EndF proving the current application, including its trailer, ends at or before 0xE2000. OTAFIX then reads each source window before erasing and compacts the payload forward in place to the page-aligned raw range 0xE2000..0xEC000; it verifies every page and the whole image before asking the MBR to copy that image over 0xF4000..0xFE000.

An ordinary application delta can be smaller or larger than this eleven-page shape. On the qualified profile, a delta larger than one page uses the minimum page-aligned flash prefix ending at 0xED000 and keeps up to 64 KiB of its logical tail in reset-retained SRAM; its detools workspace must stop at the actual flash-prefix start. A one-page-or-smaller delta stays wholly in flash. In either case the reconstructed application must stop below 0xED000. The two package kinds are mutually exclusive because they use the same flash store, and a bootloader package never uses the volatile SRAM suffix.

For internal-self-update builds, an absent or corrupt live EndF disables all internal staging before the first erase. The older 608 KiB rescue estimate is unsafe when a normally linked application may extend to 0xED000. Builds without this feature retain the legacy rescue behavior.

"},{"location":"ota_nrf52_bootloader_update/#internal-flash-target-inventory","title":"Internal-flash target inventory","text":"

The release builder consumes the allowlist in tools/mota/nrf52_internal_bootloader_targets.txt. Ten names are also literal PlatformIO environments and are enabled automatically when built directly. The other eleven are release aliases assembled from a base environment plus the lean OTA overlay; build them through build.sh, which passes the same allowlist decision to the common pre-build guard. These lean repeater/bridge targets have no OTA-owned SD/QSPI store and have an exact curated OTAFIX manifest identity:

Build target(s) Installed OTAFIX identity Boot target ID Heltec_tower_v2_repeater_lora_ota_no_external_sensors 239A0071 / TOWER_V2_OTA 1150F50E T096 lean repeater and RS232 bridge 239A0071 / T096_DFU 42354C85 Heltec_t1_repeater_lora_ota_no_external_sensors 239A0071 / T1_DFU FC556FFC T114 display and without-display lean repeaters 239A0071 / T114_DFU 0C3F2902 Mesh_pocket_repeater_lora_ota_no_external_sensors 239A0071 / MESH_POCKET_OTA 059277F4 KeepteenLT1_repeater_lora_ota_no_external_sensors 239A00B3 / KeepteenLT1_OTA DB2E7B51 Minewsemi_me25ls01_repeater_lora_ota_no_external_sensors 239A0029 / MX25_DFU 026AA982 ProMicro_repeater_lora_ota_no_external_sensors 239A00B3 / PROM_DFU AF79E8CC t1000e_repeater_lora_ota_no_external_sensors 28860057 / T1KE_DFU E6F5F03F ThinkNode_M3_repeater_lora_ota_no_external_sensors 239A00DA / TNM3_DFU 0CA41DB2 RAK_3401_repeater_lora_ota_no_external_sensors 239A0029 / 3401_DFU 23818A80 RAK4631 lean repeater and both lean RS232 bridges 239A0029 / 4631_DFU 2D0DF000 GAT562 30S/Tracker Pro/EVB Pro lean targets 239A0029 / GAT562_DFU D50D2D44 R1Neo_repeater_lora_ota_no_external_sensors 239A0029 / 4631_DFU 2D0DF000 RAK_WisMesh_Tag_repeater_lora_ota_no_external_sensors 239A0029 / RTAG_DFU C72E9C9C

Board IDs are not globally unique. For generic targets, the signed hardware ID is the exact NUL-padded 32-byte value NRF_BL_<BOARD_ID>_<DEVICE_NAME>. The wire target is the little-endian first four SHA-256 bytes of all 32 padded bytes. The installed and candidate embedded manifest pairs must match exactly. XIAO retains its deployed XIAO_BL_28860044 / XIAO_BL_28860045 identity and raw board-ID target.

The Python reference builder and release tooling audit these boot targets for duplicates and collisions with application target IDs. Generic image parsing can inspect a future canonical identity, but signing/building a package fails until that exact identity is in the qualified inventory.

Earlier GAT562 installations may report the RAK-compatible 4631_DFU identity. The installed and candidate identity pair must match exactly, so a remote bootloader update cannot migrate one of those devices to GAT562_DFU. Provision the GAT562-specific OTAFIX bootloader once through USB/BLE DFU or SWD; subsequent signed bootloader mOTA updates use target D50D2D44.

"},{"location":"ota_nrf52_bootloader_update/#embedded-continuity-and-version-policy","title":"Embedded continuity and version policy","text":"

Every remotely supplied successor retains the legacy 44-byte CRC-valid BLMF v1 record for identity continuity and diagnostics. Immediately after that record is a 32-byte BLM2/SOFT extension. The complete 76-byte envelope is fixed at the final raw-image offset 0x9FB4; relocated candidates are refused, while installed legacy-v1 discovery remains a generic diagnostic scan. The extension carries the actual packed bootloader version, SoftDevice family and FWID, application base, layout ABI, and zero compatibility/reserved fields; the legacy whole-image CRC covers the extension too. Scanners first count CRC-valid 44-byte base manifests, then interpret continuity only after one base identity remains. A malformed claimed extension therefore cannot make a duplicate identity disappear, and a sole half-present extension is rejected rather than treated as legacy.

MeshCore requires the candidate extension, exact equality between its embedded version and the signed outer mOTA version, and exact agreement with the running SoftDevice family/FWID/application layout. Qualified internal/QSPI paths may bootstrap a CRC-valid installed legacy-v1 image once; the SD path never does and requires local BLM2 provisioning. Once the installed bootloader has BLM2 metadata, the candidate version must be strictly greater. Preview low bytes are 1..254, a stable release uses 0xFF, and low-byte zero or all-ones values are rejected. There is no remote rollback/migration override; intentional rollback uses USB/BLE DFU or SWD.

The legacy no_external_sensors profiles trim selected optional environmental and ranging drivers to preserve flash headroom; they do not generally disable the I2C bus. Reduced RAK3401 and RAK4631 recipes retain INA219, INA226, INA260, and INA3221 voltage/current monitors. Those are entries in the optional sensor table, not the complete set of I2C consumers: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals where the exact board recipe enables them. RAK3401 retains its compatible RAK12500 I2C and RAK12501/L76K UART GPS paths in sensor slot A. The plain RAK4631 repeater and Serial2 bridge retain GPS. Its RAK12501 path uses Serial1; the explicitly compiled Serial1 RS232 bridge therefore omits the combined GPS provider, including RAK12500, even though RAK12500 itself is I2C rather than UART.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42. They cannot coexist on the same bus at those addresses. Leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use firmware built with -DTELEM_INA3221_ADDRESS=0x43 when both modules are installed.

Boards with onboard external flash are not silently redirected to internal staging. Mesh Solar, Nano G2 Ultra, T-Impulse Plus, ThinkNode M8, T-Echo Lite/Card, MeshTracker X1, and Wio WM1110 have board-specific QSPI hardware and need a separately matched QSPI path where available. Full Companions, other SD/QSPI/ExtraFS roles, Ethernet roles, source-only roles, and unqualified full-sensor roles are excluded from the internal-flash inventory. The exact MeshTower V2 SD role above is separately qualified for its SD path.

This internal layout is limited to nRF52840 devices with 1 MiB internal flash, the exact S140 v6/v7 map, and the 40 KiB boot region at 0xF4000..0xFE000. nRF52833 and smaller nRF52 parts cannot provide that map plus a non-overlapping 41,330-byte live staging slot, so configuration fails closed instead of selecting smaller or overlapping geometry.

"},{"location":"ota_nrf52_bootloader_update/#explicit-install-workflow","title":"Explicit install workflow","text":"

The tools/lora_ota/lora_ota.sh and .ps1 runners can perform the discovery, transfer, verification and radio cleanup for a bootloader mOTA with --no-install. They detect format 3 automatically and check the destination's bootloader-specific identity/capabilities. They do not install it: the final MID/hash confirmation below remains an explicit operator action. A mixed application/bootloader ZIP requires --zip-member.

Check the installed identity and capability marker:

ota bootloader\n

The response must show a CRC-valid exact identity, ABI 3 or newer, both FULL and INPLACE application codecs (codecs=0x5), and exact flags 0x09 for MeshTower V2 SD, 0x0A for internal shared storage, or 0x0E for XIAO QSPI. A bootloader row is visible in ota ls, but it is never autofetched or autoinstalled. Fetch its exact MID, then explicitly arm it:

ota pull <MID8> flash\n# wait for ota status to report the download ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy both confirmation values from the second ota bootloader response. Ordinary ota install rejects a bootloader package; the bootloader command rejects an application package. The FULL-codec exception exists only for that manual bootloader MID. Ordinary application FULL remains disabled on an internal single-slot node, bootloader autofetch remains off, and a partial bootloader package is not automatically resumed after an application reboot.

Before writing APRV, the application authenticates and authorizes the package: exact v3 geometry, trusted Ed25519 signer, signed/embedded identity, one unambiguous capability marker, embedded CRC, sane vectors, complete Merkle/payload/image hashes, storage-specific safe live placement, and the typed MID/hash confirmation. The manifest root and target inside the received manifest must also equal the MID/target that opened the fetch before any store is allocated. On SD, APRV, the retained geometry/container authorization, and the internal signed-image-hash token are published in that order before the reset trigger. OTAFIX consumes that application-written authorization and then independently rechecks the safety/integrity subset: strict v3 structure, canonical identity/capabilities, vectors, full payload SHA, embedded manifest CRC, the applicable live EndF/bank-settings no-overlap geometry, the SD token binding where applicable, scratch readback, and final copy hash. It does not re-run Ed25519, the signer allowlist, Merkle leaves/root, or the typed operator confirmation. Success is reported as blup:C8.

"},{"location":"ota_nrf52_bootloader_update/#failure-behavior","title":"Failure behavior","text":"

The feature fails closed when the MCU/map is wrong, required valid EndF headroom is insufficient, an unsupported external/ExtraFS role owns the target, the exact installed capability marker is absent or ambiguous, identity cannot be derived unambiguously, or any package check fails. Before the storage-specific scratch/copy step, the application and bootloader are unchanged. OTAFIX consumes the trigger and authorization before its first destructive action, so an interrupted operation cannot automatically retry a partly consumed package. On SD, the retained-RAM authorization is zero-consumed before media access; APRV and the fmt3 flash token may persist but are inert without a new explicit authenticated re-arm and reset trigger. No SD raw-sector handoff is created or consumed. Use USB/BLE DFU or SWD for initial provisioning and local recovery.

For XIAO and ordinary external-QSPI details, see nRF52 repeater LoRa OTA with external QSPI.

"},{"location":"ota_nrf52_qspi/","title":"Ota nrf52 qspi","text":"

View this page on MeshCore Docs.

"},{"location":"ota_nrf52_qspi/#nrf52-repeater-lora-ota-with-external-qspi","title":"nRF52 repeater LoRa OTA with external QSPI","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Selected nRF52840 repeater builds use their dedicated external QSPI NOR flash as a raw LoRa OTA staging device. This removes the internal-flash staging conflict: the complete .mota stays off-chip, and the bootloader can use the entire internal application region while it installs either a full image or an in-place delta.

This is a matched application-and-bootloader feature. A board merely having an nRF52840, free RAM, or pins named QSPI is not enough. Both halves must use the exact flash wiring, and ota self must confirm the store and bootloader before an update is downloaded.

"},{"location":"ota_nrf52_qspi/#supported-repeater-families","title":"Supported repeater families","text":"

QSPI staging is enabled for explicitly matched repeater-role environments (and RS232 bridge variants that directly inherit one of those repeater targets) on these currently matched families:

Heltec T114 is intentionally not in this list. Its public V1, V2.0, and V2.1 schematics show U9 (MX25R1635F) as an optional QSPI footprint, so standard T114 application and bootloader targets do not assume that external NOR is populated.

The XIAO-module derivatives use the matching XIAO or XIAO Sense OTAFIX bootloader shown by the module's INFO_UF2.TXT. Do not substitute a similar bootloader for a board with different QSPI pins. For example, the T-Echo Card and T-Echo Lite have different flash wiring and are not enabled by the original T-Echo target.

Companion builds are intentionally excluded. Some companion targets use the same external QSPI as a LittleFS message/data store, where raw OTA staging would corrupt the filesystem; other companions simply do not assign that chip to OTA. Dedicated *_room_server_lora_ota environments also enable raw QSPI staging with their own exact role identity and matched bootloader layout. For Ikoka, these cover the Handheld E22-30 and Nano/Stick 22, 30, and 33 dBm variants. The legacy *_room_server environments retain their internal-flash layout and a different OTA target identity; use the matching wired installation and bootloader migration procedure before switching to a QSPI Room image. Sensor and KISS roles do not gain QSPI staging from these recipes.

"},{"location":"ota_nrf52_qspi/#rak15001-placement-and-module-conflicts","title":"RAK15001 placement and module conflicts","text":"

RAK15001 is a 2 MiB GD25Q16C standard SPI module, not a quad-I/O flash. The dedicated RAK4631 target uses the nRF52840 QSPI peripheral in its single-data-line FAST_READ/page-program modes at 8 MHz and accepts only the module's exact C8 40 15 JEDEC ID. ota self reports QSPI store:2048K only when the expected module responds. An empty slot or a different SPI device reports QSPI store:ERR 0K, and install is refused.

The module is electrically usable in sensor slot A-D because those slots share the SPI signals and RAK15001 has onboard 10 kOhm pull-ups on WP# and HOLD#. The supported MeshCore/OTAFIX combination nevertheless requires slot C. It is the placement that remains safe when a RAK12501 GNSS is fitted in either of its supported slots, A or D: slot C avoids the GNSS PPS/reset nets on IO1/IO2 and IO5/IO6. In particular, GPS in A plus flash in B would share IO1/IO2 through the modules' auxiliary pins.

Only one device that uses the shared WisBlock SPI chip-select may be fitted. Do not combine this target with RAK13800 Ethernet, RAK15002 SD, or another SPI module. RAK13800 and RAK15001 cannot coexist because they use the same SPI chip-select. Update a RAK13800 Ethernet build locally over USB using the release's Manual UF2 or Serial DFU (.zip) download; it cannot use this RAK15001 LoRa-OTA staging target.

RAK3401 with RAK13302 remains incompatible with RAK15001. The 1 W radio uses the same WisBlock SPI clock/data pins and the same chip-select as RAK15001. Firmware cannot independently select or detect those two chips, so a stock RAK3401 + RAK15001 assembly cannot provide reliable OTA staging without a hardware chip-select rework. This restriction does not apply to the separate-CS W25Q16 wiring below.

"},{"location":"ota_nrf52_qspi/#one-w25q16-wiring-for-rak4631-or-rak3401-rak13302","title":"One W25Q16 wiring for RAK4631 or RAK3401 + RAK13302","text":"

An external Winbond W25Q16JV breakout can remain attached to a RAK19007 while the core is changed between RAK4631 and RAK3401 + RAK13302. It shares only the WisBlock SPI clock/data nets and has its own chip-select on AIN1:

W25Q16 breakout RAK19007 connection nRF52840 / Arduino pin CLK IO connector pin 26, SPI_CLK P0.03 / 3 DO / MISO IO connector pin 27, SPI_MISO P0.29 / 29 DI / MOSI IO connector pin 28, SPI_MOSI P0.30 / 30 CS J11 AIN1 P0.31 / 31 VCC J12 VDD regulated 3.3 V GND J12 GND ground

Fit a physical approximately 10 kOhm pull-up from CS to J12 VDD. That keeps the flash deselected during reset, bootloader entry, and a core-module swap. Power the breakout only from J12 VDD; never use J11 VBAT, which can exceed the flash's supply rating. The six-pin breakout does not expose WP#/IO2 or HOLD#/IO3; do not add nRF IO2/IO3 wiring. A breakout with an onboard power LED works, but that LED draws continuously and is undesirable for low-power or solar operation.

This arrangement consumes no WisBlock sensor slot. However, SPI_CLK, SPI_MISO, and SPI_MOSI are not available on the easy 2.54 mm J10/J11/J12 headers. Those three signals require a short underside-pad tap or a suitable IO-connector interposer at pins 26-28. J11 and J12 alone are not sufficient.

GPS remains supported in sensor slot A. RAK12500 uses I2C, while RAK12501/L76K uses UART plus its auxiliary control signals; neither uses the shared SPI clock/data bus. On RAK4631 the built-in SX1262 has a separate private SPI bus. On RAK3401 the RAK13302 and flash share clock/data, but have independent chip-selects: RAK13302 NSS is P0.26 and flash CS is P0.31. The matched firmware holds the radio NSS high while it temporarily hands the bus to the flash, then restores the radio SPI interface.

The breakout target accepts only a 2 MiB W25Q16 with JEDEC ID EF 40 15 and runs it at 8 MHz in standard single-data-line SPI mode. A substituted W25Q32, W25Q64, or W25Q128 fails closed instead of being treated as the OTA store. Use the complete application/bootloader identity row that matches the fitted core:

Core and radio PlatformIO environment MOTA hardware identity OTAFIX board / DFU device name RAK4631 built-in SX1262 RAK_4631_repeater_w25q16_lora_ota RAK4631_W25Q16 wiscore_rak4631_w25q16 / 4631_W25Q16_DFU RAK3401 + RAK13302 1 W RAK_3401_repeater_rak13302_w25q16_lora_ota RAK3401_RAK13302_W25Q16 wiscore_rak3401_rak13302_w25q16 / 3401_W25Q16_DFU

The physical flash wiring is identical, but the two application and bootloader pairs are not interchangeable.

"},{"location":"ota_nrf52_qspi/#header-only-w25q16-wiring-for-the-unified-rak-images","title":"Header-only W25Q16 wiring for the unified RAK images","text":"

The unified RAK repeater images and matching OTAFIX 2.4.10 compatible bootloaders also support a W25Q16 through the RAK19007 2.54 mm headers. This arrangement does not need an IO-connector interposer or underside SPI solder points:

W25Q16 breakout RAK19007 header nRF52840 / Arduino pin CLK J10 TX1 P0.16 / 16 DO / MISO J10 RX1 P0.15 / 15 DI / MOSI J11 IO1 P0.17 / 17 CS J11 AIN1 P0.31 / 31 VCC J12 VDD regulated 3.3 V GND J12 GND ground

Keep the approximately 10 kOhm CS-to-VDD pull-up. This mapping gives up the UART1 GPS and PPS pins while the flash is fitted. The unified firmware detects the exact EF4015 device before GPS discovery and leaves I2C sensor pins SDA and SCL available. RAK4631 can still use its UART2 bridge; its UART1 bridge is unavailable with header flash. RAK3401 retains its 1 W radio and shared sensor/radio power rail. Do not substitute SCL for flash clock or use SWCLK, which is the debug clock. The older dedicated W25Q16 images above expect the WisBlock SPI connector wiring and do not use this header mapping.

"},{"location":"ota_nrf52_qspi/#one-time-prerequisite","title":"One-time prerequisite","text":"

Install a QSPI-capable OTAFIX 2.4.1 preview.9 or newer bootloader for the exact board from the OTAFIX releases before using LoRa OTA. The release notes must explicitly list that board's QSPI mode. Also install the SoftDevice version expected by that target. The application refuses the install handoff when the bootloader does not advertise QSPI support.

For the external W25Q16 option, install the corresponding bootloader once before installing its MeshCore application: wiscore_rak4631_w25q16 (DEVICE_NAME=4631_W25Q16_DFU) for RAK4631, or wiscore_rak3401_rak13302_w25q16 (DEVICE_NAME=3401_W25Q16_DFU) for RAK3401 plus RAK13302. Use only a release that explicitly names that exact pairing. Do not substitute the RAK15001 bootloader or swap the two W25Q16 bootloaders merely because the base-board wiring is the same.

For the first migration from the ordinary wiscore_rak4631_board bootloader to wiscore_rak4631_board_rak15001_slot_c, use Nordic serial DFU or a compatible BLE DFU client with the exact slot-C OTAFIX combined bootloader + SoftDevice DFU package (or use SWD), then reinstall the slot-C MeshCore application. The release filename has this form:

wiscore_rak4631_board_rak15001_slot_c_bootloader-<OTAFIX-version>_s140_6.1.1.zip\n

Do not copy the slot-C bootloader-update UF2 onto the stock UF2 drive. The stock loader is bound to DEVICE_NAME=4631_DFU, while the slot-C image is bound to 4631_15001C_DFU, so that UF2 is intentionally rejected. After the one-time DFU/SWD migration, later canonical slot-C bootloader UF2 files work normally. The combined OTAFIX package above is not the MeshCore application's Serial DFU .zip; an application package does not migrate the bootloader.

After installing the repeater application, check:

get bootloader.ver\nota self\nota status\nota qspi\n

A ready RAK15001 target reports all of the following:

QSPI store:2048K\nbootloader: QSPI apply OK\nbl:QSPI\nQSPI jedec=C84015 size=2048K sr1=00 stage=jedec\n

A ready external W25Q16 target instead identifies the Winbond part:

QSPI store:2048K\nbootloader: QSPI apply OK\nbl:QSPI\nQSPI jedec=EF4015 size=2048K sr1=00 stage=jedec\n

Other supported boards can report a capacity different from 2048K; the RAK15001 and W25Q16 targets must report exactly 2048K and their respective exact JEDEC IDs. QSPI store:ERR 0K, NO QSPI, or bl:NO-QSPI means the flash wiring, flash power, chip-select pull-up, application, or bootloader does not match. Do not start an install in that state.

ota qspi is a read-only diagnostic probe available on QSPI OTA builds. It reports the exact JEDEC ID, status-register byte, last store stage, and the first latched storage error. Run it after an immediate storage error before starting another pull; later capacity probes preserve that failure detail.

"},{"location":"ota_nrf52_qspi/#capacity-and-package-types","title":"Capacity and package types","text":"

The store reads the JEDEC capacity at runtime and accepts supported 1 MiB through 16 MiB devices using 24-bit addressing. QSPI capacity is not the final firmware limit. The reconstructed application, including its 56-byte EndF trailer, must fit its build's linked application region:

SoftDevice layout Application region Maximum image S140 v7, boot-update-capable XIAO module 0x27000..0xE0000 0xB9000 (757,760 bytes) S140 v7, app base 0x27000 0x27000..0xED000 0xC6000 (811,008 bytes) S140 v6, app base 0x26000 0x26000..0xED000 0xC7000 (815,104 bytes)

The XIAO limit is deliberately 52 KiB smaller. Its linker and post-link record cap every ordinary application at 0xE0000; OTAFIX reserves 0xE0000..0xEA000 as a 40 KiB self-update scratch bank, with the remaining gap left unused before InternalFS. Full and delta application packages for these targets derive their effective application end from that 0xE0000 record and are rejected if their reconstructed image or detools geometry crosses it.

A full package needs only the new raw firmware.hex or non-merged application image. An in-place delta still needs the exact image currently running. The automation uses a conservative 0xC6000 detools workspace for external nRF52 staging so one package setting is safe for both layouts.

For online automation, QSPI is detected from ota self or ota status. For offline preparation, identify it explicitly:

./tools/lora_ota/lora_ota.sh ./release.zip target-name \\\n  --prepare-only \\\n  --platform nrf52 \\\n  --nrf-qspi \\\n  --target-id 12345678 \\\n  --target-hw Xiao_nrf52\n

Use the real target ID and hardware identity from the destination. A ready full .mota normally uses that target ID for discovery and routing. An operator can deliberately override the routing target for a role change, so target_id is not an apply-time safety assertion; the destination still enforces the package's hardware identity before approval.

"},{"location":"ota_nrf52_qspi/#explicit-xiao-bootloader-updates-over-lora","title":"Explicit XIAO bootloader updates over LoRa","text":"

This section documents the deployed XIAO raw-QSPI layout. Curated nRF52840 targets without external staging use a separate internal-flash layout with the same explicit operator safety model; see nRF52 bootloader updates over LoRa.

Bootloader delivery is available only when all of these are already true:

This is not part of normal automation. Bootloader catalog rows are labelled bootloader; even a capable node never autofetches or autoinstalls them, and ordinary ota install rejects them in every application backend. The tools/lora_ota deployment runner also refuses v3 packages. Fetch and arm one only with the explicit flow:

ota ls\nota pull <MID8> flash\n# wait for ota status to report the same bootloader download as ready\nota bootloader\n# copy the exact mid= and hash= values printed above\nota bootloader install <MID8> <HASH16>\n

The pull command is only transport intent; it does not authorize an install. The final command must reproduce both the complete staged manifest ID and the first eight bytes of its signed image hash. Before approval the application again checks package/root/payload/image hashes, Ed25519 signature and trusted allowlist, exact signed XIAO_BL_... ID, installed and incoming embedded manifest/CRC/name/board identity, vector table, ABI-3 QSPI+boot-update marker, and the adjacent CRC-covered BLM2/SOFT continuity extension at the exact final-image offset 0x9FB4. The embedded boot version must equal the signed outer package version, the SoftDevice family/FWID/application base/layout ABI must match the running platform, and a successor to installed BLM2 must be strictly newer. Low-byte-zero and all-ones versions are invalid. Remote rollback is refused; use local DFU/SWD.

After the reply drains, GPREGRET 0x6B and GPREGRET2 0x51 enter the special OTAFIX path. APRV carries the app's signature/allowlist and explicit operator authorization decision. OTAFIX independently rechecks the strict structure, identity/capabilities, vectors, payload SHA, embedded CRC, and copy hashes before using the reserved scratch bank to replace its own 0xF4000..0xFE000 region. The running application remains preserved; a rejected candidate returns to it unchanged. Post-reboot ota status reports bootloader-update diagnostics as blup:C0 to blup:CF (blup:C8 is success), separately from ordinary application blrc.

This feature cannot update a stock or old bootloader that lacks the capability marker: perform the first exact-board combined bootloader+SoftDevice migration over USB/BLE DFU or SWD. Do not attempt to bootstrap it with a v3 package.

"},{"location":"ota_nrf52_qspi/#storage-ownership-and-recovery","title":"Storage ownership and recovery","text":"

The QSPI store is raw, not a file inside LittleFS. It owns the flash from offset zero, erases 4 KiB sectors as blocks arrive, writes data before progress metadata, and verifies every programmed page. A previously interrupted download is reopened only when its header and trailer are valid; every claimed block is re-hashed before it is trusted. Between a probe, transfer operation, or checkpoint, firmware puts the NOR into deep power-down, deactivates the nRF QSPI peripheral, and turns off a board-provided flash power-enable pin. The next operation powers the chip, shifts the NOR's 0xAB wake command over GPIO, then activates the nRF QSPI peripheral and identifies the flash. Waking it before peripheral activation is required because a flash in deep power-down ignores the activation traffic itself. Merely running ota self therefore does not leave QSPI drawing active-mode current or prevent the following operation from reactivating it.

Installing a QSPI repeater build over a former companion build therefore repurposes the external flash and destroys companion filesystem data as OTA sectors are written. Back up anything important first. Returning to a companion build may require formatting its external data store.

Before changing internal application flash, the application verifies package integrity, hardware identity, signature policy, and bootloader capabilities. The target ID selects discovery/fetch routing and can be deliberately overridden; it is not a second hardware gate. The bootloader then verifies a full payload before its first application erase. For a delta, the application rejects invalid detools geometry before approval and the bootloader independently repeats the base and geometry checks before applying it. It clears the one-shot approval marker before invalidating the running image. If power is lost after application writes begin, the bank remains invalid and OTAFIX enters USB/BLE recovery rather than booting a partial image.

See Easy firmware updates over LoRa for the transfer commands and the OTA protocol for the container and handoff details.

"},{"location":"ota_nrf52_qspi/#experimental-rak15002-sd-archive-source-only","title":"Experimental RAK15002 SD archive (source-only)","text":"

RAK_4631_repeater_rak15002_sd_seeder_only is an optional RAK4631 repeater with a RAK15002 in the base board's IO slot. It keeps the reduced sensor/GPS, OLED and BLE recovery features. It automatically caches up to 16 complete peer .mota files on SD and serves them after reboot; the card also retains interrupted captures for verified resume. It is not an ordinary OTA receiver and is excluded from automatic Full/Reduced receiver releases. This profile has host regression/build checks but has not been physically tested with a RAK15002.

The official RAK15002 datasheet specifies connector pins 25/26/27/28 for CS/SCK/MISO/MOSI, pin 38 for active-low card detection (IO6), and pin 5 for unswitched 3V3. On the RAK4631 these SPI signals map to Arduino GPIO 26/3/29/30. A separate SPIM2 object at 4 MHz accesses SD; global SPI/SPIM3 remains dedicated to the radio. This exact SD module does not need the GPS-only switched sensor rail held on. The card's 3V3 supply is not software-switchable, so its idle current remains a hardware consideration. Card detection is not driven by this profile; absent or failed media reports an archive error and is retried without blocking USB startup.

Do not combine this profile with RAK13800 Ethernet, RAK15001, or another external SPI/QSPI module. Compile-time guards reject the known conflicting firmware combinations; software cannot identify every physically connected module. Use a FAT/exFAT card supported by the pinned SdFat library. Existing files outside /mota are left alone; automatic capture reserves 8 MiB free space for manual staging/work rather than filling the card.

Use ota cache for status, ota cache off to stop new captures (existing files still seed), ota cache on to resume, and ota announce to advertise. Source-only means ota install, bootloader installation, and ota dev apply are disabled even if the installed bootloader supports SD. No RAK15002 SD bootloader handoff is claimed. Update this trial firmware over USB/BLE; normal RAK4631 receiver targets and MeshTower SD self-update are unchanged.

"},{"location":"ota_nrf52_qspi/#optional-rak15002-storage-in-canonical-rak4631-images","title":"Optional RAK15002 storage in canonical RAK4631 images","text":"

This is a separate STORE path, not an expansion of the older RAK_4631_repeater_rak15002_sd_seeder_only CACHE-only experiment above. It is limited to the canonical RAK4631 Full/Reduced LoRa-OTA receiver roles (repeater, room server, Ethernet room server, and sensor) and to RAK_4631_companion_radio_full. Existing exact-NOR identities, non-OTA roles, and the CACHE-only seeder retain their previous storage contracts.

The receiver and Full Companion use the same optional card hardware, but not the same update authority:

Image type RAK15002 role Local firmware install from that card Canonical RAK4631 receiver Adaptive application staging, plus the retained mOTA archive/source Only a trusted, signed application package, after bootloader capability checks RAK_4631_companion_radio_full Cache and serve peer .mota files Never; it remains a source-only Companion RAK_4631_repeater_rak15002_sd_seeder_only Historical strict cache/source-only profile Never

The receiver chooses the card only during real OTA/cache work, not at boot or while merely reporting status. With no card it retains its existing qualified internal/NOR selection. With a card fitted, the firmware deliberately does not probe NOR as a fallback, even if the card, filesystem, or installed loader is unsuitable: both devices use CS26, so issuing a NOR probe could clock the card. RAK15002 card-detect is active-low WisBlock IO6 (Arduino pin 10), not Arduino pin 6; the SD bus is SPIM2 at 4 MHz on 29/3/30 with CS26.

The new STORE path accepts FAT16/FAT32 only. It deliberately uses the smaller FAT-only SdFat types to fit the RAK4631 flash budget and reduce its RAM use; an exFAT card is not a supported STORE volume. This restriction does not change the historical CACHE-only RAK15002 seeder or MeshTower SD profiles, which retain their existing SdFs/FsFile (FAT/exFAT) behavior.

"},{"location":"ota_nrf52_qspi/#future-receiver-loader-prerequisite-and-update-flow","title":"Future receiver loader prerequisite and update flow","text":"

The matching wiscore_rak4631_auto all-in-one OTAFIX bootloader is only a local candidate today: it is not released and has not been hardware-qualified. Do not provision it as an SD-apply upgrade. Until a released, qualified loader explicitly advertises the supplemental authenticated RAK15002-SD application capability, the receiver fails SD application apply closed and continues to use its qualified internal/NOR path when no card is selected.

Once such a loader is released, it must preserve the deployed internal/NOR capabilities while adding the separate SD application handoff record. A stock, older, or otherwise ordinary RAK4631 OTAFIX bootloader must refuse the SD handoff. Its first migration will need to be performed locally over USB/BLE DFU or SWD; it must not be bootstrapped from the SD card or with a LoRa bootloader package.

The supplemental capability is application-only: an SD card never authorizes a bootloader update. Existing qualified internal bootloader-update behavior is separate and still applies only to its exact package and loader identity. The Full Companion's archive/source feature never installs firmware locally, so it does not need this SD-apply bootloader capability.

Only after that future matching loader is released, installed locally, and qualified may a receiver use the normal explicit application flow:

ota status\nota ls\nota pull <MID8> flash\n# wait for `ota status` to show the same fetch as ready\nota install\n

ota cache manages the card-backed archive/source (ota cache off stops new captures; retained files can still seed). ota storage and ota self report the selected backend and loader state, but they intentionally do not force a removable-media probe at startup. After the future loader is present, a card-backed receiver reports SD store and authenticated SD apply OK; before then, a missing supplemental capability, unsigned application, incompatible media, or a busy store fails before reboot/approval.

"},{"location":"ota_nrf52_qspi/#rak13800-ethernet-ownership","title":"RAK13800 Ethernet ownership","text":"

The SD-capable Ethernet-room receiver RAK_4631_room_server_ethernet_lora_ota_no_external_sensors is also an explicit one-at-a-time profile. Its RAK13800 Ethernet starts off on every reboot and is not a saved setting. With the card removed and no active/cache, staged, serving, or NOR work, enable it from local USB or an existing Telnet session with either command spelling:

eth on\n# or: set eth on\neth.status\neth off\n

eth on/set eth on refuses a fitted card, retained NOR owner, or busy OTA state. eth off/set eth off is cooperative and likewise refuses to release the bus while OTA work is active. Do not use these commands as a way to switch media during a transfer.

"},{"location":"ota_nrf52_qspi/#full-companion","title":"Full Companion","text":"

The Full Companion is deliberately different: its optional RAK15002 holds peer archives for caching and serving, never its own install image. It may also compile RAK13800 Ethernet, but Ethernet and the card electrically share CS26 and cannot operate together. In this combined profile physical Ethernet starts off and set eth on is only a this-boot choice.

To use Ethernet, stop/cancel any active OTA capture or serve work, detach any host folder source, physically remove the SD card, then run:

get eth\nset eth on\n

The command refuses unsafe ownership states rather than forcing the bus. To return to the card, run set eth off, wait for it to report off, then insert the card and use get sdcard [free] or ota cache [on|off]. If a card is inserted while Ethernet is active, Ethernet fails closed; use set eth off before accessing the card. This is not a supported hot-swap procedure.

The combined RAK15002 receiver/Companion/RAK13800 path has host-side checks, but has not yet received hardware qualification. Do not treat these rules as proof that a particular card, Ethernet module, loader migration, or live card insertion has been field-tested.

"},{"location":"ota_protocol/","title":"Ota protocol","text":"

View this page on MeshCore Docs.

"},{"location":"ota_protocol/#meshcore-ota-mota-container-lora-protocol","title":"MeshCore OTA - .mota container & LoRa protocol","text":"

This is the single source of truth for MeshCore's over-the-air firmware update system (\"mOTA\"). It is written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool) in another codebase or project. Everything below is implemented in this repository and covered by host, simulation, build, or hardware tests as noted in the relevant section. Hardware qualification is target- and chain-specific; do not infer it from implementation alone. Where a section names a source file, that file is the authoritative reference for byte-level details.

Just want to update your node? See the plain-language OTA user guide - this document is the technical/wire specification.

Design goals

Source map (all under src/helpers/ota/ unless noted)

Concern File Constants, enums, flags OtaFormat.h Container/manifest parse MotaContainer.{h,cpp} Merkle tree + proofs MerkleTree.{h,cpp} EndF self-identity FirmwareInfo.{h,cpp} Wire message codec OtaProtocol.{h,cpp} Negotiated direct KISS framing / build policy OtaDirectWire.h, OtaDirectConfig.h Session engine (serve+fetch+discovery) OtaManager.{h,cpp} Multi-mota / folder relay OtaSource.h, MotaSourceSerial.{h,cpp}, MotaSeederProto.h Staging stores OtaStore.h, OtaStoreFlashNrf52.*, OtaStoreFlashEsp32.* Apply OtaApply.*, bootloader Adafruit_nRF52_Bootloader_OTAFIX Device glue (CLI/context) OtaCli.cpp, OtaContext.h Host tooling motatool (standalone Rust CLI: build/verify/inspect/serve); tools/mota/ (Python reference lib motalib.py + build/test glue)"},{"location":"ota_protocol/#1-conventions","title":"1. Conventions","text":"

Reference constants (OtaFormat.h):

Name Value ASCII / note Container MAGIC 6D 4F 54 41 mOTA Container TRAILER 76 6B 34 39 36 vk496 EndF marker 45 6E 64 46 EndF hash_algo (sha2-256) 0x12 multihash code application format_ver 0x02 ordinary full/delta application package bootloader format_ver 0x03 privileged exact-identity nRF52 bootloader package only approval = not approved FF FF FF FF erased NOR word approval = approved 41 50 52 56 APRV MFLAG_FULL 0x01 flags bit0 MFLAG_SIGNED 0x02 flags bit1 MFLAG_BOOTLOADER 0x04 flags bit2; valid only in the exact v3 bootloader profile CODEC_FULL / _SEQUENTIAL / _INPLACE 0 / 1 / 2 Section 5 PAYLOAD_TYPE_OTA 0x0C MeshCore packet type (src/Packet.h) MAX_PACKET_PAYLOAD 184 ordinary routed packet payload (src/MeshCore.h); direct KISS exception: Section 8.5.1 Default application block size 2048 block_size_log2 = 0x0B; deployed 1 KiB application packages remain accepted Bootloader-package block size 1024 fixed block_size_log2 = 0x0A for format 3 OTA discovery TX priority 250 background (OTA_TX_PRIORITY, src/Mesh.h) OTA active-transfer TX priority 0 primary (OTA_TRANSFER_TX_PRIORITY, src/Mesh.h)

Current application firmware publishes its receive/reassembly ceiling as the compact maxblk:2048 field near the front of both ota status and ota self. Keeping the field ahead of variable diagnostics ensures it survives the 160-byte remote-admin reply limit. Host updaters must treat an absent field as the deployed legacy capability (1024), reject a ready package whose manifest block size exceeds that capability, and use the reported size when turning a raw firmware image into a new application package. This capability does not change the format-3 bootloader package's fixed 1 KiB geometry.

"},{"location":"ota_protocol/#2-firmware-image-the-endf-trailer","title":"2. Firmware image & the EndF trailer","text":"

Every OTA-capable build appends a fixed 56-byte EndF trailer to its flashed image so a running node can discover its own size and self-describing identity on any MCU (no linker symbols needed). Every field is always present at a constant offset. Implemented by FirmwareInfo.cpp; appended at build time by tools/mota/pio_endf.py (post-build hook).

flashed image = BODY (image bytes) || EndF trailer\nEndF trailer (fixed 56 bytes):\n  off 0   4   \"EndF\"        45 6E 64 46\n  off 4   4   body_len      uint32 LE - length of BODY (excludes the whole trailer)\n  off 8   8   body_hash     sha2-256:8 of BODY\n  off 16  4   fw_version    uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre  (0 = unknown)\n  off 20  4   target_id     uint32 LE - sha2-256:4(pio_env): hardware + role + partition (fetch routing)\n  off 24  32  hw_id         NUL-padded ASCII hardware tag (brick-safety), e.g. \"RAK4631\" (\"\" = unknown)\n

The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).

"},{"location":"ota_protocol/#esp32-application-slot-profiles","title":"ESP32 application-slot profiles","text":"

ESP32 Companion firmware is exempt from the portable-slot limit. When an exact Full recipe exists, one expanded-partition image supplies USB, BLE, WiFi, source-only LoRa OTA, and persistent USB logging instead of separate transport artifacts. A small set of high-capacity, non-PSRAM classic ESP32 companions use 100 contacts, 8 group channels, and a 16-frame offline queue in that combined image to preserve internal-DRAM headroom. MQTT observers and ESP-NOW bridges always use FULL builds because fitting them into the legacy slot would require removing CLI and role features. Except for those FULL roles and the ESP32-C6 case below, non-companion ESP32 artifacts, including room, sensor, and repeater roles, must fit the legacy slot from 0x10000 up to 0x150000 (0x140000, 1,310,720 bytes), including the 56-byte EndF trailer. The build checks both that limit and the target's actual app partition. The ESP32-C6 no_external_sensors OTA siblings are the narrow exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app partition. ESP32 room servers and sensors use their expanded Full image for LoRa OTA. For standalone repeater and room-server roles, and for nRF52 sensors that need a lean staging profile, build.sh exposes an explicit *_lora_ota_no_external_sensors artifact: the ordinary role remains sensor-enabled, while that sibling trims selected optional environmental/ranging drivers for LoRa distribution. SolarXiao 30S and 33S use matched external QSPI staging. Their repeaters keep the existing OTA target; their room servers use an explicit *_room_server_lora_ota image with the matching OTAFIX QSPI bootloader. Integrated GPS and other board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The legacy suffix describes a driver trim, not removal of the generic I2C bus. Reduced RAK3401 and RAK4631 profiles retain INA219, INA226, INA260, and INA3221 voltage/current monitors. These are retained optional-sensor-table drivers, not the only I2C consumers: SSD1306 display, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals where selected by the board recipe. The RAK3401 OTA repeater also retains RAK12500 I2C and RAK12501/L76K UART GPS support; install either GPS module in sensor slot A because slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The plain RAK4631 OTA repeater and its Serial2 bridge retain GPS. RAK12501 uses Serial1. Its explicitly compiled Serial1 bridge therefore omits the combined GPS provider, including RAK12500, even though RAK12500 itself does not use the UART.

The firmware-configured INA3221 address and RAK12500 address are both 0x42, so those devices cannot share one bus at those addresses. Keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use firmware built with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

ESP32 siblings retain the compact browser WiFi updater and use up to 254 neighbors. Internal-DRAM-constrained targets use 50 as recorded in the artifact capability manifest; see ESP32 memory budget. RP2040 and STM32 targets are not offered because those platforms do not yet have a safe bootloader/apply path.

nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default -Ofast. This keeps the runtime software Ed25519 fallback from being expanded into tens of kilobytes of repeated curve arithmetic while retaining CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features.

WiFi-heavy non-companion roles are not reduced to fit the legacy application slot. build.sh automatically promotes every ESP32 MQTT observer and ESP-NOW bridge to the expanded FULL partition profile. These artifacts retain the complete role CLI, WebConfig where supported, display and optional sensor support, full timezone and TLS behavior, and the board's normal power-management implementation. The compact CLI is not compiled into any build. Ordinary repeater and room-server builds remain sensor-enabled; only explicitly named *_lora_ota_no_external_sensors siblings trim selected optional environmental/ranging drivers for LoRa distribution, and those siblings retain the complete CLI and target-declared I2C peripherals.

MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset during a settings save restores the last committed common preference image or publishes the completed new image; it does not leave a partially written /com_prefs file to fail on the next boot. A truncated legacy image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.

Option 3 in build.sh emits one *-full-usb-wifi-ota-* ESP32 artifact for each FULL-capable non-companion hardware/role that has a matching MQTT environment. It compiles USB packet logging and direct WiFi MQTT together. A *-full-logging-ota-* fallback is emitted only when there is no MQTT sibling; ordinary non-OTA roles compile runtime USB logging into their canonical image, so separate standard-logging artifacts are not emitted. Canonical RAK3401/RAK4631 repeater, room and sensor OTA pairs, and Tower SD repeater/room pairs, retain runtime USB packet logging in both sensor variants too. Verbose debug is off by default; packaged logging.usb.packets and logging.usb.control capability checks prevent a stale no-logger image from satisfying a resumed build. Specialized storage/transport aliases and other constrained OTA targets keep their existing logging policy. Non-MQTT FULL twins are also skipped for covered ESP32 roles. MQTT observers and ESP-NOW bridges are emitted only with expanded FULL partitions. Menu option 8, or build-full-esp32-firmwares, builds the unified profiles plus necessary fallbacks. Menu option 9, or build-full-esp32-logging-firmwares, builds only those fallbacks. FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature set, full ElegantOTA where that target declares the required library, and LoRa OTA for every included role, including room servers, sensors, observers, and bridges. They use expanded A/B partition tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB and 16 MiB boards. Explicit *_lora_ota_no_external_sensors targets are not duplicated; their ordinary repeater or room-server build is the FULL, sensor-enabled counterpart. The *-full-usb-wifi-ota-* profile enables USB packet logging and MQTT, with a persistent logging.output selector; its verbose internal debug remains off. The fallback *-full-logging-ota-* profile enables USB debug and packet logging and has no MQTT target. Install a matching *-full-usb-wifi-ota-*-merged.bin or *-full-logging-ota-*-merged.bin over USB once to write the expanded partition table. After that, its matching non-merged FULL application image can be installed through USB, WiFi OTA, or LoRa OTA. Do not install a non-merged FULL image onto a node that still has its old partition table.

Implementer note: the bootloader (and any non-Arduino consumer) MUST locate the body extent by scanning for EndF, never by trusting a stored size - see the bootloader contract in Section 12.

"},{"location":"ota_protocol/#3-the-mota-container","title":"3. The .mota container","text":"

The distributed form (host-built, wire-transferred). Parsed by mota_parse() in MotaContainer.cpp.

off            size   field\n0              4      MAGIC = 6D 4F 54 41\n4              4      MOTA_TOTAL_SIZE  uint32 LE - total container bytes (incl. manifest, leaves[],\n                                       payload, trailer). Lets a node pre-reserve staging and compute\n                                       write_start = staging_region_end - MOTA_TOTAL_SIZE.\n8              M      MANIFEST         (Section 4; M = 197 fixed + leaves[], 4*BC; no length field - BC from payload_size)\n8 + M          P      PAYLOAD          (payload_size bytes; delta or full image)\n8 + M + P      5      TRAILER = 76 6B 34 39 36\n

MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5. The manifest M includes leaves[]; the manifest-minus-leaves prefix (mfl, sent over the wire as OTA_MANIFEST) is [8, leaves_off).

Staged (in-flash) form. Written bottom-aligned so TRAILER ends at staging_region_end. Identical bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the leaves[] slots (filled as blocks arrive - Section 7) and the 4-byte approval field (on owner consent - Section 4.2). Everything else is immutable.

"},{"location":"ota_protocol/#4-the-manifest","title":"4. The manifest","text":"

Fixed layout. Every field sits at a constant offset and is always present - base_hash, signer_pubkey and signature are zero-filled when not applicable (a full image / an unsigned container). Only leaves[] is variable (one 4-byte hash per block). So the manifest-minus-leaves (mfl) is always 197 bytes and the parser is plain offset reads - no conditionals. Parsed by mota_parse_manifest().

off  size   field            notes\n0    1      format_ver       = 0x02 application, or 0x03 privileged bootloader package\n1    1      flags            bit0 FULL; bit1 SIGNED; bit2 BOOTLOADER; bits3-7 reserved 0\n2    1      hash_algo        0x12 = sha2-256\n3    4      target_id        device/arch/role discriminator (Section 9)\n7    4      fw_version       MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre   (comparable uint32)\n11   4      image_size       size of the reconstructed image (BODY||EndF)\n15   4      payload_size     PAYLOAD bytes in this container\n19   1      block_size_log2  e.g. 0x0B = 2048 (new application default); 0x0A = deployed 1024\n20   4      merkle_root      sha2-256:4 over PAYLOAD blocks (Section 6) - also the manifest_id\n24   32     image_hash       sha2-256:32 of the reconstructed image - SECURITY anchor\n56   1      codec_id         0=full/raw, 1=detools-sequential, 2=detools-in-place\n57   32     hw_id            NUL-padded ASCII hardware tag (e.g. \"RAK4631\"); same tag => bootable-compatible.\n                             SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.\n89   8      base_hash        sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.\n97   32     signer_pubkey    Ed25519 public key. 0 if not SIGNED.\n129  64     signature        Ed25519 over manifest[0, 129). 0 if not SIGNED.\n193  4      approval         FF FF FF FF = not approved; 41 50 52 56 (\"APRV\") = approved\n--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---\n197  4*BC   leaves[]         BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)\n

The signature always covers manifest[0, 129) (the head + base_hash + signer_pubkey). approval is outside the signed region so it can be flipped in place on consent without breaking the signature.

Manifest-minus-leaves size (mfl) is a constant 197 bytes for every container (full or delta, signed or unsigned). At 197 bytes the manifest exceeds one packet, so OTA_MANIFEST is always sent multi-fragment (Section 8.4, 2 fragments) and reassembled by the fetcher.

The two versions are deliberately disjoint. Version 2 accepts application packages only and rejects the BOOTLOADER bit. Version 3 accepts only flags exactly FULL|SIGNED|BOOTLOADER; a non-bootloader v3 package is invalid. Consequently, deployed v2-only application parsers reject a bootloader package before they can mistake its raw 40 KiB payload for an application image.

"},{"location":"ota_protocol/#41-signed-region","title":"4.1 Signed region","text":"

signature covers manifest bytes [0, 129) - the head + base_hash + signer_pubkey. It does not cover approval or leaves[]:

"},{"location":"ota_protocol/#42-the-approval-field","title":"4.2 The approval field","text":""},{"location":"ota_protocol/#43-privileged-nrf52-bootloader-package-profile","title":"4.3 Privileged nRF52 bootloader package profile","text":"

A v3 bootloader package has a deliberately narrow, non-extensible profile:

The incoming embedded identity must exactly match the installed CRC-valid bootloader identity. Both scans consider every aligned structurally valid candidate so magic bytes in a literal pool cannot shadow the real manifest. Duplicate accounting counts each CRC-valid 44-byte base record before interpreting adjacent continuity metadata, so a corrupt or half-present BLM2 extension cannot hide a second identity; after exactly one base record is selected, malformed claimed continuity fails closed. A package must be signed by a key already in the device's trusted allowlist; unlike ordinary application packages, there is no unsigned manual-install exception. The signed outer fw_version must equal the embedded boot version. Qualified internal/QSPI targets may bootstrap a CRC-valid legacy-v1 installed bootloader once; MeshTower SD instead requires local BLM2 provisioning because it has no safe legacy media handoff. After bootstrap every remote successor must be strictly newer and match the live SoftDevice/application layout. Low-byte zero and all-ones boot versions are invalid. Remote rollback has no override and must use local DFU/SWD.

"},{"location":"ota_protocol/#5-payload-codecs-delta-base","title":"5. Payload, codecs & delta base","text":"

PAYLOAD is either the full reconstructed image (FULL) or a delta (!FULL).

codec_id Meaning Used by 0 full / raw PAYLOAD = reconstructed image (BODY||EndF). ESP32 A/B or an external SD/QSPI nRF52 target. 1 detools sequential random read of base + sequential write of result -> ESP32 A->B inactive slot. 2 detools in-place bounded scratch; rewrites the app region in place -> nRF52 single-slot.

For deltas, base_hash = the base build's EndF.body_hash (sha2-256:8 of its BODY). A node applies a delta only if base_hash matches its own EndF.body_hash. After applying, the result MUST hash (sha2-256:32) to image_hash before it is booted - the hard security gate.

A fetcher only requests firmware it can apply. Each node declares the codec(s) it can apply (set_apply_codec/set_apply_codec2): ESP32 accepts full + sequential (+ in-place). Internal-staging nRF52 targets accept only in-place because internal flash cannot hold a second full application image. Matched SD and raw-QSPI nRF52 targets accept full + in-place because external media holds the container. A .mota with an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an external folder may accept other codecs because that path captures bytes and never installs them.

Compression is internal to the detools patch and must be supported by the applier. Patches are produced by detools 0.53.0 (tools/mota -> detools.create_patch) and decoded on-device by detools' embeddable C decoder, vendored verbatim at src/helpers/ota/detools/ (see its README.meshcore.txt). That build enables only the self-contained NONE + CRLE compressions (no malloc/liblzma/heatshrink), so MeshCore deltas use --compression crle. Do not reimplement the codec - use the vendored decoder.

"},{"location":"ota_protocol/#6-merkle-tree-sha2-2564","title":"6. Merkle tree (sha2-256:4)","text":"

Verifies each PAYLOAD block against the signed merkle_root before the whole payload exists, so corruption/forgery is localized to a block. Implemented in MerkleTree.cpp.

"},{"location":"ota_protocol/#61-proofs","title":"6.1 Proofs","text":"

A proof for block i is the ordered list of sibling digests from leaf to root. Promoted levels contribute no element. Verification (needs BC to know the tree shape):

h = leaf_i ; idx = i ; n = BC ; p = 0\nwhile n > 1:\n    if (n is odd) and (idx == n-1):        # this node was promoted\n        pass\n    else:\n        sib, side = proof[p] ; p += 1\n        h = sha2-256:4( sib || h ) if side==left else sha2-256:4( h || sib )\n    idx //= 2 ; n = (n + 1) // 2\naccept iff h == merkle_root and p == len(proof)\n

Over LoRa, leaves[] are omitted from the manifest transfer. A serving node computes a block's proof on demand from its stored leaves[] and normally sends OTA_PROOF immediately after that block's paced OTA_DATA. OTA_REQ_PROOF remains the fallback for an older source or a lost proactive proof. The fetcher fills its own leaves[i] as each verified block lands.

"},{"location":"ota_protocol/#7-block-availability-staging-resume","title":"7. Block availability, staging & resume","text":"

There is no separate availability structure. Block i is present <=> leaves[i] is non-erased (!= FF FF FF FF). Because leaves[] live in the staged flash region, availability survives reboot.

Commit order per block (crash-safe): (1) verify proof, (2) write block payload to its offset, (3) write leaves[i] last. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched (idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning leaves[] for a persistent, reopenable store. A hybrid nRF52 transfer is the deliberate exception: its SRAM-backed payload suffix is volatile, so the application refuses to adopt that staged header instead of rebuilding partial progress.

Resume (OtaManager::resumeStaged + OtaStore::checkpoint/reopenFor): an interrupted fetch resumes from the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress every N committed blocks) is runtime-tunable (ota config checkpoint <N>, 0 = only finalized containers resume). Boot-time adoption is an automatic fetch decision: current autofetch must be enabled, the stored target must equal the node target, policy signed requires the signed bit, and an enabled running-version floor requires a strictly newer manifest. An explicit MID pull may deliberately resume an older or unsigned package and keeps target 0 as a MID-only wildcard. Stores keep leaves[] in RAM until flush and never auto-GC, preserving resumable progress. The debug/operator equivalent is ota dev resume <MID8>; after a reboot the MID is mandatory, while a no-argument form may only reuse a still-active session MID. It never uses the nullptr automatic-adoption path, so a malformed MID or no active MID fails closed. The CLI refuses resume while an application or bootloader update is armed, before reopening storage can reset the approved handoff state. Raw ESP32/nRF52 staging scans every valid header before adoption: address order is not creation order. Explicit resume selects a unique matching MID/target; automatic resume refuses multiple valid headers without erasing them. A leaf-marker read error is never treated as a missing block: reconnectable storage pauses and re-verifies after reconnection, while other storage reports failure and permits an explicit retry. Hybrid nRF52 staging cannot enter this resume path after an application restart, even if its flash prefix still contains metadata; the complete logical container must be fetched again.

Flash-store note (RX-safe writes): a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa RX, so the flash stores (OtaStoreFlashNrf52/OtaStoreFlashEsp32) coalesce writes to the erase unit (4 KB page / sector) and commit each once off the per-packet path. Ordinary stores keep RAM at O(one page), not O(image); a qualified hybrid nRF52 profile additionally reserves one fixed 64 KiB staging arena. A small delta that fits page 0 does zero flash I/O until COMPLETE.

"},{"location":"ota_protocol/#8-lora-ota-protocol","title":"8. LoRa OTA protocol","text":"

Carried in MeshCore packets with PAYLOAD_TYPE_OTA = 0x0C. Every OTA packet payload is:

[0]    ota_msg_type      (OtaMsgType, OtaFormat.h)\n[1..]  body              (fixed per type; encode/decode in OtaProtocol.cpp)\n

Message types:

ota_msg_type val routing purpose OTA_ADV 0x01 discovery tiny per-node beacon (discovery tier 1) OTA_QUERY 0x02 discovery ask a source for its catalog (discovery tier 2) OTA_HAVE 0x03 discovery the catalog reply (fragmented, digest-tagged) OTA_GET_MANIFEST 0x04 transfer request a manifest's fragments (want_mask) by manifest_id OTA_MANIFEST 0x05 transfer the manifest-minus-leaves, fragmented OTA_REQ 0x06 transfer request fragments from an adaptive flight of 1-4 blocks (want_mask per block) OTA_DATA 0x07 transfer one self-describing fragment of a block's data OTA_REQ_PROOF 0x08 transfer request/re-request a missing proof OTA_PROOF 0x09 transfer the merkle proof for one block OTA_GET_LEAVES 0x0A transfer request the target's leaves[] fragments (want_mask) - warm-start only OTA_LEAVES 0x0B transfer a fragment of the leaves[] array (for host-side seed leaf-diff) "},{"location":"ota_protocol/#81-two-tier-discovery","title":"8.1 Two-tier discovery","text":"

Because a node may serve many mOTAs (its own firmware plus an external folder - Section 10), discovery is split so the periodic beacon stays tiny regardless of catalog size:

Tier 1 - OTA_ADV beacon (10 bytes, constant). Flooded as a short burst at boot, then every advert_mins minutes (default 24h; runtime-tunable via ota config advert, 0 disables the periodic re-advertise). It is also emitted immediately whenever the served set changes (e.g. a motatool folder is attached/detached), so peers learn about newly-available firmware without waiting for the next interval:

seeder_id[4]    advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id\nn_motas         uint8 - count of complete servable mOTAs (saturates at 255)\nset_digest[4]   sha2-256:4 over the SORTED set of served manifest_ids (see below)\n

set_digest is a content hash of the offering, not a counter: canonical across nodes, and it changes iff the set of served mids changes. A peer that has already catalogued this {seeder, set_digest} ignores the beacon (steady state is query-free). For a single served mota, set_digest = sha2-256:4(mid).

Tier 2 - OTA_QUERY -> OTA_HAVE (on interest only):

OTA_QUERY  (flood):  seeder_id[4]  set_digest[4]  filter_target(uint32) want_fragments(uint32)\n                     # filter_target 0 = everything; want_fragments 0 = every fragment\nOTA_HAVE   (flood):  seeder_id[4]  set_digest[4]  frag_idx(1) frag_total(1) n_rows(1)  rows[]\n  HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)\n

have_count is the number of blocks the source holds (== block_count for a complete offered image). Receivers do not advertise partial or completed downloads as new sources.

A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as OTA_HAVE rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog complete only after all frag_total fragments arrive. If any are missing after the recovery timeout, it sends another QUERY whose want_fragments bitmap names only the holes. want_fragments is an append-only extension: an original 13-byte QUERY is still accepted and means \"send every fragment.\" The heavy manifest is fetched per-mid only on commit (Section 8.3).

Fragment numbers are canonical pages of the complete catalog sorted by manifest_id. filter_target may remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages or changes frag_total. Receivers derive the unfiltered page count and each page's row count from the source's advertised n_motas. A page counts as received only if both counts match that complete geometry; filtered rows are still cached, but omitted rows leave the page eligible for an unfiltered recovery query. This also prevents legacy filtered replies which renumber pages from completing an unfiltered catalog.

"},{"location":"ota_protocol/#82-anti-storm-mandatory-at-mesh-scale","title":"8.2 Anti-storm (mandatory at mesh scale)","text":"

If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS pattern), all in OtaManager:

Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.

"},{"location":"ota_protocol/#83-fetch-handshake","title":"8.3 Fetch handshake","text":"
fetcher                                   server (any node that has the mid)\n  OTA_GET_MANIFEST(mid, want_mask)  >     (want_mask=0xFFFF first; only missing fragments on retry)\n                               <-------   OTA_MANIFEST(mid, frag_idx, frag_total, bytes)   x requested frags\n  (reassemble manifest, verify, compute geometry: BC, block_size, payload_size)\n  for each adaptive flight of missing blocks (starts at 1, grows on clean flights):\n    OTA_REQ(mid, {block_idx, want_mask}[]) >  (one packet; all fragments first, only holes on recovery)\n                               <-------   OTA_DATA(mid, block_idx, frag_off, data) x requested frags/block\n                               <-------   OTA_PROOF(mid, block_idx, n_proof, proof) x requested blocks\n    (independently reassemble + verify each block, but remain RX-silent until the flight drains)\n    [after adaptive deadline: recover one block's holes, or OTA_REQ_PROOF for a missing proof]\n    (clean flight grows by one block; recovered flight halves the next width)\n  when all blocks present: verify full merkle_root + image_hash -> COMPLETE\n

Before allocating or writing the selected store, the receiver parses the reassembled manifest and requires its merkle_root to equal the requested/wire manifest_id and its target_id to equal the catalog or explicit-pull target that opened the receive slot. The wire envelope and HAVE row are advisory; they cannot label and stage a different manifest.

"},{"location":"ota_protocol/#84-message-bodies-transfer","title":"8.4 Message bodies (transfer)","text":"

All offsets after the 1-byte type. Encoders/decoders in OtaProtocol.cpp; constants in OtaManager.h.

OTA_GET_MANIFEST:  manifest_id[4]  want_mask(uint16)   # bit k = send manifest fragment k; 0xFFFF = all\nOTA_MANIFEST:      manifest_id[4]  frag_idx(1)  frag_total(1)  bytes[]     # up to OTA_MF_FRAG=176 B/frag\nOTA_REQ:           manifest_id[4]  { block_idx(uint16)  want_mask(uint16) }[1..4]\n                   # legacy: bit k requests its 160-byte fragment\n                   # v2: bit15=marker, bit14=allow transport DEFLATE,\n                   #     bit13=2 KiB descriptor, bits0..12=fragment bitmap;\n                   #     bit12 must be zero when bit13 is set (0xFFFF stays legacy)\nOTA_DATA legacy:   manifest_id[4]  block_idx(uint16)  frag_off(uint16)  data[<=160]\nOTA_DATA v2:       manifest_id[4]  block_idx(uint16)  descriptor(uint16)\n                   stream_id[4]  data[<=171]\n                   # deployed 1 KiB descriptor: bit15=marker, bit14=DEFLATE,\n                   #   bits13..10=fragment, bits9..0=complete encoded length minus one\n                   # negotiated 2 KiB descriptor: bit15=marker, bits14..11=fragment,\n                   #   bits10..0=complete encoded length minus one; DEFLATE iff that\n                   #   length is strictly less than the manifest-derived raw block length\nOTA_REQ_PROOF:     manifest_id[4]  block_idx(uint16)\nOTA_PROOF:         manifest_id[4]  block_idx(uint16)  n_proof(1)  proof[]   # n_proof x 4 bytes\nOTA_GET_LEAVES:    manifest_id[4]  want_mask(uint16)   # bit k = send leaves fragment k; 0xFFFF = all\nOTA_LEAVES:        manifest_id[4]  frag_idx(1)  frag_total(1)  bytes[]      # up to OTA_LEAVES_FRAG=176 leaf bytes\n
"},{"location":"ota_protocol/#85-sizing-against-max_packet_payload-184","title":"8.5 Sizing against MAX_PACKET_PAYLOAD = 184","text":"message fixed overhead payload/packet OTA_DATA legacy 9 B (type+mid4+idx2+off2) OTA_FRAG_DATA = 160 -> 7 raw frags/1 KiB or 13/2 KiB OTA_DATA v2 13 B (legacy header + stream ID) OTA_FRAG_DATA_V2 = 171 -> 6 raw frags/1 KiB or 12/2 KiB; fewer when compressed OTA_MANIFEST 7 B OTA_MF_FRAG = 176 -> signed manifest ~ 2 frags OTA_HAVE 12 B 10 rows x 16 B per fragment OTA_PROOF 8 B up to ~44 sibling digests (>> any real tree)

A served mota supports up to 1024 leaves in the default 4 KiB proof scratch (about 2 MiB of payload at the new 2 KiB default, or 1 MiB for a deployed 1 KiB container); larger self-images pass a bigger scratch buffer.

"},{"location":"ota_protocol/#851-negotiated-zero-hop-kissraw-radio-transport","title":"8.5.1 Negotiated zero-hop KISS/raw-radio transport","text":"

OtaDirectWire.h defines an opt-in direct transport which bypasses the normal 184-byte Packet payload without changing Packet layout or enlarging its pool:

RF frame = F2 00 || ota_msg_type || existing OTA message body\n           2 B    <----------- at most 253 B ----------->\n

F2 selects reserved MeshCore version 4, PAYLOAD_TYPE_OTA, and direct routing; old MeshCore parsers reject that version. The second byte MUST be zero: no path, transport codes, relay envelope, or Ethernet header is permitted. Maximum RF length is 255 bytes; v2 DATA retains its 9-byte OTA header and 4-byte stream ID, leaving 240 data bytes. Thus raw 1 KiB/2 KiB blocks need five/nine DATA frames. Only transfer opcodes 0x04..0x0B are legal; discovery ADV/QUERY/HAVE MUST retain ordinary framing. V2 descriptor bits, compression permission and stream identity are unchanged. The outer framing, not a new opcode or request-mask bit, selects 240-byte geometry.

A receiver MUST first validate a manifest obtained through ordinary OTA framing. It MAY probe direct mode only when ota config hops is zero, that manifest was observed at zero relay hops, v2 is usable, and the original temporary-radio physical session/profile generation remains valid. Direct responses MUST match that fetch's manifest, transport geometry and physical session. If direct support has not yet been confirmed, the first adaptive deadline without direct DATA MUST invalidate that probe and retry ordinary 171-byte v2 framing on the original session. After confirmation, a wholly empty later flight gets one admitted same-mode sparse retry before that downgrade. Local queue rejection MUST NOT consume the retry, and valid direct DATA resets its empty-flight evidence. The existing 160-byte legacy fallback remains available afterward. Reassembly for uncommitted blocks is reset when geometry changes; already proof-verified committed blocks remain staged. Missing-fragment masks continue to drive recovery.

Endpoints MUST consume or reject this reserved envelope before normal Packet allocation, raw logs and mesh bridges. They MUST NOT relay it or reinterpret it as routed OTA; malformed, nonzero-path and unsolicited traffic cannot create an OTA context. A shared bounded raw-radio TX owner retains the selected profile generation and the OTA cancellation epoch until actual TX completion. It MUST honor carrier sensing, duty/airtime accounting and adaptive pacing, and discard pending work after cancellation, profile replacement or temporary-session expiry. Direct response trains permit the proof immediately after the final completed DATA frame; adaptive sub-1.0 quiet gaps still apply. KISS queue acceptance is not RF completion, and actual F8 TX_DONE is not receiver delivery acknowledgement.

Manifest signatures, Merkle proofs, encoded-stream checks, staged bytes and explicit install authorization are unchanged. This is a transport optimization, not a new trust or installation path. MESH_ENABLE_OTA_KISS_DIRECT defaults on for ENABLE_OTA builds using the shared raw-radio scheduler, except primary ESP-NOW builds; -DMESH_ENABLE_OTA_KISS_DIRECT=0 explicitly disables it. Enabling the build feature alone does not bypass the runtime negotiation/session gates. The separate host seeder's safe probe/serve examples and lab TX lock are in tools/lora_ota/README.md.

"},{"location":"ota_protocol/#86-temporary-radio-and-transfer-boundary","title":"8.6 Temporary-radio and transfer boundary","text":"

OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while a matching tempradio or tempradio2 window is actually running. Direct transfers remain bound to their original profile and generation; ending or replacing that window does not silently move them to the other radio. A receiver selects missing blocks in serial order into a bounded request flight. Every session starts with one block. A clean completed flight increases the next request by one block; a flight requiring fragment/proof recovery halves the next width (4 -> 2, 3 -> 2, 2 -> 1). The default compiled cap is two blocks; the RAK3401 LoRa-OTA target caps at four, so it probes 1 -> 2 -> 3 -> 4. All rows are sent in one backward-compatible OTA_REQ, and no freed slot is refilled until the current flight is finished. It never serves partial blocks. A normal install receiver never re-advertises its completed download. An SD archive node is the deliberate exception: after a fully proof-verified container is published to its persistent archive, it registers that complete file as a MotaSource and advertises it as a new seeder. This keeps each active transfer as one transmitter and one receiver while still allowing active temporary-radio repeaters between them and persistent archive nodes to improve future availability.

TempRadio is treated as a private maintenance network. Active transfer packets use priority 0, bypass the public-flood receive holdoff, use the full transmit budget without overwriting the saved normal-radio airtime factor, retain the relay role's airtime-scaled transmit collision window, and do not schedule generic flood retries. Deployed firmware predating that TempRadio budget override can be accelerated manually with a saved get af / temporary set af 0 / restore sequence. The bounded serving queue admits at most two DATA/PROOF packets ahead of the radio while preserving at least four free packet-pool entries. CAD remains enabled to arbitrate the half-duplex channel, but its busy retry is scaled to one-quarter of a packet airtime and clamped to 5-50 ms instead of the ordinary 120-360 ms cadence. Discovery traffic keeps collision jitter and background priority. The fetch deadline uses the active radio's measured maximum-packet airtime, remaining DATA/PROOF packet count, dispatcher airtime factor, and longest observed path (falling back to the configured hop horizon before one is observed), with bounded guard time. Faster SF/BW settings therefore recover loss sooner; slower or multi-hop settings do not spuriously re-request a response still on air. These changes remove software waits and duplicate bursts; they do not remove the one required forwarding transmission per hop.

"},{"location":"ota_protocol/#9-identity-trust-versioning","title":"9. Identity, trust & versioning","text":""},{"location":"ota_protocol/#10-multi-mota-serve-the-external-folder-relay","title":"10. Multi-mota serve & the external \"folder\" relay","text":"

A node serves a set of mOTAs: its own firmware plus, optionally, an external folder of .mota files it relays without holding them in flash. To peers it simply \"has N mOTAs\"; the relay is trustless (fetchers verify everything). The serve side (OtaManager) keeps a lightweight registry of what it advertises and two resident \"views\": view0 (its own firmware) and one on-demand view loaded from a source when a request targets an external mota. Every fetch message carries manifest_id, so dispatch is a registry lookup.

The USB/TCP host-folder link can also be a pull destination (the reverse direction): ota pull <mid8> folder fetches a .mota off the mesh and streams it onto the host as <mid>.mota via the seeder STORAGE ops (OP_STAT/BEGIN/WRITE/SREAD/FIN, see MotaSeederProto.h), using a FolderMotaStore as the fetch's OtaStore instead of RAM/flash. This captures an exact copy of a device's firmware - e.g. to build a delta against firmware you don't have. Resume is bookkeeping-free: BEGIN 0xFF-fills the file and, on reconnect after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), STAT+SREAD let the fetcher recompute and refill only the missing blocks. TCP disconnect preserves the selected MID, target, and destination across dynamic-context idle cleanup. Metadata initialization or seed leaf-diff restarts from the manifest on reconnect; an initialized transfer reopens and rehashes the same MID before requesting missing blocks. Host I/O failure during final verification or publication pauses too; a genuine Merkle mismatch remains a failed transfer. Explicit cancel/detach still releases the session, and a different host transport cannot take over a paused capture without cancellation. The phone-oriented BLE link is deliberately source-only and does not register a folder destination.

"},{"location":"ota_protocol/#101-the-motasource-abstraction-otasourceh","title":"10.1 The MotaSource abstraction (OtaSource.h)","text":"

Transport-agnostic provider of one or more complete .mota as random-access bytes. The same serve code drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only read() differs.

struct MotaDesc {                      // catalog metadata + region offsets (no whole image in RAM)\n  uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;\n  uint8_t block_size_log2, source_caps;\n  uint32_t total_size, leaves_off, block_count, payload_off, payload_size;\n};\nclass MotaSource {\n  virtual uint8_t count();                                  // # mOTAs offered\n  virtual bool    describe(uint8_t idx, MotaDesc& out);     // metadata + offsets\n  virtual bool    read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len);   // random-access bytes\n  virtual bool    read_deflated_block(uint8_t idx, uint16_t block,\n                                      uint8_t* buf, uint16_t cap, uint16_t* len); // optional raw RFC 1951\n};\n

Legacy source paths retain the manifest-minus-leaves and four bytes per source block, bounded by OTA_PROOFGEN_SCRATCH, then stream payload blocks on demand. Non-PSRAM ESP32 Full instead retains roots of 32-leaf groups and rereads the selected group. Its complete workspace is bounded to 1,536 bytes while retaining 4,096 blocks and byte-identical proofs. Partial groups use the same odd-node promotion as the full tree. Receive pipelines, capture, serving and wire formats remain unchanged; larger custom leaf limits retain the legacy path.

With OTA_BOUNDED_SOURCE_PROOFS=1, optional read_quantum() and read_metadata() methods bound each metadata step. SerialMotaSource uses the current negotiated single-response capacity and rejects a chunk that no longer fits after a capacity change. Proof preparation yields between chunks, and queued jobs retain their buffers across radio backpressure. See the V3 Full qualification for allocation bounds and regression coverage.

"},{"location":"ota_protocol/#102-the-mota-seeder-transport-motaseederprotoh","title":"10.2 The mota-seeder transport (MotaSeederProto.h)","text":"

A MotaSource is fed by a host that serves a folder over the device's USB serial (the same console the CLI uses - no extra hardware), on an ESP32 WiFi companion or FULL ESP32 role over WiFi (TCP), or on an nRF52 Full Companion over an encrypted BLE GATT service. The host is the standalone Rust tool motatool (motatool serve --serial <port> / --tcp <host[:port]>, which also builds + verifies + inspects .mota). The device only emits request frames while actively serving a fetch, and reads the reply synchronously, so over the shared USB console binary frames coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:

request  (device -> host):  'M' 'S'  op(1)  args...                 xsum(1 = XOR of op+args)\nresponse (host -> device):  'm' 's'  op(1)  status(1)  payload...    xsum(1 = XOR of all prior)\n\nOP_COUNT     0x01   args: -            -> payload: count(1)\nOP_DESCRIBE  0x02   args: idx(1)       -> payload: MotaDesc wire (38 B)\nOP_READ      0x03   args: idx(1) off(4) len(2)  -> payload: len bytes\nOP_DEFLATE_BLOCK 0x09 args: idx(1) block(2) off(2) len(2)\n                         -> payload: total_encoded_len(2) + requested bytes\nMotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)\n                      total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)\n                      block_size_log2(1) source_caps(1) reserved(2)\nstatus: 0 = OK, non-zero = error (out of range / past EOF).\n

SerialMotaSource splits logical reads into replies of at most 192 payload bytes. A manifest's leaf table can exceed 256 bytes and new payload blocks are normally 2 KiB; requesting either in one transaction can overrun common USB CDC/UART receive rings even though the host successfully wrote the complete reply. Chunking is internal to the transport and does not change OP_READ or the MotaSource random-access contract.

Each response uses one deadline, starting after request transmission, for its magic, header, payload and checksum together. The deadline does not restart for each received byte. USB folder sessions use 600 ms; BLE/TCP folder sessions use 3000 ms. Each chunk of a larger logical read has its own deadline. A slow trickle can therefore fail sooner than before; a maximum 197-byte response takes about 17.1 ms on a 115200-baud UART, excluding host processing latency.

A shared USB text/control session explicitly opts into retaining a timed-out response's sync/header/payload/checksum position. No subsequent USB request is sent until that old response reaches its known checksum boundary, or an actual host-session reset clears it. The request supplies the payload length: the response itself carries neither a length nor a request ID. Late binary bytes therefore cannot become USB ota folder off control text. A complete reply followed by that ordinary shutdown line still works. If a software handoff or failed attach leaves a response outstanding, USB input is quarantined until its boundary; commands typed during that wait are discarded. Leaving a log-only USB/network handoff preserves that response for the tracked consumer rather than discarding its tail as raw input. A deliberate USB mOTA entry cancels startup probe and deferred ASCII timers so they cannot reclaim the stream or emit terminal text into an active transfer. If the sender abandons a partial reply, reconnect the USB host session to recover. Bare shutdown text inside a partial payload is inherently ambiguous and is not interpreted as control. This assumes one ordered reply per request; unsolicited duplicate or reordered binary replies cannot be unambiguously separated by this legacy wire format.

Dedicated UART, BLE and TCP sources retain their legacy retry/stale-drain behavior; their absolute response deadline still applies to every request.

OP_DEFLATE_BLOCK lets a host-folder source perform the optional transport compression without linking an encoder into the embedded seeder. len=0, off=0 queries the exact encoded length; subsequent chunks are at most 190 bytes, keeping the total response payload at 192 bytes. The host independently encodes each manifest payload block as ordinary raw RFC 1951 at level 9. It returns an error for an invalid range, an unsupported operation, or a result that is not smaller than the raw block; SerialMotaSource then serves the ordinary raw v2 representation. A supporting host sets source_caps bit 0 in every descriptor. Older hosts leave that formerly-reserved byte zero, so upgraded firmware skips the optional request instead of waiting for an old daemon that silently ignores unknown operations.

Manifest fragments are retained as bounded response jobs and admitted one at a time. Their source-side gap follows the active maximum packet airtime and dispatcher duty spacing, clamped to 100-1000 ms. The 100 ms floor protects fast radios' TX-to-RX turnaround; the cap keeps the receiver's one-second manifest progress/retry observation responsive. The source uses the same radio-aware 100-3000 ms drain/turnaround gap before an unsolicited block proof, but immediately serves a legacy receiver's explicit OTA_REQ_PROOF. Relay collision delay is a separate setting: active OTA floods honor the relay role's configured txdelay, and the deployment runner temporarily uses 0.3 on managed relays.

What to plug into --serial. Use the USB serial console of an OTA-enabled MeshCore node built with OTA_FOLDER_SERIAL. The node must have a working LoRa radio plus an ota folder on command; that command confirms it can host and advertise the folder. A KISS modem will not work with motatool serve --serial: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and mota-seeder request/response transport. The separate mota_kiss_direct.py source implements that full KISS interface. An ESP32 WiFi companion or FULL ESP32 role with active WiFi is the alternative source connection: use its dedicated seeder port with motatool serve --tcp <host>:5001. An nRF52 Full Companion can instead pair with a phone or Linux host, subscribe to its mOTA request characteristic, and use protocol-v14 CMD_BLE_MOTA_SOURCE. That BLE path is source-only; it does not expose the reverse FolderMotaStore capture operations.

Device CLI: ota folder on (attach + announce), ota folder (list), ota folder off. Build flag OTA_FOLDER_SERIAL (default stream = console Serial; override OTA_FOLDER_SERIAL_STREAM + define OTA_FOLDER_SERIAL_BEGIN for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a WiFiServer on the dedicated seeder port (OTA_SEEDER_TCP_PORT, default 5001) while WiFi is usable. On a companion it is separate from the app port (TCP_PORT, default 5000); on infrastructure roles it is separate from WebConfig and browser OTA on port 80. The node auto-attaches the source when a seeder client connects and detaches when it closes (no ota folder on needed over TCP). An already-active serial folder causes a TCP client to be rejected instead of silently replacing it. Verified on hardware: a RAK4631 relays a host folder to a Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (:5001) while the companion serves a phone on :5000 - every block merkle-checked.

The attach reply and bare ota folder report host=advertised/offered. The registry is RAM-bounded (OTA_MAX_SERVE, with the node's own firmware consuming one slot), so a host may correctly index more valid files than this particular firmware can advertise. Omitted entries are now reported instead of silently disappearing. Operators should split a large chain or use a higher-capacity/SD seeder when the two counts differ.

Transport-agnostic by design. The request/response semantics (COUNT / DESCRIBE(idx) / READ(idx, off, len) over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum + resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a reliable stream and the WiFi (TCP) transport reuses it as-is - both ends just treat the socket as a byte stream (on-device, SerialMotaSource runs verbatim over an Arduino Stream-compatible WiFiClient; motatool's TcpTransport mirrors its SerialTransport). The nRF52 Full Companion's BLE GATT path also reuses the exact frame and checksum. Device requests are notifications on a dedicated characteristic and host responses are ordered write-with-response fragments on a second characteristic. Keeping the same framing makes retries and corruption handling identical across USB, TCP, and GATT. The Linux reference implementation is tools/ble_mota/ble_mota_seeder.py; a phone app can implement the same transport-free catalog operations.

"},{"location":"ota_protocol/#11-cli-surface-otaclicpp","title":"11. CLI surface (OtaCli.cpp)","text":"

User-facing OTA data should travel via CMD_OTA_* companion binary frames; the text CLI below is debug/operator oriented and replies are snprintf-bounded into a 160-byte buffer.

Commands take intuitive aliases (matched by the first word; see is_cmd in OtaCli.cpp) so they're easy to type and read - status/neighbors/pull/drop/applydelta are the canonical names, the aliases are the recommended user-facing forms. Output is plain-language (a user-facing guide lives at ota_user_guide.md).

ota help | ? | h                   list the commands\nota status | st  (or bare `ota`)   plain-language: running fw, the one fetch session (state/%/id), serving, keys\nota ls | neighbors | nbrs | updates | n [page]   paged updates (queries sources; rows arrive async via OTA_HAVE)\nota get | pull | download <mid8|#index> flash [rescue] | folder [validate]\n                                      fetch by stable mid8 (preferred) or current page index\nota install | apply | applydelta   verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader\nota rescue install <base_hash16>  internal-flash nRF52 only: recover from failed app-side EndF validation\nota bootloader [status]           capable allowlisted nRF52 repeater: installed BL identity/caps + staged confirmation\nota bootloader install <MID8> <HASH16>\n                                      explicitly verify/arm one complete trusted v3 package; never automatic\nota cancel | drop | stop           drop the fetch; durably invalidate device staging, or retain a folder partial for resume\nota announce | adv                 serve self + send a beacon now\nota self | id                      print this firmware's EndF (body/image size, base_hash)\nota qspi | storage                 QSPI nRF52 only: JEDEC/SR1/stage/latched storage error (read-only)\nota folder | fold [on|off]         attach/detach an external .mota folder (host daemon) ; bare = list\nota config | cfg | set [autofetch|autoinstall|checkpoint] ...   show/set persisted policy\nota key | keys [add|rm <hex>]      trusted signer allowlist ; bare = list\nota dev ...                        bring-up helpers (stage/recv/serve/resume <MID8>/verify)\n

For a device-backed pull, current firmware returns success only after the persistent store can no longer be reopened; flash/SD/QSPI I/O or readback failure is reported as an error even though the in-memory manager session was dropped. For a folder pull, cancellation detaches the live transfer but deliberately leaves the host .part file available for a later resume. If the shared receive engine is currently performing the MeshTower SD auto-archive capture, cancellation likewise detaches that archive transfer and retains its .part file; it does not erase the unrelated manual-install store.

"},{"location":"ota_protocol/#12-apply-bootloader-contract","title":"12. Apply & bootloader contract","text":"

A signature, when present, proves author authenticity and must pass the device allowlist. Unsigned v2 application packages remain installable when local policy permits them. A v3 bootloader package is always signed and trusted. The one-shot approval marker records local consent before the bootloader may apply it.

Bootloader testing note: always test apply with a real different image (base != target). A same-image (X->X) \"delta\" trivially reproduces the target and gives a false positive.

"},{"location":"ota_protocol/#13-versioning-of-this-spec","title":"13. Versioning of this spec","text":"

The fixed byte layout has two intentionally disjoint profiles: format_ver = 2 for ordinary application packages and format_ver = 3 only for the exact privileged bootloader profile in Section 4.3. A parser must reject v2+BOOTLOADER, v3 without exact FULL|SIGNED|BOOTLOADER, and every other version. The multihash hash_algo separately allows swapping the digest family without a format bump. Unknown codec_id / ota_msg_type values are ignored (a node simply will not fetch what it cannot apply).

"},{"location":"ota_user_guide/","title":"Ota user guide","text":"

View this page on MeshCore Docs.

"},{"location":"ota_user_guide/#updating-your-node-over-the-air-ota-user-guide","title":"Updating your node over the air (OTA) - user guide","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Before a Companion upgrade, export its contacts when possible. ESP32, RP2040 and STM32 can retain complete legacy records ahead of an interrupted final record when no backup is reported present; nRF52 has separate migration rules. See legacy interrupted contact saves for the recovery limits and next-save behavior.

This guide is for node operators: how to update your MeshCore device's firmware over the radio, in plain language. No cables, no programmer - your node can download a new firmware from a neighbour and install it. (For the technical wire format, see the OTA protocol spec.)

LoRa OTA download and installation are present only in supported Keymind destination artifacts; the receiver must already be running one of those install-capable builds. Some internal-staging nRF52 targets use a lean lora_ota_no_external_sensors target, while matched external-QSPI boards can retain their normal full-sensor repeater features. Release filenames include an OTA marker, but capability must still be confirmed on the running device. A source can be an OTA-enabled infrastructure node or a source-only Full Companion backed by motatool. Intermediate repeaters do not need OTA-enabled firmware: current repeater builds transport OTA floods opaquely, subject to their normal forwarding filters, duplicate checks, and flood limits. OTA radio traffic is accepted, generated, and relayed only while tempradio is actually running on that node. Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.

The recommended temporary OTA settings use 250 kHz bandwidth, SF5, CR5, and a 120-minute window. For a North American node currently configured for 910.525 MHz, run this on every participating node:

tempradio 910.525,250,5,5,120\n

Use the node's current permitted regional frequency in place of 910.525 when necessary.

The scripted updater checks the firmware version of the receiver and every sender before deciding whether RX power saving can remain active. At SF5/BW250, an all-v1.17.1.5-or-newer path can use effective RXPS level 8 with the tuple-selected 64-symbol physical preamble. SF5/BW500 can use level 8 with 128 symbols. Other fast tuples remain at the shortest viable preamble, normally 32. A mixed, older, or unknown long-preamble path is put in continuous receive for the update, then the receiver's exact saved RXPS setting is restored. This preserves the RX benefit without risking missed packets from a legacy 32-symbol sender.

Can my node install the update? Choose a release-table artifact explicitly labelled LoRa-OTA capable, then confirm ota self and ota status expose install support; do not infer support from the filename alone. LoRa OTA firmware is available for supported ESP32 boards and nRF52 repeater targets. Every nRF52 installation also requires the OTAFIX bootloader built for that exact board; having an OTA-capable application image alone is not enough. An intermediate repeater only relays packets and needs neither an install-capable image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.

The following nRF52 repeater families gained firmware-side LoRa OTA targets in this release. Their ordinary repeater keeps its complete declared sensor recipe; the install-capable lora_ota_no_external_sensors sibling is smaller:

The RAK3401 RAK_3401_repeater_lora_ota_no_external_sensors image trims selected optional environmental/ranging drivers but does not disable I2C. It retains the INA219, INA226, INA260, and INA3221 voltage/current monitors, board display and RTC support, and both RAK12500 I2C and RAK12501/L76K UART GPS paths. The four INA drivers are only the retained voltage/current entries in the optional environmental-sensor table; they are not the only I2C users. The SSD1306 OLED, autodiscovered DS3231/RV3028/PCF8563/RX8130CE RTCs, and RAK12500 GPS use I2C as separate board peripherals. Install one GPS module in sensor slot A. Slot D's reset/PPS lines conflict with the RAK13302 radio's BUSY/DIO1 lines.

The firmware-configured INA3221 address and the RAK12500 address are both 0x42, so those devices cannot share one bus at those addresses. To install both, leave the RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a firmware build with -DTELEM_INA3221_ADDRESS=0x43.

The reduced RAK4631 profiles likewise retain the four INA monitors. The plain repeater and Serial2 bridge retain GPS: a RAK12501 can use sensor slot A or D, and a RAK12500 can use slot A or C. The runtime RS-232 bridge defaults to Serial2 so the UART RAK12501 can retain Serial1. The merged runtime image always reserves Serial1: a bounded silent probe cannot prove that a cold RAK12501 is absent, and turning the GPS setting off cannot stop a fitted module from driving the shared UART while WB_IO2/3V3_S remains powered. Use Serial2. Serial1 requires an explicit no-GPS/dedicated image. The reservation remains fail-closed even when an I2C RAK12500 is detected. The explicitly compiled RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors target omits the combined GPS provider because its bridge owns the UART used by RAK12501; consequently that legacy image does not expose the I2C RAK12500 path either, even though RAK12500 itself does not use the UART.

Selected nRF52 repeaters with dedicated external QSPI can now stage the complete package off-chip, so their normal full-sensor repeater build can install a full image or an in-place delta. The current matched families are XIAO nRF52840 and its XIAO-module derivatives, original LilyGo T-Echo, ThinkNode M1/M6, Wio Tracker L1, SenseCAP Solar, and the dedicated RAK4631 + RAK15001 slot-C target. The list also includes a separately wired W25Q16JV on RAK19007 with either RAK4631 or RAK3401 + RAK13302. These require the corresponding exact QSPI-aware OTAFIX bootloader; see the nRF52 QSPI guide.

The W25Q16 option uses one permanent RAK19007 wiring for either core: CLK/MISO/ MOSI tap IO-connector pins 26/27/28, flash CS uses J11 AIN1, and power uses J12 VDD (3.3 V) and GND. Add an approximately 10 kOhm CS-to-VDD pull-up and never power the flash from VBAT. The three SPI signals require underside pads or an IO-connector interposer; the easy J10/J11/J12 headers do not expose them. The flash consumes no WisBlock slot, so a supported GPS can remain in slot A.

The unified RAK repeater images additionally support a header-only W25Q16 layout, using J10 TX1/RX1 and J11 IO1/AIN1. This layout needs the matching compatible OTAFIX 2.4.10 bootloader and gives up UART1 GPS/PPS; I2C sensors remain available. See the pin table.

The ordinary full-sensor RAK_4631_repeater image remains too large for the safe internal in-place update limit. Without external flash, use RAK_4631_repeater_lora_ota_no_external_sensors; it trims selected optional environmental/ranging drivers while retaining generic I2C, battery monitoring, the four INA monitors, and target-compatible GPS. A RAK4631 fitted with RAK15001 in sensor slot C can instead use RAK_4631_repeater_rak15001_slot_c_lora_ota to retain the full sensor/GPS set and stage full images or deltas off-chip.

For the external Winbond breakout, use RAK_4631_repeater_w25q16_lora_ota on RAK4631 or RAK_3401_repeater_rak13302_w25q16_lora_ota on RAK3401 + RAK13302. They retain the full sensor/GPS recipe and require exactly a 2 MiB EF4015 W25Q16 plus the bootloader for that same core/radio/flash combination. Install wiscore_rak4631_w25q16 for RAK4631 or wiscore_rak3401_rak13302_w25q16 for RAK3401 + RAK13302 as a one-time prerequisite. The physical wiring is shared, but the two firmware identities are deliberately different and cannot be substituted for one another.

"},{"location":"ota_user_guide/#the-important-part-first-its-safe","title":"The important part first: it's safe","text":""},{"location":"ota_user_guide/#how-to-talk-to-your-node","title":"How to talk to your node","text":"

Connect to your node's console - usually a USB serial terminal at 115200 baud (or whatever tool you already use to manage the node). You type ota ... commands and the node replies in plain words.

The commands have short, friendly names (and most accept aliases, so you don't have to remember exact spelling): type ota help any time to see the list, or just ota for a status summary.

"},{"location":"ota_user_guide/#common-tasks","title":"Common tasks","text":""},{"location":"ota_user_guide/#1-see-what-im-running-and-whether-anything-is-going-on","title":"1. See what I'm running and whether anything is going on","text":"
ota status\n

Shows your current firmware version, your node's update \"target\" (its hardware/role id), and whether a download is in progress.

For a denser admin view - your firmware's content id (mid) and its body hash, the fingerprint of the set you're serving, live download progress, and the current policy - use:

ota stats\n

On a remote node this is admin-only (the remote command console requires the admin password) - send it from the app's repeater command screen, or the WiFi/serial OTA console.

"},{"location":"ota_user_guide/#2-find-updates-available-near-me","title":"2. Find updates available near me","text":"
ota ls\nota ls 2                 # page 2 when more than two updates are available\n

Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words - each with a temporary number, a stable eight-hex manifest ID, its version, whether it's a full image or a small delta, how many nodes have it, and how recently it was seen. For example:

Updates 1/1 (2 src; refreshing):\n 1) 838B8169 v1.2.3 delta [same target] 3n 5s\n 2) BF0AB0C4 v1.2.0 full [unsupported] 1n 12s\n

Each row shows the version, full-vs-delta, whether it fits your node, how many nodes have it, and how long ago it was seen. The fit marker:

Run it again after a few seconds - discovery happens in the background, so the list fills in. Nothing is downloaded yet; this is just looking around. refreshing means the command has just sent asynchronous catalog queries, so run it again even when an older row is already visible. Two updates fit in each remote CLI reply; use ota ls 2, ota ls 3, and so on for later pages. Catalog rows can change while replies arrive, so use the displayed manifest ID for scripts and important operations rather than a numeric position. (ota neighbors / ota updates also work.)

"},{"location":"ota_user_guide/#3-download-an-update","title":"3. Download an update","text":"

Pick one from the list by its stable manifest ID (a number also works for interactive use), and say where to put it:

ota pull 838B8169 flash            # stage it in this node's flash, to install here\nota pull 838B8169 folder           # capture it onto a connected motatool folder as <id>.mota\nota pull 838B8169 folder validate  # warm-start capture from a motatool --seed build (much faster; below)\n

The destination is required - ota pull 838B8169 on its own just shows the choices. flash is always available (stage here, then ota install). folder appears only while a motatool serve link is attached (it shows the link, e.g. folder: tcp 192.168.4.5); it streams the firmware straight onto the host folder - nothing is staged on this node. That's how you grab an exact copy of another device's firmware off the mesh (to a .mota file) so you can later build a delta against firmware you don't otherwise have. (ota get is an alias.)

Deliberate alternate image. An operator can choose a different target ID for the same physical board by selecting its exact manifest ID from ota ls, then running ota pull <MID8> flash and ota install. The target ID controls which image is discovered and fetched; it does not lock manual installs to the current role. The hardware identity, codec, image hash, and partition checks still run. Automatic updates follow the firmware's selected target (or its declared one-way successor), so use ota config autofetch off and ota config autoinstall off if you want to keep an alternate image without automatic convergence.

validate (warm-start, advanced). Capturing a full image over the radio is slow. If you have a similar build on the computer (e.g. a fresh recompile of the same firmware), run motatool with --seed <that.mota> and add validate: the node fetches just the target's block fingerprints, keeps every block your seed already matches, and pulls over the radio only the handful that actually differ - turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.

Where the seed comes from: it is the --seed <file> you pass to motatool serve - not a file you drop into the capture (--dir) folder, which is only the destination and starts empty. There is exactly one configured seed. When you run ... folder validate, the node asks motatool to begin the capture and motatool stamps that seed's payload into the fresh .part in the same step - so it is always the file you named, with no guessing. validate is the switch: a plain folder pull ignores any seed and fetches from scratch; re-running a validate pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is trusted - every kept block is checked against the target's own fingerprints, so a mismatched or missing seed just means those blocks are fetched over the radio (correct result, only slower).

The node fetches from one source as primary traffic, with bounded adaptive request flights. Every session probes with one manifest block, then clean flights grow 1 -> 2 -> 3 -> 4 blocks on RAK3401 OTA builds. New application packages use 2 KiB blocks; deployed 1 KiB packages remain supported. All blocks in a flight share one request packet, and the receiver stays silent until the source/relays finish returning them. A recovery halves the next flight. Retry timing follows the active SF/BW airtime, duty budget, and path length, so faster settings recover sooner without a fixed one-second request colliding with a valid multi-hop response. Mesh repeaters carry the packets only while their temporary-radio windows are active. Check progress with ota status.

If a folder pull loses its link mid-transfer, ota status shows paused - the host keeps the partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never falls back to flash. A TCP disconnect preserves the selected image even if it happens before its manifest arrives or during the final save. Reconnect through the same transport; an interrupted manifest or seed comparison restarts initialization, while a partial transfer rechecks its stored blocks. The paused capture keeps its OTA workspace until reconnection or cancellation. To stop a download you no longer want:

ota cancel\n

For a download staged on the node, success means current firmware also invalidated the persistent flash/SD/QSPI copy; it reports an error if that media operation cannot be verified. For a folder capture, cancel only detaches the live session and retains the host partial so it can be resumed or removed on the host deliberately. On a MeshTower SD auto-archive capture, cancel also detaches the live archive session and retains its card partial for the archive service to resume later; use ota cache off to stop new archive captures.

"},{"location":"ota_user_guide/#4-install-a-downloaded-update","title":"4. Install a downloaded update","text":"

Once ota status shows the download is ready to install:

ota install\n

The node verifies the firmware one last time, and if everything checks out it installs it and reboots into the new version. If the check fails, it tells you why and does not install. Unsigned images normally install only through this explicit command. The MeshTower V2 SD target is stricter and requires an allowlisted signature even for a manual application install, because it authorizes removable-media bytes for the bootloader. A signed image whose signer is not in the device allowlist is rejected; trusted signed images can auto-install only when that policy is enabled and the signed version is strictly newer than the running hash-valid EndF version. Manual ota install remains the deliberate equal-version/rollback override.

After it reboots, run ota status to confirm the new version.

"},{"location":"ota_user_guide/#updating-an-nrf52-bootloader-advanced-explicit-only","title":"Updating an nRF52 bootloader (advanced, explicit only)","text":"

This is available on specially marked no-external-flash nRF52840 lean repeater/bridge builds, on the legacy XIAO nRF52840/Sense raw-QSPI builds, and on the exact Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors microSD build, after a one-time exact-board ABI-3 OTAFIX installation over USB/BLE DFU or SWD. It is not the normal firmware update path. Check support first:

ota bootloader\n

The reply must show a CRC-valid installed identity plus ABI 3 and the exact storage/boot-update capability bits for that build (0x09 for MeshTower V2 microSD, 0x0A for the shared internal store, or 0x0E for XIAO raw QSPI). A bootloader package appears as bootloader in ota ls. It is never downloaded or installed automatically, even if both OTA automation settings are enabled. Use its stable ID explicitly:

ota pull <MID8> flash\n# wait until ota status says this bootloader download is ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy both confirmation values exactly from the second ota bootloader reply. Ordinary ota install deliberately refuses this package. The privileged command requires an exact 40 KiB candidate payload in the fixed 41,330-byte container, a valid exact embedded identity/CRC and vector table, continued boot-update support, required BLM2/SOFT continuity metadata at canonical raw-image offset 0x9FB4, a boot version that exactly matches the package and is newer than installed BLM2, and a valid signature from a key already in ota key's trusted allowlist. It preserves the running application while OTAFIX replaces itself; blup:C8 in post-reboot ota status means success. Remote rollback is refused. Any node lacking this command or those capabilities must update its bootloader locally instead.

On an internal-flash target, the package shares the ordinary store below 0xED000 and bottom-aligns at 0xE2000; there is no separate reserved scratch bank. A valid live EndF must prove the current image ends at or below that address before any page is erased. If EndF is missing/corrupt or the app is too large, the pull is refused and local DFU/SWD is required. On the MeshTower V2 SD target, the application linker remains at 0xED000, but bootloader replacement needs temporary scratch beginning at 0xE0000. A hash-valid live EndF must therefore prove the complete current image ends by 0xE0000. A CRC-bound boot-settings bank must cover that complete image while also ending by 0xE0000. MeshCore binds both application and bootloader SD approval to purpose, exact raw geometry, and a normalized SHA-256 in a reset-retained MOTASDA2 record. Boot updates also bind the exact authenticated signed image hash in the E0000 token. OTAFIX consumes the retained record before media access, so a later card change or power cycle fails instead of authorizing different bytes. MeshCore never claims or writes raw sector 1. Both application and bootloader OTA wait until a matching BLM2 bootloader has been provisioned locally; preview.12 requires USB/BLE DFU or SWD first. Larger applications can continue to use normal application mOTA; only bootloader self-update is refused. See the nRF52 bootloader-update guide for the complete target inventory, storage layouts, and safety contract.

"},{"location":"ota_user_guide/#5-if-something-goes-wrong","title":"5. If something goes wrong","text":"

text ota pull <mid8> flash rescue # wait for ota status to say ready to install ota rescue install <base_hash16>

This is not a force option. It refuses a normally valid EndF, a different package hash, hardware or target mismatch, corrupt payload, and invalid/untrusted signatures. The bootloader independently hashes the running app and rejects a wrong base before writing the app. If the physical EndF is absent or the rescue commands were not already in the running firmware, recover over USB. Release chains should put this command in their first bridge and keep it in every bridge after that. The shared-internal bootloader-update builds are intentionally excluded: without valid live EndF, they refuse every internal pull before erase because their normal application can extend through 0xED000. Recover one of those builds over USB/BLE DFU or SWD. - If an install fails, the node won't boot a broken image - it lands in recovery mode: - nRF52: it appears as a USB drive; drag a known-good firmware .uf2 for that exact board onto it to recover. - ESP32: it keeps the previous firmware in the other slot and rolls back. - When in doubt, you can always re-flash over USB the normal way.

"},{"location":"ota_user_guide/#adjust-lora-ota-speed","title":"Adjust LoRa OTA speed","text":"

Use one saved speed factor for this node's LoRa OTA traffic:

get ota.speed\nset ota.speed 0.5\nset ota.speed .05\nset ota.speed 3\nset ota.speed 1\n

The range is 0.05 through 3, including decimals. 1 is the existing timing. At 0.5, adjustable OTA delays are twice as long; at 0.05, they are twenty times as long. At 3, adjustable delays are divided by three. Actual transfer speed also depends on packet airtime, the configured airtime budget, relays, and packet loss; 3 does not guarantee three times the throughput.

The factor covers OTA requests and responses (catalog, manifest, leaves, DATA, and proofs), OTA relay delays, discovery jitter, and automatic OTA adverts. Sources queue manifest and leaf replies and admit their fragments one at a time through the paced transmit queue. Repeated requests merge pending fragments; a full response queue leaves the request available for another source or a later retry. This preserves packet space for other mesh traffic. Below 1, the transmit queue also spaces out every OTA packet, including both profile copies, while letting ordinary messages run. Recovery waits grow for slower transfers; faster settings retain the existing retry allowance so they do not retry before a physical response train can finish. The radio's frequency, bandwidth, SF, coding rate, preamble, and airtime budget stay as set. The proof turnaround allowance also keeps its existing physical minimum. Dual-profile timing accounts for the participating profiles and reply copies; it does not vary with the scanner's current visit. Deliberate slow pacing is excluded from observer builds' stale-packet age, while congestion still ages queued packets normally.

Set the same factor on the source, destination, and OTA relays. It is a local setting, not a value negotiated with peers. A slow source paired with a receiver at 1 can cause unnecessary retries. The setting takes effect during an active session, survives reboot and temporary-radio expiry, and applies to OTA on either radio profile. set ota.speed 1 restores the previous pace. Ordinary chat, node adverts, and USB/WiFi source transfers keep their timing. Local verification of a staged image and leaf comparisons keep their normal maintenance rate even when LoRa pacing is slow. If the local transmit queue cannot accept a catalog, manifest, or leaf request, OTA keeps it pending without consuming a network retry. Successfully queued requests that receive no answer still have bounded retries. Blocked block requests also preserve the v2 transfer format and fragments already received; queue congestion alone does not trigger legacy fallback. Retry timing includes the primary channel when an RX-only temporary radio2 uses crossover to send requests there.

ota config speed 0.5 is an equivalent setter. get ota.speed, ota config speed, and ota speed show both the saved ota.speed setting and the effective packet pace. The latter is the slower of the saved speed and the sender's automatic loss-responsive packet-spacing factor; it does not replace the saved setting or change non-packet OTA timers. The ota config summary also shows both values. If no OTA workspace is active, the automatic factor starts at 1, so packet reflects the saved setting up to 1x. The setting is available on OTA-enabled repeaters, rooms, sensors, Companions, and seeder-only builds. Its separate /ota_speed settings file preserves the existing preference layouts and does not need an active OTA workspace.

"},{"location":"ota_user_guide/#optional-let-it-update-automatically","title":"Optional: let it update automatically","text":"

By default your node only discovers updates - it won't download or install on its own. If you want more automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.

ota config autofetch any        # auto-DOWNLOAD any compatible update for this node (still won't install)\nota config autofetch signed     # auto-download verified updates from allowlisted signers\nota config autofetch off        # back to manual (default)\n\nota config autoinstall trusted  # auto-INSTALL only a trusted signed version newer than the running EndF\nota config autoinstall off      # never auto-install (default)\n\nota config advert 1440          # re-advertise every N minutes while temp radio is running\nota config advert 0             # only advertise when a temp-radio window starts\n\nota config hops 3               # how far OTA travels: accept from / relay up to N repeater hops (default 3)\nota config hops 0               # only exchange OTA with directly-connected nodes (never relay)\n\nota config                      # show the current settings\n

With autofetch signed, add the author's public key using ota key add before expecting an automatic download. The node verifies the manifest's signature against that allowlist before planning, erasing, or writing staging storage. autofetch any remains the deliberate option for automatically downloading unsigned compatible packages; it does not authorize installation.

These policies also govern automatic adoption of an interrupted staged download after reboot. off leaves it untouched; signed requires the same allowlisted signature verification. Automatic resume also requires the stored target to match this node and its version to be newer than the running valid EndF. The signature check applies when an automatic download resumes after discovering a source or reconnecting its storage. Reissuing an explicit ota pull <MID8> remains the deliberate override for an older or unsigned partial. For bring-up/debugging, ota dev resume <MID8> performs the same explicit MID-bound re-adoption without starting a new network fetch. After reboot it requires the MID; bare ota dev resume is accepted only while an active/requested session MID still exists, and malformed or missing identifiers are rejected. Resume is refused while an application or bootloader update is armed, preserving its approved staging and pending reboot. If raw flash contains multiple valid staged headers, automatic resume leaves them untouched rather than guessing which is newest. Specify the intended MID to select its unique checkpoint; ambiguous matches are rejected. Temporary storage read failures no longer consume a download slot indefinitely: host storage pauses for reconnection, while non-reconnectable storage reports a storage error and can be retried.

A failed automatic download starts a cooldown before other automatic candidates are considered: one minute initially, doubling after further failures up to 15 minutes. Changing the offered image or source does not skip this wait. The timer uses elapsed uptime, and a completed download clears it. Manual ota pull, explicit resume, and archive captures keep their existing operator controls and are not restricted by this automatic-download cooldown.

Recommended for most people: leave both off and update by hand. Use autoinstall trusted only once you've added the signer's key (next section) and you trust them to push updates unattended. Automatic admission and final apply both reject zero, equal, or older signed versions; a dishonest catalog version cannot bypass the manifest check. Use manual ota pull plus ota install for an intentional rollback.

The MeshTower V2 SD OTA target has a separate, default-on archive policy. It saves all mOTAs it sees to the SD card so the repeater can seed them later; this does not install them and does not change the install-oriented autofetch default above. Use ota cache for status and ota cache off or ota config cache off to stop new archive captures. Already cached files remain available to peers. Manual ota pull commands take priority and an interrupted archive capture resumes later. See Preload many mOTAs from a computer for the required /mota/<manifest-id>.mota filenames and the complete TempRadio seeding workflow.

"},{"location":"ota_user_guide/#optional-only-trust-updates-from-specific-people","title":"Optional: only trust updates from specific people","text":"

If you'll use signed auto-download or auto-install, tell your node which signing keys to trust. The firmware author shares their public key (a hex string); you add it:

ota key add <public-key-hex>    # trust this signer\nota key list                    # show trusted signers\nota key rm <public-key-hex>     # stop trusting one\n

Only strictly newer updates signed by a trusted key are eligible for auto-install. Manual ota install permits an unsigned package after all integrity, hardware, base, and bootloader checks pass, except on the MeshTower V2 removable-SD path where all application installs must be signed and allowlisted. A signed package whose signer is not in the device allowlist is rejected rather than silently treated as unsigned.

"},{"location":"ota_user_guide/#sharing-updates-with-others-advanced","title":"Sharing updates with others (advanced)","text":""},{"location":"ota_user_guide/#relay-a-folder-of-firmware-from-a-computer","title":"Relay a folder of firmware from a computer","text":"

If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can hand out a whole folder of firmware files to the mesh - without storing them itself. Useful for seeding a new release to a remote area.

  1. Put the firmware files (.mota files - see below) in a folder on the computer.
  2. Install the helper tool once - the standalone motatool CLI (https://github.com/vk496/motatool) - then point it at your node and the folder - over the node's USB serial, over WiFi if it is an ESP32 WiFi companion or FULL ESP32 node, or over encrypted Bluetooth if it is an nRF52 Full Companion: git clone https://github.com/vk496/motatool && cargo install --path ./motatool # over USB serial: motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v # ...or over WiFi: the seeder is on dedicated TCP port 5001: motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v # ...or over paired BLE to an nRF52 Full Companion (protocol v14): python3 tools/ble_mota/ble_mota_seeder.py \\ --device MeshCore-MyCompanion --dir ./my_firmware/ It answers the node's requests; your node then advertises those updates to neighbours, who can ota get them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. Details: https://github.com/vk496/motatool.)

Check the device's attach reply or run ota folder: host=X/Y means the firmware is advertising X of the Y valid entries reported by the host. Serve registries are deliberately RAM-bounded on smaller builds, and the node's own firmware also consumes a slot. If X < Y, split the chain across seeders/folders or use a higher-capacity seeder; motatool saying that every file is valid does not mean every file fit on-device.

To stop, just stop the daemon - over WiFi the node auto-detaches when the connection closes; over USB you can also run ota folder off on the node. ota folder on its own lists what your node is offering. On a FULL repeater or room server, run start webconfig first if WiFi is not already active. Other FULL roles with browser OTA support can use the MeshCore-OTA access point from start ota and connect to 192.168.4.1:5001. Every LoRa participant still needs an overlapping tempradio window.

"},{"location":"ota_user_guide/#everyone-helps-share","title":"Everyone helps share","text":"

You don't have to be a gateway to help. Once any node finishes downloading an update, it automatically offers it to its neighbours too. So a new firmware spreads outward node-to-node, instead of everyone hammering the one node that had it first. Discovery remains background traffic; an actual transfer is primary traffic for the duration of its TempRadio maintenance window.

"},{"location":"ota_user_guide/#share-the-firmware-already-running-on-a-node","title":"Share the firmware already running on a node","text":"

OTA-enabled ESP32 and external-storage nRF52 repeater, room-server, and sensor builds automatically offer their running application during a TempRadio window. Both source and receiver need overlapping windows on the same radio settings. Adaptive RAK builds offer it only when the application has selected a usable, bootloader-matched external store. Internal-only nRF52 nodes do not automatically offer full images; they still need a computer-built delta to receive an update.

Start the overlapping TempRadio windows before explicitly publishing the source. Boards with an on-demand OTA workspace release it when the window ends, so the source is available only during the window. To check or publish it:

ota serve status\nota serve self\n

ota serve self selects the running application as the primary source and announces it. It replaces any manually staged primary image; attached host folders remain available. Automatic offering never replaces a manually staged primary. The receiver uses the normal ota ls, ota get <mid8> flash, and ota install commands. It must have compatible hardware, enough staging/application space, and the required OTA-capable bootloader. ESP32 reads the running A or B app partition; nRF52 reads only its application region. Private keys, ACLs, node names, and saved radio settings are not part of the shared image and are not copied from the source. Existing update paths preserve the receiver's saved data.

The generated package is full and unsigned, streamed directly from flash without keeping the whole image in RAM. This does not enable automatic download or installation, bypass trust checks, or generate deltas on-device. Trusted-only automatic installation rejects it. SD-backed nRF52 receivers also require a trusted signed application package for manual installation: those sources can export their image, but installation there needs a computer-packaged, signed .mota. Source-only Companions continue to offer host-provided files instead.

Eligible repeater, room-server, and sensor builds can compress their own-image blocks on the serving radio with a small, bounded raw-DEFLATE encoder. The receiver uses the existing tinf decoder; no second decoder is added. Each 2 KiB block is independent, and incompressible blocks, unsupported receivers, or insufficient encoder memory fall back to uncompressed transfer. Only the application image through its EndF trailer is shared, not unused partition space. This remains a full image, not a device-generated delta.

The radio does not run the computer packager's Zopfli encoder. Host-generated packages retain their stronger precompressed-block path. Allow a maintenance window long enough for the full image. ESP32 caches successfully read running-image metadata for the current boot and partition geometry, so repeated ota status requests do not rescan the application and interrupt radio servicing.

For an exact delta base from an internal-only nRF52, the existing diagnostic ota dev serve self export, followed by ota announce, remains available during TempRadio for capture onto a computer; it does not make that node capable of receiving a full image.

"},{"location":"ota_user_guide/#where-firmware-files-come-from","title":"Where firmware files come from","text":"

OTA distributes .mota files - a packaged, verifiable firmware image (full image or a small \"delta\" that only contains what changed). You get them by:

"},{"location":"ota_user_guide/#quick-reference","title":"Quick reference","text":"I want to... Command List all commands ota help See my firmware + any download ota status (or just ota) Admin: ids/hashes + serving + policy ota stats (admin-only remotely) Find updates nearby ota ls Download a listed update for installation ota get <mid8> flash Cancel a download ota cancel Install a finished download ota install Recover app-side no EndF on a legacy app-only internal nRF52 ota rescue install <base_hash16> Turn on auto-download ota config autofetch any Turn on auto-install (trusted only) ota config autoinstall trusted Trust a signer ota key add <hex> Relay a folder (gateway) ota folder on + the seeder daemon List what I'm offering ota folder Check whether I'm sharing my running firmware ota serve status Share my running firmware (ESP32 / external nRF52) ota serve self

(Older names still work too: neighbors/updates = ls, pull = get, applydelta/apply = install, drop/stop = cancel.)

"},{"location":"ota_user_guide/#a-few-terms","title":"A few terms","text":"

For the full technical details (the file format and the radio protocol), see the OTA protocol spec.

"},{"location":"packet_format/","title":"Packet format","text":"

View this page on MeshCore Docs.

"},{"location":"packet_format/#packet-format","title":"Packet Format","text":"

This document describes the MeshCore packet format.

"},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"

This is the current version-1 MeshCore wire packet structure. Older firmware may support only a subset of the path encodings and payload types described here.

[header][transport_codes(optional)][path_length][path][payload]\n
"},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (MAX_PATH_SIZE) Stores hop_count * hash_size bytes of path data if applicable payload up to 184 (MAX_PACKET_PAYLOAD) Data for the provided Payload Type

NOTE: see the Payloads documentation for more information about the content of specific payload types.

"},{"location":"packet_format/#header-format","title":"Header Format","text":"

Bit 0 means the lowest bit (1s place)

Bits Mask Field Description 0-1 0x03 Route Type Flood, Direct, etc 2-5 0x3C Payload Type Request, Response, ACK, etc 6-7 0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description 0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes 0x01 ROUTE_TYPE_FLOOD Flood Routing 0x02 ROUTE_TYPE_DIRECT Direct Routing 0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"

path_length is not a raw byte count. It packs both hash size and hop count:

Bits Field Meaning 0-5 Hop Count Number of path hashes (0-63) 6-7 Hash Size Code Stored as hash_size - 1

Hash size codes:

Bits 6-7 Hash Size Notes 0b00 1 byte Legacy / default mode 0b01 2 bytes Supported in current firmware 0b10 3 bytes Supported in current firmware 0b11 4 bytes Reserved / invalid

Examples:

"},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description 0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC) 0x01 PAYLOAD_TYPE_RESPONSE Response to REQ or ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message 0x03 PAYLOAD_TYPE_ACK Acknowledgment 0x04 PAYLOAD_TYPE_ADVERT Node advertisement 0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified) 0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified) 0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request 0x08 PAYLOAD_TYPE_PATH Returned path 0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop 0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets 0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted) 0x0C PAYLOAD_TYPE_OTA OTA-over-LoRa firmware distribution 0x0D reserved reserved 0x0E reserved reserved 0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description 0x00 1 1-byte src/dest hashes, 2-byte MAC 0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC) 0x02 3 Future version 0x03 4 Future version"},{"location":"pair_4p6_300us_validation/","title":"Pair 4p6 300us validation","text":"

View this page on MeshCore Docs.

"},{"location":"pair_4p6_300us_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"pair_preamble32_validation/","title":"Pair preamble32 validation","text":"

View this page on MeshCore Docs.

"},{"location":"pair_preamble32_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"payloads/","title":"Payloads","text":"

View this page on MeshCore Docs.

"},{"location":"payloads/#payload-format","title":"Payload Format","text":"

Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:

This document describes the shared payload envelopes implemented by the core. Application-specific request, response, control, and custom bodies can add their own formats.

NOTE: all 16 and 32-bit integer fields are Little Endian.

"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":""},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"

This kind of payload notifies receivers that a node exists, and gives information about the node

Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below

Appdata

Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node

Appdata Flags

Value Name Description 0x01 is chat node advert is for a chat node 0x02 is repeater advert is for a repeater 0x03 is room server advert is for a room server 0x04 is sensor advert is for a sensor server 0x10 has location appdata contains lat/long information 0x20 has feature 1 Reserved for future use. 0x40 has feature 2 Reserved for future use. 0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"

An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. Current CLI_DATA commands do not cause acknowledgement responses, neither discrete nor extra; their text reply is the application-level result. Repeaters still ACK the legacy plain-text form before processing it.

Repeater remote CLI keeps one volatile copy of the most recently completed reply, keyed by the authenticated sender, request timestamp, and command text. Repeating that same logical request re-sends the text reply without executing the command again. A retry must therefore preserve the original timestamp and command text. The cache is cleared by reboot and replaced by the next completed remote command; commands that intentionally produce no text reply remain silent.

Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"

Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.

Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"

Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.

Field Size (bytes) Description path descriptor 1 low 6 bits are the hash count; high 2 bits encode hash size minus one path count \u00d7 size encoded node-hash prefixes, each 1-3 bytes; the four-byte code is reserved extra type 1 low nibble is the bundled payload type, such as acknowledgment or response; high nibble is reserved extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 request sub type request data rest of payload application-defined request payload body

For the common chat/server helpers in BaseChatMesh, the current request type values are:

Value Name Description 0x01 get stats get stats of repeater or room server 0x02 keepalive (deprecated) 0x03 get telemetry request node telemetry 0x04 get min/max/avg get sensor node stats on time series data 0x05 get acl node ACL query 0x06 get neighbors node neighbors query 0x08 subscribe subscribe to telemetry push 0x09 ubsubscribe unsubscribe from telemetry push 0x0C tracker status encrypted Sensor dog tracker check to a configured owner's Companion; wire format"},{"location":"payloads/#get-stats","title":"Get stats","text":"

Gets information about the node, possibly including the following:

"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) permission mask 1 bitwise inverse mask to AND to permissions (0 = get ALL telem values)"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x04 (request sub type) start 4 starting time, seconds ago end 4 ending time, seconds ago reserved 2 should be zeroes"},{"location":"payloads/#subscribe-to-telemetry-push-sensor-nodes","title":"Subscribe to Telemetry push - (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x08 (request sub type) push tag 4 32-bit tag to be used in telemetry push _REPLY payloads timeout secs 2 subscription timeout (seconds) reserved 1 should be zero min deltas len 1 byte length of LPP encoded min_deltas min deltas (variable) LPP encoded min_deltas"},{"location":"payloads/#unsubscribe-from-telemetry-push-sensor-nodes","title":"Unsubscribe from Telemetry push - (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x09 (request sub type)"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x05 (request sub type) reserved 2 should be zeroes"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body

Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.

"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table

txt_type

Value Description Message content 0x00 plain text message the plain text of the message 0x01 CLI data CLI command OR reply text 0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message 0x03 CLI command (since v1.18+) CLI command text (explicit)

For a room post, companion firmware uses its own monotonic clock for the on-air timestamp and preserves that timestamp across application retries. Room servers track post timestamps separately from login, request, and CLI traffic. They also remember recent accepted posts by sender, timestamp, and text: an exact retry is ACKed again without storing a duplicate, while stale or same-timestamp mismatches are rejected.

"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#companion-one-key-dm-introduction","title":"Companion one-key DM introduction","text":"

A Companion may send this request immediately before a normal private text packet when it has not received an ACK from that chat contact, and on each application retry. It lets the recipient learn the sender's full public key without a broadcast advert. The ciphertext contains:

Field Size (bytes) Description tag 4 unique sender timestamp marker 4 ASCII DMK1 name 1\u201332 sender name followed by NUL signature 64 Ed25519 signature by the sender

The signed bytes are the recipient's full public key followed by the tag, marker, and NUL-terminated name. The recipient verifies that signature against the 32-byte sender key in the anonymous request header before offering a chat contact for manual acceptance, or auto-adding it if dm.one_key is enabled. A receiver that does not implement DMK1 ignores the introduction; the following normal text packet still works when it already has the sender's key. The recipient's manual advert auto-add setting does not suppress the synthetic advert for a verified, addressed DMK1 introduction. The sender's full public key remains visible in the radio packet header, as with other anonymous requests.

With dm.one_key off, the recipient replies with an encrypted DMR1 response: the original 4-byte tag, ASCII DMR1, then its 64-byte Ed25519 signature over the sender's full key followed by that tag and marker. Only a matching pending introduction and verified signature stops repeat introductions.

"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details

The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).

The sender name is unverified message text. Group messages contain no sender signature, so any channel-key holder can choose any sender name.

"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details

The data contained in the ciphertext uses the format below:

Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (bit 0), request radio2 profile (bit 1) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)

Repeaters append an optional radio2 profile trailer when request bit 1 is set. Without that bit, the response is unchanged. Legacy responders omit the trailer even when requested.

"},{"location":"payloads/#trace","title":"Trace","text":"

Trace packets use direct routing. Their normal direct-path header is empty; instead, the intended route follows a fixed nine-byte trace header in the payload. Each forwarding node appends its received SNR multiplied by four to the packet's route-accumulator field.

Field Size (bytes) Description tag 4 Sender-selected trace identifier. auth code 4 Application-defined authentication/correlation value. flags 1 Low two bits encode the route hash size. Current senders use the legacy 1 << value interpretation (1, 2, 4, or 8 bytes); receivers also accept the packed 1-4-byte interpretation where it is unambiguous. route rest Concatenated node-hash prefixes for the requested direct route.

At the destination, the application receives the tag, auth code, flags, accumulated SNR bytes, and original route bytes. Trace therefore differs from a normal direct packet whose path is carried entirely in the packet route field.

"},{"location":"payloads/#multipart","title":"Multipart","text":"

The first payload byte identifies the inner payload and how many packets remain:

Field Size (bytes) Description remaining + type 1 Upper nibble is the remaining-packet count; lower nibble is the inner payload type. inner data rest Data for the inner payload type.

The core currently creates and consumes multipart acknowledgments. For that form, the low nibble is 0x03 and the next four bytes are the acknowledgment CRC. Other multipart inner types are reserved for application or future use.

"},{"location":"payloads/#ota","title":"OTA","text":"

An OTA payload contains one OTA protocol message and is normally sent by flood. Its message types, integrity fields, and transfer state are defined in the OTA-over-LoRa protocol. Builds without OTA support can still relay an opaque OTA packet when their routing policy permits it.

"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"

Custom packets have no defined format.

"},{"location":"power_saving_diagnostics/","title":"Power saving diagnostics","text":"

View this page on MeshCore Docs.

"},{"location":"power_saving_diagnostics/#power-saving-diagnostics","title":"Power-saving diagnostics","text":"

Use set powersaving.diag on to collect the real sleep-policy decisions in RAM, and get powersaving.diag to inspect them. This is available in Companion, Repeater, Room Server and Sensor, without a separate diagnostic build. It does not override any sleep guard. The production USB/DTR policy is unchanged.

"},{"location":"power_saving_diagnostics/#suggested-test","title":"Suggested test","text":"
  1. Record get powersaving, radio/RXPS settings, logging and bridge settings.
  2. Run set powersaving.diag on, then close the USB terminal for the test.
  3. Let the node run long enough to cover noise-floor calibration, queue work and scheduled tasks; a one-second snapshot can be misleading.
  4. Reconnect and run set powersaving.diag off promptly to freeze the window.
  5. Read get powersaving.diag, get powersaving.diag calls, get powersaving.diag time and pages 3 through 8.
  6. If necessary, clear and repeat with only one feature changed.

On ESP32, pages 9 and 10 also show SDK call-entry counts in sixteen 16-second buckets starting at start_ms. These cover the first 256 seconds; outside counts later entries. Flag 1 marks entries outside that horizon, flag 2 marks saturation and flag 4 marks a call without a millisecond timestamp. A bucket with calls can establish activity within an independently timed test interval only when its whole time range fits inside that interval. Whole-window calls alone can all precede packet traffic. Buckets count SDK entries, not successful returns, physical residency or a particular packet's wake event. Check the existing call/return/error counters too. Other platforms keep pages 0 through 8.

Reconnecting/reading creates real USB or serial work and can affect the final decision and counters. Prefer accumulated time over that final why value; check its age. Off freezes recorded values, not the node itself. Re-enabling after off starts a fresh window. Collection defaults off at every boot.

"},{"location":"power_saving_diagnostics/#interpreting-the-result","title":"Interpreting the result","text":"

radio2 means the scanner cannot safely idle right now: for example, a short remaining visit, calibration, pending reception or an unvalidated platform. On nRF52 and ESP32, settled long visits can idle the MCU with power saving enabled; the RF receiver stays on. ESP32 requires working radio GPIO wake and physically stopped WiFi/Bluetooth drivers. Short visits and active transports retain polling. See radio profiles.

The role reports the first actual short-circuit blocker. It does not query otherwise skipped hardware just to populate an exhaustive list. Board guards may add reasons. Per-reason durations therefore are observed guard time, not the independent energy cost of each feature. The state page's blocked time is the union; adding per-reason totals can double-count overlap.

calls and time instrument actual nRF52 event waits, ESP32 manual light sleep and generic RP2040/STM32 WFI paths. ESP32 automatic power-management yields are not direct wait calls. A return under 100 us is counted as short; it can be a legitimate pending interrupt/event, not an SDK failure. SDK errors have their own counter and detail. Time inside the wait includes interrupt/SDK overhead; it is not measured sleep residency. USB debugging/SWD can alter power and clock behavior, so compare like-for-like setups.

These are not current measurements. Radio continuous RX, RXPS parameters, FEM/LNA gain, GPS, displays and board regulators can explain large current differences even with good MCU sleep. Use a current meter and matched settings to attribute those loads. Counters are RAM-only, saturating, and consume at most 256 bytes of collector state. Each wait-call interval must be shorter than the 32-bit micros() wrap (about 71 minutes); the normal wait paths here are much shorter. Keep gaps between policy observations below the millis() wrap (about 49 days).

"},{"location":"power_saving_diagnostics/#esp32-bluetooth-off-and-cpu-sleep","title":"ESP32 Bluetooth off and CPU sleep","text":"

Switching Bluetooth off now completes a reversible host/controller shutdown, including when Bluetooth was saved off at startup. Existing pairings, address, name and PIN are retained when it is switched back on. A pending disconnect or failed teardown keeps the CPU awake rather than treating stopped advertising as a stopped controller. get bluetooth reports a pending change until that transition finishes.

NimBLE-Arduino 2.5.1 builds include the upstream host-timer shutdown fix (NimBLE-Arduino #1185). Stopping Bluetooth preserves an already queued timer event's callback until final host cleanup. Without this fix, switching Bluetooth off can reset an ESP32 through a null callback. The version/hash-checked repair is applied to a build-local source copy; the shared library installation is unchanged.

USB host sessions, logging, active WiFi/ESP-NOW, OTA and serial work keep their existing sleep guards. This change does not sleep an ESP32 through a live Bluetooth or WiFi connection. Such connections require SDK-managed modem and automatic sleep; lowering CPU frequency or yielding alone is not proof of light sleep.

The ESP32 Bluetooth and radio2 qualification report records the tested firmware, saved-pairing checks, SDK wait measurements and packet-test failures. SDK wait time is not a current measurement.

"},{"location":"power_saving_diagnostics/#esp32-processor-speed-and-temporary-test-controls","title":"ESP32 processor speed and temporary test controls","text":"

The WebConfig Processor speed selector and set cpu.freq auto|80|160|240 save the processor clock choice. get cpu.freq reports the choice, actual MHz and supported values. The board rejects unsupported clocks. Auto retains the existing policy: Companion normally selects 80 MHz with device power saving on and its configured nominal speed with it off. Other roles retain their nominal speed in Auto. A fixed speed stays fixed when power saving changes. The selection applies immediately; a failed apply or save reports an error and attempts to restore the previous selection. Returning to Auto clears the saved override. Existing preferences load as Auto.

A damaged CPU override falls back to Auto without discarding other valid preferences. An identifiable truncated timezone record is rejected as an incomplete preferences image, so it cannot silently replace a fractional timezone with an integer value.

Local USB/Bluetooth console commands provide temporary controls for controlled radio2 experiments. They are held in RAM and reset at reboot:

Command Values Default set powersaving.test sleep VALUE on, off on set powersaving.test slice_ms VALUE 2-20 ms 10 ms set powersaving.test guard_ms VALUE 0-20 ms 4 ms set powersaving.test min_ms VALUE 1-10 ms 2 ms set powersaving.test settle_ms VALUE auto, 1-50 ms auto set powersaving.test uart_quiet VALUE 5-300 seconds, off inactive

get powersaving.test shows the current controls; set powersaving.test reset restores their defaults. Sleep on permits existing power-saving decisions; the saved device power-saving setting and every normal transport, interrupt and radio deadline guard still apply. Settle changes only when profile sleep is allowed, leaving radio calibration timing unchanged.

UART quiet is available only for an ordinary hardware UART console, such as the V3's external USB bridge. After draining its acknowledgement, it stops the UART driver and parks its Companion session, preventing new push notifications to the stopped UART from holding the CPU awake. Received messages remain in the offline inbox; other transports keep their own sessions. It automatically restores the console at 115200 baud with a fresh, silent protocol session after the chosen interval. USB logging and OTA must be off. Native USB boards reject this command. A reboot also restores the console.

To separate clock speed from CPU sleep, hold the clock at 80 or 160 MHz and compare sleep off/on at the same RF settings. Use matching transmitter and receiver preambles, unique packets without retries, and diagnostic counters that prove SDK sleep occurred. Keep every failed packet in the result. These controls are experimental tools; a reported SDK wait is not a current reading or proof of a loss-free radio link.

"},{"location":"power_saving_diagnostics/#noise-floor-sampling-and-sleep","title":"Noise-floor sampling and sleep","text":"

Startup still collects a complete baseline promptly: 64 accepted samples, at least 50 ms apart, after the radio frontend has settled. Collection keeps the radio in continuous RX when needed, but the CPU may sleep between reads. Pending packet interrupts retain their normal receive owner. Rejected readings retry at 50 ms intervals, and the existing 30-second collection timeout bounds an attempt on a busy channel.

After a successful baseline, ordinary single-profile refresh waits five minutes. A rejected rise or failed attempt retains the last usable floor and retries after one minute. Gain/tuning changes and radio recovery still request immediate recalibration. Adaptive AGC maintenance with an unchanged RF path retains a valid baseline and its existing refresh deadline; it does not cancel a pending collection or postpone a failed-sample retry. Legacy fixed-cadence AGC resets still request recalibration. Dual-profile scanning keeps its existing timing and calibration policy.

ESP32 timed sleep uses a millisecond timer capped at the next sample deadline. Repeater, Room and Sensor also preserve millisecond queue, retry, RS232 standby, scheduled-radio and maintenance deadlines, so the last fraction of a second does not become a busy-wait tail. A future sub-second deadline is not reported as blocked work; a deadline that is already due still prevents sleep. nRF52, RP2040 and STM32 retain their interrupt-idle wake paths. Other sleep guards, including USB/logging, packet queues and bridges, still apply. These changes have host scheduling tests and firmware build coverage; their power benefit requires a new matched hardware/current measurement.

See the CLI reference for all page names and commands.

The nRF52 idle-sleep investigation describes an eligible-but-not-sleeping regression, its fix, controlled hardware evidence and the limits of CPU-state sampling without a current meter.

"},{"location":"power_saving_diagnostics/#rak13302-builds-with-an-unused-onboard-radio","title":"RAK13302 builds with an unused onboard radio","text":"

The RAK3401/external RAK13302 firmware now probes the private onboard-radio bus once during startup. This supports both a RAK3401 core (no onboard SX1262) and a RAK4631 core using the external RAK13302. It is adapted from agessaman's optional-radio sleep change. RAK's internal connection table identifies the private SPI, reset, BUSY and RF-switch pins used by the probe.

A matching six-byte chip signature triggers cold sleep with the RTC disabled. The RF switch stays off, NSS/reset stay high, and the temporary SPI/data/BUSY pads are released. Sleep confirmation reads BUSY only: another SPI transaction would wake the chip. Each BUSY wait is bounded to 20 ms, including when no chip is fitted. A timeout or mismatched signature does not block startup of the external radio. Debug-enabled startup logging reports asleep, absent, busy-timeout or sleep-failed when the USB debug output is available; a missing chip normally produces busy-timeout, not a verified sleep result.

This happens before the boot-voltage check, so a low-voltage shutdown cannot skip the unused-radio sleep attempt. It does not cycle the shared GPS/storage rail, change external-radio recovery, or enable its FEM before initialization succeeds. There is no runtime probing or automatic primary-radio selection.

Ordinary RAK4631 firmware is unchanged: its onboard SX1262 is the active primary radio and must not be disabled. Use the external RAK13302 image for that hardware arrangement, not a normal RAK4631 image. This startup change has host regression tests; sleep-current improvement still needs matched current measurements on the physical core/module combination.

"},{"location":"power_saving_diagnostics/#board-regulator-initialization","title":"Board regulator initialization","text":""},{"location":"power_saving_diagnostics/#user-selectable-regulators-and-crash-recovery","title":"User-selectable regulators and crash recovery","text":"

mcu.regulator and radio.regulator independently select auto, dcdc or ldo where the board supports those modes. auto preserves its existing board default. The choice is saved and takes effect after reboot; the two getters show requested/active mode, fallback, crash count and recovery availability. An explicit setter clears the corresponding fallback and streak, even when the same value is saved again. See the CLI commands.

Processor selection applies to the existing, circuit-supported nRF52 REG1 DC-DC path. It does not select REG0, change an external PMIC, or expose an ESP32/RP2040 CPU regulator control. Radio controls apply to the supported SX1262, LR1110/LR1121 and Meshnology W12 LR2021 implementations. DC-DC is offered only when that board's regulator circuit is supported. LDO-only boards reject dcdc; unverified LR2021 switching circuits keep their existing mode.

Integrated STM32WL targets retain their existing supply configuration. Their MCU and radio share the regulator, so independent selections need a coordinated implementation and board circuit verification before they can be offered.

Automatic recovery uses reset-retained RAM on supported platforms. A domain must have been armed in DC-DC mode before the crash, with a valid last battery sample of at least 50%, no more than 60 seconds old at its last heartbeat. The third consecutive qualifying crash selects LDO before that domain starts again and saves the fallback. MCU and radio streaks are separate; if both were using DC-DC during the same qualifying failures, both can fall back. This is a recovery action, not proof that a regulator caused the crashes.

Low/unknown battery, stale samples, cold starts, intentional reboots, sleep wakes and unclassified resets break a streak. Corrupt retained state cannot add a crash. Five healthy minutes clear the running streak; a later crash starts a new one. The saved LDO fallback survives normal resets and power cycles. It stays in LDO until a user explicitly selects a mode again, with no automatic return to DC-DC. Crash bookkeeping stays in retained RAM rather than writing flash at every heartbeat.

During timed Morse TX/RX, periodic battery sampling and failed-save retries wait until timing ends. Recovery heartbeats continue, and a deferred sample keeps its original timestamp, so it can become too stale to qualify a crash. Overdue sampling and retries resume on the next eligible service tick.

Battery eligibility follows the board's battery measurement and configured percentage curve. USB supply voltage alone does not establish that a physical battery is at 50%; validate battery-only starts and the board's absent-battery reading when qualifying recovery. Boards without a trustworthy battery reading should report it invalid. A getter reporting recovery unavailable still permits supported manual mode selection.

ESP32 WebConfig exposes supported controls under Radio \u2192 Advanced \u2192 Power regulators, showing active mode and fallback status. Unsupported domains and DC-DC options are hidden. Saving reboots only after the authenticated save completes successfully. Clear fallback on restart lets a user retry the current saved mode without first choosing a different one.

For hardware qualification, compare both modes with battery-only cold starts, normal reboots, sleep/wake, BLE/WiFi and radio TX/RX under load. Confirm that three injected qualifying watchdog/fault resets select LDO, that fewer than three and low/unknown-battery resets do not, and that an explicit setter restores the requested choice. Also check current consumption and voltage during TX; native recovery tests and successful firmware builds do not establish these physical results.

The regulator qualification record binds the compiled packages, software checks and Mercerwood bench results to their tested source revisions and lists the remaining physical checks.

"},{"location":"power_saving_diagnostics/#existing-board-defaults","title":"Existing board defaults","text":"

RAK4631 Companion shares the nRF52840 REG1 regulator path with the other RAK4631 roles. auto preserves its DC-DC default; the former Companion-only force-LDO flag has been removed. Saved mode selection is deferred until board ADC and primary filesystem initialization are complete, before display, radio and wireless startup. This retains boot-voltage protection and keeps REG0 and storage layout unchanged. The shared initializer uses the SoftDevice API when BLE's stack is already active and the hardware register otherwise.

RAK documents DC/DC support for the module. The related upstream battery-startup report #1878 concerned Repeater firmware, with BLE Companion reported working on an affected board. It did not establish filesystem corruption or a Companion-specific DC/DC failure. PR #1914 also changes boot-voltage/wakeup handling and is not proof that every failure in that thread has the same cause.

Host startup tests cover both SoftDevice states and deferred regulator selection with boot-voltage protection preserved. Firmware compilation is not hardware qualification: compare battery-only cold starts, low-voltage recovery, BLE operation and settings writes/reboots on the actual board/baseboard revisions before a fleet rollout. USB power can mask the reported startup symptoms. A current meter is still needed to quantify the power difference.

The following boards also now call their declared NRF52BoardDCDC initializer instead of bypassing it with NRF52Board::begin(). This applies to every role that uses the board class, not just Companion, and does not require enabling the runtime sleep policy. The nRF52840 hardware manages the enabled regulator according to load. This is REG1 only, not a blanket change to REG0 or an external LDO, PMIC output, or radio regulator.

Board Circuit evidence used for REG1 support Heltec T1 T1 V1.0 schematic, DCC-to-DEC4 inductors and capacitors. Heltec MeshPocket MeshPocket schematic, DCC-to-DEC4 L1/L2 and C5/C6. Heltec MeshSolar and MeshTower V2 HT-N5262M module circuit, L1/L2 and C1/C2. Heltec links this circuit from its MeshTower V2 resources; MeshSolar's datasheet identifies the module. LilyGO T-Echo T-Echo schematic, REG1 DC/DC network and explicit DC/DC-enabled design note. Ikoka Handheld, Nano and Stick nRF52 XIAO nRF52840 circuit, DCC L1 and DEC4 capacitors. The designers identify XIAO as the MCU for Handheld, Nano and Stick. Minew ME25LS01 Manufacturer datasheet, electrical schematic page 9: MCU DCC through ML2/ML3 to DEC4_6 and MC15/MC16. Nano G2 Ultra Manufacturer schematic published as the 19 June 2023 design, NRF52840 sheet: DCC through L13 (10 uH) and L12 (15 nH) to DEC4, with C41/C42. These components are not marked DNM.

The startup tests execute each real begin() with SoftDevice off/on and power management compiled out/in. Normal startup defers the selected mode until the saved settings and recovery state can be read, while retaining one common board initialization. The board class still defines the circuit-supported default; the settings layer applies it before display/radio startup.

Nano G2 Ultra was deliberately excluded from upstream PR #1246 because its LC circuit had not yet been confirmed. That discussion asks for circuit confirmation or testing; it does not identify a Nano-specific DC-DC failure. The published schematic supplies that circuit evidence. Nano now approves the REG1 DC-DC default through the shared board initializer, retaining saved LDO selection and crash fallback in the main firmware roles. KISS applies the board default directly. REG0, GPIO assignments, ADC conversion, peripheral startup/shutdown and USB/DFU identity are unchanged. The schematic title block does not identify a PCB revision; battery-only starts, current draw and radio load on actual hardware remain physical acceptance checks.

The Nano validation record binds the issue/circuit review, native checks and six passing build selections to source c6ccbb5ab: BLE/USB/Full Companion, Repeater, Room Server and KISS. No Nano hardware was flashed during these checks.

ThinkNode M3/M4/M6 and generic ProMicro remain unchanged pending exact circuit or physical-board confirmation; their DC/DC inheritance alone is not evidence. ThinkNode M1/M8 and Keepteen LT1 retain their plain NRF52Board initializers for the same reason. The MeshTracker X1's LR2021 regulator likewise remains unchanged. Do not turn on a switching regulator without its required inductor/capacitor network. The MCU and LoRa regulator choices are independent.

"},{"location":"power_saving_diagnostics/#meshnology-w12-transmit-preparation","title":"Meshnology W12 transmit preparation","text":"

The W12 adopts the GC1109 TX preparation from upstream PR #3579, but keeps the PA shut down in ordinary standby rather than permanently selecting TX mode. Before sub-GHz packet or Morse CW transmission, it temporarily enables the LF PA in standby, waits 1 ms, starts TX, then restores the normal standby mask. The TX mask stays unchanged during that restoration. Failure paths restore all affected DIOs and request radio recovery if the PA state cannot be confirmed. The 2.4 GHz path and other LR2021 boards do not use this LF prearm policy.

LF/HF FEM rail changes now settle for 1 ms instead of 100 ms. Packet-wake deep sleep retains only the selected FEM rail; terminal shutdown retains both rails off. This is separate from the W12's existing circuit-verified DC/DC setting.

Native fault-injection tests cover the mask transitions and power retention; RF envelope and battery-current measurements on a physical W12 are still needed to validate the TX improvement and quantify power use.

"},{"location":"pr7_review_validation/","title":"Pr7 review validation","text":"

View this page on MeshCore Docs.

"},{"location":"pr7_review_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"preamble_detection_investigation/","title":"Preamble detection investigation","text":"

View this page on MeshCore Docs.

"},{"location":"preamble_detection_investigation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"preset_test/","title":"Preset test","text":"

View this page on MeshCore Docs.

"},{"location":"preset_test/#temporary-radio-test-builder","title":"Temporary radio test builder","text":"

Choose a window and radio tuple to create a shareable temporary-radio test URL.

MeshCore \u00b7 default 48-hour temporary preset test

Scheduled Starts in \u2014 \u2014 Start End Display time zone Need to leave the test early?

Send this command to return to your saved radio settings in one minute:

Copy leave command KeyMind Cascade firmware and advanced commands

Use these only if your firmware supports the listed commands or you need dual-radio operation, scheduling, or clock correction.

Firmware schedule epochs:

Primary scheduling requires a Simple Repeater build. Companion tempradio2 and tempradioat2 require dual-profile firmware. If a command is unknown, use only the method your installed firmware supports.

Enter the test times in the selected time zone and choose the radio tuple. The generated URL converts the times to exact UTC instants, keeps the date punctuation readable, and keeps tz so the page displays them in the organizer's local time zone. With no URL settings, the builder starts at 5:00 PM tomorrow and ends at 5:00 PM two days later in the selected time zone, creating a 48-hour window.

Start date and time End date and time Selected time zone Detecting browser time zone\u2026 Use browser time zone

Loading time zone map\u2026

Click a region to select its IANA time zone. The initial selection comes from tz= when present; otherwise it uses your browser's time zone. Map design inspired by zones.arilyn.cc; boundaries from Timezone Boundary Builder and \u00a9 OpenStreetMap contributors. Test radio profile Frequency (MHz) Bandwidth (kHz) 7.810.415.6 20.831.2541.7 62.5125250500 Spreading factor 5678 9101112 Coding-rate denominator 5678 TX output for estimate (dBm) This estimate-only value does not change the TempRadio commands. Generate test URL

Copy generated URL Open generated page

Advanced use: start and end also accept ISO-8601 timestamps with explicit UTC offsets or Unix epoch seconds. If tz is omitted, the page uses the browser's time zone. The other URL parameters are freq, bw, sf, cr, and estimate-only tx.

"},{"location":"preset_test/#preset-test-title","title":"910.100 MHz / 500 kHz / SF8 / CR7","text":""},{"location":"preset_test/#stock-test-title","title":"Stock firmware: copy this tempradio command","text":"Copy tempradio command

Before sending it, run get radio and save the result. No clock setup is needed. Your saved settings return automatically.

"},{"location":"preset_test/#schedule-mode-title","title":"Scheduled command timing","text":"Choose the syntax supported by the node KeyMind Cascade \u00b7 +minutes (default) Absolute Unix time

Relative commands calculate whole minutes from now to the shared start and end. Both offsets use the node's same command-time snapshot, so no clock correction is needed. Copy the command shortly after it is shown.

Absolute commands use fixed UTC epochs. Verify or compensate for the node clock with the controls below before queuing them.

"},{"location":"preset_test/#continuity-plan-title","title":"Bridges and early-revert plan","text":"

The test will include bridges. If the Puget Sound area experiences a widespread power outage or loss of cellular service, we will end the test early. A stock node's one-minute temporary test profile then ends on that node's own saved radio settings.

"},{"location":"preset_test/#save-settings-title","title":"Before changing a radio","text":"

Run get radio and record the saved primary tuple. Both primary TempRadio methods return to that saved tuple; they do not store a return tuple inside the temporary command. On a Companion, also run get radio2 and get radio2.cross so you can restore any non-default secondary-profile setup.

"},{"location":"preset_test/#clock-check-title","title":"Check the node clock before scheduling","text":"

The primary tempradioat and Companion tempradioat2 schedulers use UTC Unix time. Run clock on every node that will be scheduled and compare it with the browser UTC time below. It should agree to within about a minute. A wrong clock can start late, start immediately, or cause the schedule to be rejected.

Browser UTC now \u2014 Browser Unix time \u2014 Node clock reported by clock (UTC, optional) Paste the node's clock reply immediately after it arrives. The firmware form 02:42 - 22/9/2026 UTC is also accepted. Leave this blank to keep the normal UTC schedule epochs unchanged. Apply clock conversion

No node-clock correction is applied. Scheduled commands use the normal UTC epochs.

This changes only the absolute tempradioat and tempradioat2 epochs shown on this page; it does not issue a clock-setting command. The correction is for this node and this open page only: it is never added to generated URLs, and is cleared on reload. If the node clock later syncs or jumps, delete and queue the schedule again.

Remote admin session

In a MeshCore client that supplies the sender timestamp, sync and then verify:

clock sync\nclock
Local USB or browser console

Copy this fresh, run it immediately, and then verify with clock:

time 0\nclock
Copy current clock command If the clock is ahead and cannot move backward

If setting the correct time returns ERR: clock cannot go backwards, run the command below. It resets the clock to an older fallback date and reboots the node immediately, so it sends no reply. After the node reconnects, copy and run the fresh local clock command above, verify with clock, and then add the Companion tempradioat2 schedule again. The reboot clears any pending scheduled entries.

If resetting or correcting this remote node's clock is not safe, leave it unchanged and use the optional node-clock conversion above instead.

clkreboot
Copy clock reset command

The Companion schedule must be queued while both its start and end are in the future and within the firmware's roughly 24-day scheduling horizon. Scheduled entries are held in RAM and disappear if the node reboots.

"},{"location":"preset_test/#join-now-title","title":"Companion: use both frequencies","text":"

These commands become available one hour before the official start. Their timeout shrinks so the Companion returns at the same end time.

Companion using both frequencies Dual profile Copy commands

rxtx permits transmission on the second profile; radio2.cross on copies ordinary Companion traffic across the primary and temporary profiles.

"},{"location":"preset_test/#schedule-title","title":"Schedule a supported node in advance","text":"

A Simple Repeater build can schedule its primary radio with tempradioat. A Companion can instead schedule its second profile with tempradioat2. The default KeyMind Cascade form uses +minutes for both endpoints. Switch to Absolute Unix time above for fixed UTC epochs and the node-clock correction tools. Both forms switch at the common start and restore saved settings at the common end. Room-server and sensor roles use the stock join command above instead.

Room server or sensor No advance schedule

Use the stock tempradio command above after its setup window opens.

Simple Repeater primary radio Primary schedule
Copy schedule commands

This is available only on a Simple Repeater build with primary scheduled-radio support. It changes the primary tuple at the start and restores the saved tuple at the end. If get tempradioat returns Error: unsupported, use the stock join command above instead.

Companion using both frequencies Dual-profile schedule
Copy schedule commands

Crossing is saved independently and remains on after the temporary second profile ends. Restore its previous value after the test; off is the normal default.

"},{"location":"preset_test/#cancel-title","title":"Cancel a schedule or adjust the timeout","text":"

For a primary or Companion schedule, the all form removes every temporary schedule in that family. To preserve another schedule, first run the corresponding get command and replace all with its displayed entry number.

Stock: leave in 30 minutes
Copy 30-minute command

This replaces the current temporary timeout with 30 minutes, then restores the saved primary tuple automatically.

Simple Repeater \u00b7 scheduled primary radio
Copy cancel commands

Before the start, this removes the queued temporary schedule. While it is running, it also restores the saved primary tuple after the command reply drains.

Companion \u00b7 before it starts
Copy cancel commands Companion \u00b7 while it is running Leave now
Copy restore commands

If get radio2.cross was not off before the test, restore that recorded value instead.

Leave in 30 minutes
Copy 30-minute commands

These commands clear any absolute second-profile schedule and give the test profile a fresh 30-minute lease. The saved second profile returns at expiry, but crossing remains on; afterward, restore the value you recorded before the test (off is the normal default).

"},{"location":"preset_test/#share-title","title":"Build a temporary radio test URL","text":""},{"location":"preset_test/#radio-estimates-title","title":"Radio estimates","text":"Nominal LoRa bitrate\u2014 Estimated sensitivity\u2014 TX output used\u2014 Estimated link budget\u2014

The bitrate is the nominal LoRa physical-layer rate; usable payload throughput is lower. Sensitivity assumes a 6 dB receiver noise figure and the standard LoRa SNR threshold for the selected spreading factor. Link budget is TX output minus that sensitivity, before antenna gain, cable loss, path loss, interference, and implementation differences.

"},{"location":"qr_codes/","title":"Qr codes","text":"

View this page on MeshCore Docs.

"},{"location":"qr_codes/#qr-codes","title":"QR Codes","text":"

This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.

"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"

Example URL:

meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n

Parameters:

"},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"

Example URL:

meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n

Parameters:

"},{"location":"radio_chirp_math_validation/","title":"Radio chirp math validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_chirp_math_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_dwell_policy_validation/","title":"Radio dwell policy validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_dwell_policy_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_profile_switch_validation/","title":"Radio profile switch validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profile_switch_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_profiles/","title":"Radio profiles","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profiles/#two-lora-profiles-on-one-radio","title":"Two LoRa profiles on one radio","text":"

radio2 lets one LoRa transceiver alternate between two frequencies or modulation settings. It is useful for listening to two networks, or keeping your usual channel available during a LoRa firmware update.

The second profile and radio2.cross both default to off. Saved settings survive firmware updates; selecting a profile does not change the crossing policy.

There are at most two active profiles. tempradio temporarily replaces radio; tempradio2 temporarily replaces radio2. These are time-shared receive windows, not simultaneous receivers. Receiving or transmitting a packet on one profile makes the other unavailable until that packet finishes.

The second-profile commands are shared by repeater, Companion, room server, sensor and standalone terminal-chat firmware with a LoRa radio. ESP-NOW-only devices reject LoRa profile commands. KISS is a raw host-controlled modem: it exposes equivalent RAM-only binary SetRadio2/SetTempRadio2 commands and logical KISS Data ports, documented in the KISS modem protocol. It does not use the Mesh CLI's persistent profile file or Mesh retry queues.

"},{"location":"radio_profiles/#quick-setup","title":"Quick setup","text":"

Keep the primary channel configured with radio, then add the second:

set radio2 910.5,500,8,5,rx\nget radio2\nget radio2.status\n

The tuple is frequency in MHz, bandwidth in kHz, spreading factor, coding-rate denominator, mode, optional preamble in symbols. 5 means coding rate 4/5.

"},{"location":"radio_profiles/#mediamesh-presets","title":"MediaMesh presets","text":"

Enable the saved MediaMesh profile with one command:

set radio2 mediamesh\n

This sets 911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, automatic preamble. For the same profile at 125 kHz, use:

set radio2 mediamesh-range\n

The RF settings match the presets documented by MeshCore Open TAK and Wadamesh TAK. Nodes exchanging media need matching radio settings.

Both leave the primary profile and radio2.cross unchanged. The center is 625 kHz above a 910.525 MHz primary. Automatic preamble remains scan-aware; neither preset fixes it at 32 symbols. See preamble for the switching measurement and recommendations.

On Companions, the default tx.media auto sends recognized binary photo/voice traffic only on an active rxtx MeshCore second profile, including tempradio2. Apps do not need to set tx.media. Ordinary traffic keeps its existing route. If the second profile cannot transmit MeshCore, media uses the normal channel/global route; set tx.media radio2 instead requires radio2 with no primary fallback. See application TX routing.

"},{"location":"radio_profiles/#discover-the-local-radio2-profile","title":"Discover the local radio2 profile","text":"

Clients can request a repeater's current MeshCore radio2 profile through public, zero-hop discovery. Set bit 1 (0x02) of the DISCOVER_REQ flags byte: 0x82 for the full public key, or 0x83 with the prefix-only bit also set. The type filter, tag and optional since fields keep their existing meanings. Omit since or send zero when requesting current settings; radio changes do not update the discovery timestamp.

The requested DISCOVER_RESP appends this version-1 trailer immediately after the public key, at payload offset 38 (full key) or 14 (8-byte prefix):

Field Bytes Meaning version 1 1 flags 1 Bit 0: configured; bits 1\u20132: mode (0 off, 1 rx, 2 rxtx); bit 3: temporary; other bits zero frequency 4 Little-endian unsigned integer, MHz \u00d7 1000, rounded bandwidth 4 Little-endian unsigned integer, kHz \u00d7 1000, rounded SF 1 Spreading factor CR 1 Coding-rate denominator (5 means 4/5)

For example, the MediaMesh preset reports frequency 911150, bandwidth 250000, SF 7, CR 5 and flags 0x05. An active tempradio2 reports its current settings with bit 3 set. Clients use automatic preamble; the trailer does not carry a preamble value.

When radio2 is off, unsupported, or using a foreign protocol, the trailer is only [1][0]. Full responses are therefore 50 bytes with a profile or 40 bytes without one; prefix-only responses are 26 or 16 bytes. Without the request bit, responses remain 38 or 14 bytes. A legacy responder's missing trailer means unsupported, not disabled. Clients should ignore unknown versions and incomplete trailers.

Replies remain on the main radio with zero hops and share the existing limit of four replies per two minutes. Existing visibility controls still apply. Discovery is public and unauthenticated. See control data for the base packet fields.

"},{"location":"radio_profiles/#channel-spacing","title":"Channel spacing","text":"

When setting a saved, temporary or scheduled second profile, the response warns if its occupied bandwidth overlaps the primary channel or comes within 6 kHz of either edge. This check uses both channel bandwidths, regardless of SF or coding rate. The change still applies. The warning also appears when reading the second profile, including while the switching timing self-test is pending.

The warning suggests a nearby center frequency with the same bandwidth, SF and coding rate that clears the buffer. It chooses the nearest whole-kHz frequency on the requested side of the primary channel (above for identical centers), checks the radio's supported frequency range, and tries the other side if needed. The suggestion is not applied automatically.

For example, with radio at 910.525 MHz / 62.5 kHz, a second 62.5 kHz profile at 910.550 MHz produces:

WARN radio2 overlaps radio1 (6kHz guard); try 910.594MHz\n

The center spacing must exceed (primary bandwidth + second bandwidth) / 2 + 6 kHz; touching that boundary also warns. Frequency comparisons use the stored RF centers rounded to the nearest Hz. This is a channel-spacing aid; choose the suggested frequency only when it also suits the network and local band plan.

Mode Receive on profile 2 Transmit on profile 2 rx Yes Only infrastructure replies explicitly configured with tx.reply ... force rxtx (also rx&tx) Yes Yes off No No

For transmission on both permanent profiles:

set radio2 910.5,500,8,5,rxtx\nset radio2.cross on\n

The same encoded message is queued separately for each allowed transmit profile. Each has its own direct/flood retry slots and channel-busy backoff. A forwarding echo on one profile cannot cancel the other profile's retry. Packet storage and the physical transceiver's airtime budget are shared; exhausted queues can still drop packets.

"},{"location":"radio_profiles/#keep-the-main-channel-during-an-update","title":"Keep the main channel during an update","text":"
set tempradio2 910.5,500,8,5,rxtx,120\nget tempradio2\n

This enables the second profile for 120 minutes (2 hours). Locally generated LoRa OTA requests use the temporary second profile. Infrastructure OTA responses follow tx.reply below (both TX-capable profiles by default). Ordinary local traffic stays on the primary profile by default. Use an OTA-capable build and the usual OTA setup on every participating node; a temporary profile does not add OTA support to firmware built without it.

Local settings take effect after a short reply allowance. On repeaters, immediate remote radio2/tempradio2 changes (including off) wait until their exact reply copies finish, with at least one successfully transmitted. A reply that cannot be queued or whose copies all fail cancels the change. Queued replies have a five-minute deadline; an already-transmitting copy may finish under the normal radio watchdog. This confirms transmission, not reception by the remote client. An active temporary profile keeps its radio generation until those tracked reply copies finish or are retired, but its remaining lease continues counting down during that wait.

Permanent remote changes are prepared in an uncommitted file before replying; the saved configuration is replaced only after reply transmission. If that replacement fails, the requested settings remain pending and the commit retries. An expired temporary profile returns to the last saved configuration during those post-reply storage retries; the uncommitted settings do not become active. Local get radio2.status reports this pending state. Roles without the tracked remote reply path require local USB for these immediate secondary-profile changes.

Temporary periods and schedules live in RAM and disappear on reboot. Waiting for a reply does not extend a temporary lease. Timer expiry also uses monotonic time, so setting the clock backwards cannot extend a temporary session.

"},{"location":"radio_profiles/#repeater-room-server-and-sensor-replies","title":"Repeater, room-server, and sensor replies","text":"

These roles default to tx.reply both: locally generated replies are queued on radio and on an active radio2 that permits TX. This includes replies to remote CLI/login/telemetry requests, ACKs, returned paths, delayed GPIO command completion, room subscription posts, and locally served LoRa OTA catalog, manifest, data, proof, and leaves responses. Each copy has its own retry state. Forwarded packets, periodic adverts, sensor pushes, and OTA requests continue to use the normal crossing rules.

get tx.reply\nset tx.reply both\nset tx.reply both force\nset tx.reply radio\nset tx.reply auto\n
Choice Replies transmit on both Primary plus an active rxtx second profile. Default. radio Primary only. radio2 Second only; no primary fallback. auto Normal radio2.cross behavior, including permanent/temporary isolation. off Neither profile. This also suppresses remote CLI replies and ACKs.

Append force to both or radio2 to allow these replies on an active second profile configured as rx. Without force, rx remains receive-only. Sending the command again without force removes the override. It never turns on radio2 off, changes the saved rx mode, or enables ordinary forwarding/TX there. Existing crossover packet filters still apply to the extra copy.

The explicit choices override radio2.cross, including off and mixed permanent/temporary profiles. radio means the active primary profile, including tempradio; radio2 also includes an active tempradio2. For example, an OTA source can retain its normal channel while allowing OTA and management responses on a temporary receive profile:

set tempradio2 910.5,500,8,5,rx,120\nset tx.reply both force\n

The source must have OTA support and an active temporary-radio session as usual. A node fetching an update still needs rxtx for its OTA requests. Forcing replies does not change OTA's receive gates, scan timing, or preambles.

This setting survives reboot; older saved profiles adopt both without force. The getter reports when the chosen second profile cannot transmit. Already queued replies retain their choice; disabling/changing a profile invalidates stale queued packets. A parameterized repeater tempradio command waits for both admitted reply copies to drain before changing the primary settings; at least one must transmit successfully. Those tracked replies omit alternate paths and transport retries so no late success copy can outlive the handoff. Queue capacity and the shared transceiver's airtime budget still limit delivery. OTA data/proof admission counts both copies against its queue credit and reserves receive capacity before allowing the response.

Companions use the per-contact/channel choices below; tx.reply is an infrastructure setting.

"},{"location":"radio_profiles/#companion-messages-on-both-profiles","title":"Companion messages on both profiles","text":"

To send ordinary Companion messages on both radio and an active tempradio2, configure the temporary profile in rxtx mode and enable crossing:

set radio2.cross on\nget tempradio2\nget radio2.cross\nget radio2.status\n

rxtx permits transmission on the second profile; radio2.cross on copies new messages between the two profiles. TX=a,b reports primary and secondary transmit counts. Crossing also applies to OTA traffic.

The crossing setting is saved and remains enabled after the temporary session ends or the node reboots. Run set radio2.cross off to restore the default isolation between profiles. The firmware picker's Companion tempradio2 instructions include both choices and the status commands.

"},{"location":"radio_profiles/#companion-tx-routing-by-contact-or-channel","title":"Companion TX routing by contact or channel","text":"

Companions can save a transmit choice for each contact and each configured channel. The commands apply to messages sent from the phone app, USB/BLE/TCP Companion clients, and the text terminal:

set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nget tx.channel 0\nset tx.channel #wardriving radio\nget tx.channel #wardriving\n

Channel indices match the app and the terminal's channels list; Public is normally slot 0. Names must match exactly, including capitalization and any leading #. Quotes always select a literal name, so \"1\" is a channel named 1, while unquoted 1 selects slot 1. Quote contact names beginning with key: to distinguish them from public-key selectors. Duplicate names are rejected: use the channel index or a contact's public key instead.

Choice Locally addressed traffic transmits on auto Existing behavior, including radio2.cross and temporary-profile isolation. This is the default. radio The primary profile only. radio2 The second profile only; sending fails if it is off or RX-only. both The primary profile and the second profile whenever the second is enabled for TX. off Neither profile for this contact/channel. Reception stays enabled.

Explicit choices override radio2.cross, including off and the isolation between a permanent and temporary profile. They do not turn on radio2 or change its rx/rxtx mode. For example, leave radio2.cross auto and use set tx.channel 0 both to send Public messages on both radio and an active tempradio2, while other channels keep their default routing.

radio means the currently active primary profile, whether permanent or tempradio; radio2 similarly includes tempradio2. There are still at most two active profiles. Getters show the saved choice and active= for newly initiated messages with the current profile configuration. A temporarily unavailable second profile is reported explicitly.

Contact selection uses the full stored public key after resolving a unique name or key prefix. It never routes by the short on-air destination hash. For key selection, prefix 12\u201364 hexadecimal digits (whole bytes) with key::

set tx.user key:0123456789AB radio2\nget tx.user key:0123456789AB\n

Replace the example prefix with your contact's key; use more digits if it is ambiguous. get tx.user and get tx.channel report override counts and query syntax. To return a target to the global behavior:

set tx.user \"Alice Jones\" auto\nset tx.channel 0 auto\n

A user's setting covers addressed texts, login/CLI/telemetry requests, replies, ACKs, and returned paths to that contact. Thus off also prevents ACKs to that contact. Channel settings cover group text and group data, including an explicitly routed group datagram. Contact choices do not filter other people's messages inside a group channel. Adverts, raw traces, raw packets, and OTA packets without a contact/channel destination retain global routing. Packets and their retries already queued retain the choice used when created; changing or disabling the radio profile still invalidates stale queued work. Message timeout estimates use the selected profiles' airtime, independently of the scanner's current receive profile.

Choices survive reboot and normal phone-app updates to the same contact or channel key. Replacing a channel's key or deleting/replacing a contact clears its choice. They use reserved bytes in existing saved records, so record sizes and the phone protocol are unchanged. Older firmware ignores these bytes and may reset the choices when it saves the records.

"},{"location":"radio_profiles/#companion-media-and-ssh-tx-routing","title":"Companion media and SSH TX routing","text":"

Companions also save independent application-data routes:

get tx.media\nset tx.media radio2\nget tx.ssh\nset tx.ssh radio\n

Both accept auto|radio|radio2|both|off and default to auto. An explicit application route overrides the channel's TX choice for that datagram only; tx.media auto chooses only radio2 whenever its active profile permits rxtx MeshCore transmission, including an active tempradio2. This overrides the channel/global route for recognized media even when an app has never configured tx.media. Otherwise, media uses the normal channel/global route. tx.ssh auto always preserves that normal route.

The explicit radio2 choice never falls back to radio, and requires an enabled rxtx MeshCore profile. both uses the profiles available for MeshCore transmission. off blocks transmission, not reception. Getters show the saved choice and current active= mask; auto shows active=radio2 while media automatically selects radio2, and active=channel/global otherwise, because the effective route then depends on the destination channel (or global rules for raw data). Settings survive reboot; a failed save returns an error and keeps the previous choice. Already queued packets retain their original route.

tx.media recognizes Open TAK photo/voice channel data type 0x5643, raw clip packets 0x56/0x57, and well-formed binary fetch requests 0x72. This covers CMD_SEND_CHANNEL_DATA and CMD_SEND_RAW_DATA, plus visible raw media in CMD_SEND_RAW_PACKET. Fully encoded encrypted group packets submitted through CMD_SEND_RAW_PACKET retain their existing route. Text announcements (VE3:, VM1:, VI1:), text fetch requests (VR3:), ordinary chat, contact ACKs, adverts, OTA, and unrelated raw data keep their normal routing. The format is defined in Open TAK's voice/media wire implementation.

tx.ssh uses a new opt-in application contract, not automatic inspection of encrypted SSH or arbitrary IP tunnels. A tunnel client must use either:

The firmware does not add/remove the prefix, decrypt the tunnel, or run an SSH server. Both endpoints must understand this framing. Untagged existing tunnel apps remain unchanged and are not affected by tx.ssh until they adopt the contract. Other channel data types do not acquire an SSH route merely because their contents start with SSH. Raw packets still require a direct path, including zero-hop; this does not enable raw flooding.

For a media-only side channel, use set radio2 mediamesh with the default radio2.cross off and tx.media auto. Ordinary text can stay on the primary profile without routing every raw application onto radio2. Choose set tx.media radio2 if media must fail rather than use the primary when radio2 is off, RX-only, or owned by another protocol.

"},{"location":"radio_profiles/#esp32-companion-ethernet-over-lora-kiss","title":"ESP32 Companion Ethernet over LoRa (KISS)","text":"

ESP32 Full Companion builds with a supported SX1262/SX1276 modem can carry raw Ethernet on radio2 while the primary profile stays MeshCore. This separate feature is named ethernet, not tx.ssh: it transports the whole Ethernet link, including ARP, TCP acknowledgments, IP fragments and SSH on any port. There is no Ethernet-to-MeshCore forwarding.

The mode is off by default. It requires an explicit RF tuple; it never chooses a frequency or starts WiFi automatically. For a dedicated lab link:

set ethernet.radio 909.5,500,5,5,512\nset ethernet on\nget ethernet\nget ethernet.radio\nget radio2.timing\n

Use an appropriate frequency and permitted power for your network. Both ends must match frequency, bandwidth, SF, coding rate and sync word. Ethernet uses sync word 0x16, explicit headers, normal IQ and a LoRa CRC. The optional preamble defaults to 32 symbols; it can be set explicitly (8\u201365528, subject to driver/timing limits). SX1276 requires SF7 or higher.

Do not assume 32 symbols is enough for a fast SF5/BW500 link. Run get radio2.timing after its switching self-test completes, and accommodate the slower end of the link. The lab example uses a 32.8 ms preamble and was tested on a Heltec V4/V3 pair, not an I/O-expander-controlled board. Both primary and secondary preambles matter if primary MeshCore traffic must also be heard while time-sharing. Longer preambles consume airtime; they do not prevent half-duplex collisions.

On that V4/V3 pair, the actual MeshCore-open SSH backend verified the remote host key, authenticated with a public key, opened a PTY and received exactly 4096 test bytes with a matching SHA-256 check. Radio1 stayed on the USA receive profile (910.525,62.5,7,5); its TX counter remained zero on both boards. Raw Ethernet still had losses, so this demonstrates TCP/SSH recovery, not loss-free RF or a minimum preamble guaranteed for every board. The bench used an explicit test-image TX-frequency lock; it did not transmit test packets on the USA profile.

The settings are saved in a checked atomic NVS blob. set ethernet off closes the host connection and releases only this feature's runtime radio2 lease; it does not rewrite the saved MeshCore profiles. A manually configured radio2 must already match the Ethernet RF tuple, including explicit preamble, and be rxtx; its protocol is borrowed only while Ethernet is enabled. An explicit saved radio2 off, a temporary/scheduled profile, or conflicting r2-/mt- channels prevents automatic takeover. Those channel aliases cannot be added while Ethernet is enabled. Clear conflicts or disable Ethernet first. Only one secondary profile can be active: Meshtext and Ethernet cannot share radio2 at the same time.

"},{"location":"radio_profiles/#attach-a-linux-ethernet-interface","title":"Attach a Linux Ethernet interface","text":"

Connect the Companion to station WiFi. Its station IP exposes a single KISS-over-TCP listener on port 5003, separate from Companion binary TCP 5000, OTA TCP 5001 and the existing text terminal on TCP 5002. Use tncattach on the host:

sudo tncattach -T -H 192.168.1.100 -P 5003 --ethernet --mtu 230 --noipv6 --ipv4 10.10.10.10/24\n

Replace the station IP. Attach the other end with a different IP in the same subnet, then use normal host tools such as ping or SSH. A peer running MeshTNC's Ethernet-over-LoRa mode uses the same raw Ethernet framing; configure its PHY to match this link. No custom SSH prefix or MeshCore packet wrapper is added.

Enable only on a trusted LAN. KISS has no authentication and permits raw RF transmission. The service binds to the station address, not the setup AP; do not expose port 5003 to the Internet. The ESP32 does not run an SSH server: SSH connects the attached host computers.

The host MTU must fit one RF frame: MTU 230 plus the 14-byte Ethernet header is 244 bytes. Ethernet frames of 14\u2013255 bytes are supported; larger frames are rejected, never truncated. There is no firmware fragmentation protocol, compression, retransmission or autonomous Ethernet forwarding. Queues are bounded and host TCP is backpressured. Host disconnects, WiFi/IP changes, disabling and profile changes discard stale queued/partial host traffic; an RF frame already transmitting cannot be unsent.

One transceiver time-shares reception between the two profiles. Short preambles on an unmodified peer can be missed while listening to MeshCore; TCP handles losses but performance is not equivalent to two physical radios. Host/codec and firmware-build tests do not establish an RF interoperability or throughput guarantee. nRF52 and non-Full Companion builds do not acquire this Ethernet service. tx.media and tagged tx.ssh remain independent MeshCore application policies; neither controls this raw Ethernet link.

"},{"location":"radio_profiles/#companion-channel-name-routing-aliases","title":"Companion channel-name routing aliases","text":"

Companions recognize the case-sensitive prefixes r2- and mt-, immediately after the optional leading #. Keep the prefix in the app's displayed name. For MeshCore aliases, the firmware strips exactly one prefix when deriving an automatic hashtag key:

#emergency       -> hashtag key for #emergency\n#r2-emergency    -> the same key for #emergency\n#mt-emergency    -> the same key for #emergency only in MeshCore fallback mode\n

Only the app's automatic, name-derived hashtag key is normalized; a supplied private key is not replaced. Non-hashtag names such as r2-Private also retain their supplied key. Empty or nested prefixes (r2-, #r2-mt-emergency) are rejected. MeshCore hashtag names remain case-sensitive.

When radio2 is configured (rx or rxtx), r2- binds the channel to radio2 for both send and receive, including emergency retransmissions. It overrides global crossing and cannot be changed with set tx.channel; rename the channel to remove the binding. RX-only radio2 cannot send. If radio2 is off/unconfigured, ignore the routing prefix and use ordinary MeshCore routing instead. The key stays the same as radio2 is enabled, disabled, or a temporary window expires.

If both the ordinary and r2- slots have the same key, a packet received on radio2 selects the r2- slot; a packet received on radio1 selects the ordinary slot. Global packet deduplication still delivers each packet once, not once in each slot. Receiving a radio2-only channel on the wrong profile does not mark that authenticated packet seen, so it can still arrive later on radio2.

The saved channel record retains the original displayed name and app-supplied key for editing and sharing. Runtime copies resolve the actual wire key. Repeater/room/sensor hashtag behavior is unchanged; those nodes can join the canonical MeshCore channel (#emergency). Older Companion firmware does not recognize these aliases.

"},{"location":"radio_profiles/#local-emergency-channel-notice","title":"Local emergency-channel notice","text":"

When a Companion app adds #emergency (or #r2-emergency), the firmware seeds its local chat with this notice, using #EMERGENCY as the sender:

#EMERGENCY: This is an emergency channel. Do not use for testing or small talk.\n

This is a local onboarding message, not an RF transmission or an emergency alert. It does not change the node's name. It is added only after the channel is successfully saved, not on every connection, reboot, key edit, or alias rename. Deleting and adding the channel again adds a fresh notice. Queued real messages are delivered first: the notice waits for an empty receive inbox and for held one-key DMs to be released, retrying as the app downloads messages. If later traffic fills the inbox, the local notice is the first channel message eligible for eviction. As with other pending received messages, it is RAM-backed until downloaded by the app; a reboot before download can discard it.

"},{"location":"radio_profiles/#meshtext-channel-chat","title":"Meshtext channel chat","text":"

MESH_ENABLE_MESHTEXT_CHAT enables the text-only Meshtext backend in the Companion. It defaults on for ESP32-S3 Full Companion builds using SX1262 LoRa and for the RAK4631 Full Companion starting with 1.17.1.10 preview3. An ESP-NOW primary and other builds default off. RAK4631 1.17.1.10 preview2 and earlier builds did not include this backend. This is not a replacement for stock peer firmware or a cross-protocol bridge.

In an enabled build, an mt- name always requests Meshtext, even if radio2 is off or setup is blocked. It never falls back to sending MeshCore frames. In a feature-disabled build, mt- remains an ordinary MeshCore naming alias while radio2 is off; with radio2 configured, MT channel changes/sends are unsupported. The r2- MeshCore behavior above is unchanged in either build.

With the region configured, join #mt-longfast, for example, through the app's normal hashtag-channel editor. A recognized preset name selects the stock peer channel name (LongFast) and, only for an automatically name-derived hashtag key, the expanded public AQ== key. This public key is not a secret. An explicitly supplied private AES-128 or AES-256 key stays private and unchanged. A custom name such as mt-Private uses the exact tail (Private) as its wire name and the supplied key; custom wire names are limited to 11 bytes and need a manual radio2 profile or another MT channel with a recognized preset. Preset names are ASCII case-insensitive; the mt- marker is still case-sensitive.

Recognized aliases and modulation are:

Alias tail Stock wire name BW (kHz) SF CR shortturbo ShortTurbo 500 7 5 shortfast ShortFast 250 7 5 shortslow ShortSlow 250 8 5 mediumturbo MediumTurbo 500 9 5 mediumfast MediumFast 250 9 5 mediumslow MediumSlow 250 10 5 longturbo LongTurbo 500 11 8 longfast LongFast 250 11 5 longmod or longmoderate LongMod 125 11 8 longslow LongSlow 125 12 8

The selected region must have room for the bandwidth; for example, 500 kHz presets are unavailable in the supported EU868 plan. Use these CLI commands:

get mt.status\nget mt.region\nget mt.slot\nget mt.hops\nget mt.retry\nget mt.rebroadcast\nget mt.node\nget mt.shortname\nset mt.node !12345678\nset mt.node default\nset mt.region auto\nset mt.region us\nset mt.slot 0\nset mt.hops 7\nset mt.rebroadcast off\nset mt.shortname HALO\nset mt.shortname default\nsend mt.nodeinfo 0\nget radio2\nset radio2 auto\n

mt.region accepts auto, us, eu868, eu433, or anz. auto infers US only from a primary frequency at least 902 MHz and below 915 MHz, EU868 from 869.4\u2013869.65 MHz, and EU433 from 433\u2013434 MHz. The US/ANZ overlap at 915\u2013928 MHz is ambiguous: explicitly select us or anz. mt.slot is a 1-based frequency slot, or 0 for the stock preset-name-derived slot; unavailable slots are blocked. Check get mt.status after configuration; a successful settings save does not mean the MT backend is active.

Outgoing MT messages default to a hop limit of 7. set mt.hops <0..7> changes this budget, and get mt.hops or get mt.status shows it. 0 sends only the initial over-air hop with no forwarding budget; larger values permit protocol-compatible peers to forward within that budget. The Companion itself does not relay MT packets unless explicitly enabled as described below.

get mt.node shows the effective Meshtext sender node ID. By default it is SHA-derived from the MeshCore public identity and remains stable for that identity. set mt.node !12345678 sets a saved override; exactly eight hexadecimal digits are required, with or without the leading !. Input is hexadecimal, not decimal, and is never silently truncated. IDs 00000000 through 00000003 and ffffffff are reserved and rejected. set mt.node default clears the override and restores the identity-derived ID.

The example ID is illustrative: choose an ID unique among your protocol-compatible peers. Collisions can merge different senders into one identity and interfere with packet deduplication. Changing the MT ID does not alter the MeshCore private key, reset the outgoing packet-ID sequence or send automatic traffic. Settings changes are refused while an MT transmission is pending. Neither node IDs nor text labels are authenticated.

set mt.shortname <label> saves a Meshtext-only NodeInfo name of 1\u20134 Unicode scalar characters, with valid UTF-8 and at most 16 encoded bytes. Combining characters count separately. get mt.shortname shows the saved/effective name. Stock NodeInfo accepts only four UTF-8 bytes, so the effective wire name is the longest complete UTF-8 prefix that fits. The saved value is not changed. set mt.shortname default clears the override and uses the last four hexadecimal digits of the effective MT node ID. This does not change the MeshCore node name, add a label to chat text or transmit anything.

After the first chat packet actually finishes transmitting on a configured MT channel, the Companion queues a separate, GPS-free NODEINFO_APP broadcast. Automatic metadata waits for a 250 ms gap after the most recent successful MT transmission, including retries, so a stock peer can re-arm its receiver. Explicit send mt.nodeinfo does not wait for this automatic-metadata gap. This happens once per used channel and sender identity per boot, not for every chat message. A rejected or failed chat send does not trigger it. There is no NodeInfo transmission just from booting, receiving traffic, changing settings or joining a channel, and there is no periodic NodeInfo timer. Changing the node name cancels old-name metadata retries; the next new successful chat announces the updated name without sending from the rename.

send mt.nodeinfo <channel slot> still explicitly queues one standard NODEINFO_APP broadcast on the selected configured MT channel. The slot is the Companion channel index, not mt.slot (the frequency slot). The packet contains the actual sender ID, the global MeshCore node name as its long name (UTF-8-safe truncated to 24 bytes), the effective short name, CLIENT_MUTE by default (CLIENT when mt.rebroadcast is on), and the no-DM flag. Both automatic and explicit NodeInfo omit GPS/coordinates, MAC, public keys and unsupported metadata. The usual radio2 TX-mode, pending-TX, airtime and profile guards apply. The reply says queued, not delivered or acknowledged.

Received NodeInfo is still ignored; this backend does not maintain a node database.

Outgoing Meshtext chat and automatic or explicitly requested NodeInfo set OK to MQTT on by default, allowing a compatible receiving gateway to upload those packets. This does not start an MQTT client, bridge networks, request a reply, or mark the packet as having arrived through MQTT. The 228-byte text limit is unchanged.

Region, frequency slot, hop limit, shortname, node-ID override and rebroadcast choice are saved atomically together; changing one preserves the others. Current 40-byte MT04 settings include all of them. New firmware accepts valid legacy MT01/MT02 settings and defaults the shortname to the sender-ID suffix and the node ID to the MeshCore identity; MT01 also defaults the hop limit to 7. Valid MT03 settings preserve both overrides. All legacy versions default rebroadcasting to off. Older MT firmware cannot read MT04, so downgrading does not preserve access to these settings. Invalid or unsupported stored settings block MT operation rather than silently selecting another network.

Private channels can be provisioned directly through the Companion CLI:

set mt.preset <longfast|shortturbo|...>\nset mt.channel <slot> <mt-name> <32/64 hex digits|AQ==>\nget mt.channel <slot>\ndelete mt.channel <slot>\n

set mt.preset accepts the recognized presets above and explicitly saves a manual rxtx radio2 RF tuple using the current region and frequency slot. It is not an automatic owner: deleting the channel does not remove that saved RF configuration. Set region/slot first; changing them later does not overwrite the manual frequency, so rerun set mt.preset if you intend to retune it.

set mt.channel accepts a 32-digit hexadecimal AES-128 PSK, a 64-digit hexadecimal AES-256 PSK, or the literal AQ== for the public key. It does not accept arbitrary Base64 private keys. The stored format interprets an all-zero upper 16-byte half as AES-128, including in a 64-digit input; use a nonzero upper half for AES-256. Slot numbers are zero-based and build-specific, as in the app. get mt.channel reports name, wire name and AES size, never the PSK. The firmware console masks key input echo; keep your client-side logs and stored keys private as well. SET and DELETE save transactionally and restore the previous channel if storage fails. DELETE applies only to an MT channel, not a MeshCore slot.

For a private channel named Team on a protocol-compatible peer:

set mt.region us\nset mt.preset longfast\nset mt.channel 1 mt-Team <your-32-or-64-hex-digit-key>\nget mt.channel 1\nget mt.node\nset mt.shortname HALO\nget mt.status\nsend mt.nodeinfo 1\nchannel 1 Hello\n

Replace the key placeholder with your actual hexadecimal PSK and send after get mt.status reports active. The peer must use the same preset, frequency, PSK and exact case-sensitive wire name Team; mt- is only the local marker. Alternatively, set mt.channel 1 mt-longfast <your-key> preserves a private PSK but uses wire name LongFast and can acquire automatic RF without set mt.preset, provided radio2 automatic ownership is permitted. This route is available for both 128-bit and 256-bit private keys even when the stock app cannot provision or export the latter.

When radio2 was never manually configured, a recognized preset can acquire an automatic rxtx profile. Its RF tuple is RAM-only and stays available for RX while the required channels exist. Removing the last MT channel, losing its last recognized-preset dependency, or detecting a conflict releases only the automatic profile. Explicit manual RF settings are retained: named aliases must match their BW/SF/CR and fit the selected region, but do not overwrite their frequency. The runtime MT overlay uses its required 16-symbol preamble; the saved manual preamble is retained.

With a Meshtext secondary, an automatic primary MeshCore preamble keeps its normal SF/BW-derived wire length (32 symbols for the USA SF7/BW62.5 preset), so scan recommendations do not silently lengthen normal-network transmissions. An explicit primary preamble is still honored. Automatic sizing for two MeshCore profiles is unchanged; mixed-network reception remains best-effort and time-shared.

set radio2 off is an explicit manual veto, including after reboot. Older saved off profiles are conservatively treated as manual. Local set radio2 auto clears the saved manual RF/off choice and permits automatic ownership after two seconds; it rejects pending changes, leases and schedules rather than cancelling them. get radio2 distinguishes auto-owned, off (manual) and off (automatic allowed). Temporary profiles, scheduled changes and reply commit barriers take precedence and can suspend MT operation. Automatic cleanup does not undo later manual edits.

All MT channels share this one secondary modem profile. Different recognized presets, or mixing any MT and r2- channels, are rejected instead of selecting whichever channel was edited last. Loaded conflicts also block MT traffic. Neither radio2.cross on nor MeshCore channel routes cross MT frames between protocols.

The app uses its existing channel index/text commands and receive events. Outgoing MT chat is plain text exactly as entered, with the sender node ID in the standard radio header. It contains no custom name envelope or NodeInfo. Incoming text uses the exact sender-ID prefix !12345678: before the received payload when passed to the MeshCore app. Changing the shortname only changes future NodeInfo, not chat or the packet's sender ID; set mt.node changes the actual Meshtext sender ID. Neither setting change emits advertisements. A successful send response means queued, not acknowledged by a remote node. This backend does not relay MT packets by default. It does not provide ACKs, administrative commands, direct messages, periodic node-info adverts or a node database. MeshCore emergency forwarding does not apply to MT traffic. Interoperability is with stock peers' PSK channel text using AES-CTR, not AES-CCM. AES-CTR channel traffic is not authenticated; successful decoding is not proof of sender identity.

The codec supports AES-128 and AES-256 and up to 228 UTF-8 bytes of wire text, without an added label/node-ID prefix. The standard app channel editor still provisions 16-byte keys. An MT-only 32-byte binary SET/opt-in GET extension is available for capable clients, with protection against legacy 16-byte edits truncating an existing 256-bit key; see Companion protocol. This does not give stock apps seamless 256-bit provisioning or sharing. Current V3 Companion receive events carry at most 165 text bytes including the node-ID prefix; longer incoming MT text is UTF-8-safe truncated for the app.

Radio1 and radio2 still time-share one physical radio. The device cannot receive either network while tuned to the other or transmitting; long MT airtime, particularly LongFast, can leave substantial MeshCore receive gaps. The compatible protocol's fixed short preamble also does not provide the scan-aware long preamble described below. Do not treat this as continuous coverage of both networks or guaranteed message delivery.

"},{"location":"radio_profiles/#heard-repeat-retries","title":"Heard-repeat retries","text":"

MT channel chat uses the Companion's saved flood.retry.count budget: 0 disables retries, and 1\u201315 allows that many extra transmissions after the initial send. Locally originated MT traffic is hop zero for the flood.retry.path gate, so a gate of 0 still permits it; the group-data gate does not apply to channel text. mt.hops 0 disables heard-repeat retries because peers cannot forward a packet with no remaining hop budget; the original chat and its first-use NodeInfo still transmit normally. Retries keep the exact encoded payload and packet ID, allowing peers to recognize duplicates. They use conservative, airtime-based flood spacing and the normal CAD and airtime limits rather than transmitting continuously.

get mt.retry is read-only diagnostics: it reports retry transmissions, matching heard repeats, exhausted retry sequences, pending work and the configured retry count. Use set flood.retry.count, not this getter, to change the budget.

A matching over-air copy with a lower remaining hop limit cancels the retry sequence. Merely receiving another message, an unchanged-hop copy or a packet with the same ID but different payload does not. A heard repeat indicates a compatible peer forwarded the packet; it does not confirm that a particular person or app received it. This does not add DM delivery receipts or ACKs.

NodeInfo follows the metadata retry cap: at most one extra send, only when flood.retry.count is nonzero and flood.retry.advert is on. Its wait adds one minute to the airtime-based delay, matching the conservative self-advert policy. Turning retry count off cancels retained retries. Channel, identity, settings or radio-profile changes invalidate retained work rather than sending it on a different network. The bounded pending-frame storage may reject a new send when full. Forwarding other people's decoded channel text is a separate, default-off option below. MeshCore repeater prefix/bridge IDs and flood-filter rules are not MT routing rules.

"},{"location":"radio_profiles/#optional-decoded-text-rebroadcasting","title":"Optional decoded-text rebroadcasting","text":"
get mt.rebroadcast\nset mt.rebroadcast on\nset mt.rebroadcast off\n

This saved option defaults to off. mt.maybe_rebroadcast is an alias for mt.rebroadcast; get mt.status also shows the current choice.

When enabled, only broadcast text successfully decrypted and parsed with a configured channel's key, and admitted to the local receive queue, is eligible. Unknown channels, wrong keys, malformed packets, DMs, NodeInfo, GPS and other non-text traffic are not relayed. A packet must have a valid remaining hop budget. AES-CTR is unauthenticated: decoding does not authenticate its sender.

Before queuing a relay, the Companion waits five additional packet airtimes calculated from that packet's actual length and the current radio2 settings, plus the normal flood jitter. Existing CAD, receive-busy and airtime-budget waits still apply; this does not replace or shorten them. Hearing another forwarded copy while waiting cancels the redundant relay. An unchanged-hop retry from the original sender does not cancel it.

The forwarded frame retains the original sender, packet ID, channel and ciphertext; only remaining hops and the relay-node byte change. It is sent at most once, without originated-message retries or an automatic local NodeInfo announcement. At most one relay occupies the existing bounded chat delivery slots, with no additional frame-sized buffer. A new local chat can displace an unsent relay. Full queues skip the relay, and work delayed more than a minute past its due time expires. Changes to the channel/key, identity or radio profile cancel stale work instead of forwarding it on another network. Turning the option off cancels pending relay work; settings edits are refused while a radio transmission is already queued or in progress.

Relays stay on Meshtext radio2. This is not a MeshCore bridge, and ordinary MeshCore forwarding rules remain unchanged. Enabling it does not add periodic traffic or cause NodeInfo to be sent just because something was received.

"},{"location":"radio_profiles/#choose-whether-traffic-crosses-between-profiles","title":"Choose whether traffic crosses between profiles","text":"
get radio2.cross\nset radio2.cross auto\nset radio2.cross on\nset radio2.cross off\n
Active primary Active secondary auto on off (default) radio radio2 Cross Cross Isolated tempradio tempradio2 Cross Cross Isolated radio tempradio2 Isolated Cross Isolated tempradio radio2 Isolated Cross Isolated

This table describes MeshCore profiles. A Meshtext secondary profile is always isolated, including with radio2.cross on.

New configurations default to off. An explicitly saved auto, on, or off setting is preserved across firmware updates and reboots.

These are the global rules used by Companion targets set to auto; an explicit contact/channel choice above overrides crossing for its own outbound packets. Recognized media uses the separate tx.media auto selection described above. Ordinary crossing respects rx. The infrastructure-only tx.reply ... force override above permits locally generated replies on that profile. With crossing allowed, a packet received there may still be forwarded on the primary profile. Existing routing, forwarding and packet-filter settings apply. An RX-only tempradio2 also blocks locally generated OTA requests under auto or off; they do not fall back to the normal primary channel. Use rxtx for a fetching node that needs to exchange requests and data. Infrastructure OTA responses follow tx.reply independently of this crossing table.

"},{"location":"radio_profiles/#mcu-power-saving-while-scanning","title":"MCU power saving while scanning","text":"

On nRF52, set powersaving on also permits interrupt-driven MCU idle during a settled radio2 or tempradio2 listening visit. The RF receiver stays on. This includes a Meshtext LongFast visit (SF11 / 250 kHz), whose minimum visit is about 37.7 ms. No extra command is needed.

The scanner first completes its switch self-test and both noise-floor baselines. It polls again for pending packets, calibration, changed settings, retries, or short visits. All normal USB, logging, bridge, OTA, queue and role sleep guards still apply. An nRF52 periodic interrupt bounds each wait to about 0.977 ms; admission reserves another 0.488 ms before the visit deadline and is rechecked immediately before sleeping. If that wake source is not available, scanning keeps polling.

ESP32 also permits MCU idle during settled long visits. It requires radio GPIO wake and physically stopped WiFi/Bluetooth drivers. Its timer slices are at most 10 ms, reserve 4 ms before the visit deadline, and require at least 2 ms usable time. Absolute deadlines are checked again after wake-source setup; expired visits, pending interrupts or setup failures retain polling. USB, logging, active wireless services and the other role guards remain blockers. Other MCU families retain polling until their timing is validated.

The integer admission gate requires at least 49 RTC2 ticks (about 1.495 ms) remaining after the work checks. A configured visit must also cover the loop work before that point. Eligibility is therefore per visit, not an SF/BW cutoff: a fast SF5 secondary can still idle the MCU when the primary profile gives it a sufficiently long listening visit.

For example, the RAK3401 bench with USA primary 910.525,62.5,7,5,32 and secondary 909.5,500,5,5,rxtx,384 had roughly 9.4/21.3 ms visits. MCU sleep was observed in 35% of 4265 debugger samples with power saving on, versus zero of 2129 with it off. This is a CPU-state spot check, not a current or Ethernet throughput measurement. Enabling power saving changed neither preamble.

Radio RX duty cycling remains suspended while scanning, then its previous setting is restored when radio2 stops. Meshtext's fixed 16-symbol preamble does not have the same sleep margin as a longer MeshCore preamble. Adding RF sleep inside a scan visit would introduce extra blind time, so this optimization saves MCU activity without shortening either listening visit.

"},{"location":"radio_profiles/#preamble","title":"Preamble","text":"

Append the preamble to the end of a command:

set radio2 910.5,500,8,5,rxtx,88\nset tempradio2 910.5,500,7,5,rxtx,120,128\nset radio 909.5,62.5,7,5,32\nset tempradio 909.5,62.5,7,5,120,32\n

Existing primary radio, tempradio, radioat and tempradioat commands accept the optional trailing preamble where those primary commands are supported by the role. Getters include the preamble. Omit it, or use auto/0, to calculate it automatically. Explicit values are used as entered, subject to the radio's limits and safe airtime arithmetic.

Automatic dual-profile preambles round up to a multiple of eight, with a 32-symbol minimum and a 512-symbol maximum. When a second profile activates, the radio first self-tests four successful RX-to-RX switches in each direction using the actual tuples. It defers the test while receiving, transmitting or busy, and never counts a failed switch. The 512-symbol automatic preamble is provisional until the test completes. The largest successful hop, rounded up with a 10% guard, then supplies the per-switch allowance for both the automatic preambles and the scan dwell. There is no fixed switch-time floor. The measurement is reset when either profile changes, including when a tempradio2 session starts or ends; it is not saved to flash. The rapid startup hops briefly interrupt reception on each channel; a new packet beginning during this test may be missed. An already detected packet defers the test instead of being interrupted.

Displayed preamble recommendations and warnings wait until the active pair has run for at least two minutes, measured from its first successful RX-to-RX hop. Four successful hops in each direction are still required. A pair change starts a fresh wait; an unchanged or rejected request does not. Use a temporary lease longer than two minutes if you want its recommendation before it expires.

This is a display-only delay. The bounded startup test, guarded maximum switch budget, automatic preambles, ordinary scanning and MCU-idle behavior do not wait two minutes. Live switch and scan diagnostics remain available, marked pending with wait=Ns until advice is ready. Waiting does not discard an early maximum or replace it with an average. No extra retunes are forced at the deadline; a successful normal hop completes the display wait.

Each idle scan starts with 4.6 symbols on the slower profile, then spends the remaining budgeted time on the faster profile, also with a 4.6-symbol floor. Slower means a longer LoRa symbol (2^SF / bandwidth), regardless of profile number. The slower preamble sets the return deadline. The calculation allows two slow visits within that preamble, reserving the self-tested, 10%-guarded switch time per hop. There is no extra fixed main-loop allowance. Fast-channel transmit preambles include sixteen acquisition symbols. For a primary SF7 / 62.5 kHz profile with a 763 us measured maximum switch (840 us after the 10% guard, from the BLE-on T1000-E trial):

Secondary profile Automatic secondary preamble Previously tested explicit preamble Primary preamble SF9 / 500 kHz 32 32 32 SF8 / 500 kHz 40 64 32 SF7 / 500 kHz 64 80 32

With preamble 32 on that slower profile, its visit is 9.421 ms and the faster visit is 21.667 ms, plus the actual switching time. Increasing the slow preamble also increases the time available on the fast channel. If the symbols are equal in length, the primary profile goes first. Detected packets hold the current channel until reception finishes.

The SF8 / 500 kHz value is calculated from the same formula as every other profile; the older pair-specific 88-symbol floor has been removed. The explicit values come from historical production validation: the final fast-channel attempts each received 50/50 packets at 4.8 slow symbols. Automatic values use the measured switch allowance below. Those explicit-preamble tests used the older 4.8-symbol policy; they do not validate the newer 4.6-symbol slow dwell. The 4.6/0.3 ms two-profile bench received 197/200 at SF7/62.5 + SF8/500 with explicit 32-symbol preambles. The newer four-channel 5.1/6.1/7.7 tests also had misses, so this is a timing policy, not a claim of zero packet loss. The automatic fast preamble covers the blind interval during the slow visit, both switches and the acquisition margin. These settings do not guarantee reception during overlapping packets or long pauses in the firmware loop. Transmitters on each channel also need a long enough preamble. Changing the receiver's setting does not lengthen packets sent by other nodes.

"},{"location":"radio_profiles/#chirp-calculator-and-warnings","title":"Chirp calculator and warnings","text":"

get radio2.timing (alias get radio.timing) reports the active pair in radio,radio2 order: dwell in chirps, recommended preambles in symbols, and the switching/loop allowances. During the two-minute advice wait it reports timing self-test pending, the remaining wait and the current provisional switch allowance/preambles, without a final need= recommendation. On nRF52 boards, enabling radio2 uses RTC2 for both retune timing and scan visits (about 31 us resolution); turning radio2 off stops RTC2 and returns to the usual micros() clock. Previously, the nRF52 micros() fallback counted 1,024 Hz RTOS ticks: a real sub-millisecond retune could be reported as exactly 977 us, and two ticks as 1,953 us. Those old numbers are timer artifacts, not measured RF costs. For SF7/62.5 on radio and SF8/500 on radio2, one live BLE-on read returned:

> chirps=4.60,42.84; need=32,40; switch=706us; loop=0us; test=4/4,4/4 ready; WARN recommended preamble: radio2=40\n

Later good hops in another BLE-on trial raised the measured maximum to 763 us (840 us with the 10% guard); the recommendation stayed at 40 symbols.

The measured switch duration is wall time, so an interrupt during the hop is included. Bluetooth's SoftDevice may still preempt application code; this clock correction does not stop it. Ordinary Companion/USB work in the main loop does not run inside the synchronous retune, but it can delay when the next scan visit begins. The zero fixed loop allowance is not a guarantee of lossless reception under arbitrary application load.

WARN recommended preamble identifies each profile whose recommendation exceeds the standard 32 symbols or its explicitly selected value. For example, with SF7/62.5 on the primary profile, set radio2 910.5,500,7,5,rx,32 initially replies with timing self-test pending. Once active and measured, get radio2 warns:

> 910.500,500.000,7,5,rx,32; WARN recommended preamble: radio2=64; short override: radio2\n

Successful set radio ... replies also include the recommended preamble for each affected profile, calculated with the requested settings. A short override note identifies an explicit value below the estimate; it is not silently rewritten. Warnings are also included in profile settings/getters, scan info, temporary settings and schedule acknowledgments. These preamble-bearing replies also include switch=547us (or the current measured value) after the self-test for that exact profile pair. The first ACK for a new pair says timing self-test pending instead: it is sent before activation, so it cannot contain the future measurement. Query get radio2 after activation to see the measured value beside the resulting preamble. off disables the dual-profile warning. get radio2.status shows self-test progress and the resulting guarded switch budget; its separate max field remains a lifetime diagnostic that can include failed/rolled-back retunes and is not used for calibration. If the recommendation exceeds 512, it additionally reports WARN need >512; an automatic preamble stays at 512, which may not be enough for reliable reception on that pair. Explicit preambles are not rewritten and still use the radio's ordinary validation limits. A timing readout does not change settings or transmit a packet.

The shared RadioProfiles::calculateChirpTiming() function accepts both tuples, optional visit times in microseconds, a per-switch allowance and loop margin. Using microseconds throughout:

T[i] = 2^SF[i] * 1000 / BW_kHz[i]\nslow = profile with the longer T (radio wins ties)\nL[slow] = max(requested_visit_us[slow], ceil(4.6 * T[slow]))\nL[fast] = max(requested_visit_us[fast], ceil(4.6 * T[fast]))\nO    = 2 * switch_us\nC    = L[radio] + L[radio2] + O\n\nP[slow] = round_up_to_8(max(32, 2 * C / T[slow]))\nP[fast] = round_up_to_8(max(32, (L[slow] + O) / T[fast] + 16))\n

Automatic selection uses the minimum visits to choose a preamble, then allocates the fast visit from half the slow preamble's duration. Diagnostics feed the actual allocated visits back into the same function, including longer visits caused by an explicit slow preamble. Too-short slow preambles that cannot fit both minimum visits remain rejected by the existing configuration validation.

The automatic scheduler uses these same role-specific minima and computes:

slow_dwell = ceil(4.6 * T[slow])\nfast_dwell = max(ceil(4.6 * T[fast]),\n                 floor(P_configured[slow] * T[slow] / 2 - slow_dwell - O))\n

For the tested SF7/62.5 + SF8/500 pair with slow preamble 32, this yields 9,421 us slow and 21,197 us fast (41.40 fast chirps) with the 1,075 us guarded switch allowance above. Swapping radio and radio2 swaps the allocations automatically.

Production scheduling and automatic preambles use the self-tested maximum successful switch plus 10%, without an additional fixed loop allowance. Subsequent successful hops can only raise that allowance until the next profile change. The measured recommendation is rounded up to eight symbols and capped at 512 for automatic settings. The calculator and warnings retain the uncapped recommendation so a slow board is not shown as safe merely because it hit the limit. Older V4 tests peaked at 836 us (827 us in the 4.6/0.3 ms run), and Indicator reached 8428 us; these are historical measurements, not default budgets for other boards. The model preserves two slow-profile return opportunities and the existing 16-symbol fast-profile acquisition allowance. These are conservative policies, not a fitted success curve, and neither packet overlap nor arbitrary application stalls can be made safe by a finite preamble recommendation.

See the implementation and validation notes.

"},{"location":"radio_profiles/#schedule-the-second-profile","title":"Schedule the second profile","text":"

Use UTC Unix timestamps (seconds), or +N for N whole minutes from now:

set radioat2 910.5,500,8,5,rxtx,START[,PREAMBLE]\nset tempradioat2 910.5,500,8,5,rxtx,START,END[,PREAMBLE]\nget radioat2\nget radioat2 1\nget tempradioat2\nget tempradioat2 1\ndel radioat2 1\ndel tempradioat2 all\n

Replace START and END with timestamps or +N, and the optional PREAMBLE with a number; omit the square brackets. Each schedule family has four slots. Times must be in the future and within 24 days. Overlapping temporary second-profile sessions are rejected. radioat2 changes the saved second profile when it starts; tempradioat2 restores the saved profile at its end. Pending schedules do not survive reboot.

For example, set tempradioat2 910.5,500,8,5,rxtx,+5,+15 starts in 5 minutes and restores in 15 minutes (10 minutes active). Both offsets are measured from the same node-clock snapshot when accepting the command, not from each other. set radioat2 910.5,500,8,5,rxtx,+5 schedules a saved change in 5 minutes. Offsets must be positive whole minutes. Absolute and relative endpoints can be mixed; bare numbers still mean UTC seconds. Existing clock requirements apply.

"},{"location":"radio_profiles/#return-to-one-profile","title":"Return to one profile","text":"
set radio2 off\n

This saves the disabled state and cancels all second-profile temporary activity and schedules.

set tempradio2 off\n

This ends temporary second-profile activity, cancels its temporary schedules and restores saved radio2. If the saved second profile is off, this returns to one profile. Packets/retries bound to an expired or changed session are discarded.

"},{"location":"radio_profiles/#power-saving-and-diagnostics","title":"Power saving and diagnostics","text":"

Dual mode suspends RX power saving and MCU idle sleep so receive visits can run promptly. The saved RX power-saving setting returns when profile 2 is off. The scan uses normal receive, following the CAD/RX measurements, with the updated 4.6-symbol dwell on the slower channel, followed by the remaining fast-channel visit. It does not use CAD for scanning. The separate CAD check before an initial transmission still follows cad.

SX1262 dual-profile mode keeps the oscillator running in XOSC standby between visits, so each retune does not need to restart the TCXO. It restores the prior standby policy when returning to one profile, together with the saved RXPS setting. Radio reinitialization temporarily uses RC standby until the TCXO has been configured again. Other radio families retain their existing behavior. This does not disable the TCXO supply or its initial startup delay, and it is not conditional on 500 kHz bandwidth. Keeping the oscillator on uses additional power during the short standby intervals.

The switch-time self-test is not a forced settling delay or a proof of RF acquisition. The earlier standalone keep-warm scan experiment averaged about 1.19 ms of overhead per hop (scan-cycle time minus both receive windows, divided by two). That experiment used a leaner receive path; it is not a measurement or guarantee of the production implementation. Measure the target board before shortening its timing margins or transmit preambles.

The production-path XIAO timing test measured 1.434 ms average / 1.467 ms maximum with warm standby, compared with 3.166 ms average using RC standby: about 1.732 ms saved per hop. It covered 12,000 retunes across SF7/8/9 at 500 kHz paired with SF7/62.5 kHz. These measurements include observing BUSY low, but exclude application/UI scheduling and are not a packet-delivery qualification or a guarantee for other boards, especially those using I/O expanders.

A same-image follow-up batching SF/BW/CR into one modulation command reduced warm switching from 1.444 ms to 1.158 ms average (batched maximum 1.198 ms), saving another 0.286 ms / 19.8%. RadioLib's modulation caches and LDRO calculation remain synchronized; frequency, preamble, packet guards, and RX restart are unchanged. A later HIL-only screening tested redundant standby/IRQ/buffer/packet setup and SPI transfer changes. Its fastest candidate measured 0.535 ms mean on XIAO but 8.203 ms on the Indicator, whose radio GPIOs go through an I2C expander. The validated production implementation now opts XIAO S3 WIO and Indicator LoRa into buffered 8 MHz SPI and a guarded fast-RX state machine. The heltec_v4_kiss_modem and Station_G3_ESP32_kiss_modem targets use the same guarded path only for KISS dual-profile scanning; their normal board roles keep their established HAL/timing policy. V4 uses its existing 160 MHz role override and Station G3 uses its base 240 MHz setting, but both still need an exact-image hardware timing check before a nominal 600 \u00b5s target is claimed. It skips redundant standby, IRQ mapping, buffer-base writes and modem queries only during an owned continuous RX-to-RX retune. Preamble/packet parameters (including the IQ workaround), stale-IRQ clearing, RX commands and BUSY waits remain. TX, CAD, sleep, reset, RXPS and failed commands revoke reuse; failed RX startup rolls back the profile. Sleep entry uses standby and wake restores the existing TCXO voltage/delay. Other variants do not opt in to faster SPI or the fast-RX state machine. Within a valid owned fast-RX retune, identical acknowledged SF/BW/CR/LDRO settings now reuse the previous modulation command: frequency-only hops omit SetModulationParams (0x8B). A changed effective LDRO value also forces a write, including when automatic LDRO policy changes. Ordinary setters and lost RX context invalidate that acknowledgement; initial setup, reset, sleep, failed commands and full RX setup conservatively reapply the tuple. Frequency, preamble/packet setup, IRQ clearing and BUSY checks are unchanged. This follow-up has same-image hardware measurements: frequency-only hops averaged 0.452 ms on XIAO and 7.601 ms on Indicator. The mixed-bandwidth timing figures above predate it. Four-channel SF6 scans still missed packets; faster switching is not proof of loss-free acquisition. See production and USB validation for current measurements, lifecycle tests and limitations. The Indicator result shows why each board now measures its own switch time. That measurement adjusts automatic preambles and dwell, but it cannot verify RF acquisition or lengthen packets transmitted by other nodes.

Single-channel noise-floor calibration and RSSI interference comparison are suspended while scanning. Retries use the radio's preamble/header activity indicators. LR2021 extra-SF side detectors are suspended and restored as well.

get radio2.status reports the active second-profile mode, MeshCore RX/TX counts for each profile, switch count, switch failures, longest measured switch and both effective preambles. A long switch time or rising error count indicates that the chosen settings need investigation on that board.

get radio2.scan shows which profile goes first, the two idle receive-window lengths in microseconds, and both effective preambles. It reports off in single-profile mode.

"},{"location":"radio_profiles_validation/","title":"Radio profiles validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profiles_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_receive_calibration/","title":"Radio receive calibration","text":"

View this page on MeshCore Docs.

"},{"location":"radio_receive_calibration/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"rak3401_1.17.1.7_legacy24/","title":"Rak3401 1.17.1.7 legacy24","text":"

View this page on MeshCore Docs.

"},{"location":"rak3401_1.17.1.7_legacy24/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"rak3401_mota_chain/","title":"Rak3401 mota chain","text":"

View this page on MeshCore Docs.

"},{"location":"rak3401_mota_chain/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"role_feature_switches/","title":"Role feature switches","text":"

View this page on MeshCore Docs.

"},{"location":"role_feature_switches/#feature-switches-by-role-11715-usa-cascade","title":"Feature switches by role - 1.17.1.5 USA Cascade","text":"

Use these settings with the exact board's canonical release image. Former logging, power-saving, FEM-gain, and rotated-display variants are now runtime choices where the hardware supports them. A setting cannot add missing radio hardware, MQTT code, storage, or an OTA partition. Check the download's .capabilities.json and the firmware picker. For exact old device/variant names, search the 1.17.1.5 variant map: it maps all 1,361 previous release entries to 1,325 covered choices or 36 excluded entries. Many old choices share one current image; the command tables below explain how to select their former behavior. Excluded entries have no download.

Search terms are alternative wording for finding these instructions, not CLI aliases. Use the commands in the role-specific tables and examples.

"},{"location":"role_feature_switches/#open-the-usb-web-console","title":"Open the USB web console","text":"

Search terms: USB terminal, serial console, ASCII terminal, browser terminal, web serial.

Open the MeshCore USB web console in Chrome or Edge, connect a data-capable USB cable, close other applications using that port, and select the device. Use 115200 baud when prompted. Full Companion, Repeater, Room Server, and Sensor start with an ASCII USB terminal. Run board, then ver on any role.

If a Companion app has already switched USB to its binary protocol, send:

+++MESHCORE-TERM-START\n

To hand the port back to a Companion app or USB MOTA host, send +++MESHCORE-TERM-STOP, close the console, and connect the app/tool. Dedicated USB/BLE/WiFi Companion images can start in binary mode; use the start token there too. KISS firmware uses its modem protocol and is outside this release.

The linked console runs in your computer's browser over USB. It does not need node WiFi or set webui on. WebConfig is a separate settings website served by supported ESP32 images. Both infrastructure and WiFi Companion WebConfig include a browser command terminal (set wifi.cli on), enabled by default on the LAN. The Companion browser CLI uses the same terminal commands as USB, including contact import, chat, recipient selection, and streaming replies. Full ESP32 Companion also exposes this terminal at TCP port 5002. See the terminal command guide.

"},{"location":"role_feature_switches/#which-old-variant-setting-should-i-use","title":"Which old variant setting should I use?","text":"

Search terms: old firmware variants, restore features, turn features on or off, enable MQTT, disable MQTT.

These controls apply to every board with the corresponding compiled feature; check the hardware requirements below.

Former choice Current image / runtime control USB logging or Station G2/G3 logging variant Same role's canonical image; set usb.logging on / off USB logging, WiFi MQTT, or both MQTT-capable image with USB logging; set logging.output usb, wifi, both, or off Companion WiFi-MQTT variant set mqtt.enabled on / off; configure broker slots through the CLI or WebConfig _ps power-saving variant set powersaving on / off; check get powersaving on every role _femoff / FEM-gain variant set radio.fem.rxgain on / off; separate TX switch where controllable RX boost variant set radio.rxgain on / off on a supported radio iKOKA rotated-display variant Full Companion: set display.rotation 180; 0 restores the board default Separate USB, BLE, WiFi, Ethernet, or Terminal Chat Companion Exact board's Full Companion where listed; supported transports are included, with SenseCAP selection below RS232 repeater variant consolidated into the ordinary repeater set bridge.enabled on / off on a build containing the RS232 bridge ESP-NOW bridge or primary ESP-NOW radio Select the matching hardware/role image first; bridge and primary-radio channel commands differ LoRa OTA / external-storage variant Still choose the exact receiver/storage image and matching bootloader; this is not a software on/off switch

Companion, Repeater, Room Server, and Sensor use the same command names for shared settings. Use get <setting> to check and set <setting> <value> to change them. A command requires its feature to be compiled into the build. set usb.logging on|off changes only USB logging. On MQTT-capable builds, set mqtt.enabled on|off changes only MQTT and keeps broker settings; set logging.output off|usb|wifi|both selects both outputs together. set usb.debug on|off separately saves verbose USB diagnostics. Debug output requires both USB switches on; RAW/RX/TX packet records need only usb.logging. Leave usb.debug off for USB-to-MQTT packet capture.

Current source also includes a saved USB logging watchdog: get usb.watchdog and set usb.watchdog off|on|auto. Missing state defaults to Auto, qualifying from 14 continuous healthy USB stats-polling days, not a cable or open TTY alone. Confirmed client loss can trigger USB-only repair; MCU resets require On and an entire physical-fault tier, growing from one hour to one week. This repairs the node's USB connection, not the host Pi or MQTT service.

"},{"location":"role_feature_switches/#full-companion-commands","title":"Full Companion commands","text":"Setting Enable / select Disable / restore Read back Device power saving set powersaving on set powersaving off get powersaving LoRa RX power saving set radio.rxps on set radio.rxps off get radio.rxps Radio RX boost set radio.rxgain on set radio.rxgain off get radio.rxgain External FEM RX gain set radio.fem.rxgain on set radio.fem.rxgain off get radio.fem.rxgain External FEM TX gain set radio.fem.txgain on set radio.fem.txgain off get radio.fem.txgain ESP32 USB logging set powersaving off, then set usb.logging on set usb.logging off get powersaving, get usb.logging nRF52 second USB logging port set usb.logging on reboot set usb.logging off reboot get usb.logging Verbose USB diagnostics set usb.debug on while USB logging is on set usb.debug off get usb.debug reports saved intent USB logging watchdog (current source) set usb.watchdog auto or on set usb.watchdog off get usb.watchdog; native USB and durable storage required MQTT master set mqtt.enabled on set mqtt.enabled off get mqtt.enabled, get mqtt.running, get mqtt.status ESP32 persistent WebConfig set webui on set webui off get webui ESP32 WebConfig browser console set wifi.cli on set wifi.cli off get wifi.cli ESP32 temporary setup AP start webconfig ap stop webconfig get webui Display rotation set display.rotation 90, 180, or 270 set display.rotation 0 get display.rotation Temporary MOTA radio window tempradio 910.525,250,5,5,120 normalradio tempradio

Saved settings apply immediately unless noted. Rotation and gain controls report unsupported hardware instead of creating that feature. Fresh Full Companion preferences enable device power saving and leave USB logging off; existing saved preferences win after an update. USB debug defaults off, including on upgrade from preferences without that field. Its saved intent survives switching the USB master off. Verbose messages still require compiled debug support; changing this preference adds no missing code and does not require a reboot.

ESP32: logging and binary Companion traffic share one USB port. Turn logging off before handing USB to an app/MOTA host. nRF52: the optional second CDC port is for logs; use the primary port for Companion/MOTA. Adding/removing the second port requires a reboot; the optional suffix shown above requests it. Full Companion includes complete ASCII RAW packet records and decoded RX/TX summaries, independent of verbose debug. The nRF52 log port is output-only; CLI/identity queries belong to interface 00. A USB-to-MQTT program that expects both logs and CLI replies on one port cannot use interface 02 alone.

"},{"location":"role_feature_switches/#mqtt-controls-shared-by-companion-and-infrastructure","title":"MQTT controls shared by Companion and infrastructure","text":"

Search terms: Companion MQTT settings, MQTT on, MQTT off, broker configuration.

Configure a broker through the MQTT tab or with the same commands on either role. For example:

set mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.enabled\nget mqtt.running\nget mqtt.status\n

set mqtt.enabled off disconnects brokers while preserving their settings. set mqtt.enabled on allows configured brokers to reconnect. An enabled switch is separate from a running connection: check get mqtt.running and get mqtt.status. The MQTT tab's Enable MQTT checkbox controls this same saved switch. Slot credentials and presets are available through get/set mqtt1.*, mqtt2.*, and the remaining supported slots; secrets stay masked in the browser. WiFi Companion accepts these commands through its USB terminal and browser CLI; Full Companion also accepts TCP port 5002.

set mqtt1.preset none disables just that slot. Publication controls such as set mqtt.status off do not disconnect brokers. USB logging is independent. Images without MQTT code do not show MQTT cards or accept MQTT controls.

"},{"location":"role_feature_switches/#companion-wifi-bluetooth-gps-and-board-exceptions","title":"Companion WiFi, Bluetooth, GPS, and board exceptions","text":"

Configure ESP32 WiFi with:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nget wifi.status\n

set wifi.powersave min enables WiFi modem sleep; none disables it. max is accepted only where the BLE/WiFi coexistence policy allows it. Read back with get wifi.powersave. This is separate from device power saving and RXPS. The assigned WiFi button/display switch controls WiFi services on supported boards. stop webconfig closes only the portal; it is not a WiFi master switch. On Bluetooth-capable ESP32 and nRF52 Companions, get bluetooth reports the running state and set bluetooth on|off saves the service preference. Plain off requires another active management connection; set bluetooth off force explicitly permits disconnecting the only client. See Bluetooth controls for reply draining and boot-transport restrictions. Ethernet controls remain specific to the board's recipe; there is no universal Ethernet-off command.

T1000-E Companion: USB data takes priority over Bluetooth. Once a computer enumerates USB, the firmware stops Bluetooth advertising and disconnects any Bluetooth client, even if no serial application is open. Bluetooth resumes 500 ms after USB disconnects, unless it was switched off manually. Battery power and a charge-only wall supply leave Bluetooth available. The saved Bluetooth setting, pairing PIN and bonds are preserved. get bluetooth reports the USB pause; set bluetooth on saves the choice for after unplugging.

SenseCAP Indicator Full only: set companion.transport wifi followed by reboot selects WiFi; set companion.transport ble followed by reboot selects Bluetooth. Check get companion.transport. USB remains available. Primary ESP-NOW Indicator images keep their mesh radio in either selection.

GPS-equipped builds: use get gps, set gps on, and set gps off on Companion and infrastructure. The Companion app's gps=1 / gps=0 custom setting controls the same GPS. See GPS tracking for location-sharing settings.

"},{"location":"role_feature_switches/#repeater-room-server-and-sensor-commands","title":"Repeater, Room Server, and Sensor commands","text":"

These roles use CommonCLI. Feature-dependent commands require the relevant hardware/build; MQTT is present in observer builds, and WebConfig is absent from some portable builds. The USB browser console still works without it.

Setting Enable Disable Read back Live USB logging ESP32 1.17.1.5: set powersaving off, then set usb.logging on; other platforms: set usb.logging on set usb.logging off get powersaving, get usb.logging Verbose USB diagnostics set usb.debug on while USB logging is on set usb.debug off get usb.debug reports saved intent Capture RX log to node storage log start log stop log prints the capture locally RS232 / ESP-NOW bridge master set bridge.enabled on set bridge.enabled off get bridge.enabled, get bridge.running, get bridge.type Independent RS232 in combined Full MQTT images set rs232.enabled on set rs232.enabled off get rs232.enabled, get rs232.running Independent ESP-NOW in combined Full images set espnow.enabled on set espnow.enabled off get espnow.enabled, get espnow.running MQTT periodic status publication set mqtt.status on set mqtt.status off get mqtt.status shows connection status MQTT packet publication set mqtt.packets on set mqtt.packets off get mqtt.packets SNMP on supported MQTT infrastructure set snmp on, then reboot set snmp off, then reboot get snmp MQTT raw packet publication set mqtt.raw on set mqtt.raw off get mqtt.raw MQTT receive capture set mqtt.rx on set mqtt.rx off get mqtt.rx MQTT transmit capture set mqtt.tx on (or advert) set mqtt.tx off get mqtt.tx MQTT master set mqtt.enabled on set mqtt.enabled off get mqtt.enabled, get mqtt.running, get mqtt.status ESP32 persistent WebConfig set webui on set webui off get webui WebConfig browser command terminal set wifi.cli on set wifi.cli off get wifi.cli LoRa RX power saving set radio.rxps on set radio.rxps off get radio.rxps Radio RX boost set radio.rxgain on set radio.rxgain off get radio.rxgain Controllable FEM RX / TX gain set radio.fem.rxgain on / set radio.fem.txgain on set radio.fem.rxgain off / set radio.fem.txgain off Corresponding get radio.fem.rxgain / get radio.fem.txgain GPS, when compiled set gps on set gps off get gps

Use set usb.logging on|off on every role. Adding the optional reboot suffix requests a reboot only when changing the USB interfaces requires it, as on nRF52 Full Companion. log start/stop records to storage independently of live USB logging. Use log erase to delete that capture.

Fresh ESP32 Full infrastructure installs default to USB logging off. Upgrades retain the saved choice. With device power saving enabled, an idle node can sleep after the setup AP closes and all radio services stop; live USB logging, an attached native USB host, and active MQTT/ESP-NOW still hold it awake.

For quiet packet capture in current firmware, use set usb.debug off and set usb.logging on. Verbose diagnostics default off for fresh and upgraded preferences without the new field. The debug choice is saved separately; turning logging off, or selecting logging.output wifi, does not erase it. Debug output is effective only when USB logging is on and the image includes the relevant debug code. RAW/RX/TX records remain enabled without debug.

For ESP32 1.17.1.5 USB logging, run these as separate commands in the role's text terminal (or remote admin CLI on infrastructure):

set powersaving off\nset usb.logging on\nget powersaving\nget usb.logging\n

Check for power saving off and USB logging on. This avoids the released ESP32 USB sleep bug, including the G3 report. Both settings are saved; turning logging off later does not automatically restore power saving. LoRa RXPS is independent. nRF52 logging does not need this ESP32 workaround.

With the G3 sleep correction, enabled live USB logging keeps ESP32 USB serviced and blocks light sleep, including when a host closes the port or disconnects. CPU idle/yield remains available. set usb.logging off removes that blocker; an attached native USB host still prevents sleep. On Full infrastructure, set logging.output usb and both enable the same USB blocker; wifi and off remove it. File capture with log start is independent. The original 1.17.1.5 binaries require the set powersaving off workaround described in the release note.

"},{"location":"role_feature_switches/#shared-mqtt-and-logging-output","title":"Shared MQTT and logging output","text":"

Search terms: MQTT settings, MQTT on, MQTT off, logging output, USB and WiFi logging.

On any Companion or infrastructure build with both MQTT and USB logging compiled:

Command USB logs MQTT bridge set logging.output off Off Off set powersaving off, then set logging.output usb On Off set logging.output wifi Off On set powersaving off, then set logging.output both On On

get logging.output reports the selection. Fresh unified Full infrastructure preferences select both; Full Companion starts with USB logging off. Saved settings override these defaults. To toggle only MQTT while keeping USB logging unchanged, use set mqtt.enabled off / on. Neither setting turns LoRa repeating off. Repeater forwarding uses set repeat off / on and get repeat separately. Repeaters can opt in to routed trace diagnostics with set repeat.trace on while repeat stays off; the saved exception defaults off and does not relay other payload types. See repeat-off traces. The selector changes USB master output, not saved usb.debug intent. Direct WiFi MQTT is independent of that USB verbosity preference.

The set powersaving off step above is the 1.17.1.5 ESP32 USB workaround. WiFi/MQTT-only logging does not need it while the Repeater/Room Server MQTT bridge is running: that sleep guard already exists in the released firmware. Use get mqtt.running to check that MQTT is running; an enabled preference alone is not the running state. set logging.output wifi keeps USB logging off and uses that MQTT guard.

For a custom broker on an MQTT-capable Companion, Repeater, or Room Server:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nset mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.status\n

Set mqtt1.username / mqtt1.password if required by that broker. Configure only as many slots as the board supports. set mqtt1.preset none disables slot 1; other configured slots remain enabled, within the build's supported slot count. Disabled and partially configured slots remain saved across reboot. For a custom server URL, set mqtt1.port 0 clears the port override so the URI supplies it. set mqtt.status off disables status messages, not MQTT itself. See the MQTT reference for presets, TLS, credentials, and slot limits.

"},{"location":"role_feature_switches/#infrastructure-power-saving-and-bridges","title":"Infrastructure power saving and bridges","text":"

Use get powersaving, set powersaving on, and set powersaving off on every role. The preference is saved. Hardware and active USB/network services decide when sleeping is possible; the command names do not change between roles. Device power saving is separate from WiFi modem sleep and LoRa RXPS.

On RS232-capable repeater images, stop the bridge before changing its serial port or baud rate, then restart it:

set bridge.enabled off\nset bridge.baud 115200\nset bridge.enabled on\n

RAK4631: set bridge.uart 2 selects UART2 while the bridge is stopped. Canonical GPS-enabled builds reserve UART1 for GPS; UART1 requires a dedicated GPS-free image. ESP-NOW bridge: use set bridge.channel <1..13> and set bridge.format wrapped / raw where supported. Primary ESP-NOW mesh: use set espnow.channel <1..13> and reboot; this is a different radio setting.

For infrastructure WebConfig on the LAN, use start webconfig / stop webconfig. To force a setup AP on an observer, first run set mqtt.enabled off, then start webconfig ap. When finished, run stop webconfig and restore set mqtt.enabled on if you did not reboot.

"},{"location":"role_feature_switches/#updating-and-sending-mota","title":"Updating and sending MOTA","text":"

Search terms: mOTA, LoRa OTA, update over LoRa, wireless firmware transfer.

All released Full Companions can serve MOTA to other nodes. Close the console and run motatool serve --serial /dev/ttyACM0 --dir ./motas -v on the USB host. See Full Companion instructions for WiFi/BLE source commands and the bounded temporary radio setup.

Role / hardware Start self-update Stop / requirement ESP32 Repeater, Room Server, Sensor with WiFi updater start ota or start ota ap; WiFi starts as needed and WebConfig stops automatically; open the returned URL (normally port 80, /update) stop ota ESP32 Full Companion with two application slots start ota or start ota ap; returned URL uses port 8080, /update stop ota; single-slot Full builds use USB nRF52 infrastructure with Bluetooth DFU start ota enters the Bluetooth update flow Matching application DFU ZIP and board bootloader required Qualified LoRa OTA receiver Follow LoRa OTA directions Exact destination package, storage profile, and overlapping temporary radio windows

For nRF52 OTAFIX installations use the exact board/storage build from OTAFIX 2.4.6. Its retained-RAM handoff is required by the new internal-flash hybrid receiver images. Full Companions are sources and remain normally USB-updated.

The complete infrastructure CLI, role/build matrix, and Full Companion feature guide provide details.

"},{"location":"room_services/","title":"Room services","text":"

View this page on MeshCore Docs.

"},{"location":"room_services/#room-history-personal-mailboxes-and-information-board","title":"Room history, personal mailboxes, and information board","text":"

Room Server firmware keeps one room per radio. Existing app login, chat, catch-up, and Access Control formats remain supported. The information board and local Wi-Fi chat use the browser page below; the stock mobile app does not have an information-board screen.

"},{"location":"room_services/#personal-mailboxes","title":"Personal mailboxes","text":"

The single room also provides personal mailboxes. Each mailbox belongs to a complete public-key identity, independently of the public chat backlog. Regular room readers and writers can choose their own access policy and delivery mode; administrator permission is not required.

Access Who may leave mail Closed (default) The owner only Public Any authenticated room writer or administrator Private Up to eight selected full public-key identities, plus the owner

Read-only users can manage and read their own mailbox but cannot send mail. Room bans still apply to both senders and recipients. A private allow list does not grant room login or writing permission; approved senders must also have room writer access. Reading, acknowledging and deleting mail always use the authenticated owner's identity, never a name or shortened key supplied in a request. Changing access does not delete already accepted mail.

Mailboxes start closed and are created only when the owner saves a policy. There are at most 32 mailbox owners, 16 queued messages across the radio, four queued messages per owner, and eight selected senders per mailbox. Messages contain 1-512 UTF-8 bytes. A full queue rejects new mail without replacing old mail. Available filesystem space can impose a smaller limit.

The server stores mail separately from public chat. Mail is encrypted on each LoRa connection, but is plaintext in the server's filesystem: the room operator is trusted. This is not end-to-end encryption between sender and recipient. Other ordinary users cannot list or read your inbox. Mail bodies are not included in public chat, notices, or the Wi-Fi administrator dashboard. Wi-Fi room access uses local HTTP, so use a trusted Wi-Fi network.

"},{"location":"room_services/#mail-commands-for-regular-users","title":"Mail commands for regular users","text":"

Use an encrypted CLI request to the room, or send the same command as a direct message prefixed with !mail from a normal Companion app. These direct messages are reserved mailbox commands and never become public room posts:

!mail mode private\n!mail allow <complete-64-hex-sender-public-key>\n!mail deny <complete-64-hex-sender-public-key>\n!mail mode public\n!mail mode closed\n!mail delivery mailbox\n!mail delivery chat\n!mail settings\n!mail settings 1\n!mail send <complete-64-hex-recipient-public-key> dog lost\n!mail check\n!mail inbox\n!mail list 1\n!mail read <message-id>\n!mail read <message-id> <next-offset>\n!mail ack <message-id>\n!mail delete <message-id>\n

In a CLI-capable client, use mail ... without !. These commands affect only the requesting identity. CLI list/settings replies use entry cursors; continue at the returned next= while it is below total= or allowed=. List entries show the immutable message ID, byte length and sender key; a short route may show a sender prefix. Reads return readable text with id, off, next and total; continue at next until it equals total. Text chunks end on a UTF-8 character boundary. The route and command framing limit LoRa submissions to less than the 512-byte storage limit; the browser can submit a full 512 bytes.

Reading never removes a message. ack confirms receipt and durably removes it; delete deliberately discards it. Both retain a bounded retry receipt. Exact sender/request/body retries return the original ID, including after receipt or reboot; conflicting reuse of that request ID is rejected. Each owner retains up to eight submission receipts. Queued receipts stay retained; the oldest retired receipt is evicted when needed. A very old retry after its retired receipt has been evicted can enqueue again.

mail delivery mailbox suppresses automatic public chat and topic delivery to that identity, including retries and public backlog counts. It does not prevent explicit public-board reads, change routes or erase chat. Returning to chat resumes from the existing catch-up cursor. A copy already transmitted or queued can still arrive while switching modes. Unreadable saved mailbox policy blocks automatic chat rather than silently enabling it.

"},{"location":"room_services/#sleeping-tracker-example","title":"Sleeping tracker example","text":"

A dog tracker can keep its mailbox private, approve its owner's sender identity, and select mailbox-only delivery. The owner leaves dog lost while the tracker is asleep. At its next scheduled check-in, the tracker authenticates to the room, checks its inbox, retrieves that message in resumable chunks and confirms receipt only after handling it. Lost replies and another sleep cycle do not remove unread mail. Chat history and topics are not downloaded automatically.

Room Server provides the room-side queue and polling protocol. A tracker still needs client firmware that schedules check-ins and interprets authenticated commands. Ordinary mailbox text is never executed as a CLI command, and does not itself enable a tracker, wake it remotely or change its GPS reporting rate. For a reachable owner's Companion, the optional lost-status auto responder can also answer a private Am I lost? question with a saved Yes or No.

The opt-in Sensor dog tracker currently checks a reachable owner's Companion through tagged encrypted requests. It does not yet log in to a Room Server or automatically poll these mailboxes.

"},{"location":"room_services/#wi-fi-mailbox-management","title":"Wi-Fi mailbox management","text":"

Open /room, join with your room credentials, and open Personal mailbox. The page shows your complete browser mailbox address, public/private/closed access, selected senders and mailbox-only delivery. It provides sending, paged inbox metadata, selective body downloads, resumable reads, explicit receipt confirmation and deletion. Unsaved drafts survive failed requests. The page honors saved mailbox-only mode before its initial chat download; Refresh chat remains an explicit choice.

The browser has its own identity derived from its secret token and the server identity. It cannot claim your radio's mailbox using a shared room password. Mail sent to a radio key is retrieved by that radio; mail sent to the browser address is retrieved using that browser token. Keep that token to retain access to the same mailbox. Disconnecting clears downloaded private content from the page's memory; the queued server mail remains.

Administrators have a Mailboxes panel with paged owner metadata, queued counts, access policy, selected senders, blocking, and confirmed deletion or purge. Enter a full owner address to provision a radio's mailbox before it checks in. Policy changes compare the loaded revision, including creation revision zero; concurrent changes require a refresh. The dashboard does not download other users' message bodies. Inbox and administrator index pages use snapshot revisions to reject mixed pages after a concurrent change.

"},{"location":"room_services/#lora-mailbox-protocol","title":"LoRa mailbox protocol","text":"

Authenticated room users send encrypted PAYLOAD_TYPE_REQ requests. The usual four-byte request tag precedes each payload and is echoed before the response. All multi-byte integers are little-endian. Subtype 0x0b is separate from chat catch-up and board subtype 0x0a.

Operation Payload after tag Settings/check 0b 00 + expected revision u32 + allow-list cursor u8 Inbox index 0b 01 + expected revision u32 + entry cursor u8 Read 0b 02 + immutable message ID u32 + offset u16 Send 0b 03 + full recipient key (32 bytes) + body length u16 + complete body Confirm receipt 0b 04 + message ID u32 Delete 0b 05 + message ID u32 Allow sender 0b 06 + expected revision u32 + full sender key (32 bytes) Deny sender 0b 07 + expected revision u32 + full sender key (32 bytes) Access 0b 08 + expected revision u32 + mode u8 (closed=0, public=1, private=2) Delivery 0b 09 + expected revision u32 + mailbox-only u8 (0 or 1)

Start index/settings at revision zero and cursor zero, then use the returned revision on subsequent pages. next=255 means complete. Sends are single requests, without fragmented uploads; the complete body must fit the encrypted request route. Read responses contain raw bytes in chunks of at most 128, reduced as needed by the reply route. Continue by offset plus copied byte count. Reads never acknowledge receipt implicitly. Logical requests allow only zero trailing encryption padding.

Responses begin with subtype, operation and status. Statuses are: 0 success, 1 invalid, 2 storage unavailable, 3 write failure, 4 not found, 5 stale revision, 6 forbidden, 7 full, 8 successful duplicate, 9 conflicting request ID. Full response layouts are documented in RoomMailProtocol.h. Mutations require space for their complete confirmation before writing.

"},{"location":"room_services/#optional-persistent-chat-history","title":"Optional persistent chat history","text":"

History uses RAM by default. From a local console or authenticated remote administrator session:

get room.history\nset room.history on\nset room.history off\nroom.history.clear\n

Enabling persistence first saves the current RAM ring. The newest 32 posts then survive reboot. Disabling retains the archive and stops disk writes; the next enable replaces it with the current RAM ring. Clearing removes disk and RAM posts, leaving the topic and information board alone.

Accepted posts append 192-byte records. At 64 records the journal compacts to the newest 32. Its maximum size is 12,300 bytes; compaction temporarily also needs a verified snapshot of about 6.2 KB. This is bounded recent history, and its filesystem records are not encrypted at rest.

A post is saved before the server sends its ACK or advances the live ring, retry cache, or sender replay timestamp. Full storage and unsuccessful writes retain the previous chat and withhold the ACK. A recoverable incomplete or corrupt final append keeps its verified earlier records. Corruption before later complete records preserves the entire journal and blocks replacement. Unreadable, unsupported, or corrupt primary metadata is also preserved until storage is repaired.

A reboot after storage completes but before the sender receives the ACK can recover a valid post without confirming it to the sender. Exactly-once delivery across that crash boundary is not guaranteed.

Catch-up timestamps stay increasing across restored history and backward clock corrections without setting the radio's RTC from archived posts. New posts use a six-second monotonic delivery delay; restored posts can be delivered immediately. Set the clock correctly for useful displayed dates.

The Companion also withholds a room-message ACK when its receive queue cannot retain the message. Rejected delivery does not advance catch-up, schedule receipt persistence, or emit a new-message notification.

"},{"location":"room_services/#saved-acl-roles-and-moderation","title":"Saved ACL roles and moderation","text":"

An operator's nonzero setperm assignment survives reboot and is protected from automatic room-client eviction. Assigned read-only and read/write users appear in Access Control alongside administrators. Automatic guest-password read/write sessions remain transient; password-created administrators remain retained. The existing permission byte is unchanged.

Guest-password logins do not allocate new durable login replay records, so guest churn cannot fill the bounded replay store. Existing records remain authoritative and are advanced, including guest-password records created by older firmware. This change does not reclaim space in an already full 128-identity replay store. After reboot or transient client eviction, an identity without such a record can have captured guest login and message traffic accepted again. Assign an explicit read/write role with setperm <key> 2 when durable login replay protection is required. Room-post deduplication and per-sender post replay timestamps remain in RAM: captured old posts can also be accepted after a legitimate newer login restores a lost session.

ACL updates use the existing delayed settings save. Allow at least five seconds after setperm before removing power so the assignment reaches storage.

setperm <complete-64-hex-public-key> 1\nsetperm <complete-64-hex-public-key> 2\nsetperm <complete-64-hex-public-key> 3\nsetperm <complete-64-hex-public-key> 0\nroom.ban <complete-64-hex-public-key>\nroom.unban <complete-64-hex-public-key>\nget room.bans\nget room.bans 2\nset room.post.rate 12\nset room.poll.rate 60\nget room.post.rate\nget room.poll.rate\n

Roles are 1 read-only, 2 read/write, and 3 administrator; 0 removes the assignment. Guest-password login cannot demote an explicit administrator or upgrade an explicit read-only user. Empty passwords are not enabled credentials; public passwordless read-only login still requires its existing setting.

The persistent ban list accepts up to 32 complete identities, never names or prefixes. It excludes new logins and existing sessions, including posts, requests, path updates, ACKs, and pushes. It leaves existing posts and ACL assignments intact. Use another authorized administrator or a local console to undo a ban on your own identity.

Rates are per identity per shared monotonic minute. 0 disables a limit (the default); the maximum is 65,535. The post limit covers new chat posts. The poll limit covers room requests, including keep-alive, status, telemetry, ACL and board reads, and authenticated browser operations. Login does not reset counts. Exact accepted retries are not charged again. Old keep-alive retries cannot rewind catch-up or cancel a newer pending push. Rejected posts do not enter the retry cache.

Changing to a different identity creates a different user. These controls complement forwarding and airtime policies.

"},{"location":"room_services/#your-own-return-routes","title":"Your own return routes","text":"

Room readers, writers and administrators can use a private CLI request to inspect or change the room's route back to their own radio:

get outpath\nget outpath path\nset outpath A1B2C3,D4E5F6\nset outpath path\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath direct\nset altpath clear\n

Each hop is 2, 4 or 6 hexadecimal characters; all hops in a path use the same width. direct means zero repeaters. clear enables normal discovery again. outpath flood forces flood replies until you change that selection. The first command word is case-insensitive, and an optional Companion CLI prefix is preserved in the reply.

These commands always affect the authenticated requesting identity. They cannot select another user's public key, change room settings, grant roles, read passwords or run other administrative commands. Active public read-only sessions can manage their own routes too. Bans still block access, and management roles alone do not grant room-user command access.

Commands require encrypted CLI message framing. Typing them as ordinary room chat posts sends chat text instead. Use a CLI-capable client; this feature does not grant administrator status or add a non-admin command screen to the mobile app. No new protocol message type is required.

get outpath path reports the reciprocal route observed after the most recent flood login. It can return > path pending during its one-minute observation window. The read does not change the selected route; ordinary room discovery still learns a live route automatically. A route explicitly selected while waiting is protected from the delayed login PATH. set outpath path selects the observed route, or leaves the selection unchanged if none is available.

When the primary and alternate routes are valid and different, chat, topic updates and CLI/data replies send one encrypted copy along each route. The primary owns ACK tracking, catch-up and retries; the alternate has no separate retry loop. Identical routes send one copy. A missing packet for the alternate does not prevent primary delivery. A flood primary does not send an alternate copy; altpath flood also disables that secondary copy, so prefer set altpath clear when removing it. Keep-alive and post ACKs retain their existing primary-route behavior.

Routes are saved for already-retained ACL entries, including explicit readers and writers and password-created administrators. Ordinary guest-password and public-reader sessions keep route selections only for the current session; changing a route never makes their permissions permanent. Failed saves restore the previous route and report an error. Repeating an unchanged selection or an exact request retry does not cause another settings write.

"},{"location":"room_services/#choose-where-chat-catch-up-starts","title":"Choose where chat catch-up starts","text":"

Room readers, writers and administrators can skip older unread messages for their own radio using the same encrypted CLI request framing as route commands:

get room.catchup\nroom.catchup keep 20\nroom.catchup keep 0\nroom.catchup before 2026-10-10\nroom.catchup before 2026-10-10T18:30:00Z\nroom.catchup before 1791655200\n

keep 20 leaves the newest 20 unread retained messages to deliver and skips the older backlog. keep 0 skips all currently retained unread chat. Own posts do not count toward unread messages. A date skips unread messages strictly older than that cutoff; date-only values mean midnight UTC. A numeric cutoff is a Unix timestamp in seconds. A count larger than the backlog is a no-op.

Skipping advances only the requesting user's delivery cursor. It never deletes room history, rewinds a cursor or suppresses another user's delivery. A future date advances only through actual retained messages; new messages remain deliverable. Pending delivery and retries for a skipped post are cancelled; an outstanding topic remains pending. Delayed ACKs, routine keep-alives and normal reconnects cannot restore the skipped backlog. A copy already queued or transmitted can still arrive; skipping stops subsequent catch-up delivery.

Saved ACL users persist an advanced catch-up cursor through the existing filtered contacts store. Failed saves restore delivery state and report an error. Transient sessions retain changes only for the current session. History retention remains 32 posts; this command cannot recover older discarded chat.

"},{"location":"room_services/#administrator-controls-for-any-radio-user","title":"Administrator controls for any radio user","text":"

From a local console or authenticated administrator CLI session, use a user's complete 64-character public key to edit either route or their catch-up position:

room.user <complete-64-hex-public-key> outpath A1B2C3,D4E5F6\nroom.user <complete-64-hex-public-key> altpath 71CE82,BA09F0\nroom.user <complete-64-hex-public-key> altpath clear\nroom.user <complete-64-hex-public-key> keep 20\nroom.user <complete-64-hex-public-key> before 2026-10-10T18:30:00Z\n

Names and shortened keys cannot select a user. Administrators can change any existing radio user's routes without changing that user's role. Route and catch-up persistence follows the same rules as the user's own commands. The browser dashboard below provides activity, delivery and route details for all radio users; these views require administrator credentials.

"},{"location":"room_services/#local-wi-fi-chat-on-esp32","title":"Local Wi-Fi chat on ESP32","text":"

ESP32 Room Server images with WebConfig have an offline browser room page. It is disabled by default and uses the existing WebConfig listener:

set room.web on\nstart webconfig\n

Open http://<reported-radio-IP>/room. Without a usable router, use start webconfig ap, connect the phone to its access point, and append /room to the reported address. Existing setup AP time limits and Wi-Fi/OTA ownership rules apply. Starting OTA shuts down this listener. Compact images and boards without WebConfig retain LoRa chat and CLI access.

Enter the guest or administrator room password, or leave it empty for explicitly enabled public read-only access. Readers cannot post or edit notices; only administrators can publish or delete them. Each operation rechecks credentials and bans. Use private Wi-Fi: the local page uses HTTP, and network access alone does not grant administrator permission.

Room-browser password checks share one budget across all browsers and tokens. Five newly counted wrong passwords exhaust a 30-second window measured from its first counted failure. Until that window ends, every request with a nonempty password is refused, including correct guest/admin passwords and polls from an already connected admin tab. Correct passwords do not refund failures, and refused requests do not extend the window. The last four wrong passwords are remembered; repeats among those four do not count again. A wrong nonempty password still counts when read-only access admits it as a guest. An empty password never consumes this budget and remains usable for explicitly enabled public read-only access.

This limit belongs to /api/room. The WebConfig admin login at /api/login has its own address limits and shared budget for the same admin password; neither endpoint clears or bypasses the other's limit. The room page does not require a WebConfig session, and this browser limit does not change radio logins. If password requests are blocked, wait for the window to end or use the local console to manage the node.

The browser generates a secret token and its own moderation identity. It does not import a radio private key or inherit a claimed radio key's ACL role. The Access and history panel shows the complete browser identity for banning. Posts share the LoRa chat history and include the display name; combined name and text must fit 151 UTF-8 bytes.

Passwords stay in page memory. The token, sequence and display name are saved locally; use one active room tab per browser identity. Interrupted submissions retry the same sequence and body, then poll the owned result. Retry floors are bounded to twenty minutes and the current boot. The page stops retrying after twenty seconds and preserves an unconfirmed draft. After a restart or uncertain result, refresh and check before submitting again.

Authentication, storage and radio work run on the mesh loop. HTTP callbacks only admit a bounded request and return its owned result. No arbitrary CLI operation is exposed. Disable room access with set room.web off; stop the portal with stop webconfig.

"},{"location":"room_services/#wi-fi-room-administration","title":"Wi-Fi room administration","text":"

Connect to /room with the room administrator password and open Room administration, or visit http://<reported-radio-IP>/room#admin-panel. This management area offers:

The dashboard rechecks administrator credentials for each operation and rejects a write from an earlier boot. Route, catch-up, access, topic and history edits include the loaded value; a changed value requires a refresh instead of silently overwriting someone else's change. Each setting saves independently. Access changes use the existing delayed ACL save; the page reminds you to leave power on for at least five seconds. Removing a saved assignment does not ban guest-password access; use Block to deny that identity altogether.

Readers and writers cannot retrieve these radio-user activity/delivery views or management operations. Losing administrator access clears cached management data from the page. User names, messages and routes are rendered as text.

Choose chat shown in this browser filters the local display to the newest N retained messages or a chosen start date. It changes only the browser view; it does not move a radio user's cursor or delete posts. The administrator's selected-user catch-up controls do change that radio user's delivery position.

"},{"location":"room_services/#information-board","title":"Information board","text":"

Eight persistent article slots have titles up to 63 UTF-8 bytes and bodies up to 2,048 bytes. Index revisions and article versions survive reboot. Bodies use two bounded file banks per slot, streamed verification, and atomic index publication.

Available filesystem space determines how much fits. Small internal nRF52 filesystems cannot hold every article at maximum size alongside persistent history and settings. Leave space for the history snapshot and article/index replacement; a full filesystem rejects the change and retains existing data.

In /room, refresh the notice index and select an article to download. Unselected bodies are not pushed or downloaded. Chunks are at most 128 bytes; completed chunks remain available within the page after a lost connection. A new article version starts a new transfer. The administrator editor checks the version of the loaded article before saving to prevent lost edits.

Small notices also support administrator/local CLI management:

room.board.put 1 Trail conditions|Bridge closed until Saturday.\nget room.board\nget room.board 1\nget room.board.read 1 <version> 0\nroom.board.del 1 <version>\n

Index arguments are zero-based entry cursors; follow the returned next=. Long titles can reduce a reply to one entry. Put uses TITLE|BODY and still obeys the command transport's total length limit; use the browser for a full 2 KB article. Read replies contain bounded Base64 and ID, version, offset, total length and byte count. Continue at offset plus count. Deletion requires a current nonzero version; reusing a deleted slot assigns a newer version.

"},{"location":"room_services/#lora-board-read-protocol","title":"LoRa board read protocol","text":"

Logged-in readers, including read-only guests, can use encrypted PAYLOAD_TYPE_REQ requests to the existing single room. The usual four-byte tag precedes these payloads and is echoed before responses. Multi-byte integers are little-endian.

Operation Payload after tag Index 0a 00 + expected revision u32 + entry cursor u8 Article 0a 01 + article ID u8 + expected version u32 + offset u16

First index page: revision 0, cursor 0. Subsequent pages require the returned revision. Response: subtype, operation, status, revision u32, total count, start cursor, next cursor, then up to two complete entries. Each entry is ID u8, version u32, body length u16, title length u8 and title bytes. Continue while the next cursor is less than the total count; the index is complete when the next cursor equals the total count. An empty board returns total count 0 and next cursor 0.

Article response: subtype, operation, status, ID u8, version u32, offset u16, total body length u16, chunk length u8, then raw bytes. The route's encrypted reply capacity may reduce the chunk. Continue at offset plus length; offset equal to total returns successful EOF. Never combine different versions.

Statuses: 0 success, 1 invalid, 2 storage unavailable, 3 write failure, 4 not found, 5 stale version. A capacity too small for a full header may return only subtype, operation, and invalid status. On stale revision/version, reload the index and restart the affected transfer.

"},{"location":"room_services/#regression-coverage","title":"Regression coverage","text":"

Production handlers and stores are tested across ESP32, nRF52, STM32, Pico and generic filesystem APIs, including full storage, corruption, interrupted publication, queue rejection, ACL reboot/eviction, quotas, clock correction and allocation failure. ARM checks verify the new counters fit the existing client union.

The pinned official app JavaScript consumes real firmware room frames and login responses. Chromium executes the bundled browser page with real backend response fixtures, testing untrusted text, lost replies, draft preservation, selective reads, resume and UTF-8 limits. These browser fixtures do not claim a physical LoRa/Wi-Fi transfer.

"},{"location":"rs232_bridge_setup/","title":"Rs232 bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"rs232_bridge_setup/#rs-232uart-bridge-setup-and-diagnostics","title":"RS-232/UART bridge: setup and diagnostics","text":"

Two supported repeaters can exchange MeshCore packets over a wired UART link. Each repeater can use the LoRa settings of the network on its side:

LoRa network A <--> Repeater A <-- serial link --> Repeater B <--> LoRa network B\n

Use the exact board's repeater image with RS-232 support. Check get bridge.type: its answer must include rs232. This transport is optional; its presence in an image does not mean its UART is enabled. A Companion image or a room-server build without RS-232 does not gain the transport from these commands.

"},{"location":"rs232_bridge_setup/#configure-the-serial-link","title":"Configure the serial link","text":"

Use the UART and pin map for the exact board and image. Connect the two sides with the appropriate serial wiring and any required line drivers; a board's UART pins do not automatically provide RS-232 voltage levels. Avoid a UART already occupied by another peripheral.

Open each board's USB console. Run the following on both boards, using the same baud rate:

set rs232.enabled off\nset bridge.baud 115200\nset bridge.source tx\nset bridge.delay 500\nset repeat on\nset rs232.enabled on\nget rs232.enabled\nget rs232.running\nget rs232.status\nget rs232.transport\n

Use bridge.uart only where the board offers a supported choice, while RS-232 is stopped. Canonical GPS-enabled RAK4631 builds use UART2 (set bridge.uart 2); UART1 requires a compatible GPS-free image. The command rejects UART choices unavailable in the build or unsafe for an active peripheral.

rs232.enabled selects the UART independently of MQTT and ESP-NOW in combined images. The historical bridge.enabled alias can select ESP-NOW in MQTT images, so use the transport-specific rs232.enabled control for these checks.

bridge.source tx bridges packets transmitted on LoRa after the repeater's forwarding decisions. An existing bridge.source rx setup is also valid: it bridges received packets before those decisions. Normal scope, filters, duplicate suppression and repeat settings still affect mesh delivery. Configure each side's LoRa network separately; see the bridge CLI reference.

"},{"location":"rs232_bridge_setup/#distinguish-power-state-from-transport-activity","title":"Distinguish power state from transport activity","text":"

get rs232.status retains the existing heartbeat and automatic-standby report. It separates saved intent, actual UART state, standby/probe phase and confirmed versus legacy/unknown peer liveness. get rs232.transport adds the serial transport diagnostic view. A running UART means local startup succeeded; it does not prove the other board received a frame. With RS-232 explicitly disabled, its transport/counter commands may report diagnostics unavailable because there is no current UART bridge object.

Heartbeat and negotiated automatic standby are on by default. A fresh, session-matched heartbeat reply proves bidirectional communication with a supporting peer. Ordinary incoming mesh data proves only inbound reachability. See heartbeat and automatic standby before changing power settings. A diagnostic probe does not replace heartbeat negotiation, enable a disabled UART or establish peer authentication.

"},{"location":"rs232_bridge_setup/#test-each-serial-direction-without-lora-forwarding","title":"Test each serial direction without LoRa forwarding","text":"

Updated RS-232 bridge images include runtime diagnostics without a BRIDGE_DEBUG build flag or continuous USB logging. On both boards:

rs232 resetstats\nget rs232.transport\nget rs232.stats test\n

On A, send one probe:

rs232 test A1B2C3D4\n

OK - test queued id=A1B2C3D4; confirm on peer confirms local queueing only. On B, read get rs232.stats test: rx must increase and rxid must match A1B2C3D4. Then send rs232 test B1C2D3E4 on B and confirm that ID on A. These are two one-way tests. Local UART write completion alone is not peer receipt.

The optional ID is exactly eight hexadecimal digits, without 0x; omitting it uses uptime in milliseconds. Probes are transport diagnostics, not security tokens or MeshCore packets. They do not prove normal mesh delivery or authenticate a peer. There is no automatic echo, retry or LoRa forwarding. Older firmware ignores the extension and cannot report a received ID; use an updated peer on both sides. Sending a test and resetting counters require a local console and do not save preferences.

A write deadline records one failure. An entirely unsent probe is cancelled; if the UART already accepted a prefix, the bridge retains and drains the original suffix before starting another frame. Each main-loop pass does bounded work, without retransmitting the prefix. This avoids leaving an older receiver partway through a control frame. A late completion can increment sent while the earlier fail remains; confirm receipt using the other board's rx change and matching rxid.

"},{"location":"rs232_bridge_setup/#locate-a-mesh-forwarding-failure","title":"Locate a mesh forwarding failure","text":"

If both probe directions work, send one fresh identifiable message from a Companion on network A and compare these pages on both repeaters:

get rs232.stats\nget rs232.stats rx\nget rs232.stats tx\nget rs232.stats drops\n

Repeat in the other direction with a different message. Compare counter changes for the attempt rather than treating old totals as its result. Admission to the mesh means queued for normal processing; it does not prove a completed LoRa transmission or end-to-end delivery. If both repeaters already heard a message directly on LoRa, duplicate suppression can reject a later serial copy.

The RX page separates framing, checksum, packet parsing, partial-frame timeout and mesh-pool pressure. The TX and drops pages distinguish UART backpressure from stopped/busy state, filters and duplicate suppression. Byte counters include heartbeat and diagnostic traffic as well as mesh frames; they do not measure mesh payload alone.

Counters belong to the current bridge object. They survive automatic standby, but disabling or reconfiguring RS-232 can replace the object and clear them. Reboot and rs232 resetstats also start a new observation window.

The RS-232 diagnostic CLI reference describes the status fields, counter pages and their observation window.

"},{"location":"rs232_bridge_setup/#optional-short-usb-debug-capture","title":"Optional short USB debug capture","text":"

Record both get usb.logging and get usb.debug, then enable both gates with set usb.logging on and set usb.debug on. Send the probes or one fresh mesh message and restore both original settings afterward. The ordinary image emits bounded RS232: updates from its main loop at most once per second when diagnostic state changes. A busy USB console can skip an update; use the counter pages for exact values. Neither logging gate is required for the CLI counters or test probes.

These diagnostics identify local serial activity and rejection stages. They do not certify a physical cable, line driver, or end-to-end LoRa path without the corresponding peer and message checks.

"},{"location":"s3_memory_soak_validation/","title":"S3 memory soak validation","text":"

View this page on MeshCore Docs.

"},{"location":"s3_memory_soak_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sensor_tracker/","title":"Sensor tracker","text":"

View this page on MeshCore Docs.

"},{"location":"sensor_tracker/#sensor-dog-tracker","title":"Sensor dog tracker","text":"

Dog tracker mode is an optional mode in the Sensor firmware. It acquires a new GPS position, sends an encrypted check to one configured owner's Companion, and waits for that owner's saved lost status. The owner's answer controls the tracker's reporting rate and awake window.

Tracker mode starts off. It is compiled by default for GPS Sensor builds on nRF52, ESP32, and RP2040; compact STM32 builds exclude it by default. A build can override MESH_ENABLE_SENSOR_TRACKER. GPS hardware must be detected and available for position acquisition.

"},{"location":"sensor_tracker/#set-up-the-owner","title":"Set up the owner","text":"

Use a Companion build with lost replies enabled. Add the tracker as a saved contact on the owner's Companion, using its full public key. The responder accepts saved Chat or Sensor contacts for the encrypted tracker check.

Set the answer locally on the owner's Companion:

set lost.reply no\nget lost.reply\n

Use set lost.reply yes when the dog is lost. Use set lost.reply off to return an unknown status. These are owner settings; an incoming tracker check cannot change them. A normal private Am I lost? message also receives the saved answer where the text responder is available, but the automatic Sensor tracker uses an encrypted request with a matching response tag.

The owner must be reachable during the tracker's receive window. A Room or public message board does not substitute for the owner's Companion in this mode.

"},{"location":"sensor_tracker/#set-up-the-tracker","title":"Set up the tracker","text":"

Configure the Sensor through its local text console. Replace the key below with the owner's full 64-character hexadecimal public key:

set tracker.owner <owner-public-key>\nset tracker.interval 1800\nset tracker.lost.interval 60\nset tracker.path direct\nset tracker.mode dog\nget tracker status\n

Check clock on both devices before enabling the mode. If needed, use time <current-UTC-epoch-seconds> to set the tracker clock. The owner's Companion accepts map coordinates only when the request time is within five minutes of its own clock. Tracker acquisition waits for the provider's validated multi-sample GPS clock sync as well as its position, allowing cold-start clock recovery within the acquisition window. If clock sync cannot finish, an operator can still set the clock locally. Saved routing limits do not reset to grant floods after clock loss.

direct selects a direct radio path with no repeaters. If a repeater path is known, use comma-separated repeater prefixes, for example set tracker.path aa,bb or set tracker.path aabb,ccdd. All prefixes must have the same length: one, two, or three bytes. set tracker.path clear keeps the path unknown; the flood fallback delay still applies. Encrypted path replies can update the retained path.

Command Meaning get tracker status Show mode, saved owner status, intervals, flood lock, clock and storage state. get tracker.owner Show the configured owner's full public key. get tracker.path Show whether the route is direct, saved, or unknown, and its hop count. set tracker.owner <full-key> Save the destination identity. set tracker.mode dog Enable automatic checks and tracker power control. set tracker.mode off Stop automatic checks and restore normal Sensor operation. set tracker.interval <seconds> Normal interval, 60 to 86400 seconds; default 1800. set tracker.lost.interval <seconds> Lost interval, 60 to 86400 seconds; default 60. set tracker.path <hex\\|direct\\|clear> Save a known path or mark it unknown. tracker check Schedule a check now, if no check is already pending.

Tracker configuration and routing checkpoints use a separate durable store. Mode changes do not replace the saved manual GPS or power-saving preferences. If storage cannot safely retain configuration or a flood reservation, automatic tracker checks fail closed. Volatile filesystem recovery is unsuitable for this mode because a restart could otherwise reset the flood limits.

"},{"location":"sensor_tracker/#reporting-and-sleep","title":"Reporting and sleep","text":"

Each check asks the GPS manager for a new position and validated GPS clock sync. It discards the previous parser fix and previous time-validation samples, then stops once both are ready or after 120 seconds. An old cached position is not reported as a new tracker fix. If acquisition gets a position but clock sync times out, the fresh position is retained. If no position is acquired, the encrypted check marks the failure and carries no position. A retained parser fix or one unvalidated time sample cannot set the tracker clock.

The tracker then allows 20 seconds for the tagged owner response. No uses the normal reporting interval. Yes uses the shorter lost interval and requests a 120-second awake window, renewed by further Yes responses. This permits the tracker to remain reachable while the owner continues to report it as lost. An unknown answer does not replace the last saved Yes or No. Intervals run from the completed check; GPS acquisition and the response window add time.

Between active windows, tracker mode pauses radio reception when there is no queued radio work, temporary radio setting, update operation, or active console that needs service. The radio resumes before the next check. Unsolicited messages sent while reception is paused cannot be received; the owner should set the status and wait for the next check. A delivery ACK alone does not contain the owner's lost answer.

Tracker mode temporarily suppresses ordinary periodic advertisements, subscriptions, and Sensor alerts. Manual requests can still be served while the radio is awake. Leaving tracker mode restores the normal Sensor behavior.

This uses idle or light sleep while retaining runtime state. It does not use the nRF52 battery-protection SYSTEMOFF path described in nRF52 power management. Receiver power control depends on the hardware: T1000-E retains the GPS backup supply for faster fixes, and shared GPS power rails may prevent physically cutting receiver power.

nRF52 tracker sleep arms a real one-shot software timer before each System ON wait. Failure to allocate or queue that timer keeps the loop polling. Sleep chunks are at most 30 seconds so clock and watchdog housekeeping continue. ESP32 light sleep uses the board's existing timer wake. RP2040 uses the core's interrupt-driven idle wait; its physical tracker wake behavior is not yet qualified by these host tests.

"},{"location":"sensor_tracker/#flood-fallback","title":"Flood fallback","text":"

Automatic tracker checks retain a known direct path and try it while the owner cannot be reached. A failed check starts an outage; further failures do not restart that outage's six-hour clock.

The cutoff and flood checkpoint survive sleep and restart. Invalid or backward clock changes prevent fresh flood eligibility. A forward GPS or manual clock correction starts another conservative six-hour quiet wait; it does not reset the outage or the flood checkpoint. Repeater observations and unrelated public traffic cannot unlock a stopped tracker. Ordinary Room responses, login traffic, and manual messaging keep their existing routing.

"},{"location":"sensor_tracker/#encrypted-check-format","title":"Encrypted check format","text":"

This uses the existing encrypted peer PAYLOAD_TYPE_REQ and PAYLOAD_TYPE_RESPONSE datagrams. Request subtype 0x0C is inside the encrypted body; it is separate from the packet header's OTA payload type. All multi-byte fields are little-endian.

Request field Bytes Meaning Tag 4 RTC-based request identity, durably increasing across reboot. Subtype 1 0x0C. Version 1 1. Flags 1 1 for fresh GPS, 2 for failed acquisition, or 0 for no position; other flags are rejected. Latitude 4 Signed degrees times 10,000,000. Longitude 4 Signed degrees times 10,000,000. Battery 2 Millivolts; carried for reporting, not a lost-status decision.

The request has 17 logical plaintext bytes and zero padding to 32 bytes. Latitude and longitude must be zero when no fresh position is present.

Response field Bytes Meaning Tag 4 Exact request tag. Subtype 1 0x0C. Version 1 1. Status 1 0 Unknown, 1 Safe, 2 Lost. Awake lease 2 Seconds; the Companion requests 120 for Lost and zero otherwise.

The response has nine logical plaintext bytes and zero padding to 16 bytes. The client authenticates the configured owner's complete public key, requires the pending tag within its receive window, and caps an awake lease at 300 seconds. Short, oversized, nonzero-padded and unknown-version bodies are rejected. An encrypted returned-path packet may bundle the same response and provide a route for subsequent direct checks.

"},{"location":"sensor_tracker/#remaining-work","title":"Remaining work","text":"

This mode implements GPS checks and an owner's saved lost answer. It does not provide Bluetooth pings every 30 seconds, an automatic Room mailbox poller, public-board subscriptions, or a mailbox command interpreter. Room private mailboxes and the public board remain separate features. Mail text is not executed as a tracker or console command.

Battery life and reliable wake behavior still need measurements on the actual hardware, including GPS reception, repeater availability, and the chosen reporting intervals.

"},{"location":"separated_radio_modulation_cache_validation/","title":"Separated radio modulation cache validation","text":"

View this page on MeshCore Docs.

"},{"location":"separated_radio_modulation_cache_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_10_vs_100hz_full_sample_validation/","title":"Sf10 10 vs 100hz full sample validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_10_vs_100hz_full_sample_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_10hz_cr_detour_validation/","title":"Sf10 10hz cr detour validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_10hz_cr_detour_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_full_retune_twice_validation/","title":"Sf10 full retune twice validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_full_retune_twice_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_nrf52_double_write_settling_validation/","title":"Sf10 nrf52 double write settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_nrf52_double_write_settling_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_rollback_failure_validation/","title":"Sf10 rollback failure validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_rollback_failure_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf125_settling_limits_validation/","title":"Sf125 settling limits validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf125_settling_limits_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf5_250_dwell_validation/","title":"Sf5 250 dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf5_250_dwell_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_125_settling_validation/","title":"Sf6 125 settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_125_settling_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_5p1_trace_validation/","title":"Sf6 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_5p1_trace_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_channel_scan_validation/","title":"Sf6 channel scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_channel_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf8_5p1_trace_validation/","title":"Sf8 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf8_5p1_trace_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"solarxiao_power/","title":"Solarxiao power","text":"

View this page on MeshCore Docs.

"},{"location":"solarxiao_power/#solarxiao-carrier-profiles-mppt-and-battery-controls","title":"SolarXiao carrier profiles, MPPT and battery controls","text":"

SolarXiao Repeater and QSPI OTA Room Server firmware support the CN3791 Base, older BQ24650 Advanced and LTC4162/INA260 MPPT carriers in Full and Reduced builds. Use USB CLI or the authenticated administrator CLI over LoRa. Ship mode and saved-policy reset require local USB. Commands do not change RF settings.

"},{"location":"solarxiao_power/#automatic-detection-and-manual-overrides","title":"Automatic detection and manual overrides","text":"

There are three power profiles: auto, basic and mppt. Base and Advanced share basic because their onboard power telemetry and software controls are the same; their different analog charger designs require no different driver. The default is auto when no carrier selection has been saved. Saved MPPT selections remain manual overrides, and saved Base/Advanced selections load as basic. Use set solar.board auto to enable detection on such an installation. The firmware environment, hardware/OTA identity and flash layout are unchanged.

get solar.board\nset solar.board auto\nget solar\nget solar capabilities\nget solar readings\nget solar history\n

Auto mode checks the INA260 manufacturer's ID (0xfe = 0x5449) and device ID (0xff & 0xfff0 = 0x2270) at carrier address 0x40. An address ACK alone is not enough: another device such as INA219 can share that address. Detection only reads identity registers; it does not configure/reset either chip. The first probe occurs in the loop after initialization, then retries every five seconds until a valid identity is found. A positive result identifies MPPT for these carrier designs and is retained until reboot or a new board selection. Temporary communication loss does not relabel a previously detected MPPT.

Without a verified INA260, auto reports self-only and keeps retrying. It does not claim to distinguish Base from Advanced: neither has a digital identity, and missing responses can also mean an MPPT power/bus fault. Charger access, saved-override replay and native carrier telemetry stay blocked until a valid INA260 identity is found. XIAO self readings remain available throughout. The MPPT carrier's INA260 was previously read with USB power only; detection does not require the LTC4162 charger to respond at SOLAR/VIN.

Manual settings are available when the physical carrier is known:

Setting Behavior set solar.board auto Verify INA260 before enabling MPPT; otherwise self-only, retrying. set solar.board basic Base/Advanced self battery voltage and MCU temperature; no native I2C access. set solar.board mppt Enable native access without requiring INA260 detection; existing charger write identity checks still apply.

Legacy base and advanced command values are compatibility aliases for basic; they are not separate modes. Readbacks and help use the three canonical names. Existing Base/Advanced records load as basic without being rewritten; new Basic saves use the original Advanced record value, preserving the same self-only behavior across upgrades.

For Base or Advanced, basic reports the common hardware capabilities and live XIAO battery ADC and processor temperature. It does not claim which analog charger is fitted. The existing self telemetry stays on channel 1, with voltage and temperature as different LPP types. It adds no invented charger flags, current, watts, battery NTC temperature or panel measurements. get solar battery aliases the self readings; the MCU temperature is not a battery or charger temperature. If a node already reports battery voltage and MCU temperature on channel 1, this profile adds no further physical measurements. It adds carrier detection, manual overrides and capability reporting; the voltage/temperature history commands also already exist in builds that include telemetry history.

The 2025 Base Standard schematic uses a CN3791 analog charger with no digital interface or INA monitor. Battery voltage reaches the XIAO BAT input. Charge status drives an LED, and the other charger signals are not connected to XIAO measurement/control inputs. Base therefore needs no extra telemetry driver: only self battery voltage and MCU temperature are available, just like Advanced. Base Lite publishes a README but no schematic, so its exact connections are not independently qualified here.

The 2025 Advanced schematic has no INA monitor or digital charger interface. Charger status pins drive LEDs, NTC goes only to the charger, and current/voltage/MPP setpoints use hardware resistors, switches and a solder jumper. An OTA update cannot measure those signals or change those setpoints without additional hardware connections.

Manual Basic mode suppresses native LTC4162/INA260 polling, register reads/writes, replay and native measurements on channels 2..13. get mppt, set mppt ... and mppt ... explain that Basic Base/Advanced layouts have no digital charger interface. Pending auto mode only probes INA260 identity; it does not access the charger. The generic sensor scanner still reserves native addresses 0x40 and 0x68, so it cannot bypass that policy. Optional external sensor channels still start at 14. Historical samples retained from a previous carrier selection describe the old samples, not current sensor availability.

Voltage history reserves native channel IDs 2..5 at boot, even while Auto is pending or Basic is selected. This is schema allocation, not chip access or a measurement. It lets later detection, charger input power or a manual mode change supply valid samples without restarting or clearing unrelated history. Inactive readings remain missing, and empty channels do not appear in populated history results. Native and optional I2C voltage history use the existing get telemetry.volt.i2c <channel> [1-4] reader: four days of 30-minute RAM samples.

Where telemetry history is included, get solar history points to the existing get telemetry.volt [1-7] and get telemetry.temp [1-7] commands: 30-minute samples, seven days in RAM. History starts filling after boot and resets at reboot. Voltage trends can help assess a remote installation, but are not a measurement of solar power, charge current or charge state. Radio/status and history-transfer commands remain the existing role commands.

The selection is stored separately in an eight-byte, versioned checked record at /solar_board, using atomic publication and full public readback. A corrupt, unreadable or unconfirmed selection blocks native access and replay. Self ADC and MCU readings remain available. An explicit successful set solar.board recovers selection. With an unavailable filesystem, the selection cannot be saved and native access remains blocked.

When auto verifies MPPT or a manual MPPT selection re-enables native access, the firmware reloads any retained /solar_power charger overrides. Changing the carrier does not erase those overrides, change RF/PA settings, modify battery protection, or write charger defaults. The INA260 identity verifies the monitor; LTC4162 chemistry and cell-count checks still independently guard charger writes.

Radio target and role selection remain separate from the power profile:

Fitted module Repeater Room Server E22 M30S solarxiao_30S_repeater solarxiao_30S_room_server_lora_ota E22 M33S solarxiao_33S_repeater solarxiao_33S_room_server_lora_ota

All three power profiles work in these role builds. Room uses its existing onboard-QSPI OTA target, identity and application layout, with the matching board/storage-aware OTAFIX bootloader. The older plain *_room_server targets remain separate compatibility/recovery recipes; this native interface is qualified on the QSPI OTA Room targets. No legacy identity is remapped.

The published Base Standard schematic specifies M30S; the pictured 2W Advanced uses M33S. Charger detection cannot identify the PA variant or select its TX power limits.

A remotely deployed node must first receive compatible firmware before these commands can be used. Deployment still requires its installed target and bootloader compatibility to be checked when it is reachable.

See the Room Server and late-detection history validation and three-profile validation. This adds no measurements beyond existing self voltage/MCU temperature and has not been installed on the remote Advanced board.

"},{"location":"solarxiao_power/#mppt-carrier-commands","title":"MPPT carrier commands","text":"

Start with:

get mppt\nget mppt battery\nget mppt power\nget mppt alerts\nget mppt saved\nget mppt fields 0\n

fields returns three names per page with rw, ro or locked. Increment the page to enumerate the register interface. get mppt <field> returns units and the raw register. get mppt raw 0x43 reads a documented register; arbitrary register writes are not exposed. nan in a summary means unavailable, not zero.

"},{"location":"solarxiao_power/#identity-and-readings","title":"Identity and readings","text":"

The carrier bus uses SDA P0.09 and SCL P0.10. INA260 at 0x40 must match its TI manufacturer and device IDs. Its signed current, voltage and power readings do not require charger input power. The driver does not reset/configure INA260. LTC4162 at 0x68 uses little-endian SMBus words. Access normally requires power at SOLAR/VIN; USB alone can leave the charger unavailable.

get mppt info reports the chemistry/profile code, live cell count, last observed nonzero cell count, MPPT, JEITA, charger state and telemetry validity. The actual bench board identified as FST (chemistry code 6); this distinguishes it from FAD, FFS, Li-ion and SLA variants. The ID does not uniquely identify the M suffix or package/temperature grade. Cell count comes from hardware straps; zero means unknown while the charger is inactive. Software cannot change chemistry/cells.

Readings cover battery/input/output voltage, signed battery/input current, charger die and modeled NTC temperature, battery series resistance, charge/CV/ absorb timers, JEITA region, actual DAC settings, system/status bits and all three alert banks. A retained ADC bank is not presented as live while telemetry is off. Battery ADC voltage/resistance need a currently valid cell count. Questionable resistance and an open/disconnected NTC return unavailable in the summaries.

"},{"location":"solarxiao_power/#settings-on-the-4s-fst-carrier","title":"Settings on the 4S FST carrier","text":"

Use set mppt <field> <value>. Booleans accept on or off only.

Fields Behavior mppt.enabled Enable automatic solar maximum power tracking. charge.enabled Suspend/resume charging; does not disconnect the battery or system output. telemetry.force, telemetry.fast Force the charger ADC on, or use fast telemetry. These affect power consumption. input.limit_ma Input current limit; rounds down to the hardware step, never above the request; maximum request 3200 mA. input.minimum_mv Manual input-voltage floor; MPPT must be off. Rounds up in 140.625 mV steps, maximum request 35000 mV. jeita.enabled Select the fixed multi-region JEITA profile or basic temperature-qualified manual targets. charge.current_ma, charge.cell_mv Manual targets require JEITA off. Current rounds down in resistor-dependent steps, up to 3200 mA. This 4S board interface limits voltage requests to 3412.5..3600 mV/cell in 12.5 mV steps. thermal.start_c, thermal.end_c Die thermal regulation thresholds, -40..125 C; start must remain below end. termination.enabled, termination.current_ma C/x termination switch and nonnegative current threshold. termination.cv_seconds, termination.total_minutes 16-bit safety timers; zero disables the corresponding timer. alert.* Voltage/current/die/NTC/resistance thresholds and enabled alert masks; use field names/units from fields.

FST locks the six JEITA thresholds, regional current/voltage values, recharge threshold and absorb voltage/time. These remain readable. Manual target registers are continuously overwritten by JEITA when JEITA is on; read actual dac.* fields to see the active regulation targets. Basic temperature-qualified charging still uses the fixed outer temperature limits when JEITA is off.

All configuration writes require a fresh FST identity and a live cell count of 4 or unknown. Other variants remain readable but cannot receive this board's saved settings. Every setting uses a masked register update and verifies readback before success. Unrelated bits are preserved; one-shot BSR is not replayed by configuration writes. A failed write/save attempts to restore the previous field and reports whether rollback was verified.

"},{"location":"solarxiao_power/#saving-and-actions","title":"Saving and actions","text":"

Successful settings save an explicit override in a versioned, CRC-protected /solar_power record, using atomic file replacement plus public-file readback. The board reapplies only saved overrides after restart or charger power loss, with a five-second retry interval. It does not write default settings on first boot. JEITA-owned targets are not repeatedly written while JEITA is enabled. A failed/corrupt settings file blocks configuration saves and automatic replay.

These actions acknowledge the request, not completion of charging, resistance measurement or entry into ship mode. Read the corresponding status afterward.

See the bench acceptance report for exact firmware identity, measured readouts and test limits.

"},{"location":"solarxiao_power/#conversion-defaults-and-carrier-revisions","title":"Conversion defaults and carrier revisions","text":"

The supplied Standard schematic shows R5/RSNSI and R14/RSNSB at 10 milliohms, a 10 kohm NTC bias and a 10 kohm NTC. It shows an L42M single-cell charger, so it does not independently confirm component values on this 12 V FSTM revision. Defaults use those resistor values and the LTC4162 reference thermistor beta of 3490 K. Readings identify the model as reference until the corresponding calibration values are explicitly set.

Calibration settings, saved without changing hardware target registers:

get mppt cal.rsnsb_mohm\nget mppt cal.rsnsi_mohm\nget mppt cal.ntc_bias_ohm\nget mppt cal.ntc_r25_ohm\nget mppt cal.ntc_beta\n

Set these with the same set mppt syntax after confirming actual components. Allowed sense resistance is 0.1..1000 milliohms, NTC/bias resistance 100..1000000 ohms, beta 1000..10000 K. The simple beta temperature model assumes an unpadded NTC divider. It does not model series/parallel thermistor dilution networks. Changing calibration changes units/conversions, not already saved register codes. INA260 uses its integrated shunt and does not use these sense-resistor values.

"},{"location":"solarxiao_power/#telemetry-channels","title":"Telemetry channels","text":"

The following carrier channels apply to MPPT mode. Advanced mode reports the existing self voltage/MCU temperature on channel 1 and omits native channels.

Carrier telemetry requires the environmental telemetry permission. Channel 1 remains the XIAO internal ADC; it is separate from the 4S carrier battery and its existing startup/low-voltage policy is unchanged.

Channel Values 2 INA260 battery voltage, signed current, power 3 LTC4162 input voltage, signed current, computed nonnegative input power 4 Charger system output voltage 5 Charger battery ADC voltage and signed current 6, 7 Die temperature, modeled NTC temperature 8, 9, 10 Generic sensor integers: charger state, charge status, system bits 11, 12, 13 Generic sensor integers: limit, state and status alert masks 14 onward Optional environmental sensors, in discovery order

Unavailable values are omitted. Voltage history reserves channels 2..5 and marks unavailable samples invalid. The generic sensor scanner cannot claim carrier addresses 0x40/0x68; this prevents an INA219 driver from configuring the INA260 and keeps carrier channels stable across profiles and power states. No state-of-charge percentage or coulomb counting is inferred from voltage.

References: SolarXaio hardware, LTC4162-F Rev A, INA260.

"},{"location":"spiffs_regular_file_reads/","title":"Spiffs regular file reads","text":"

View this page on MeshCore Docs.

"},{"location":"spiffs_regular_file_reads/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"stats_binary_frames/","title":"Stats binary frames","text":"

View this page on MeshCore Docs.

"},{"location":"stats_binary_frames/#stats-binary-frame-structures","title":"Stats Binary Frame Structures","text":"

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"

The CMD_GET_STATS command uses a 2-byte frame structure: - Byte 0: CMD_GET_STATS (56) - Byte 1: Stats sub-type: - STATS_TYPE_CORE (0) - Get core device statistics - STATS_TYPE_RADIO (1) - Get radio statistics - STATS_TYPE_PACKETS (2) - Get packet statistics

"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"

The RESP_CODE_STATS response uses a 2-byte header structure: - Byte 0: RESP_CODE_STATS (24) - Byte 1: Stats sub-type (matches command sub-type): - STATS_TYPE_CORE (0) - Core device statistics response - STATS_TYPE_RADIO (1) - Radio statistics response - STATS_TYPE_PACKETS (2) - Packet statistics response

"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"

Total Frame Size: 11 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x00 (STATS_TYPE_CORE)\n    uint16_t battery_mv;\n    uint32_t uptime_secs;\n    uint16_t errors;\n    uint8_t  queue_len;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"

Total Frame Size: 14 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x01 (STATS_TYPE_RADIO)\n    int16_t  noise_floor;\n    int8_t   last_rssi;\n    int8_t   last_snr;       // Divide by 4.0 to get actual SNR in dB\n    uint32_t tx_air_secs;\n    uint32_t rx_air_secs;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"

Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":""},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x02 (STATS_TYPE_PACKETS)\n    uint32_t recv;\n    uint32_t sent;\n    uint32_t flood_tx;\n    uint32_t direct_tx;\n    uint32_t flood_rx;\n    uint32_t direct_rx;\n    uint32_t recv_errors;    // present when frame size is 30\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n    \"\"\"Send command to get core stats\"\"\"\n    cmd = bytes([56, 0])  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n    serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n    \"\"\"Send command to get radio stats\"\"\"\n    cmd = bytes([56, 1])  # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n    serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n    \"\"\"Send command to get packet stats\"\"\"\n    cmd = bytes([56, 2])  # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n    serial_interface.write(cmd)\n
"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n    response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n        struct.unpack('<B B H I H B', frame)\n    assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n    return {\n        'battery_mv': battery_mv,\n        'uptime_secs': uptime_secs,\n        'errors': errors,\n        'queue_len': queue_len\n    }\n\ndef parse_stats_radio(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n    response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n        struct.unpack('<B B h b b I I', frame)\n    assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n    return {\n        'noise_floor': noise_floor,\n        'last_rssi': last_rssi,\n        'last_snr': last_snr / 4.0,  # Unscale SNR\n        'tx_air_secs': tx_air_secs,\n        'rx_air_secs': rx_air_secs\n    }\n\ndef parse_stats_packets(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n    assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n    response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n        struct.unpack('<B B I I I I I I', frame[:26])\n    assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n    result = {\n        'recv': recv,\n        'sent': sent,\n        'flood_tx': flood_tx,\n        'direct_tx': direct_tx,\n        'flood_rx': flood_rx,\n        'direct_rx': direct_rx\n    }\n    if len(frame) >= 30:\n        (recv_errors,) = struct.unpack('<I', frame[26:30])\n        result['recv_errors'] = recv_errors\n    return result\n
"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n    serialInterface.write(cmd);\n}\n
"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n    battery_mv: number;\n    uptime_secs: number;\n    errors: number;\n    queue_len: number;\n}\n\ninterface StatsRadio {\n    noise_floor: number;\n    last_rssi: number;\n    last_snr: number;\n    tx_air_secs: number;\n    rx_air_secs: number;\n}\n\ninterface StatsPackets {\n    recv: number;\n    sent: number;\n    flood_tx: number;\n    direct_tx: number;\n    flood_rx: number;\n    direct_rx: number;\n    recv_errors?: number;  // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 0) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        battery_mv: view.getUint16(2, true),\n        uptime_secs: view.getUint32(4, true),\n        errors: view.getUint16(8, true),\n        queue_len: view.getUint8(10)\n    };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 1) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        noise_floor: view.getInt16(2, true),\n        last_rssi: view.getInt8(4),\n        last_snr: view.getInt8(5) / 4.0,  // Unscale SNR\n        tx_air_secs: view.getUint32(6, true),\n        rx_air_secs: view.getUint32(10, true)\n    };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n    const view = new DataView(buffer);\n    if (buffer.byteLength < 26) {\n        throw new Error('STATS_TYPE_PACKETS frame too short');\n    }\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 2) {\n        throw new Error('Invalid response type');\n    }\n    const result: StatsPackets = {\n        recv: view.getUint32(2, true),\n        sent: view.getUint32(6, true),\n        flood_tx: view.getUint32(10, true),\n        direct_tx: view.getUint32(14, true),\n        flood_rx: view.getUint32(18, true),\n        direct_rx: view.getUint32(22, true)\n    };\n    if (buffer.byteLength >= 30) {\n        result.recv_errors = view.getUint32(26, true);\n    }\n    return result;\n}\n
"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":""},{"location":"telemetry_decoder/","title":"Telemetry decoder","text":"

View this page on MeshCore Docs.

"},{"location":"telemetry_decoder/#telemetry-decoder","title":"Telemetry decoder","text":"

The decoder runs on the documentation website. GitHub's Markdown preview shows the instructions but cannot run the decoder's buttons or JavaScript.

Paste raw hexadecimal packet data copied from the Let's Mesh packet analyzer to decode a repeater's temperature, battery-voltage, external I2C-voltage, or radio noise-floor snapshot. The decoder also accepts the payload hex without its MeshCore packet header and the Base64 pages returned by repeater or room-server CLI commands. Decoding happens entirely in this browser; pasted data is not uploaded or sent anywhere.

The Source ID in a raw-packet result is the first eight bytes of the sending repeater's public key. Match that 16-character hex value against the public-key prefix recorded for your repeaters. It comes from the telemetry payload itself, so it is available even when only the payload was copied.

"},{"location":"telemetry_decoder/#send-raw-telemetry-packets","title":"Send raw telemetry packets","text":"

From an administrator CLI session, first configure a direct route to the MQTT observer that will receive and upload the raw packets. Use direct when the observer is a zero-hop neighbor:

set data.tx path direct\nsend telemetry.tx now\n

For a routed observer, provide its comma-separated hop hashes instead:

set data.tx path A1B2,C3D4\nsend telemetry.tx now\n

The shared route does not enable telemetry. Automatic telemetry is off on a fresh install; to explicitly keep the route manual-only before sending:

set telemetry.tx schedule off\nsend telemetry.tx now\n

The send telemetry.tx now command queues one TTB1 temperature packet, one TVB1 battery-voltage packet, and IVB1 chunks for every connected I2C voltage channel. TTB1 and TVB1 carry up to 165 samples. Each IVB1 carries 64, so a full 192-point/four-day channel takes three packets. Channels whose retained history is entirely zero are treated as disconnected and are not sent. The command requires at least one collected base sample and works even when the automatic schedule is off.

It also queues a N1B1 noise-floor packet once the main radio has completed a 15-minute history bucket. With radio2 enabled, it queues a separate N2B1 packet for that profile once it has a completed bucket. Each holds the latest 255 positions: up to 63 hours 45 minutes at 15-minute resolution. A position can be Missing; failed measurements do not silently shorten the timeline. The existing automatic transmission cadence is unchanged. Recording a new noise-floor position every 15 minutes does not transmit a packet every 15 minutes.

Schedule changes are saved transactionally; a failed write does not truncate the previous preferences. A legacy route must be successfully migrated into data.tx before its schedule can run. Corrupt, unreadable, or ambiguously committed preferences stop automatic and pending sends. get telemetry.tx shows fault=prefs, and manual sends return an error instead of acknowledging work that cannot run. Such a storage fault must be repaired before settings can be changed; the firmware does not overwrite an unmigrated legacy file.

"},{"location":"telemetry_decoder/#decode-a-packet","title":"Decode a packet","text":"

If you are reading this on GitHub, open the working decoder on the documentation website to paste a packet, load an example, or download CSV.

Enable JavaScript on the documentation website to use the decoder.

"},{"location":"telemetry_decoder/#analyzer-hex-examples","title":"Analyzer hex examples","text":"

The buttons load synthetic, protocol-valid zero-hop RAW_CUSTOM packets. A real snapshot can contain many more samples. Routed packets also contain path bytes before the telemetry payload magic; the decoder finds and validates the payload automatically.

"},{"location":"telemetry_decoder/#temperature","title":"Temperature","text":"
3E00545442311122334455667788800092651E0008000102354A4E5082\n
"},{"location":"telemetry_decoder/#battery-voltage","title":"Battery voltage","text":"
3E00545642311122334455667788800092651E000800010264C8FEFFDC\n
"},{"location":"telemetry_decoder/#external-i2c-voltage","title":"External I2C voltage","text":"
3E00495642311122334455667788800092651E00020800000004019026927109C427107FFF\n
"},{"location":"telemetry_decoder/#radio1-noise-floor","title":"Radio1 noise floor","text":"
3E004E3142311122334455667788800092650F00080044CB77DF\n

All four examples identify the source as 1122334455667788.

"},{"location":"telemetry_decoder/#cli-history-pages","title":"CLI history pages","text":"

The same page continues to decode the padded Base64 returned by these administrator commands:

Data Newest page Older-page example Samples per page MCU temperature get telemetry.temp get telemetry.temp 2 48 (24 hours) Battery voltage get telemetry.volt get telemetry.volt 3 48 (24 hours) I2C voltage get telemetry.volt.i2c 2 get telemetry.volt.i2c 2 4 48 (24 hours) GPS position get telemetry.gps get telemetry.gps 2 24 (12 hours) Main-radio noise floor get telemetry.noise get telemetry.noise 2 Up to 176 (44 hours) Radio2 noise floor get telemetry.noise2 get telemetry.noise2 2 Up to 176 (44 hours)

Paste either the complete reply beginning with > or Base64 alone. For noise replies, the optional two-character CLI correlation prefix is accepted too (01|Base64 or 01|> Base64). For example:

get telemetry.volt 1\n> EkDUcWoeMAAB5+bl5eTj4uLh4ODf3t7d3Nvb2tnZ2NfX1tXU1NPS0tHQ0M/Ozc3My8vKycnI/w==\n

Run get telemetry.volt.i2c without a channel first to list connected channels. For each channel, collect pages 1 through 4. Paste all four reply lines into the decoder at once; it merges their timestamps into one 192-point, four-day table and downloads them as one CSV. The same merging works for the three IVB1 analyzer packets from a full channel. Inputs must have the same telemetry type, source, LPP channel, radio profile, and sample interval.

Noise history retains eight days (768 quarter-hour positions) per profile in RAM and resets on reboot. Page 1 is newest; up to five pages are available as history fills. Paste the replies for one radio together to merge them. Main-radio and radio2 pages remain separate. A full noise CLI page is 117 decoded bytes or 156 Base64 characters, fitting the current CLI reply buffer over USB and administrator LoRa CLI. Radio2 history is read with get telemetry.noise2, including when it is not being transmitted.

An INA3221 exposes its three enabled hardware inputs as three consecutive LPP channels. When it is the only external sensor these are normally 2, 3, and 4, in hardware-input order. Other sensors can shift the numbers, so copy the IDs reported by get telemetry.volt.i2c rather than assuming them.

"},{"location":"telemetry_decoder/#reading-the-table","title":"Reading the table","text":"

Each noise position combines three fresh noise-floor estimates collected roughly five minutes apart within its UTC-aligned 15-minute bucket. If any estimate is -100 dBm or lower, the minimum wins, including a bucket containing both low and high values. Otherwise, if any estimate is -80 dBm or higher, the maximum wins. Otherwise the three estimates are averaged. Minimums round downward, maximums upward, and averages to the nearest encodable value. Values below -120 dBm clamp to -120 dBm; values above -64 dBm become MAX. Fewer than three fresh, valid estimates produce Missing. These are idle RSSI noise estimates, not received-packet RSSI measurements.

GPS history remains available through the administrator CLI, but telemetry.tx never puts GPS in RAW_CUSTOM packets. Location data therefore must come from a CLI Base64 page rather than analyzer hex.

For the byte-level layouts, see Read repeater telemetry history.

"},{"location":"terminal_chat_cli/","title":"Terminal chat cli","text":"

View this page on MeshCore Docs.

"},{"location":"terminal_chat_cli/#terminal-chat-cli","title":"Terminal Chat CLI","text":"

Below are the commands you can enter into the Terminal Chat clients:

For a timed RF carrier test on SX1262 or LR1110, use cw on 2.5 or cw2 on 2.5, then cw off to stop early. See carrier-wave RF checks for profile selection, limits, and status commands.

"},{"location":"terminal_chat_cli/#companion-wifi-browser-terminal","title":"Companion WiFi browser terminal","text":"

On ESP32 WiFi Companions with WebConfig, open the node's LAN IP address and select CLI. The browser runs the same commands as the USB terminal and Full Companion's TCP terminal on port 5002, including contact import, contact selection, messages, remote login/commands, and delayed RF replies. Run help to list commands compiled into the device. Enable the tab with set wifi.cli on and sign in with the node admin password. The setup AP does not expose this full terminal.

For example, copy the complete meshcore://... string from card on another Companion, then run these commands on the receiving Companion:

import meshcore://<full-contact-card-data>\nlist\nto <contact-name-or-prefix>\n

Replace the placeholders; a bare public key is not a contact card. Import is queued for signature validation, so use list to confirm the contact appears. Explicit import works with manual contact addition enabled and bypasses the automatic discovery type/hop filters. It still respects contact storage limits; a full contact table follows the configured overwrite policy. to remains selected for subsequent send, path, login, and cmd commands within this terminal session.

Only one USB, TCP, or browser text terminal owns the session at a time. An idle startup USB prompt can be borrowed by WiFi. Use disconnect or the browser's disconnect button to release it. Closing the page releases the session; if the connection disappears, it expires after 60 seconds without a poll. Long replies page into the browser automatically, and incoming replies continue appearing after a command finishes. Terminal scrollback starts at 4 KiB and can grow to 32 KiB for unread output, preferring PSRAM; it shrinks after reading and is freed when the session expires. If output exceeds the available buffer while unread, the browser reports the missing output.

USB protocol-switch and USB MOTA ownership commands still apply to the USB connection. Use the WiFi MOTA seeder on port 5001 for a host folder over WiFi.

"},{"location":"terminal_chat_cli/#local-maintenance-commands","title":"Local maintenance commands","text":"

Supported ESP32-S3/SX1262 Full Companions also support Meshtext channel chat:

set mt.shortname MIKE\nsend mt.nodeinfo 0\nchannel 0 Hello\n

Use a configured mt- channel slot. Chat carries only the entered text. After the first successful over-air chat transmission per used channel/identity per boot, the firmware queues a separate standard name/ID NodeInfo advertisement. Automatic metadata waits for a 250 ms quiet TX gap so the peer can resume RX. send mt.nodeinfo can also request one explicitly. Neither contains GPS or coordinates. No NodeInfo is sent just from booting, configuration, receiving traffic or a timer. The long name comes from the existing node name; get mt.shortname shows the effective four-byte short name. These commands are not added to smaller or feature-disabled builds.

MT chat uses flood.retry.count (0\u201315) to retry when no matching forwarded copy is heard. A heard repeat stops retries, but is not a recipient delivery receipt. NodeInfo has at most one retry, only with a nonzero count and flood.retry.advert on, with an extra one-minute wait. For example:

get flood.retry.count\nset flood.retry.count 3\nset flood.retry.advert on\nget mt.retry\n

get mt.retry shows retry/heard/exhausted counters, pending work and the configured count; it does not change settings. With set mt.hops 0, no peer can forward a copy, so heard-repeat retries are disabled. The original chat and first-use NodeInfo are unaffected.

This does not make the Companion relay other people's MT packets. See heard-repeat retries for the airtime limits, exact-packet matching and cancellation rules.

Local means a connection directly to the node: USB, BLE or WiFi/Ethernet Companion protocol, a TCP terminal, or the browser CLI on the LAN. These connections can use the following commands when the role and build include the corresponding feature:

Command Result stats-core, stats-radio, stats-radio-diag, stats-packets Runtime diagnostics get public.key Read-only node identity public key; Companion returns > followed by 64 uppercase hex characters get prv.key Node identity private key; Companion requires private key export enabled in the build get password Infrastructure admin password; ESP32 WiFi Companion reports management-password status without revealing it; other Companions report no admin password password <new-password> Change the saved admin password on infrastructure and ESP32 WiFi Companion erase Erase stored identity and settings; reboot for a fresh node get wifi.pwd Stored WiFi password on WiFi builds get mqttN.password, get mqttN.token Stored credentials for MQTT slot N get acl, log Infrastructure ACL or captured packet log; these stores do not exist on Companion get guest, set guest on, set guest off, get guests [page] Repeater guest-login control and current guest sessions get logins [page] Recent repeater login attempts, with 12-character public-key prefixes

The command text is the same for direct connections. A binary Companion app can send 0x42 (CMD_RUN_CLI_COMMAND) followed by the ASCII command, without switching into terminal mode. The response is 0x1D (RESP_CODE_CLI_REPLY) followed by the reply text. For example, payload 42 67 65 74 20 70 61 73 73 77 6f 72 64 runs get password. USB framing remains the normal Companion frame; BLE and TCP use their existing Companion transport framing.

set freq <MHz> is available on these local connections and through authorized LoRa administration, just like set radio. It saves the frequency without changing bandwidth, spreading factor, or coding rate and requires a reboot to apply. Infrastructure roles also apply the board's transmit-power limit.

Commands relayed to another node using cmd travel over LoRa and retain the remote restrictions on local-only maintenance and secret reads. These local permissions do not add an ACL or packet-log store to Companion. Use list or the binary contacts operations for Companion contacts. Interactive chat commands still use the text terminal or their corresponding binary protocol operations.

The browser CLI requires station/LAN mode, wifi.cli on, and the node admin login. Configuration forms keep password fields masked; explicit local CLI getters return their values, except ESP32 WiFi Companion's status-only get password. Existing USB/BLE/TCP clients and explicitly authorized remote CLI contacts retain their administrative command access. HTTP authentication does not add a password handshake to the raw TCP services.

"},{"location":"terminal_chat_cli/#companion-contact-and-channel-tx-profiles","title":"Companion contact and channel TX profiles","text":"

Choose where the Companion transmits to a contact or group channel:

set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nset tx.channel #wardriving radio\nget tx.channel 0\n

Values are auto, radio, radio2, both, and off. auto follows radio2.cross; explicit choices override it. Radio2 must still be in rxtx mode, and a radio2-only send fails when it cannot transmit. These settings persist and also apply to messages sent by the phone app. Use exact names, a channel index, or key:<unique public-key prefix> for a contact. See the routing guide for which requests/replies are covered and how to reset a choice.

"},{"location":"terminal_chat_cli/#companion-media-and-ssh-tx-profiles","title":"Companion media and SSH TX profiles","text":"
get tx.media\nset tx.media radio2\nget tx.ssh\nset tx.ssh radio\n

Each setting accepts auto, radio, radio2, both, or off, defaults to auto, and is saved across reboot. tx.media handles Open TAK binary media; tx.ssh requires a tunnel app using the explicit SSH application tag, not an existing untagged SSH connection. See the application routing guide for the framing and routing rules. Ordinary text keeps its existing route.

"},{"location":"terminal_chat_cli/#esp32-full-companion-ethernet","title":"ESP32 Full Companion Ethernet","text":"

Use set ethernet.radio <MHz>,<kHz>,<SF>,<CR>[,<preamble>], then set ethernet on. get ethernet reports state; set ethernet off closes the KISS service and releases its radio2 lease. Settings survive reboot. This is a separate whole-link mode, not the tagged tx.ssh application route. Normal USB/BLE/Companion TCP stay in Companion mode. The host instead uses KISS-over-TCP port 5003 on trusted station WiFi, with MTU 230. See Ethernet setup and radio2 restrictions.

"},{"location":"terminal_chat_cli/#companion-usb-mode","title":"Companion USB mode","text":"

Updated 1.17.1.6 USB Companion builds, including ordinary and Full builds, start in ASCII at 115200 baud. A valid framed app command automatically selects Binary Companion. You can open a terminal without a start token:

picocom --baud 115200 /dev/ttyACM0\n

Current source waits for input before printing the USB banner. Press Enter or type a command. This preserves the clean first response needed by binary apps; see USB client validation for release status.

For older firmware or a port already in Binary mode, use:

picocom --baud 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

--initstring sends this exact terminal-start line automatically:

+++MESHCORE-TERM-START\n

The carriage return is required in Binary mode. Control tokens are recognized only as complete CR/LF-delimited lines, so the same text embedded in unrelated unframed input cannot switch modes accidentally.

Binary Companion frames can contain terminal control bytes. The spchex input map renders those bytes as bracketed hexadecimal during the short transition instead of allowing them to change the local terminal's character set or display state. It leaves high-bit bytes unchanged so a UTF-8 terminal displays emoji and non-ASCII text normally. Do not add 8bithex unless you explicitly want UTF-8 bytes displayed as sequences such as [f0][9f][91][8b]. Once the terminal banner appears, the start sequence has already succeeded; do not enter it again as a terminal command.

Send the following exact sequence to return to the binary protocol:

+++MESHCORE-TERM-STOP\n

If logging shares the primary USB port (ESP32), first run set usb.logging off. With logging still on, the stop token reports an error and keeps ASCII logging active. nRF52 Full's separate logging port can remain enabled.

An observable USB session reset restores ASCII after clearing the previous client's state. Native USB with DTR can detect a terminal closing; ESP32 hardware USB Serial/JTAG detects bus resets and physical host loss. USB-to-UART bridges usually cannot detect a terminal closing. Use the start token on ports that remain in Binary mode; idle time does not select ASCII.

Both modes use the same port at 115200. Selecting 57600 is not a portable mode switch: native USB CDC devices ignore the requested baud, while USB-to-UART devices really change the UART timing and receive corrupt data. Binary mode is the framed Companion API used by apps and meshcli; close the terminal before opening that port from an app.

All USB Companion builds automatically switch when they see a complete <-prefixed Companion frame at an empty prompt. The explicit start/stop tokens remain available. See Companion USB CLI and binary switcher for the state machine and limitations.

"},{"location":"terminal_chat_cli/#commands","title":"Commands","text":"

ESP32 and nRF52 Full Companions include an offline World English Bible lookup:

get reader 3:16\nget reader 2\nget reader 2:2-12\n

These select a verse, an entire chapter, or an inclusive verse range from the embedded corpus. Local USB/TCP/browser terminals stream the complete selection in small chunks, keeping the radio loop running while the terminal drains. A new command or disconnected session cancels unfinished output. Text uses plain ASCII punctuation with unchanged wording and capitalization.

In the app's USB/BLE CLI, each reply stays within the existing frame limit. Follow the displayed Next: get reader ... command to continue a long verse or advance through the selected chapter/range. The optional final number is the page within the first verse of that command, for example get reader 21:17 2. The continuation command retains the requested range's end. See Reader lookup and compression for the source, supported build profiles and memory costs.

set freq {frequency}\n

Set the saved LoRa frequency. Example: set freq 915.8. Available over local connections and authorized LoRa CLI; reboot to apply.

set tx {tx-power-dbm}\n

Sets LoRa-chip transmit power in dBm. The firmware rejects values outside the radio/board limit; external-PA boards can have a lower input-power ceiling than the radio chip itself.

set name {name}\n

Sets your advertisement name.

get bluetooth.name\nset bluetooth.name {name|default}\n

Shows or changes the complete Bluetooth device name without changing the mesh advertisement identity. A custom name may contain spaces and is limited to 31 valid UTF-8 bytes. default restores MeshCore-<node name>. The saved change takes effect after reboot. get ble.name and set ble.name are short aliases.

set pin {0-999999}\n

Saves the Bluetooth pairing PIN. For example, run set pin 654321, then reboot to activate it. set pin 0 removes the saved override and restores the build's default/generated PIN policy after reboot; it does not disable Bluetooth. Existing bonds can reconnect without PIN entry, so forget the phone's device entry and pair again if needed. See Bluetooth pairing PIN.

get bluetooth.mac\nset bluetooth.mac {address|random|random-every-boot|random-after-connect|default}\nget bluetooth.stealth\nset bluetooth.stealth {on|off}\n

Shows or changes the Bluetooth identity on Companion builds with BLE. A literal address must be BLE random-static, such as C2:11:22:33:44:55. random generates and saves one address; random-every-boot generates a new one on each startup (random everyboot is also accepted). random-after-connect keeps its address across unused power cycles, then rotates on the next boot after an authenticated connection. default or clear restores the factory address without changing the stealth flag. Reboot, forget the old phone entry, and pair again after an address change. get ble.mac and set ble.mac are short aliases.

The independent bluetooth.stealth flag defaults off and preserves the chosen address policy. With it on, the node is discoverable until its first authenticated pairing, then accepts only that bonded peer. Custom and saved random addresses keep the bond across boots; rotating modes reopen pairing when the address rotates, leaving stealth enabled. It still transmits the directed or allowlisted packets BLE requires for reconnection. Repeating on keeps the bond. Send off, then on, then reboot to reopen pairing manually. ble.stealth is the short alias. Flag changes require reboot.

set lat {latitude}\n

Sets your advertisement map latitude. (decimal degrees)

set lon {longitude}\n

Sets your advertisement map longitude. (decimal degrees)

set af {air-time-factor}\n

Sets the transmit air-time-factor. Deprecated - use set dutycycle instead.

get powersaving\nset powersaving {on|off}\n

Shows or changes Companion device power saving. On ESP32 this controls CPU and GPS idle behavior; it does not change LoRa RXPS or WiFi modem sleep.

get usb.logging\nset usb.logging {on|off} [reboot]\nget usb.debug\nset usb.debug {on|off}\n

Shows or changes persistent USB packet output and a separate verbose-debug preference in an ordinary USB-loggable Companion or Full Companion. usb.logging is the master; debug output requires the master and usb.debug to be on. RAW and RX/TX packet records need only the master. Use set usb.debug off with packet logging for USB-to-MQTT capture. get usb.debug reports saved intent, even while the master is off. Disabling the master does not clear it. Debug defaults off for fresh installs and older preferences without the new field; it still needs compiled debug support. Changing debug verbosity takes effect without a reboot.

Current source also accepts get usb.watchdog and saved set usb.watchdog off|on|auto. Auto is the default and requires 14 continuous healthy logging-client days before saving On. Exact USB stats polls, not merely USB attachment, renew the client lease. See USB logging watchdog for USB-only repair, increasing physical-fault reboot intervals, status fields, and ownership/storage safeguards.

USB Companion and Full start off on a fresh install with the logging master disabled to protect framed traffic. nRF52 Full changes its interface count after a reboot and keeps Companion on interface 00. Every ESP32 Full Companion has one TTY and needs no reboot: logging uses the active text terminal, disables framed Binary Companion on USB, and continues accepting set usb.logging off. Turning logging off leaves the port in the normal ASCII terminal, matching fresh firmware. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion afterward. Full Companion emits complete received frames as uppercase ASCII RAW: hex followed by decoded RX summaries where possible; TX summaries are separate. These are best-effort records, not atomic pairs or a durable capture. nRF52 interface 02 is logging-only: send CLI/control commands on interface 00. A single-port bridge that queries identity on its logging port needs separate control-port support before it can use that dedicated log endpoint.

reboot\n

Sends an acknowledgement, then reboots the Companion one second later. The delay gives either the USB terminal or the Full Companion TCP terminal on port 5002 time to deliver the reply before its transport disappears.

get radio.rxps\nget radio.rxps.config\nset radio.rxps {off|on|level 1-10 [preamble 16|32]|rx_us sleep_us}\n

Shows or changes LoRa receive duty cycling on supported radios. get radio.rxps.config also reports the saved level and preamble assumption so automation can restore a level-based preference exactly. Fresh Cascade builds select level 8 with a 16-symbol timing assumption. The preamble argument controls the RXPS calculation; it does not change the physical wire preamble. A configured level is the minimum: when faster SF/BW settings shorten the timing window, firmware raises the effective level only as far as needed, up to level 10. Starting with v1.17.1.5, SF5-SF8 packets normally use a 32-symbol physical preamble. Firmware selects 64, then 128, only when every shorter choice fails to enable RXPS at any level. The configured values remain unchanged, so every radio change recalculates from the saved minimum. A slower tuple returns to that exact level and the shortest viable wire preamble.

If neither adjustment leaves enough time for the radio to wake, RXPS stays logically enabled but receives continuously instead of rejecting the radio setting or starting an invalid duty cycle. The Companion terminal's get radio.rxps reports this as mode=continuous-fast and reports any effective-level or effective-preamble adjustment; get radio.rxps.config reports the persisted preference. A later compatible SF/BW change resumes duty cycling without another RXPS command.

For the SX1262+TCXO boards tested here, these fast combinations are the useful RXPS boundary profiles (CR does not change the RXPS preamble timing):

SF BW (kHz) Wire preamble Effective preamble Minimum effective level RX / sleep 7 500 32 32 7 2731 / 6101 us 6 250 32 32 7 2731 / 6101 us 5 125 32 32 7 2731 / 6101 us 5 250 64 64 8 1252 / 6424 us 6 500 64 64 8 1252 / 6424 us 5 500 128 128 8 626 / 6398 us 5 62.5 32 16 10 4096 / 6272 us

The 64- and 128-symbol rows require the v1.17.1.5-or-newer adaptive-preamble contract on every sender that may reach the RXPS receiver. Cascade/USA builds on a Heltec V4 and WisMesh Tag (RAK4631 target) passed 16/16 packets in each direction at both SF5/BW250/64 and SF5/BW500/128, CR5, 909.950 MHz. An older sender makes a long-preamble timing window unsafe, so use continuous RX for a mixed deployment. Retry packets use the same physical preamble as other packets.

get wifi.powersave\nset wifi.powersave {none|min|max}\n

Shows or changes the persisted WiFi modem-sleep policy on ESP32 WiFi Companion builds. Full Companion requires at least min while BLE is present and rejects none. The WebConfig WiFi card and the normal binary Companion protocol expose the same setting; binary clients do not need the terminal-start token. On an ESP32 Full Companion whose primary mesh radio is ESP-NOW, max is also unavailable because maximum modem sleep can make the station miss ESP-NOW broadcasts, which the access point does not buffer. A previously saved max value is capped to and reported as min, and a new max selection is rejected. Such a WiFi/BLE/primary-ESP-NOW build therefore uses min.

get espnow.channel\nset espnow.channel <1-13>\n

On builds whose primary mesh radio is ESP-NOW, this shows or saves the primary radio's channel. The default is channel 1. Use only a channel permitted in your region, and restart or power-cycle the node after changing it. Every primary ESP-NOW peer must use the same channel.

On Full Companion, the setup AP and infrastructure-WiFi station also share that channel, so the router's 2.4 GHz radio must remain fixed to it. Power saving does not permit separate channels. wifi.powersave max is unavailable on these ESP32 Full builds and min is the coexistence setting. This channel setting is also distinct from bridge.channel, which controls the optional ESP-NOW bridge transport. An updated LoRa-primary ESP-NOW bridge can join this raw primary transport by selecting the same channel and running set bridge.format raw; its default wrapped format remains the bridge-to-bridge protocol.

get radio.rxgain\nset radio.rxgain {on|off}\nget radio.fem.rxgain\nset radio.fem.rxgain {on|off}\nget radio.fem.txgain\nset radio.fem.txgain {on|off}\n

radio.rxgain changes the radio chip's boosted receive-gain mode. The FEM settings control the external receive-path LNA or transmit-path gain on supported boards. Changes are applied immediately and saved across reboots; changing either receive-gain path also recalibrates the radio noise floor. Boards without the respective control report it as unsupported.

time {epoch-secs}\n

Set the device clock using UNIX epoch seconds. Example: time 1738242833

advert\n

Sends an advertisement packet

clock\n

Displays current time per device's clock.

ver\n

Shows the device version and firmware build date.

card\n

Displays your 'business card', for others to manually import

import {card}\n

Imports the given card to your contacts.

list {n}\n

Lists favorite contacts first, then all remaining contacts. Each group is ordered by the most recent advertisement. Optional {n} limits the displayed contacts after applying that order.

show\nshow adverts {on|off}\nshow channels {on|off}\nshow emergency {on|off}\n

Controls unsolicited receive output in the USB terminal. Plain show reports the current settings, and show {category} reports one category. At boot, advertisements and ordinary channel messages are hidden while #emergency messages are shown. The three controls are independent, so show channels on does not override show emergency off.

These filters affect terminal printing only. Messages still enter the offline queue and are delivered through the binary Companion protocol. Changes remain active when switching between terminal and binary mode and reset to their defaults after reboot.

to\n

Shows the name of current recipient contact. (for subsequent 'send' commands)

to {name-prefix}\n

Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)

path\n

Shows the saved outgoing path for the current to recipient. This command and all path changes require a recipient to be selected first.

path direct\npath clear\npath {hop-hash...}\n

Sets the outgoing path used by subsequent login, send, and cmd commands. direct selects a zero-hop route. clear forgets the saved route, causing the next operation to use flood routing and allowing normal path discovery to learn a replacement.

Explicit paths use spaces, commas, or a mixture of both between hop hashes. Each hop must contain exactly 2, 4, or 6 hexadecimal digits, and every hop in one path must use the same width. Whitespace and hexadecimal letter case do not matter. The setting is saved with the selected contact.

For example:

to Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\npath\nlogin my-admin-password\n
login {admin-password}\n

Sends a remote login request to the current recipient. Select a repeater, room, or other remotely managed node with to {name-prefix} first. The password is masked with * while it is entered and must be 1-15 UTF-8 bytes; longer passwords are rejected instead of truncated. Login uses the route shown by path: a known or explicitly set route is direct, while an unknown route is flooded.

Login results arrive asynchronously. A successful modern response displays the remote ACL permissions byte and server protocol level. A wrong password, an unreachable target, or a server that does not support remote login normally produces a timeout because those nodes do not send a rejection packet.

cmd {remote-command}\n

Sends CLI data to the current recipient. Wait for the login result before sending the first command. The remote node applies its own ACL permissions, and any reply appears asynchronously as CLI -> from {name}. The response window is 300% of the route estimate. A routed send is displayed as DIRECT via path {hop,...} with the exact prefixes copied into the packet; in MeshCore, DIRECT is the route class for an explicit path, not a synonym for zero hops. A matched reply also shows its local round-trip time, measured from queueing the command through receiving the result. This includes both radio directions and remote execution; it is not execution-only CPU time. Only one terminal cmd can be pending at a time.

For example:

to Hilltop Repeater\nlogin my-admin-password\nLOGIN -> Hilltop Repeater accepted (ACL permissions 0x03, server v13)\ncmd ver\ncmd get radio\n

Repeater guest logins default to off. An admin can use cmd set guest on to enable them or cmd set guest off to disable them and remove current guest sessions immediately. These settings are saved and need no reboot. When enabled, the configured guest.password applies; its blank default permits passwordless guest access. Admin and assigned ACL access are unaffected. Room-server login settings are unchanged.

Use cmd get guests or cmd get guests 2 from an admin session to inspect the repeater's guest entries. Each page shows one full public key and its last activity time in UTC epoch seconds. Guests are held in RAM, so this is a list of accepted sessions rather than proof that a device is currently reachable. They are cleared by a reboot or set guest off and remain separate from get acl.

Use cmd get logins or cmd get logins 2 from an admin session to inspect recent login attempts. The repeater retains up to 16 distinct public keys in RAM, newest attempt first, with up to two rows per page. Each row shows a 12-character hex key prefix, the latest UTC epoch time, result, and attempt count. Repeated attempts update the same full key's row; different keys sharing a displayed prefix remain separate records.

Results distinguish accepted logins, wrong passwords, disabled guests, replay/state rejection, and unavailable ACL capacity. Only authenticated, complete login requests are recorded; anonymous owner, region, or clock requests are excluded. Passwords and shared secrets are never stored. This bounded history clears on reboot, survives set guest off, and does not show who is currently online. A zero timestamp means the node's clock was unset.

The exact commands and permissions depend on the target firmware. cmd does not run a command on the local Companion; it sends the text over LoRa to the selected node.

Incoming direct-route messages are labeled ROUTED, not DIRECT. Forwarders consume direct-route prefixes as the packet travels, so the destination cannot recover the reply's actual hop history from the received packet. Use trace when the return route itself must be verified.

send {text}\n

Sends the text message (as DM) to current recipient.

trace\n

Traces the saved round-trip route to the current recipient and displays the SNR at each hop. Select the recipient first with to {name-prefix}.

trace {name-prefix}\n

Traces a recipient directly without changing the current to selection. A trace requires a known direct path; use normal messaging or path discovery first if the terminal reports that no valid path is available. Only one terminal trace can be pending at a time, and a missing response is reported as a timeout.

For example:

to Hilltop Repeater\ntrace\ntrace Downtown\n

The displayed route uses one- or two-byte node hashes and per-hop SNR values. Saved three-byte paths are traced with two-byte prefixes because the trace packet format has no three-byte hash-size mode.

To trace an explicit route instead of a saved contact path, provide the prefix size followed by the complete ordered route:

trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n

Prefix separators may be spaces, commas, or any mixture of them. Each prefix must contain exactly 2, 4, or 8 hexadecimal digits for a 1-, 2-, or 4-byte trace respectively. Three-byte traces are not supported.

The prefixes are used exactly in the order entered. To receive the trace result, enter the complete outward route followed by its return route, as in the mirrored examples above. A route that does not return to this node will eventually report a timeout.

reset path\n

Resets the path to current recipient, for new path discovery. This is retained as an alias for path clear.

public {text}\n

Sends the text message to the built-in Public group channel.

channels\n

Lists the configured channel slots and names without exposing their secrets.

channel {name-or-slot} {text}\n

Sends a message to any configured channel by its exact name or numeric slot. Use the slot shown by channels when a channel name contains spaces.

For example:

channels\nchannel #rgdata Hello from Eugene\nchannel 2 Another message\nshow channels on\n

Messages are UTF-8. Emoji use multiple bytes toward the available message length, which also includes the sender-name prefix added over the air.

"},{"location":"tx_beacons/","title":"Tx beacons","text":"

View this page on MeshCore Docs.

"},{"location":"tx_beacons/#transmit-only-text-beacons","title":"Transmit-only text beacons","text":"

Up to 12 disabled-by-default slots can send a short text message using their own frequency, bandwidth, spreading factor, coding rate, power, and preamble. MeshCore beacons use the Public channel. Meshtext beacons use the public preset channel, such as LongFast or MediumFast, with its standard public key.

The default cycle is 24 hours. Choose 12, 24, 36, or 48 hours:

get beacon.interval\nset beacon.interval 24\n

Slots are spread evenly across the selected cycle: 1, 2, 3, or 4 hours apart. Slot 1 is due after the first spacing interval, slot 12 after a full cycle, then the cycle repeats. Empty slots are not compacted. Scheduling uses uptime, not UTC; correcting the clock does not compress the spacing. Reboot starts a fresh schedule, with no immediate startup burst.

"},{"location":"tx_beacons/#configuration","title":"Configuration","text":"
set beacon 1 meshcore 910.525,62.5,7,5,14,32 A MeshCore node is listening here\nset beacon 2 meshtext us longfast 14 A MeshCore node is also on LongFast\nset beacon 3 meshtext us mediumfast 14 A MeshCore node is also on MediumFast\nget beacon\nget beacon 2\ndelete beacon 2\ndelete beacon all\n

MeshCore tuple order is MHz,kHz,SF,CR,dBm[,preamble]; CR is 5 through 8, representing 4/5 through 4/8. The default beacon preamble is 32 symbols. Meshtext takes an explicit us, eu868, eu433, or anz region, a recognized preset, and dBm. Its frequency is calculated using the stock preset channel; its preamble is fixed at 16 symbols. Invalid modem, region, power, or text settings are rejected. Use settings permitted for your hardware and location. Power is checked against the driver's range at the beacon frequency and any explicit board limits. External-PA drivers interpret it in antenna dBm using their calibration range; the normal receive profile's power is unchanged.

Each message body is limited to 80 bytes and is prefixed with the node name. Settings are saved separately from ordinary preferences and channels. Configuration changes do not immediately transmit a beacon.

A structurally valid saved slot that the current board or firmware cannot transmit remains available through get beacon. Its scheduled opportunity is skipped, while supported slots continue normally. Use set beacon to repair that slot or delete beacon to remove it; unsupported Meshtext slots do not consume packet IDs. This also permits repairing an out-of-range power saved by older firmware. Corrupt images retain the existing safe backup recovery; without a valid backup they remain held. Unreadable or unknown-version files remain held without transmitting or overwriting them.

"},{"location":"tx_beacons/#radio-ownership-and-limits","title":"Radio ownership and limits","text":"

These are ordinary channel text messages, not MeshCore identity advertisements or Meshtext NodeInfo. Meshtext beacons request no ACK and have no forwarding budget. MeshCore Public uses one locally transmitted flood; other MeshCore nodes may relay that flood according to their own policies.

The transmitter briefly borrows the physical radio, sends once, and restores its normal receive settings, power, and RX power saving. It does not occupy, replace, or persist changes to radio2, including a temporary second profile. Meshtext beacon identity is separate from Companion Meshtext chat; beacon packet IDs are committed to storage before use to avoid reusing chat nonces. Keep manually assigned Meshtext node IDs unique; a manual chat ID can still collide with another sender, including the separate beacon sender. A missing or unreadable active beacon ID file stops Meshtext beacons rather than rolling back to a potentially older backup. MeshCore beacons remain independent.

Beacons share the normal airtime budget and yield to due MeshCore traffic. They do not interrupt a pending packet, Morse operation, carrier test, or noise-floor calibration. Busy-channel, preparation, and transmission failures skip that opportunity; there is no automatic RF retry or catch-up burst. A separate transmit-start guard permits at most one beacon attempt per hour. Receive restoration retains exclusive ownership until it is confirmed. An unavailable/suspended radio does not transmit a beacon.

The feature is available on supported ESP32, nRF52, and RP2040 LoRa builds where flash and runtime RAM qualification pass. Tiny STM32 images leave it disabled to preserve existing features and filesystem space. The Meshtext packet-format adapters cover SX1262 (including the Indicator LoRa variant), SX1268, LLCC68, SX1276 (including external-PA variants), LR1110, LR1121, LR2021, and STM32WLx. The STM32WLx adapter is available to explicitly enabled builds, but does not enable beacons in the space-constrained STM32 artifacts. An opt-in Wio-E5 Mini USB Companion link exceeded its unchanged application limit by 19,820 bytes; the three existing STM32 Sensor targets exceeded theirs by 37,000-42,736 bytes. Enabling those artifacts needs separate size work, not just a radio capability flag or a smaller filesystem. ESP-NOW-only boards have no LoRa transmitter and cannot send these packets.

Choose a region and preset the physical chip, board RF path, and antenna support. In particular, SX1268 requires a compatible band such as EU433, not US or ANZ. LLCC68 supports MediumFast (SF9/BW250), but not LongFast (SF11/BW250). Invalid modem combinations are rejected, not silently changed to a different preset or transmitted as MeshCore. Local RF rules and the board's power limits still apply.

Protocol entry and restoration use each chip's checked sync-word, CRC, explicit-header, and normal-IQ configuration. Beacon TX temporarily disables LR2021 receive side detectors and restores their saved configuration afterward. Enabling this feature does not enable Companion Meshtext receive/chat support on other roles.

Schedules are best effort. A busy or sleeping node can miss an opportunity; the feature does not promise delivery, RF sensitivity, or a particular retuning duration on unmeasured hardware.

"},{"location":"udp_bridge_setup/","title":"Udp bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"udp_bridge_setup/#wifi-udp-bridge-up-to-eight-ip-peers","title":"WiFi UDP bridge: up to eight IP peers","text":"

Supported ESP32 repeater images can bridge MeshCore packets directly over an existing WiFi network, without an MQTT broker. Each board has one UDP listening port and up to eight configured IPv4 address/port destinations. Eight is the per-board peer limit; it is not a limit on the router's number of WiFi clients.

LoRa network A <--> Repeater A <-- existing WiFi/LAN --> Repeater B <--> LoRa network B\n

The bridge starts off on new and upgraded installations. It operates only with a connected station interface and a valid local IPv4 address. It pauses during SoftAP or AP+STA operation, WiFi disconnection, browser OTA, and incompatible WiFi ownership transitions, then resumes when station operation is available. Opening a captive configuration access point does not create a UDP bridge network. USB/LoRa CLI access remains useful for inspecting a paused bridge.

With saved WiFi credentials, enabling UDP requests a connection to that network. The web page can share the same station connection. An unused automatic recovery AP returns to station mode when UDP needs the network; connected setup clients and unfinished browser/OTA work keep their link. An explicitly requested start webconfig ap stays active until released. Use stop webconfig when finished with that AP, then check get udp.status. Switching WiFi back on does not enable UDP if you saved udp.enabled off.

"},{"location":"udp_bridge_setup/#router-client-isolation","title":"Router client isolation","text":"

Client isolation is an optional access-point or guest-network policy, not a requirement of WiFi. When enabled it can prevent two wireless devices from talking directly, even though both can reach the Internet. A similar effect can come from VLAN rules or host firewalls. The bridge cannot bypass those rules.

Use a trusted SSID/VLAN that allows the two devices to exchange UDP, or ask the network administrator to allow the bridge peers and port. Disable isolation only where that network policy is appropriate. Simply changing the UDP port, enabling MQTT on a repeater, or configuring a second peer address does not make an isolation rule disappear. Some routers isolate only clients on the same access point; others isolate all clients or the entire guest network. Verify the policy on the router in use.

Reserve each board's DHCP address on the router. The bridge stores numeric IPv4 addresses, not DNS names; a changed lease can otherwise point a peer at the wrong device. No Internet port forwarding or public exposure is required.

"},{"location":"udp_bridge_setup/#configure-two-boards","title":"Configure two boards","text":"

Use firmware containing the UDP bridge. Connect both boards to the existing WiFi network using the normal WiFi settings. Read get wifi.status on each board to find its local address once connected. The commands below use example addresses; substitute the actual reserved addresses.

For a LoRa-capable SenseCAP Indicator, use the Full SenseCapIndicator-LoRa_repeater target on the original 8 MiB/OPI-PSRAM board. The distinct SenseCapIndicator-LoRa-N16R2_repeater target is for 16 MiB/QSPI PSRAM hardware. Both retain the LoRa expander and display; their PSRAM modes are selected before app startup and the images are not interchangeable. The Indicator Companion and primary-ESP-NOW images do not provide this bridge.

Both boards use the existing bridge.secret as their shared authentication secret. Set the same strong secret on both before enabling the transport. Do not paste it into shared diagnostic captures. The transport authenticates packets with HMAC-SHA256; it does not encrypt their contents. Use trusted LAN access and WPA2/WPA3 protection as appropriate for the network.

On board A, whose reserved address is 192.168.1.50:

set udp.enabled off\nset udp.port 4404\nset udp.peer.1 192.168.1.51:4404\nset bridge.source rx\nset repeat on\nset udp.enabled on\nget udp.status\n

On board B, whose reserved address is 192.168.1.51:

set udp.enabled off\nset udp.port 4404\nset udp.peer.1 192.168.1.50:4404\nset bridge.source rx\nset repeat on\nset udp.enabled on\nget udp.status\n

Each receiving board must also list the sender's listening address/port as a peer; this is an explicit allowlist, not discovery or a broadcast service. Both ports can be changed independently, but each destination must match that peer's actual listening port. Unicast IPv4 addresses and ports 1..65535 are supported; multicast, loopback, unspecified and broadcast destinations are rejected. Remove a destination with set udp.peer.1 off. Slots 1..8 have the same behavior, and duplicate endpoints are rejected.

The UDP settings are saved separately from the historical common preferences. An invalid or truncated record starts with UDP disabled. A rejected save keeps the previous runtime preferences; an uncertain filesystem result requires a reload before further changes. OK - UDP settings saved acknowledges saved intent, not network connectivity or confirmed peer delivery.

"},{"location":"udp_bridge_setup/#test-each-ip-direction-without-sending-on-lora","title":"Test each IP direction without sending on LoRa","text":"

Wait for get udp.status to show ready peer sessions. Each peer establishes an authenticated session using the shared secret, a fresh boot/session identity and a challenge exchange. Replay checks reject stale data and diagnostic probes.

From board A's local USB console:

udp test 1 A1B2C3D4\nget udp.stats test\n

On board B:

get udp.stats test\n

Board B's rx should increase and rxid should match A1B2C3D4. Reverse the test with a different ID to check the other direction. udp test 1 generates an ID from uptime; an explicit ID must have exactly eight hexadecimal digits. The peer number is the local configured slot, not a global board identifier.

A queued/sent result confirms only local acceptance by the UDP stack. UDP itself does not guarantee delivery. A received probe proves authenticated transport receipt on that peer; it does not prove LoRa forwarding. Probes do not enter the mesh packet pool, echo, retransmit or transmit on LoRa. Test and counter-reset commands require a local connection; normal configuration uses the existing authenticated CLI access policy.

"},{"location":"udp_bridge_setup/#diagnose-mesh-traffic","title":"Diagnose mesh traffic","text":"
get udp.status\nget udp.stats\nget udp.stats rx\nget udp.stats tx\nget udp.stats drops\nget udp.stats test\n

Status separates saved intent from network readiness and authenticated peer sessions. ready counts established boot sessions, not current peer liveness; an offline peer can retain its session until a restart or reconfiguration. Use a fresh received probe to establish current reachability. Counter pages distinguish framing/authentication problems, unknown source endpoints, replay, mesh filters/duplicates, packet-pool pressure and outbound queue/socket failures. udp resetstats starts a new counter window without changing saved settings or restarting the bridge.

If no peer session becomes ready, check both stored endpoints, matching bridge.secret, station connectivity, isolation/firewall policy and the peer's actual firmware. If sessions/probes work but mesh traffic does not, check bridge.source, filters, duplicate counters and each side's LoRa setup. bridge.source rx is valid; tx instead selects packets after LoRa forwarding decisions. These source/filter settings are shared with other compiled bridges.

The peer list contains direct destinations for locally received/transmitted LoRa traffic. An incoming UDP packet enters the local LoRa mesh; it is not relayed to other UDP peers, including through its later local LoRa TX callback. A hub therefore does not provide UDP-to-UDP transit between its peers. Normal mesh duplicate/filter checks still apply, including when several repeaters hear the same LoRa packet. The configured eight-peer fan-out is bounded; it does not increase LoRa capacity or promise delivery through an arbitrary IP topology. Avoid unnecessary parallel paths and loops.

For a short USB diagnostic capture, record get usb.logging and get usb.debug, then enable both:

set usb.logging on\nset usb.debug on\n

Restore both previous values afterward. Bounded UDP: updates are emitted from the main loop at most once per second when diagnostic state changes. The counter pages and probes work without either logging gate. Never include WiFi passwords or get bridge.secret in logs you share.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/","title":"Upgrading 1.17.1.8 to 1.17.1.9","text":"

View this page on MeshCore Docs.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#upgrading-from-11718-to-11719-over-wifi","title":"Upgrading from 1.17.1.8 to 1.17.1.9 over WiFi","text":"

For a supported ESP32 node that already has compatible application slots, use its own MeshCore-OTA access point and the matching application-only image. This measured V4 route avoids dependence on the site's WiFi credentials and LAN reachability. USA Cascade defaults remain 910.525 MHz, 62.5 kHz, SF7 and CR5; existing saved radio settings take precedence after an update.

Select the exact board, hardware variant and role in the firmware picker. Download the image before connecting to the node's update network. The release's manifests and SHA-256 inventory identify the exact files. Preserve identity and settings using the tools appropriate to the installed role before an upgrade.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#choose-the-installation-route","title":"Choose the installation route","text":"Installed node File and route Exact ESP32 board/role with compatible dual-OTA slots Matching application-only .bin through its working browser updater. Supported legacy dual-OTA layout needing expansion Exact board/role migration ZIP from the selected release's utility page; follow its README and use its supplied Full application. Layout without a matching migration package, single application slot, or no working supported updater Matching -merged.bin through that board's USB/serial flashing procedure. nRF52 Matching application UF2/DFU ZIP or a separately documented LoRa OTA route; this ESP32 browser procedure does not apply.

An application-only update cannot replace a partition table. Where available, get storage.layout reports the live layout; a version label does not establish it. For older firmware, use the partition lookup guide. The tested V4 already had two 6.25 MiB slots in the expanded 16 MiB layout. That result does not qualify a migration from legacy 1.25 MiB slots.

Never upload a -merged.bin or target-partitions.bin to an application-only browser updater. A merged image includes bootloader and partition-table offsets for USB/serial installation. A migration package's target table is a verification artifact, not an application.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#recommended-11718-v4-example-use-the-nodes-ap","title":"Recommended 1.17.1.8 V4 example: use the node's AP","text":"

The actual published Full V4 build tested was 9053038f. From an authenticated USB, Bluetooth or LoRa command session, send these commands one at a time, waiting for each reply:

stop webconfig\nstop ota\nstart ota ap\n

Stopping WebConfig explicitly is needed for this old build's HTTP-port handoff. Stopping a previously running uploader ensures the old start ota ap starts the AP route. The new 1.17.1.9 startup behavior should not be assumed on the old receiver.

Join the node's MeshCore-OTA WiFi network. Open the exact URL returned by the command, normally http://192.168.4.1/update for an infrastructure node. Confirm that the page identifies the intended node, select its matching 1.17.1.9 application-only .bin, and upload once. Keep the node powered through the transfer and its automatic restart.

After restart, reconnect with the normal configuration app or console and run:

ver\nget storage.layout\n

Compare ver with the selected image and confirm identity, ACL, radio settings and the other saved settings used by the node. A command unsupported by the installed image is not proof of an incorrect layout; use the lookup guide when necessary.

The old 1.17.1.8 page can remain at 100% or miss HTTP completion even after the new application installs and the node restarts. Check the running version before repeating an upload. Browser transmission progress alone does not prove successful installation.

For 1.17.1.9 receivers, start ota starts WiFi when needed, stops WebConfig to free its HTTP port, and reports the IP address and network to use. start ota ap explicitly selects the node AP even if a LAN uploader is already running. WebConfig's own terminal explains that the handoff closes the portal; send it through USB, Bluetooth or LoRa instead. On a compatible two-slot Full Companion, use the returned port-8080 URL; single-slot Full Companions require USB for self-update. Use stop ota if abandoning an update.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#lan-and-phone-examples","title":"LAN and phone examples","text":"

If an old 1.17.1.8 infrastructure node is already reachable on the intended WiFi LAN, retain that connection. Otherwise configure it through its existing WebConfig UI, then verify the live address with get webui. From a separate USB, Bluetooth or LoRa session:

stop webconfig\nstop ota\nstart ota\n

Open the exact returned LAN URL, for example http://192.168.1.40/update. The upload computer or phone must be able to reach that address. On 1.17.1.8, confirm the actual connection after changing credentials; a serial WiFi setter alone does not prove that WebConfig uses the new pair. If the LAN is weak or blocks client-to-client traffic, use the node AP route above.

A phone near the node can download the correct application while it has Internet access, join MeshCore-OTA, and use its browser at the returned address. Keep that WiFi connection even if the phone warns it has no Internet. This is the same intended node-AP route, but an actual phone browser upload was not physically tested in this run.

A phone hotspot is useful only if the node and uploading device can both join it and reach one another. Confirm the node's reported LAN URL before uploading; hotspot client isolation can prevent this. The controlled shared LAN test used a Pi access point, not a real phone hotspot, so a phone hotspot is not the qualified default recommendation. For a node with only LoRa reachability, use the LoRa OTA guide and exact matching target/storage package.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#when-partition-expansion-is-required","title":"When partition expansion is required","text":"

Use the exact migration ZIP selected for this board and role. Its README defines the supported old updater, live-source checks and preservation set:

  1. Upload that package's wifi-bridge.bin through the supported old updater.
  2. Join the migration network described in its README and wait for Expanded layout ready.
  3. Upload the same package's full-application.bin through its /update page.
  4. Verify the new layout, version, identity, ACL and supported saved settings.

Do not substitute a loose release application for the ZIP's supplied Full application: migration packages can carry a required older target identity. Do not combine stages from different packages. The bridge writes the partition-table sector; loss of power during that write can require cable recovery. See the complete migration directions for the exact preservation limits and supported WiFi/LoRa routes. Actual legacy-layout V4 migration was not physically tested in this run.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#stock-g2-migration-limitation","title":"Stock G2 migration limitation","text":"

The separate stock 1.14.1 G2 trial reached the 1e421ff4 Full application over WiFi after partition expansion, but its final isolated network check reached the client/DHCP deadline without a verified G2 client or any HTTP request. The complete cable-free migration is therefore not qualified. Plan for USB recovery if starting from that stock image.

The G2 was subsequently updated and verified through separate USB maintenance on release source 88c851080, with its original identity, ACLs, 22 common settings and saved extras preserved. That result does not qualify the failed WiFi-only final check. A legacy RX-gain loading correction preserves an unambiguous saved gain of on from compact stock preferences; ambiguous older zero padding retains the board default.

The detailed test report retains the original failures, host/network recovery and exact source revisions. Latest-source physical V4 testing was unavailable; the measured older upgrade routes below do not establish every board or starting version.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#measured-upgrade-scope","title":"Measured upgrade scope","text":"

The published 1.17.1.8 Full V4 installed newer applications over node AP and a controlled shared LAN, with automatic restart and preserved identity and ACL, despite missing old-page HTTP completion. On the earlier c5fe8a0e source, the unchanged native AP pages returned HTTP 200/OK for G2 and V4 uploads; the V4 upload completed in 16.392 seconds and its first ordinary clock reply took 1.010 seconds against the unchanged 5-second console deadline. These tests used compatible expanded layouts and do not qualify every board, browser, phone or starting version. See the 1.17.1.9 verification scope.

"},{"location":"usb_logging_watchdog/","title":"Usb logging watchdog","text":"

View this page on MeshCore Docs.

"},{"location":"usb_logging_watchdog/#usb-logging-watchdog","title":"USB logging watchdog","text":"

Current source includes a saved USB logging watchdog in Companion, Repeater, Room Server, and Sensor. It repairs the radio's USB connection; it cannot restart a Pi, restore SSH, or prove MQTT or disk delivery. Supported backends are ESP32 native TinyUSB/HWCDC and nRF52 TinyUSB. An external USB-to-UART adapter is not an observable native-USB backend. Unsupported targets compile out recovery timers and statefile transactions; their watchdog commands report unsupported rather than saving an unusable mode. Management still reports the logging master with USB support false.

"},{"location":"usb_logging_watchdog/#commands-and-defaults","title":"Commands and defaults","text":"
get usb.watchdog\nget usb.watchdog.last\nset usb.watchdog auto\nset usb.watchdog on\nset usb.watchdog off\n

Setters accept exactly one lowercase mode, save it before applying, and do not reboot immediately. They do not enable usb.logging or usb.debug.

Mode Behavior auto Default for missing watchdog state, including upgrades. Requires 14 continuous healthy logging-client days before saving on, with the retry tier reset to one hour. A previously confirmed client can receive USB-only recovery before promotion, but Auto never reboots the MCU. on Explicitly arms physical USB recovery and eventual MCU reboot. A confirmed stats client also gets USB-only recovery if its stats polls stop; missing polls alone cannot authorize MCU reboot. off No watchdog probes, automatic USB recovery, qualification, or MCU reset. Packet logging remains independent.

Existing valid saved On/Off choices win. Auto qualification is RAM-only and restarts on reboot, USB disconnect, observed stall, expired client lease, logging master Off, or an explicit mode change. A node that has never confirmed a logging client does not cycle USB in Auto merely because no cable is attached.

"},{"location":"usb_logging_watchdog/#confirming-a-logging-client","title":"Confirming a logging client","text":"

The existing meshcoretomqtt five-minute stats-core, stats-radio, and stats-packets polls renew a 15-minute lease. Only complete, exact stats commands received on the actual USB logging endpoint count; BLE, network, LoRa, management reports, and ordinary framed Companion traffic do not. No new heartbeat command is required. A cable, open TTY, DTR, or kernel USB ACK alone cannot qualify Auto.

logger=1 requires the USB logging master, an observed host and reader, a fresh stats lease, and no physical TX stall. Any program polling those commands can qualify: this is not application identification or proof that packets were parsed, published to MQTT, or written to disk. A one-time manual stats query can also establish the lease; use watchdog Off when later USB-only reconnection would be unwanted.

On nRF52 Full Companion, only the dedicated logging CDC (*-if02) establishes the lease. That endpoint accepts bounded stats-poll markers without replying with statistics or exposing arbitrary CLI/configuration commands. Queries and configuration still use primary CDC (*-if00), whose stats queries do not prove a logging reader. Existing single-port bridges needing identity/CLI replies still require separate control-port support.

The default nRF52 framework logger follows the same runtime USB logging and debug settings. Bluefruit flash-event diagnostics use the selected bounded logging stream, including dedicated CDC on Full Companion, so saving contacts does not insert ASCII into the primary Companion protocol. A disconnected or full logging endpoint drops diagnostic output without falling back to the control port. Explicit framework UART and RTT logger selections keep their existing destinations.

On ESP32 USB-Serial/JTAG builds, ESP-IDF diagnostics also follow both usb.logging and usb.debug. Driver tasks submit bounded text; the USB owner emits it through the same session and writer guards as functional output. Logging-off, host changes and congestion discard pending diagnostics instead of falling back to the raw SDK console. Fragmented diagnostics from concurrent SDK tasks remain best effort. UART and native TinyUSB routes are unchanged.

"},{"location":"usb_logging_watchdog/#recovery-timing-and-safety","title":"Recovery timing and safety","text":"

For a physical host/reader outage, or pending TX with no real completion, software USB recovery is requested after five minutes. Re-enumeration follows after another 60 seconds if health is not restored. These are one staged attempt per outage, not a repeated five-minute reboot loop. Busy backends retry at most once every five seconds without consuming an attempted stage.

After a confirmed client's stats lease expires, USB-only repair is due five minutes later: normally 20 minutes after its last stats poll. This applies in Auto and, once a client is confirmed during that boot, On. On with no stats client observed remains compatible with non-stats log readers. A gap in stats polls does not by itself reboot a healthy USB transport.

MCU reboot requires On, a physical USB fault lasting the entire saved tier, and the USB recovery grace periods. Tiers grow after reset authorization: 1, 2, 4, 8, 16, 32, 64, 128, then 168 hours (one week). An older app-only stats outage cannot shorten a later physical fault's interval. Ten minutes of healthy monitored operation resets a raised tier to one hour.

Native TX-completion events are progress evidence. On the pinned ESP32 HWCDC backend, an exclusive check of both the empty software queue and acknowledged hardware FIFO also completes a pending write: the SDK does not emit another TX event for the final FIFO acknowledgement. An empty software queue alone does not establish completion. Queueing, purging, reinitializing, or accepting the optional bounded minute probe is not proof of recovery. The watchdog never waits for a host. USB connection reports, especially HWCDC's host/reader flags, are SDK approximations and do not reliably identify an open TTY process. HWCDC therefore uses physical host presence with a 100 ms continuous-loss check rather than its unreliable open-reader heuristic. A brief status dip does not revoke a valid stats lease. A real USB bus reset invalidates the lease immediately; sustained host loss and actual TX stalls remain observable.

Recovery waits for active Binary USB clients, mOTA/update owners, unsaved contacts, radio tests/calibration/temporary-radio operations, functional CLI work, and selected/in-flight radio transmissions to clear. Stale ASCII output from a disconnected or stalled host does not indefinitely prevent recovery. Before MCU reset, the next tier must commit and pass readback; the firmware then rechecks transport health and ownership. Re-enumeration preserves the configured USB descriptors, but can disconnect host applications and lose best-effort packet records.

"},{"location":"usb_logging_watchdog/#persistence-and-status","title":"Persistence and status","text":"

The independent, versioned, CRC-protected /usb_wdg statefile does not change existing role preference offsets. Unreadable/corrupt state or a volatile fallback filesystem disables automatic recovery/promotion. Failed commits remain fail-closed; an explicit setter can retry or repair this statefile on durable storage, applying only after complete verification. It cannot make volatile storage durable. A verified backup repairs a conclusively missing primary after an interrupted rename; unreadable live state is not replaced automatically.

Current writes use the 33-byte UW2 format, including the latest watchdog event. Valid 20-byte UW1 files still load with their saved mode, tier, and counters; they upgrade on the next legitimate event/configuration save, not merely on boot. Each attempted software recovery or re-enumeration saves its event and counters once. Probes, idle ticks, ownership/backend deferrals, and status reads do not write flash. A failed save leaves the live event marked unsaved and automatic actions fail closed until explicit repair.

"},{"location":"usb_logging_watchdog/#last-event-review","title":"Last-event review","text":"
get usb.watchdog.last\n

This returns the latest event only, not an unbounded history. New events replace older ones; exporting periodic management reports can retain observations off-node. Mode changes preserve the record. Erasing the filesystem or restoring a backup can remove or roll back it.

The event includes an advisory Unix epoch from the node's RTC, uptime seconds at that recorded boot, a saturating event sequence, action, reason bitmask, and durability status. Uptime extends the millisecond clock across rollover. RTC time is not verified UTC: default clocks can start at a plausible fixed date, and a later time correction does not rewrite old events. Epoch zero is allowed and can mean no usable timestamp source. Use a synchronized node clock for meaningful calendar times; sequence and recorded uptime remain useful when clock time is wrong.

Reason bits are 1 host absent, 2 reader absent, 4 physical TX stalled, and 8 previously confirmed stats client inactive. Physical failures can set multiple bits; a stats-only expiry uses bit 8 and cannot authorize an MCU reset. Actions are soft-recovery, reenumerate, reboot-requested, and reboot-cancelled. Software actions mean the backend reported an attempt, not that USB was successfully repaired. A reboot request is persisted with the next backoff tier before authorization; health/ownership are rechecked after the commit. A subsequent veto records cancellation immediately in RAM. If OTA or another owner makes storage unsafe, its durable save waits for a safe application tick; power loss before that save can leave the earlier reboot intent on disk. Neither the intent record nor the existing reboot counter proves a completed physical reset. Power loss or a hardware main-loop watchdog can occur without any opportunity to save an exact trigger time; this record covers the staged USB logging watchdog.

get usb.watchdog includes these compact fields:

Field Meaning usb, log, host, reader, logger Backend supported; logging master; observed physical USB host/reader; confirmed healthy stats client. stall, stage, step Physical TX stall; stage 0 idle, 1 software wait, 2 re-enumeration wait, 3 reset pending; saved tier index 0-8. retry, idle, auto Saved MCU interval, current monitored fault age, and continuous Auto qualification age, in seconds. Retry is not an action countdown. defer, fs Ownership/backend deferral and verified durable persistence readiness. rec, boot Attempted USB recoveries and durable reset authorizations. Attempts save the event and recovery count together; a failed save can leave the live recovery count unsaved. The reset count can include a later safety/health veto; it is not a count of completed physical reboots.

The MGR2 management report exports the USB status and the same latest-event record as a frozen, authenticated public snapshot. Updated Python and browser decoders retain released MGR1 support; older decoder copies must be updated to read MGR2. Events appear on the normal management schedule, not as immediate alerts. Reading status does not renew the logging-client lease.

Concurrent readers receive one coherent status publication, including its tier, counters, and latest event. Reads never wait for a writer. If three bounded attempts cannot obtain a snapshot, status conservatively reports deferred recovery and no verified persistence, ages, counters, or event. It preserves published hardware/logging/mode intent and protects a possibly active reader. This temporary fallback cannot authorize a reset. The separate awake hint keeps an already durable watchdog policy serviced while publication is busy.

On nRF52, the CPU can wait for interrupts between watchdog checks in both on and auto modes. Its clock and wake interrupts continue running, so this does not restart recovery deadlines or disable the watchdog. Active USB work and unfinished transport detach/reattach still prevent that idle wait. Other platforms retain their existing sleep guard, including protection against sleep modes that restart the application clock.

"},{"location":"usb_logging_watchdog/#verification","title":"Verification","text":"

The two qualification stages below precede the subsequent upstream USB/GPS and repeater trace merge. Their firmware byte margins describe those earlier images, not the current merged source.

Initial watchdog qualification on 2026-10-03, before the latest-event extension, passed 109 USB regression tests, 143 native test cases, 11 management protocol tests, and the browser decoder checks. Tests execute the production policy, runtime, marker parser, role ownership guards, and native USB boundaries with simulated clock, filesystem, and FIFO faults. The 14-day qualification uses an advanced test clock, not a hardware soak. Bounded console output, unread hosts, session races, and exact stats markers are covered.

Those initial compiled checks passed Station G2 Full Companion, Repeater, and Room Server; XIAO nRF52 Full Companion and Repeater (full and reduced OTA qualifications); and ordinary RAK 3x72 STM32 USB Companion. The STM32 image retains all CLI features and its 32 KiB filesystem, but has only 368 bytes of app flash margin; its unsupported USB watchdog machinery is compiled out.

Latest-event verification on the same date passed 116 USB regression tests, 15 management tests with fatal AddressSanitizer/UndefinedBehaviorSanitizer checks, and the browser decoder tests. Coverage includes timestamp callbacks, clock corrections and rollover, legacy-state migration, commit failures, reboot-intent cancellation, deferred cancellation saves, and authenticated cross-language decoding of the development report formats. The extension was called MGR3 during those pre-release checks, then assigned to MGR2 because the earlier USB-only MGR2 layout had never been released. Current decoders support released MGR1 and the finalized MGR2 layout; this reassignment was not a migration of deployed MGR2 reports. The full native suite passed 1,713 cases during implementation; the final packet validator also passed a fresh 116-case routing suite and 5,855,168 previous/current validator comparisons under fatal sanitizers.

The extension compiled in standard Station G2 Full Companion and Repeater, XIAO nRF52 Repeater, and RAK 3x72 USB Companion. The normal build.sh STM32 recipe at version 1.17.1.9 retains all CLI features and the 32 KiB filesystem, with 372 bytes of app flash margin; longer version strings or future changes can still exceed that tight budget. No features or filesystem space were cut to fit this extension. This is not an all-board release qualification or a physical USB recovery test. No hardware was flashed, and verification artifacts stayed outside out.

The subsequent USB/GPS/repeater-trace merge passed all 1,718 native cases, 133 USB regression methods, 25 management/event/watchdog methods plus the browser decoder, and 10 GPS/trace/persistence methods. Companion preference tests exercise six platform/layout variants under fatal sanitizers. The compact final preference writer also matches the previous full image for 256 tail-value sets per variant and preserves committed files through every partial-tail write/readback boundary and rename failure.

Compile checks for that merge passed standard XIAO S3 Full Companion, Station G2 Repeater and Room Server, native-USB XIAO S3 USB Companion with all five session/progress hooks linked, and XIAO nRF52 Repeater. The final compact writer passed both the bare STM32 CI recipe and the normal 1.17.1.9 recipe; the latter uses 229,020 of 229,376 app flash bytes (356 bytes of margin), retaining all features and the 32 KiB filesystem. The bare recipe is tighter still. These checks do not qualify every board or physical USB recovery. No device was flashed and existing out artifacts were not changed.

Finalizing the event-capable layout as MGR2 passed 1,719 native cases, 16 management tests with real C++/Python/browser cryptography, the standalone browser decoder checks, and the bare STM32 Companion compile. Coverage retains MGR1, rejects retired formats and old prototype ciphertext, and verifies that invalid management sends release their packet without queueing it.

"},{"location":"usb_serial_backpressure/","title":"Usb serial backpressure","text":"

View this page on MeshCore Docs.

"},{"location":"usb_serial_backpressure/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"v4_pixel5_font_trial/","title":"V4 pixel5 font trial","text":"

View this page on MeshCore Docs.

"},{"location":"v4_pixel5_font_trial/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"old-releases/","title":"Old Releases","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/#old-releases","title":"Old Releases","text":"

Historical release notes and their supporting files are kept here for reference. Use the current 1.17.1.8 release notes for current firmware.

"},{"location":"old-releases/1.17.1.5-doc-audit/","title":"1.17.1.5 doc audit","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.5-doc-audit/#markdown-audit-11715-usa-cascade","title":"Markdown audit \u2014 1.17.1.5 USA Cascade","text":"

Audited on 2026-09-05 against firmware source 26303793, the resolved build configuration, and the qualified 1.17.1.5 staging manifests. The scope was all 71 tracked Markdown files present at the start, including root documents, docs, examples, tools, tests, variants, and vendored library documentation. Generated/ignored build directories and external repositories were outside that scope.

"},{"location":"old-releases/1.17.1.5-doc-audit/#corrections","title":"Corrections","text":""},{"location":"old-releases/1.17.1.5-doc-audit/#verification-and-limits","title":"Verification and limits","text":"

All original Markdown files were scanned for relative links, section anchors, source-path references, version/default claims, and role/OTA terminology. Flagged operator instructions were checked against CommonCLI, its observer extension, Companion command handlers, board configuration, and release manifests. Brace-expanded source references and SDK-relative paths were retained as such. Historical measurements, changelogs, upstream findings, contribution/security policies, and licenses were not rewritten as new validation results.

The documentation site passed a strict MkDocs Material build. Relative repository links and section anchors were checked separately, including documents outside the MkDocs source directory. Console and current bootloader links were checked on their primary sites. This was not a crawl of every third-party URL or a new hardware/firmware qualification; no claim is made that every board was physically tested or every protocol statement independently re-proved.

"},{"location":"old-releases/1.17.1.5-doc-audit/#original-file-coverage","title":"Original file coverage","text":"

\u201cRetained\u201d means no correction was identified by this documentation audit; it does not certify all historical statements as current hardware behavior.

File Disposition AGENTS.md Scanned; retained ALERTS.md Scanned; retained CHANGELOG.md Historical record retained CONTRIBUTING.md Scanned; retained MEMORY_MONITORING.md Scanned; retained MQTT_IMPLEMENTATION.md Corrected or clarified MQTT_INTERNALS.md Corrected or clarified MQTT_OWNERSHIP.md Historical record retained MQTT_SNMP.md Corrected or clarified README.md Corrected or clarified RELEASE.md Corrected or clarified RESTORE_UPSTREAM_NOTES.md Historical record retained SECURITY.md Scanned; retained STABILITY_TESTABILITY_HANDOFF.md Historical record retained UPSTREAM_BUGS.md Scanned; retained WEB_CONFIG_REVIEW.md Corrected or clarified arch/stm32/Adafruit_LittleFS_stm32/README.md Vendored documentation/license retained arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md Vendored documentation/license retained arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md Vendored documentation/license retained docs/WiFi.md Corrected or clarified docs/cli_build_matrix.md Corrected or clarified docs/cli_command_availability.md Corrected or clarified docs/cli_commands.md Corrected or clarified docs/companion_offline_queue.md Scanned; retained docs/companion_protocol.md Scanned; retained docs/companion_radio_full.md Corrected or clarified docs/docs.md Scanned; retained docs/esp32_memory_budget.md Scanned; retained docs/faq.md Corrected or clarified docs/filter_tool.md Scanned; retained docs/firmware_picker.md Corrected or clarified docs/flood_filtering.md Scanned; retained docs/full_companion_features.md Corrected or clarified docs/full_companion_usb_switcher.md Corrected or clarified docs/gps_tracking.md Scanned; retained docs/halo_keymind_settings.md Corrected or clarified docs/hardware_validation_checklist.md Corrected or clarified docs/host_cli_service.md Scanned; retained docs/index.md Corrected or clarified docs/kiss_modem_protocol.md Scanned; retained docs/lora_ota_automation.md Corrected or clarified docs/mbedtls-tls-footprint.md Scanned; retained docs/nrf52_companion_storage_recovery.md Scanned; retained docs/nrf52_power_management.md Scanned; retained docs/number_allocations.md Scanned; retained docs/ota_easy.md Corrected or clarified docs/ota_meshtower_v2_sdcard.md Corrected or clarified docs/ota_nrf52_bootloader_update.md Corrected or clarified docs/ota_nrf52_qspi.md Corrected or clarified docs/ota_protocol.md Corrected or clarified docs/ota_user_guide.md Corrected or clarified docs/packet_format.md Scanned; retained docs/payloads.md Scanned; retained docs/qr_codes.md Scanned; retained docs/rak3401_mota_chain.md Historical record retained docs/releases/1.17.1.5.md Corrected or clarified docs/spiffs_regular_file_reads.md Scanned; retained docs/stats_binary_frames.md Scanned; retained docs/telemetry_decoder.md Scanned; retained docs/terminal_chat_cli.md Scanned; retained docs/usb_serial_backpressure.md Scanned; retained examples/host_cli_service/README.md Scanned; retained lib/PsychicMqttClient/CHANGELOG.md Vendored documentation/license retained lib/PsychicMqttClient/README.md Corrected or clarified mesh-america/README.md Corrected or clarified test/README.md Scanned; retained test/test_mqtt_prefs_codec/FIXTURES.md Historical record retained tools/mota/README.md Scanned; retained tools/sensecap_indicator_font/README.md Scanned; retained tools/sensecap_indicator_rp2040/README.md Scanned; retained variants/sensecap_indicator-espnow/README.md Scanned; retained"},{"location":"old-releases/1.17.1.5/","title":"1.17.1.5","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.5/#meshcore-11715-dev-usa-cascade","title":"MeshCore 1.17.1.5 Dev - USA Cascade","text":"

Use feature switches by role for exact on/off commands for Companion, Repeater, Room Server, and Sensor, including MQTT, logging, power saving, GPS, and board exceptions. Open the USB web console for the default ASCII terminal on Full Companion and infrastructure images.

The firmware identifier and main release tag follow v1.17.1.5-halo-keymind-cascade-dev-<commit>, matching the 1.17.1.4 naming convention. Supplemental pages retain the repeater-room-, utility-, logging-, lora-ota-, and full-profiles- prefixes where those outputs exist. The Companion page is marked Latest; the supplemental pages remain development prereleases.

This release uses the USA/Canada radio preset and Cascade runtime defaults. The release build records the resolved frequency, bandwidth, spreading factor, and coding rate. The offline USA Cascadia fallback is 910.525 MHz, BW62.5, SF7, CR5. Saved node settings survive ordinary firmware updates.

"},{"location":"old-releases/1.17.1.5/#memory-corrections-in-aa20e927","title":"Memory corrections in aa20e927","text":"

All 583 published profiles (the 540-profile matrix plus 43 supplemental Full repeaters) were checked against the new runtime RAM policy. All 11 failing profiles have replacements, and the same queue sharing has been applied to the other 41 nRF52 Full profiles: 52 corrected profiles in total, initially built from aa20e927. The existing release tags stay in place; corrected filenames end in aa20e927, except Wireless Paper Full's capacity-restoring follow-up, 1e4d1e16. Companion remains Latest. Both Mesh America catalogs and the firmware picker point to the replacement files.

All 43 nRF52 Full Companions keep 256 offline frames normally and lend 128 slots to mOTA while needed. Wireless Paper Full now also shares its queue and keeps 350 contacts and 40 channels. The other six corrected ESP32 Full profiles below use 150 contacts, preserving their 256-frame queue and Full transports. Export contacts before upgrading if you have more than 150; entries beyond that limit may be unavailable and a later save may omit them. This warning applies to those six profiles and the superseded aa20e927 Wireless Paper image.

Wireless Paper Full's 1e4d1e16 update has 256 message slots normally and 128 while an mOTA session borrows its workspace. Idle WiFi listening leaves all 256 slots available. If more than 128 unread messages are queued, sync them with an app before starting mOTA. Source stop/disconnect restores all 256 slots and frees ESP32 scratch buffers. USB, Bluetooth, WiFi, the display and the partition layout are retained. The application is 1,869,976 bytes; use its app-only .bin for a compatible WiFi update, or its merged image for USB installation.

The T096 and T1 LoRa OTA repeater corrections allocate the diagnostic manual staging buffer only when requested, recovering about 4 KiB at startup. Ordinary mOTA uses its existing staging store. ota dev clear releases a manual stage; allocation failure reports an error before writing. These same-version repairs require an explicit exact-target manual LoRa installation or USB; forward-only trusted autoinstall does not select an equal-version repair.

Available RAM below means capacity before startup allocations, not free heap after boot. The Wireless Paper row includes its latest 1e4d1e16 update:

Failing original profile Original bytes Replacement bytes Required bytes Generic_ESPNOW_companion_radio_full 101,920 139,456 136,192 Heltec_Wireless_Paper_companion_radio_full 150,144 161,336 152,576 Heltec_Wireless_Tracker_companion_radio_full 148,496 186,032 169,986 Heltec_ct62_companion_radio_full 99,104 136,640 136,192 Heltec_t096_companion_radio_full_femon 54,724 74,060 73,728 Heltec_t1_companion_radio_full 55,044 74,380 73,728 Heltec_v3_companion_radio_full 143,840 181,376 173,056 Xiao_C3_companion_radio_full 98,880 136,416 136,192 heltec_tracker_v2_companion_radio_full_femon 133,136 170,672 169,986 Heltec_t096_repeater_lora_ota_no_external_sensors 61,588 65,676 65,538 Heltec_t1_repeater_lora_ota_no_external_sensors 61,964 66,052 65,538

Each replacement has a matching .memory.json with its ELF hash, RAM budget and firmware-file hashes. The complete audit distinguishes original binary/log calculations from new ELF checks. All 52 exact firmware builds, their native file pairs, and 150 native mOTA tests passed, alongside memory, allocation-failure, shared-queue and picker regressions. Wireless Paper's follow-up also passed complete TCP mOTA transfers with address/leak sanitizers, repeated disconnects, queue-full refusal, idle listening and USB/TCP ownership changes. Physical boot/load/soak validation remains pending.

"},{"location":"old-releases/1.17.1.5/#g3-usb-disappearance-with-power-saving-enabled","title":"G3 USB disappearance with power saving enabled","text":"

The published 26303793 firmware has an ESP32 native-USB sleep bug that matches a Station G3 report: USB disappears from the connected Pi after a few minutes, the display button stops responding, but the repeater still answers over LoRa. Hardware confirmation of this report is pending.

Cascade enables device power saving by default on fresh settings. After two minutes, the repeater can enter light sleep when no serial terminal is open, even though the Pi has enumerated the USB device. Light sleep stops USB service; the radio GPIO can still wake the CPU, while the G3 display button is only polled. Closing a terminal must not be treated as unplugging the USB host. Espressif documents the underlying native USB sleep limitation.

To enable USB logging on an ESP32 1.17.1.5 image with logging support, disable power saving first. Use the role's text terminal, or the repeater's remote admin CLI if USB has already disappeared:

set powersaving off\nset usb.logging on\nget powersaving\nget usb.logging\n

The read-back commands should report power saving off and USB logging on. For an image without USB logging, use just set powersaving off and get powersaving to apply/check the sleep workaround. Disabling power saving saves the setting and takes effect immediately; it does not erase the node or change LoRa RX power saving (radio.rxps). If USB does not reappear after disabling sleep remotely, power-cycle once and check that the port and display button remain usable beyond the two-minute mark. Ordinary reflashing can retain the saved power-saving setting, so check it during comparisons with another firmware build too.

On unified Full infrastructure, also run set powersaving off before enabling USB through set logging.output usb or set logging.output both. These are separate commands. Turning logging off does not automatically restore the saved power-saving setting. nRF52 logging does not need this ESP32 workaround.

WiFi/MQTT-only logging does not need set powersaving off while the Repeater/Room Server MQTT bridge is running. The original 26303793 code already prevents device sleep in that state. Select set logging.output wifi where supported and check get mqtt.running; get mqtt.enabled alone only checks the saved preference. MQTT-capable Full Companion now uses the same mqtt.enabled and logging.output commands as infrastructure. Disabling MQTT preserves broker settings; the MQTT tab exposes the same master switch.

The source correction keeps native USB serviced while a host is enumerated, including when its terminal is closed. Enabled live USB logging also blocks ESP32 light sleep, even without a host. FreeRTOS can still idle/yield; this does not disable the saved power-saving setting or Companion CPU scaling. With logging off, USB power from a charger still permits sleep.

The corrected G3 uses its user button as a light-sleep wake source alongside LoRa. Button activity keeps the CPU and display awake for at least two minutes; holding or pressing the button again extends the interval. The usual display timeout can keep the screen on longer. An intentional long-press power-off retains its existing action. After the wake interval, sleep is allowed when power saving is enabled, logging is off, no native USB host is attached, and no running bridge or pending work needs service.

Host simulations cover both native USB modes, terminal close/reconnect, logging toggles, OTA inhibition, simultaneous button/LoRa wake configuration, and the two-minute interval across timer rollover. The Station G3 repeater download remains the original 26303793 build; continue to use the logging workaround above on that image.

"},{"location":"old-releases/1.17.1.5/#t096-full-companion-bluetooth-and-menu-freeze-report","title":"T096 Full Companion Bluetooth and menu freeze report","text":"

A report against Heltec_t096_companion_radio_full_femon describes no usable Bluetooth despite a displayed PIN, followed by a freeze while stepping through the device menu. Reflashing both with and without a full wipe gave the same result. The exact freeze and Bluetooth behavior still need confirmation on a physical T096 with the source correction below.

The published 26303793 Full image leaves only 54,724 bytes (53.4 KiB) between the linked heap boundaries. Its static RAM check did not account for the allocations made at startup: the 160x80 color framebuffer alone needs 25,602 bytes, followed by task stacks, the packet pool, filesystem buffers, and message previews. This leaves inadequate runtime headroom. The pinned Bluefruit library also ignores failure to create its BLE/SOC worker tasks, and the sensor menu could write through a failed CayenneLPP allocation. Those are code paths consistent with this report; a PIN is not proof of a working Bluetooth radio.

The correction applies to every nRF52 Full Companion, keeping its offline queue at 256 frames during normal use. mOTA borrows the upper 128 slots for its roughly 19 KiB session state only when needed, leaving 128 message slots during mOTA use. Starting a source or a TempRadio discovery session acquires this workspace. Stopping or disconnecting the USB/Bluetooth source restores all 256 slots, even if the temporary radio window is still open. A discovery-only session returns the slots when its TempRadio window ends. No heap resizing is involved.

Unread messages keep their order across both transitions. If more than 128 frames are pending, mOTA refuses to start and asks you to sync unread messages with a Companion app first; it does not discard them to make room. OTA configuration and signer keys survive reuse of the workspace. While mOTA owns it, the normal queue overflow policy applies to the remaining 128 slots.

The T096 profile requires at least 72 KiB of heap space at link time. It retains 350 contacts, 40 channels, the color display, sensors, USB, Bluetooth, Bluetooth DFU, and USB/Bluetooth mOTA sending. This reserve is space for runtime allocations, not a measurement of free heap after boot. Bluetooth startup now checks both worker tasks and all registered services; a failed partial initialization does not repeat every five seconds. The sensor page reports low memory if its buffer could not be allocated.

Regression tests transfer complete mOTA containers through the actual shared context, check queue wraparound and message order, and cover source stop, disconnect, conflicting USB/Bluetooth ownership and repeated sessions. They also inject worker-task and GATT-service failures, verify that retries do not initialize the stack again, open the sensor path with a failed allocation, and use the real linker assertion to reject the old heap budget. The original T096 Full download has been replaced by the corrected aa20e927 image on the same release page. Another wipe is not a fix for the old compiled memory budget.

For the corrected T096 build, use its matching application UF2 through the existing USB bootloader. Preserve the bootloader and node data. Check Bluetooth discovery, pairing and message exchange on battery power and with USB attached; then step through every menu page, return from screen-off, and check USB and Bluetooth mOTA sending. Hardware validation is pending.

"},{"location":"old-releases/1.17.1.5/#firmware-roles-and-update-support","title":"Firmware roles and update support","text":"

Use the exact board's Full Companion image when available. Every Full Companion must pass linked-firmware checks for MOTA sending, its text terminal, and the platform's source transports. ESP32 supplies USB and WiFi sources; nRF52 supplies USB and encrypted Bluetooth sources. Full Companions remain USB-updateable and do not require LoRa self-installation. Optional ESP32 WiFi self-update uses port 8080 where the existing partition layout has two application slots; nRF52 retains Bluetooth DFU with a compatible bootloader.

Read Full Companion feature switches for on/off commands, USB logging, power saving, WiFi/Bluetooth selection, GPS, MQTT, MOTA sending, and update directions.

Infrastructure artifacts must have a verified wireless self-update path. Option 3 reports and omits infrastructure on platforms without a wireless updater. The report is ota-excluded-targets.txt. Source/seeder capability alone does not count as self-update support. ESP32 qualification checks both application slots and OTA metadata; nRF52 Bluetooth qualification checks the linked DFU service and its application ZIP. LoRa installation on nRF52 still requires the exact matching OTAFIX bootloader and storage arrangement.

Classic ESP32 capacity limits are recorded in each artifact's capability manifest. Generic E22, Heltec V2, Meshadventurer, and T-Beam OTA repeaters, plus the TLora MQTT repeater, use 50 neighbors where needed to preserve the required internal RAM reserve. T-Beam MQTT repeater profiles retain the full rule engine with 31 flood rules; constrained classic ESP32 ESP-NOW bridges with logging use 47. Meshadventurer Full uses 100 contacts, 30 group channels, and a 16-frame offline queue while retaining its MOTA and transport features.

Since 1.17.1.4, this branch also includes USB session and contact-persistence fixes, automatic nRF52 ExtraFS recovery, improved Indicator setup/display handling, compressed MOTA transport, and measured ESP32 memory limits. Qualified internal-flash nRF52840 receivers now reserve 64 KiB of retained RAM to extend application MOTA staging beyond the available flash prefix. This handoff requires the exact board's OTAFIX 2.4.6 bootloader. Install that bootloader before relying on LoRa updates from these new hybrid receiver images: they reject application containers larger than one flash page when the installed bootloader lacks the retained-RAM capability. QSPI/microSD roles and Full Companions retain their existing storage layouts. An interrupted hybrid download must be fetched again after an application restart or power cycle. See the protocol for the exact admission and reset checks.

"},{"location":"old-releases/1.17.1.5/#build-using-option-3","title":"Build using option 3","text":"

The equivalent reproducible command is:

OUTPUT_DIR=.releases/1.17.1.5-halo-keymind-cascade-dev \\\nbash build.sh build-firmwares-logging-matrix \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade \\\n  --require-ota --skip-kiss --resume --background\n

Or run bash build.sh, select option 3, set version v1.17.1.5-halo-keymind-cascade-dev, and retain USA Cascadia/Cascade. Option 3 enables the infrastructure OTA requirement by default. It merges supported runtime USB logging into canonical artifacts and builds the expanded ESP32 profiles. It runs one PlatformIO process at a time.

The background launcher prints its persistent log and status paths. Each target also gets a log in OUTPUT_DIR/build-logs/. Resume accepts only artifacts with the current capability schema and a successful qualification; infrastructure also needs a verified update method. Any failed target leaves the matrix unsuccessful and must be resolved before publication.

--allow-no-ota is an explicit development override for cable-only infrastructure. Those outputs do not belong in this OTA-required release. Use a separate output directory when changing the radio or settings profile. Use a fresh release output directory after changing the source revision or firmware version so packaging cannot mix old and new binaries.

"},{"location":"old-releases/1.17.1.5/#local-release-staging","title":"Local release staging","text":"

After the matrix completes successfully, scripts/package_cascade_release.py groups the qualified images using the previous GitHub release naming. Each group includes the firmware files, capability manifests, TSV/JSON inventory, checksums, build notes, and Full Companion directions. The staging directory's root FIRMWARE-PICKER-1.17.1.5.html opens the local downloads; the copies inside each release group use the planned GitHub download URLs. Staging does not push commits, create remote tags, or publish a GitHub release.

"},{"location":"old-releases/1.17.1.5/#published-release-set-and-small-slot-heltec-v4-updates","title":"Published release set and small-slot Heltec V4 updates","text":"

The capability JSON and target inventories now report both Bluetooth DFU and LoRa mOTA for all 60 nRF52 receiver profiles. The original report omitted the nRF52 LoRa method. Each affected manifest includes ota_update_requirements.lora with its storage type, accepted package types, exact-base requirement for deltas, and the matching OTAFIX bootloader link. Internal-flash receivers accept in-place deltas; supported QSPI/SD receivers also accept full application packages. The 20 retained-RAM receivers require OTAFIX 2.4.6's matching handoff support. These reports verify the released application bytes and storage layout; they do not certify the bootloader installed on a physical device. Firmware binaries and tags are unchanged. Full Companion mOTA sending remains separate from self-update capability.

Published firmware tags remain at source commit 26303793. Documentation can advance independently. The original matrix qualified 540 profiles (1,080 firmware files) from 551 attempts; 11 oversized portable attempts have expanded Full replacements. The variant map maps all 1,361 previous-release choices to 1,325 covered choices and 36 excluded entries. Multiple old choices map to the same current artifact.

For Heltec V4 with an existing compatible 1.25 MiB OTA application slot, the standard heltec_v4_repeater application is 1,179,200 bytes and the lean heltec_v4_repeater_lora_ota_no_external_sensors application is 1,232,024 bytes. Both fit that slot. Start the WiFi uploader with start ota ap, join MeshCore-OTA, and open http://192.168.4.1/update. Upload the application .bin; use stop ota to close an unused uploader. Their merged USB installers write two 6.25 MiB application slots. Expanded Full V4 applications require the larger layout and do not fit a 1.25 MiB slot.

"},{"location":"old-releases/1.17.1.6/","title":"1.17.1.6","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.6/#meshcore-11716-dev-usa-cascade","title":"MeshCore 1.17.1.6 Dev - USA Cascade","text":"

This release builds the current keymindCascade source after repairing the unit-test regressions. Its firmware identifier is v1.17.1.6-halo-keymind-cascade-dev-<commit>. The recorded commit identifies the source for every firmware file; binaries from earlier releases are not reused.

Use the firmware picker to match the exact board, radio, display and flash variant. The Companion release is marked Latest. Supporting release pages retain the repeater-room-, utility-, logging-, lora-ota- and full-profiles- prefixes where those outputs exist, and remain development prereleases.

The build uses the USA/Canada radio preset and Cascade runtime defaults. Build records include the resolved radio settings; the offline USA Cascadia fallback is 910.525 MHz, BW62.5, SF7, CR5. Ordinary updates retain saved node settings.

"},{"location":"old-releases/1.17.1.6/#published-matrix","title":"Published matrix","text":"

The GitHub release contains 553 qualified firmware profiles across five release pages. The initial matrix completed all 565 attempts: 537 passed, 16 failed, and 12 standard ESP32 profiles exceeded the 1.25 MiB portable slot. All 16 failures have passing repair builds; all 12 portable exclusions have qualified expanded Full alternatives. Smaller profiles that fit remain available.

The initial firmware uses 306feebe, the 14 nRF52 repairs use 283acd27, and the two Tracker MQTT repairs use 1274956f. Each firmware filename and target manifest records its source. Existing firmware assets and release tags were preserved when the repairs were added.

See the failure and repair report and portable-profile exclusions. Full alternatives require their matching expanded partition layout; they do not fit a deployed 1.25 MiB slot.

"},{"location":"old-releases/1.17.1.6/#two-lora-profiles","title":"Two LoRa profiles","text":"

radio2, tempradio2, radioat2 and tempradioat2 add a second profile to LoRa-equipped Companion, Repeater, Room Server, Sensor and terminal firmware. The second profile starts disabled. It supports receive-only or receive/transmit operation, separate retry ownership and backoff, and explicit off commands.

One transceiver alternates between at most two active profiles. Reception and transmission occupy that transceiver, so a packet on one profile interrupts listening on the other. This does not provide two simultaneous receivers.

By default, traffic can cross between two permanent profiles or two temporary profiles. A permanent/temporary pair stays isolated. radio2.cross auto|on|off controls this policy; receive-only still forbids transmission on profile 2. tempradio2 can carry LoRa OTA traffic while the primary profile remains available between packets. Use a firmware profile that supports the required OTA role.

Radio commands accept an optional trailing preamble, and their getters include the effective preamble. Automatic preambles round up in steps of eight. Paired with SF7/BW62.5/preamble 32, the automatic BW500 values are 48 symbols at SF9, 88 at SF8 and 120 at SF7. Configure transmitters with matching preambles too.

Dual-profile receive uses normal RX with the slower profile visited first. RX power saving and device sleep are suspended while scanning, then the saved settings apply again in single-profile mode. See the setup guide and command matrix and hardware measurements.

"},{"location":"old-releases/1.17.1.6/#other-changes-since-11715","title":"Other changes since 1.17.1.5","text":"

Feature switches and update commands are documented in feature switches by role, Full Companion features and the ESP-NOW bridge guide.

"},{"location":"old-releases/1.17.1.6/#validation-and-installation","title":"Validation and installation","text":"

The release is gated on the unit-test workflow for its source commit. CI repairs replace stale display assertions with compiled tests of bookmark saves across battery/USB transitions and message summaries in each inbox mode. The sleep harness checks both MQTT and dual-profile scanning.

The release packager checks each published profile's capability manifest, firmware-file hashes, native file pairs and runtime RAM budget. Infrastructure must have a verified wireless update path. The target manifests and exclusion report describe the actual published matrix; a build check is not a physical-board test.

Dual-profile timing was measured on a Heltec V4 with a XIAO ESP32-S3/Wio LoRa transmitter. Finite strong-signal tests and an OTA catalogue exchange passed; these tests do not establish weak-signal performance, simultaneous traffic reliability, a complete hardware OTA installation, or months of uptime.

ESP32 WiFi updates use the application .bin. The -merged.bin image includes boot and partition data for USB installation. nRF52 uses the board's .uf2 or native application DFU .zip; LoRa OTA also needs the exact destination package and a compatible OTAFIX bootloader. Check the per-profile capability manifest before choosing an update method.

"},{"location":"old-releases/1.17.1.6/#reproduce-the-release","title":"Reproduce the release","text":"

Follow-up builds repair the constrained profiles without relaxing qualification:

Use a clean checkout of the release commit and a fresh output directory:

OUTPUT_DIR=.releases/1.17.1.6-halo-keymind-cascade-dev \\\nbash build.sh build-firmwares-logging-matrix \\\n  --firmware-version v1.17.1.6-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade \\\n  --require-ota --skip-kiss --resume --background\n

Run only one PlatformIO process in that checkout. Let the matrix finish even when individual targets fail. The packager normally requires a successful full matrix. For an initial release with gaps, scripts/package_cascade_release.py accepts --allow-partial with --version 1.17.1.6, the original completed build status and source commit. It lists every failed attempt in BUILD-FAILURES.md and BUILD-FAILURES.json; each published file must still pass all qualification and memory checks. Repair the failures and add the corrected profiles afterward. PORTABLE-PROFILE-EXCLUSIONS.md and .json separately list standard ESP32 profiles that exceed the 1.25 MiB slot and are redirected to the expanded Full pass. Those size deferrals are not compiler failures. Qualified smaller images remain available; Full alternatives require their matching partition layout.

"},{"location":"old-releases/1.17.1.7/","title":"1.17.1.7","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.7/#meshcore-11717-usa-cascade","title":"MeshCore 1.17.1.7 USA Cascade","text":"

This development release uses 910.525 MHz, 62.5 kHz bandwidth, SF7, and CR5 with Cascade defaults. Saved radio settings take precedence. The firmware source is 2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1.

The release family contains 536 qualified firmware profiles on five GitHub release pages. The companion page is the main release; the repeater and room server, sensor and utility, LoRa OTA, and expanded ESP32 Full pages have the same version suffix. The firmware picker selects the exact board, role, storage and update file.

"},{"location":"old-releases/1.17.1.7/#installation","title":"Installation","text":""},{"location":"old-releases/1.17.1.7/#build-verification","title":"Build verification","text":"

The original matrix produced 532 qualified images and four STM32WL USB Companion flash overflows. Those four were rebuilt individually with packet diagnostics omitted and passed capability, flash size and memory checks. The repair record identifies them. The local release package also verified all 94 ESP32 migration archives and generated SHA-256 sums.

Automated qualification does not establish physical update compatibility on every board. Verify critical deployed devices with the matching image and recovery path before a fleet upgrade.

"},{"location":"releases/1.17.1.5-doc-audit/","title":"1.17.1.5 doc audit","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.5-doc-audit/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.5/","title":"1.17.1.5","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.5/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.6/","title":"1.17.1.6","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.6/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.7/","title":"1.17.1.7","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.7/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.8/","title":"1.17.1.8 USA Cascade","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.8/#meshcore-11718-usa-cascade","title":"MeshCore 1.17.1.8 USA Cascade","text":"

This release uses 910.525 MHz, 62.5 kHz bandwidth, SF7, and CR5 with Cascade defaults. Saved radio settings take precedence. The firmware source is 9053038ffa7f481e4b0af2bb8382210683a2c317.

The release family contains 536 qualified firmware profiles on 5 GitHub release pages. The Companion page is GitHub's Latest release; supporting pages have the same version suffix. The firmware picker and Mesh America catalogs select this family.

"},{"location":"releases/1.17.1.8/#changes-since-11717","title":"Changes since 1.17.1.7","text":""},{"location":"releases/1.17.1.8/#installation-and-partition-expansion","title":"Installation and partition expansion","text":""},{"location":"releases/1.17.1.8/#verification-and-known-limits","title":"Verification and known limits","text":"

The original matrix passed 526 targets and failed ten. The 526 passing packages were preserved byte-for-byte; only the ten failures were rebuilt using corrected build configurations. Five ESP32 Full Companions use 100 contacts. Generic ESP-NOW and Heltec V3 retain 256-frame queues; CT62, Tracker V2 and XIAO C3 use 224-frame queues. Their Full transports remain enabled. Two Indicator LoRa Full images retain 350 contacts and 512 queued messages, with a 320x320 internal render canvas scaled to the 480x480 panel. Three STM32 USB Companions use branch-based switch code generation to fit the existing app boundary. The downloadable REPAIR-PROVENANCE.json records each repair and its tooling commit.

All published firmware passed capability, flash size and runtime memory qualification. nRF52 UF2 files passed address/format checks. All 94 ESP32 migration archives passed package integrity checks. Each release page includes target manifests and SHA-256 checksums. The unit-test workflow for this source passed.

The Test Results archive includes measured reports and 1.17.1.8 Preview 1 qualification evidence. Reports for older firmware remain historical and do not qualify this release.

Qualification does not establish physical compatibility on every supported board. Review the exact image's capability and memory manifests, especially sensor and contact-capacity reductions. Reduced nRF52 LoRa OTA profiles retain only their documented sensor set.

The reported T1000-E/iPhone Bluetooth state where iOS shows a connection but the app keeps waiting has not been conclusively reproduced or resolved. This release includes the verified Bluetooth fixes above; it does not claim that report is fully fixed.

"},{"location":"releases/1.17.1.9/","title":"1.17.1.9 USA Cascade","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.9/#meshcore-11719-usa-cascade","title":"MeshCore 1.17.1.9 USA Cascade","text":"

Firmware source: 88c85108005a30a730fa7b78b2899b210754f3be. Release packages and their public assets passed validation. Hardware observations below retain the exact source and installation route tested.

USA Cascade defaults are 910.525 MHz, 62.5 kHz bandwidth, SF7 and CR5. Saved radio settings take precedence. Match the exact board, role and storage layout before installing an image.

"},{"location":"releases/1.17.1.9/#changes-since-11718","title":"Changes since 1.17.1.8","text":""},{"location":"releases/1.17.1.9/#firmware-profiles","title":"Firmware profiles","text":"

Choose the exact hardware variant and role in the firmware picker. Full includes the transports and optional sensors supported by that board's qualified recipe; it does not add peripherals that the hardware cannot support. Runtime controls select USB logging, power saving and included bridge modes without a separate image.

ESP32 normally has one public Full image per board and role. A migration ZIP may contain a different required target identity; use its supplied Full image for that migration. Station G2's ordinary Full image retains an Observer-named OTA identity, even though it is the combined Full profile.

nRF52 Repeater, Room Server and Sensor provide both Reduced sensors + LoRa OTA and Full supported sensors + LoRa OTA. Pick Full when its supported optional sensors are needed. Both keep the exact board/role/storage identity and need the matching OTAFIX bootloader. Full Companion remains the combined Companion choice. See profile details.

"},{"location":"releases/1.17.1.9/#installation","title":"Installation","text":"

An ESP32 merged .bin installs bootloader, partitions and application over USB. An app-only .bin requires a compatible existing layout. Partition expansion requires the exact board and role migration ZIP from this release family's utility page, or the matching merged image over USB. Follow the ZIP's README and use its supplied Full application. get storage.layout reports the live layout on firmware that supports it.

For the measured ESP32 upgrade route and examples, read Upgrading from 1.17.1.8 over WiFi. Download the selected image before joining the node's update access point.

Check the image's flood-rule capacity before updating a repeater. Move active rules into the supported slots and save them before installing a smaller table. An over-capacity policy is retained on storage but remains inactive until compatible firmware is installed.

Switching from TinyUSB to native hardware CDC changes the USB product name and Linux /dev/serial/by-id path. Update serial consumers to the new device path after the first upgrade. Hardware CDC descriptors are fixed in silicon; firmware cannot retain a custom TinyUSB product name. Do not identify a radio solely by a changing /dev/ttyACM number. See Espressif's explanation.

nRF52 application UF2 and DFU ZIP files do not install a bootloader. Use the exact board/storage OTAFIX mapping in the firmware picker. See the role controls, Full Companion guide, and USB web console.

"},{"location":"releases/1.17.1.9/#verification-scope","title":"Verification scope","text":"

The completed release contains 463 qualified firmware profiles across 5 stable release groups with 1988 verified public assets. All 114 nRF52 Full/Reduced sensor pairs and 94 exact ESP32 board/role migration packages passed checksum, capability, memory, identity and layout checks for firmware source 88c851080. Public asset digests and the exact release family marked GitHub Latest were independently verified.

Download the 1.17.1.9 USA Cascade family and its migration utilities. These verified releases supply the firmware picker and Mesh America catalogs.

Production-code host regressions cover the official configuration GUI's five-second command deadline, app protocol and ACL replies, USB lifecycle, OTA admission/MD5/completion/error handling, shared WiFi credentials, AP and MQTT ownership, and legacy RX-gain loading. Synthetic peripheral tests cover boards unavailable for physical testing. They do not establish every board's physical USB, radio, WiFi or power behavior.

The latest-source G2 ordinary Full Observer passed a separate USB application installation with complete application readback, exact normal runtime and EndF identity, and unchanged checked boot/partition/NVS/OTA-data/filesystem regions. Its read-only post-boot check matched the original identity, passwords, two Admin ACLs, all 22 common settings, seven supported bridge fields and saved extras. Core error flags were zero. MQTT was running with valid effective configuration; broker connectivity was not tested. Original TTY settings, three host services and the Pi network were restored.

The legacy stock 1.14.1 G2 WiFi-only migration is not qualified end to end. The earlier 1e421ff4 trial completed partition expansion and the supplied Full application's WiFi upload, but its final network check reached the client/DHCP deadline without a verified G2 client or any HTTP request. Final image/layout/identity/settings verification through HTTP remains unproven. The successful latest-source USB maintenance above does not qualify that cable-free path. Plan for USB recovery when starting from this stock image; do not rely on the migration trial as a verified LoRa or WiFi recovery procedure.

A published 1.17.1.8 Full V4 installed newer applications over its node AP and a controlled LAN on an already-expanded layout. Its old uploader could miss HTTP completion despite installation and restart; check ver before retrying an apparently stalled page. Current-source physical V4 testing was unavailable. Earlier V4/G2 AP uploads and quiet USB tests remain evidence for their recorded revisions. A bridge-setting command also missed its five-second deadline; later healthy uptime and readback did not turn that timeout into a pass.

The detailed browser OTA and release-candidate report retains the complete source-specific results, failures and recoveries. See also the native V4 console report, Linux Web Serial report, and USB backpressure results. These checks do not measure fleet reliability, MCU sleep/current draw, stack high-water or sustained RF throughput, or qualify every browser, phone, operating system, bootloader or starting firmware version.

"},{"location":"research/","title":"Research","text":"

View this page on MeshCore Docs.

"},{"location":"research/#research","title":"Research","text":"

Engineering investigations, design notes, diagnostic methods, and historical update-chain studies live here. For measured outcomes, see Test Results; for current installation instructions, use the documentation homepage.

"},{"location":"research/#memory-and-storage","title":"Memory and storage","text":""},{"location":"research/#diagnostics-and-update-chain-studies","title":"Diagnostics and update-chain studies","text":""},{"location":"research/companion_contact_cache/","title":"Companion contact cache","text":"

View this page on MeshCore Docs.

"},{"location":"research/companion_contact_cache/#companion-contact-caches","title":"Companion contact caches","text":"

Full Companions on ESP32 and nRF52 without PSRAM, plus nRF52 USB/BLE profiles with more than 300 contacts, keep 16 outgoing paths and 16 shared secrets in RAM. Every contact remains in the contact table; selecting a different contact loads its saved path as needed. This recovers about 25.4 KiB of internal RAM in the 350-contact ESP32 qualification builds.

See the qualification results for the six-board RAM comparison, physical tests, and shared-secret timings.

PSRAM boards retain their existing complete contact table in external RAM. Lower-contact legacy profiles and ordinary ESP32 USB/BLE/WiFi Companion profiles retain inline paths and secrets. High-contact nRF52 USB/BLE profiles (including Wio Tracker L1) enable the same cache, keeping all 350 contacts while making room for the shared 256-frame offline/DM pool. The build flag MESH_CONTACT_CACHE=0 or 1 overrides this policy for qualification; it is not a runtime feature switch.

The ordinary Wio Tracker L1 BLE build reproduced a runtime RAM failure with 48,964 bytes available against 59,968 required. Enabling the existing cache for high-contact nRF52 profiles recovered 30,200 bytes: the linked build has 79,164 bytes available and a 19,196-byte margin against the unchanged budget. It keeps 350 contacts, 40 channels and 256 normal offline frames. As on the cached RAK profiles, up to 15 held one-key DMs temporarily use queue slots; their storage is shared instead of reserving a second pool. This is a linked-capacity result, not a physical load/soak test.

"},{"location":"research/companion_contact_cache/#paths-and-persistence","title":"Paths and persistence","text":"

The 16 resident paths use least-recently-used replacement. Contact records keep small handles to their saved paths, including 1-, 2-, and 3-byte hop hashes. The existing 152-byte contact record and Companion app frame formats are unchanged. ESP32 uses /contacts3; nRF52 also supports its paged contact store and migration from /contacts3. An erase is not required.

A hot path needs no flash read. A cold path is read from its existing contact record. ESP32 reuses one open contact-file reader to avoid repeated SPIFFS metadata scans during synchronization and saving. It closes that reader before replacing the file or recovering a saved transaction.

Changing a copied contact preserves the old path for a pending app response or rollback. Dirty entries are saved before eviction. If storage cannot be read, written, or represented completely, the operation fails instead of sending along a different route. Contact I/O failures request bounded recovery between callbacks. A raw cached-path read fault first rechecks the current immutable paths without resetting accepted RAM changes or their save deadline. Persistent faults or uncertain file publication reload a readable live image, otherwise a readable backup, and discard only unrecoverable contact data if neither works. Recovery releases a pending contact-list snapshot before resetting the table but preserves its requesting route; the changed table revision starts a fresh list that can finish normally. Explicit session cancellation still stops the stream. nRF52 preserves healthy pages when another page is damaged. No archival .bad files are kept. If damaged contacts cannot be removed, the node remains usable with volatile contacts; explicit saves still report failure, while reboot/shutdown remain available. Identity, credentials, channels and unrelated settings are not erased. Allocation/capacity failures and shared filesystem-migration authority errors are not corrupt-contact evidence and do not authorize this reset. Failed source discovery also preserves contact files; if authority stays unknown, use volatile contacts instead of deleting readable pages or selecting a potentially stale view. The path checksum also rejects a cold path changed underneath a live handle; it does not add a new checksum to the legacy ESP32 on-disk format.

"},{"location":"research/companion_contact_cache/#legacy-interrupted-contact-saves","title":"Legacy interrupted contact saves","text":"

ESP32, RP2040 and STM32 Companions can load the complete records before a partial final record in a legacy /contacts3 file. This includes cached and uncached ESP32 builds. The file must contain at least one whole 152-byte record, and every whole record must be readable. The partial record is ignored; loading does not truncate the source. The next successful ordinary contact save writes the complete loaded contacts through the platform's existing transaction writer.

On ESP32 and RP2040, this salvage applies only when the presence check reports /contacts3.bak absent. A readable complete backup takes precedence over the incomplete primary. An unreadable backup keeps the existing bounded retry and recovery behavior; the partial primary is not used to declare that backup expendable. ESP32 also defers salvage when backup presence is uncertain. RP2040's filesystem API does not distinguish a metadata I/O error from a missing file. STM32 does not select a contact backup.

This recovers a readable prefix from an interrupted older in-place save, not arbitrary corruption. Legacy records have no file checksum, and the existing route validation and repair still apply. A file with no complete record, or one that remains unreadable after recovery retries, can still be discarded by contact-only recovery. Failure to remove damaged data leaves contacts volatile and explicit saves unavailable, without erasing identity or unrelated settings.

nRF52 retains its separate legacy-size validation and paged-store migration; this partial-tail salvage does not apply to nRF52. The existing ExtraFS recovery guide describes its separate mount and metadata recovery boundaries.

"},{"location":"research/companion_contact_cache/#shared-secrets-and-power","title":"Shared secrets and power","text":"

Both platforms reuse the 16 most recent secrets from RAM. Cache misses use a platform-specific policy:

Platform Miss behavior ESP32 Recalculate the secret without reading or writing a secret file. nRF52 Try a saved LittleFS entry, then recalculate if it is unavailable.

On the V4, a measured SPIFFS secret lookup took 132,427 microseconds while key exchange took 21,932 microseconds. ESP32 therefore uses recalculation on a RAM-cache miss. It avoids secret-file writes and reduces time awake for this measured workload. These are elapsed times, not measurements of energy in joules; other ESP32 hardware has not been physically benchmarked.

On a RAK3401, a LittleFS lookup took 7,812 microseconds versus 31,250 microseconds for key exchange. The nRF52 backend therefore uses saved flash entries to avoid repeat key exchange when a usable entry exists, including after reboot. Saved entries match the full peer public key and a fingerprint of the local key pair, and have a record checksum. Importing a different private or public key invalidates old results. A corrupt or missing saved entry is recalculated. The native tests use the firmware's actual Ed25519 key-exchange library and verify agreement with the peer's independently calculated secret.

Derived secrets are expendable. Their storage keeps a reserve for contacts, preferences and filesystem metadata; a full filesystem leaves the calculated secret usable in RAM. nRF52 packs 56 entries into a 3,844-byte page to use its 4 KiB flash blocks efficiently. Small filesystems may not have space to persist secrets for all 350 contacts. A miss without a usable saved entry still performs key exchange.

MESH_CONTACT_SECRET_FLASH_CACHE=0 or 1 overrides the miss policy for qualification builds. The optional ESP32 flash backend groups eight entries per file. The normal ESP32 policy does not create or use those files.

"},{"location":"research/companion_contact_cache/#inspect-the-cache","title":"Inspect the cache","text":"

From the Full Companion's ASCII USB terminal or authenticated local CLI:

get contact.cache\n

Cached builds report paths=16 secrets=16, miss=calculate or miss=flash, plus these boot-session counters:

Field Meaning ram_hits Shared secret reused from RAM. flash_hits Shared secret loaded from flash without recalculation. calculations Shared-secret key exchanges performed. save_skips Calculated secrets that could not be persisted, including storage reserve/backoff.

Uncached builds report paths=inline secrets=inline. The normal memory command on supported ESP32 Full Companions shows live heap and queue capacity.

get contact.cache.timing reports the last successful flash lookup and last key calculation in microseconds. Zero means no such operation has completed since boot. These timings exclude secret writes and are useful for comparing the work avoided on a valid flash hit.

"},{"location":"research/companion_contact_cache/#offline-messages-during-mota","title":"Offline messages during mOTA","text":"

Contact caches are independent of the offline message queue. Builds using the shared mOTA queue retain 256 frames normally and 128 while mOTA owns its workspace, then restore 256. No unread frames are discarded to start mOTA: synchronize the queue and retry if it exceeds the retained capacity. PSRAM-backed queues keep their existing capacity and allocation policy.

Each queue frame occupies 177 bytes. Retaining 32 instead of 128 would make another 16,992 bytes (16.6 KiB) available inside the shared storage. It would not increase free heap by itself: the normal 256-frame storage is statically reserved, and the current mOTA context already fits in the loaned half. More mOTA scratch data would have to share that space to obtain an additional RAM saving. These changes keep the existing 128-frame mOTA limit.

"},{"location":"research/companion_contact_cache/#build-the-six-capacity-trials","title":"Build the six capacity trials","text":"

The optional environments in platformio.nimble.ini combine NimBLE, these contact caches, and the shared mOTA queue at 350 contacts, 40 channels and 256 normal offline frames. They remain outside the ordinary release matrix. The ordinary six constrained release recipes still use their established 150-contact limits pending wider hardware qualification.

Enable platformio.nimble.ini as described in the NimBLE trial guide, then build one environment at a time. For example:

OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \\\n  Heltec_v3_ram_trial_companion_radio_full_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-cache-trial \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n
Hardware Trial environment Heltec V3 Heltec_v3_ram_trial_companion_radio_full_nimble Wireless Tracker Heltec_Wireless_Tracker_ram_trial_companion_radio_full_nimble Tracker V2, FEM on heltec_tracker_v2_ram_trial_companion_radio_full_femon_nimble Heltec CT62 Heltec_ct62_ram_trial_companion_radio_full_nimble XIAO ESP32-C3 Xiao_C3_ram_trial_companion_radio_full_nimble Generic ESP-NOW Generic_ESPNOW_ram_trial_companion_radio_full_nimble

The firmware RAM guard remains enabled. A successful linked budget is distinct from live free heap and physical Bluetooth/LoRa qualification on each board.

The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and coredump stay at the same addresses as the ordinary huge_app.csv Full layout. Install the trial's merged image over USB first to install its partition table. Subsequent WiFi updates must fit the new slots. An application-only update from the ordinary single-slot Full layout cannot change that layout or enable WiFi OTA.

"},{"location":"research/companion_contact_cache/#shared-contact-index-storage","title":"Shared contact index storage","text":"

Companion and terminal-chat builds share compact contact indexes in BaseChatMesh. Tables with at most 65,535 total slots use 16-bit sort and matching-result entries; larger configurations retain the original int type. Contact limits, PSRAM allocation fallback and saved/wire formats are unchanged.

On the existing 32-bit platforms, a static 350-contact table saves 764 bytes from these arrays; a static 100-contact table saves 264 bytes. A 350-contact PSRAM configuration saves 96 internal bytes and 716 PSRAM bytes. Cached nRF52 records also shrink from 96 to 92 bytes by moving runtime storage bookkeeping into an alignment gap. The matched RAK4631 Full build saves 2,200 static RAM bytes in total. These figures do not describe infrastructure ACL tables.

See the 2026-10-19 qualification report for source-bound builds, tests and remaining board-specific memory limits.

"},{"location":"research/companion_contact_cache/#regression-checks","title":"Regression checks","text":"
python3 -B test/test_contact_indices.py\npython3 -B test/test_companion_contact_lifetime.py\npython3 -B test/test_contact_cache.py\npython3 -B test/test_companion_uncached_storage.py\npython3 -B test/test_companion_contact_partial_tail.py\npython3 -B test/test_companion_contact_persistence_contract.py\npython3 -B test/test_companion_contact_stream_contract.py\npython3 -B test/test_nrf52_extrafs_contract.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_t096_full_memory.py\npython3 -B test/test_firmware_ram.py\n

The cache tests compile the production path/secret cache code and extracted production persistence, routing and request functions with filesystem/radio adapters. They exercise 350 routes, eviction, snapshot rollback, page migration, identity changes, read/write/rename faults, handle exhaustion, packet release on a failed path read, and recovery after a simulated reset between file renames. They do not simulate a physical flash power cut. The partial-tail tests also check readable-prefix preservation, backup precedence, deferred normalization on save and reload on ESP32, RP2040 and STM32 using filesystem adapters. Real flash power-loss and full-volume qualification remains separate from these host checks.

tools/hil/contact_cache_serial_stress.py checks all 350 paths over USB, replaces them with a different generation, and optionally exercises secret eviction. Use an otherwise empty test node and exclusive access to its Companion data port. --populate writes test contacts; --secrets transmits 23 directed LoRa datagrams to synthetic peers. For an ESP32 cache trial:

python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \\\n  --populate --generation 1 --secrets --expect-calculate --reboot\npython3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \\\n  --generation 1 --secrets --expect-calculate\n

Use the actual device path. Omit --expect-calculate for the nRF52 flash policy. The second invocation verifies the persisted paths after reboot.

"},{"location":"research/esp32_full_build_audit_2026-10-06/","title":"Esp32 full build audit 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#why-esp32-builds-still-have-options-besides-full","title":"Why ESP32 builds still have options besides Full","text":"

Audit date: 2026-10-06. Source baseline: 2ee62b981435df7dc4bcd79de39e61a4d5e98652 on keymindCascade.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#implemented-consolidation","title":"Implemented consolidation","text":"

After the audit, the ordinary ESP32 release policy was changed to publish Full only. The exact selector replay goes from 317 planned images to 172, removing all 145 standard/reduced slots. All 172 previous Full target identities, compiled PlatformIO environments, and Full overlay flags remain identical; no new Full compilation recipe or capacity requirement is introduced. The baseline findings below describe the policy before this change.

The ordinary board/role resolvers omit ESP32 reduced aliases. Every selected infrastructure Full image is built once, using its existing exact recipe and identity. Generated Full Companion keeps its registered USB/BLE/WiFi source environment. Resume packaging removes stale standard, minimal, transport, runtime-setting, and development artifacts instead of reintroducing them. Unverified ordinary Full images fail qualification. Other chip families keep their existing profile policies, and KISS remains a distinct host-modem role.

The measured T-LoRa UART/MQTT transport split and capacities within Full are preserved. Direct explicit recovery recipes and reviewed partition-migration packages remain available separately. The existing same-partition identity allowlist is deliberately retained for OTA compatibility checks; broadening release selection does not authorize an app-only update across layouts.

Not every board has a reviewed wireless expansion package. Targets outside the migration catalog require USB/serial for a partition-table change. The updated migration guide explains this distinction and the supported cable-free routes.

The complete baseline inventory contains all 636 primary logical names and four optional local HIL names, including their exact Full mappings. The post-change selector comparison records zero standard/reduced slots, zero duplicate identities, and no change to any previous Full source recipe.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#decision-being-audited","title":"Decision being audited","text":"

Prefer one Full image for each exact board and firmware role. A smaller image needs a demonstrated RAM limit or a demonstrated failure to fit the largest safe application layout for that board and role. An image failing an old, small OTA slot does not establish either condition.

This audit examines the resolved PlatformIO recipes and the release builder's generated aliases and routing. It distinguishes options a user can explicitly build from artifacts that the normal release matrix actually publishes. Existing measurements are identified by their original revision; they are not presented as fresh builds of the audit baseline. This is a source and evidence audit followed by release-policy consolidation and focused validation, not a rebuild or hardware qualification of every ESP32 target.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#what-full-means","title":"What Full means","text":"

Full is a feature profile within a firmware role. A Full Companion combines the board's qualified app transports and text terminal. A Full Repeater, Room Server, or Sensor keeps the supported infrastructure features, logging, and OTA support. These roles have different jobs and remain separate images. Different radios, pin maps, displays, and expansion hardware also remain separate exact board recipes.

Full does not mean every board has identical contact counts, queue lengths, or routing-table capacities. Reducing a table to meet a measured RAM budget can preserve the Full feature set. Nor does a compiled feature have to run all the time: runtime settings can turn off WiFi, logging, or other supported services without installing a different image.

There are two implementations with confusingly similar names. Named *_companion_radio_full targets are Full Companion even when the infrastructure ESP32_FULL_BUILD overlay is not set. Counting only that overlay would incorrectly call these images non-Full.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#why-the-other-options-exist","title":"Why the other options exist","text":""},{"location":"research/esp32_full_build_audit_2026-10-06/#genuine-ram-constraints","title":"Genuine RAM constraints","text":"

The T-LoRa V2.1-1.6 combined UART, MQTT, and ESP-NOW image has a retained linked measurement of 190,488 available internal bytes against 196,984 required bytes: a 6,496-byte shortfall. It therefore retains separate UART/ESP-NOW and MQTT/ESP-NOW Full images. This is evidence for a transport-combination exception, not for dropping Full on the entire board. See the firmware RAM policy.

Other constrained boards already run Full with board-specific capacities. The 2026-10-02 Companion audit records passing ordinary Full builds for Heltec V2/V3, Wireless Tracker, Tracker V2, CT62, XIAO C3, Generic ESP-NOW, T-Beam, and T-LoRa. Some margins are small: Generic ESP-NOW had 1,536 bytes and Tracker V2 had 2,254 bytes above the unchanged linked runtime allowance. A pass establishes that exact image's budget, not sustained runtime or room for future feature growth.

PSRAM does not eliminate all RAM limits. Bluetooth, WiFi, DMA buffers, and some allocations still require internal RAM. The guards exclude PSRAM from the internal budget; classic ESP32 additionally has a static DRAM-placement limit. See the classic ESP32 budget.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#older-installed-partitions-and-update-identities","title":"Older installed partitions and update identities","text":"

Portable standard and *_lora_ota_no_external_sensors images can fit a historical 1.25 MiB OTA slot. On qualified targets, Full fits after expansion, but ordinary app-only OTA cannot rewrite its partition table. LoRa OTA also checks the exact target identity and layout; merely renaming a Full image does not make it a valid update for an old installation.

This explains recovery and migration artifacts. It does not establish a hardware capacity exception. Once a supported device has migrated, its ordinary role image should be Full. A small migration bridge remains useful as a tool, rather than as a second everyday firmware choice. The migration guide describes the supported layouts, preservation checks, and current limitations of cable-free migration.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#legacy-transport-and-settings-recipes","title":"Legacy transport and settings recipes","text":"

Many USB-only, BLE-only, WiFi-only, Terminal Chat, power-saving, and FEM-default recipes still exist so an exact old name can be built. Where an exact Full replacement exists, the normal release matrix already suppresses many of these duplicates. Their presence in PlatformIO is not proof that they must appear in the firmware picker or that Full cannot fit.

The release builder also retains explicit --standard and explicit reduced OTA requests. These selectors intentionally override automatic Full selection. That is a compatibility/developer policy, not a measured RAM or maximum-flash finding.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#different-roles-and-development-tools","title":"Different roles and development tools","text":"

KISS modem firmware exposes a raw modem protocol; it is not the Companion application protocol or a MeshCore Repeater. Its existence is a role decision, not an argument that the board cannot support Full. Partition migrators, expanders, legacy-layout seeds, and simulation fixtures likewise have a specific test or installation job. The normal release matrix excludes those development tools. They should be considered separately from everyday non-Full node images.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#eligibility-rules-and-missing-qualification","title":"Eligibility rules and missing qualification","text":"

The infrastructure Full selector uses recipe metadata, including admin, MQTT, and ESP-NOW markers. Companion Full qualification also needs an exact combined recipe or a qualified replacement. These rules can exclude an option without measuring its RAM or largest safe flash layout.

An absent Full recipe, an eligibility-list omission, or an old compile failure is an unproven exception. It requires qualification or an explicit product decision; it must not be described as a demonstrated capacity failure.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#what-counts-as-the-maximum-partition","title":"What counts as the maximum partition","text":"

Flash is divided among boot metadata, applications, and persistent data. Infrastructure self-update requires two application slots; it cannot use the entire chip as one app slot. Companion Full can be a host-backed LoRa OTA sender without being a LoRa self-update destination, so some Companion layouts intentionally use one larger application slot and require USB for self-updates.

T-Beam 1W Full Companion keeps LilyGo's factory-compatible single 3 MiB app slot even though the board has 16 MiB flash. This is a boot/layout and source-role contract, rather than a failure of Full to fit its physical flash.

Physical flash Normal expanded infrastructure slot Upper dual-slot ceiling with only 64 KiB data storage 4 MiB 2,031,616 B (0x1F0000) 2,031,616 B (0x1F0000) 8 MiB 3,342,336 B (0x330000) 4,128,768 B (0x3F0000) 16 MiB 6,553,600 B (0x640000) 8,323,072 B (0x7F0000)

The upper ceilings assume 64 KiB before the first application, equal 64 KiB-aligned application slots, and 64 KiB of filesystem storage, with no additional coredump or mandatory data region. They are arithmetic bounds, not qualified partition plans. Required data, preserved offsets, or a larger minimum filesystem can lower the safe bound. The existing 8/16 MiB layouts retain substantially more storage and a coredump partition; their current slot sizes are therefore not the absolute physical maximum.

SenseCAP Indicator's 8 MiB preserved-data Full layout is a further exception: it keeps 2,621,440-byte (0x280000) OTA slots and the old filesystem/coredump addresses. Its first slot ends at the preserved filesystem boundary, so both usable OTA slots have that limit despite the larger chip. This is a data-layout constraint; it must be distinguished from an absolute flash-capacity limit.

A Full size failure against the legacy 1.25 MiB ceiling, or against an existing 8/16 MiB default slot alone, is insufficient evidence for a permanent minimal image. The audit must first establish the board's actual flash size, the role's update requirements, and its minimum safe persistent-data layout.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#baseline-audit-inventory-and-outstanding-qualification","title":"Baseline audit inventory and outstanding qualification","text":"

The primary repository configuration contains 500 concrete ESP32 environments and 136 builder-generated aliases: 636 logical target names. They are recipes and identities, not 636 separate everyday images. The canonical resolver keeps 374 names, including 43 KISS modems; the default non-KISS node selection contains 331 names.

Evaluating the real release-matrix selectors and the per-build Full promotion rules gives 317 planned ESP32 node artifact slots: 172 Full and 145 non-Full. These are pre-build selections, not an assertion that 317 current binaries were produced. Size-driven fallback, failed qualification, and the final package's capability-based consolidation can change the final output.

Planned non-Full group Count Why selected Finding under the requested capacity-only exception rule *_lora_ota_no_external_sensors 99 Generated reduced OTA identity explicitly selects standard Compatibility/profile policy; no per-target RAM or maximum-layout failure is required before selecting it Legacy images in the partition-migration inventory 24 Builder deliberately retains portable output during transition to expanded Full Old installed-layout compatibility, rather than maximum hardware capacity Other complete standard images with a supported Full route 22 Their names are outside the curated Full-only and migration promotion lists Missing policy promotion/partition qualification; not demonstrated capacity exclusions

Every primary canonical node target has a supported Full route, after mapping a reduced identity to its feature-rich base. That establishes routing, not that every current Full image has passed a fresh RAM/flash build or hardware validation. No retained current-baseline failure establishes that all 145 non-Full selections are necessary because Full cannot fit RAM or the largest safe partition.

The 22 complete standard choices outside the migration list are:

These are candidates for exact Full/layout qualification, rather than an approved instruction to relabel their old binaries. The T-LoRa transport combination exception above still applies.

All 52 canonical ESP32 Companion selections are already named Full; there are zero canonical non-Full Companions. All 21 standalone ESP32 Terminal Chat names map to exact Full Companion replacements. Most surviving old transport recipes are therefore direct-build compatibility choices rather than current release duplication.

This PC's ignored local configuration additionally loads four ESP32 radio bench fixtures. Three are excluded by the utility-name filter. The fourth, profile_four_tx_v4_rx, escapes that filter and adds one standard selection when the bench configuration is loaded. It is a local release-leakage risk, not a clean primary-configuration node option; it is excluded from the counts above and listed separately in the inventory.

The capacity evidence snapshot retains 38 historical linked reports for 14 ESP32 target identities, together with original failures, repairs, app sizes, and partition tables. All 38 reports pass their recorded budgets. Every bound artifact still present in this checkout matches its saved hash; eight referenced files are absent, so their saved reports cannot be fully rebound here. These results are historical evidence, not complete current-source qualification.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#method-and-policy-locations","title":"Method and policy locations","text":"

PlatformIO resolved the configuration once. The builder was then sourced with that cached configuration and a failing pio stub; the audit evaluated its actual selectors and intercepted build calls without compiling or flashing. The local configuration only adds HIL include files, so those 11 optional environments (four ESP32) were removed from the cached primary inventory without changing ordinary resolved options. A second selector replay confirmed the primary counts. Independent policy and memory reviews checked the results.

Relevant source locations at the audited baseline are:

The audit itself did not flash a device. The resulting consolidation changes ordinary release routing and resume selection; existing firmware recipes and capacity guards are retained.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#fresh-validation-of-the-consolidation","title":"Fresh validation of the consolidation","text":"Exact Full target Final app size / slot Linked internal available / required Result LilyGo_Tlora_C6_repeater_ 2,010,024 / 2,031,616 B 283,088 / 139,200 B Passed; 21,592 B flash margin Heltec_v2_companion_radio_full 2,275,368 / 3,342,336 B 184,680 / 173,840 B Passed; 10,840 B linked RAM margin

These sequential embedded builds use the audit baseline plus the release-rule changes, version v1.17.1.9-halo-keymind-cascade-dev-fullaudit. The C6 uses its supported expanded 4 MiB dual-OTA layout. The generated V2 Full Companion correctly compiles Heltec_v2_companion_radio_wifi; its named Full identity is preserved. Both produced verified capability manifests and firmware-bound RAM reports. No physical flashing or runtime qualification is claimed.

Focused release/profile regressions cover the real matrix scheduler, exact Companion recipe mapping, stale-artifact selection, preserved T-LoRa transport split, direct recovery access, and unchanged non-ESP32 choices. The complete test/test_build_profiles.sh suite passed, as did 69 Python tests covering selection, partitions, RAM, and packaging. The 58 selection/partition/RAM cases ran on native Linux; the 11 packaging cases ran on Windows Python 3.13 because Linux Python 3.10 lacks the packaging code's pre-existing hashlib.file_digest API. The selector evidence records these results and the fresh builds' bound artifact hashes.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#recommended-release-rule","title":"Recommended release rule","text":"

Use Full as the everyday choice per exact board and role wherever it passes the existing RAM guards and the largest safe, supported partition layout. Retain capacity-tuned Full recipes when they preserve the board's features. Keep genuinely incompatible transport combinations separate, with their measured reason documented. Present migration/recovery tools separately from ordinary firmware and retain old names as compatibility aliases where useful.

Require a passing ELF-bound RAM report and the exact application/partition sizes for every capacity exception. Do not allow an unexplained name-based exception to masquerade as a hardware limit. Runtime startup, reconnect, service-toggle, OTA, and sustained heap checks remain necessary for Full qualification; build success alone does not cover them.

"},{"location":"research/esp32_memory_budget/","title":"Esp32 memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"research/esp32_memory_budget/#classic-esp32-image-memory-budget","title":"Classic ESP32 image memory budget","text":"

Classic ESP32 has 320 KiB of internal DRAM, but at most 160 KiB can hold statically allocated data. The remaining DRAM is available only through the runtime heap. Bluetooth, tracing, and SDK reservations further constrain the usable region. See Espressif's memory types documentation.

An image can therefore approach its static limit while PlatformIO reports less than 40% of the 320 KiB total in use. PSRAM does not expand the region used by ordinary internal .data and .bss allocations.

"},{"location":"research/esp32_memory_budget/#build-enforcement","title":"Build enforcement","text":"

Every classic ESP32 environment inheriting esp32_base runs scripts/check_esp32_dram.py. The release builder also adds the script when an ESP32 profile replaces the inherited extra scripts. The check runs before image generation, merging, and uploading, including cached nobuild uploads. It requires the ELF and linker map to be present for those cached operations. The check script must load after merge-bin.py so the merge target retains its pre-action. A merge also requires the bootloader/partition flash layout; nobuild cannot create a bootable merged image when that layout is absent.

The check reads dram0_0_seg from the final linker map. SDK reservations are already reflected in that region; Bluetooth's reservation is not subtracted a second time. The occupied span runs from the region origin to _heap_start, including .data, .bss, .noinit, and alignment gaps. The allowed region is the smaller of the linker's region and 160 KiB.

Builds must leave at least 8 KiB free within that static region. This is an additional project margin against near-full images, not an Espressif hardware limit or a prediction of free heap. A profile may require a larger margin with custom_esp32_static_dram_reserve in bytes; values below 8192 are rejected. Missing or unrecognized map boundaries also fail the check.

For the Heltec V2 build used to investigate the repeater report:

Shared repeater defaults Static span Static region Remaining Result Previous 255 scope slots 124,020 B 124,580 B 560 B Rejected Tuned 31 scope slots 108,308 B 124,580 B 16,272 B Passed

These measurements remove the board's existing scope-capacity override to exercise the shared defaults. The normal Heltec V2 profile already used 31 scope slots. Exact sizes vary with build flags and SDK versions.

To inspect an existing classic ESP32 build:

python3 scripts/check_esp32_dram.py .pio/build/Heltec_v2_repeater/firmware.map\npython3 test/test_esp32_dram.py\n

ESP32-S2, S3, and C-series chips have different memory maps and do not inherit this classic ESP32 check.

"},{"location":"research/esp32_memory_budget/#boot-versus-running-memory","title":"Boot versus running memory","text":"

The build gate checks static placement needed to load and start the image. It never counts heap-only RAM toward that static budget. Allocations made with new or malloc in global constructors consume heap and are not included in the static span. Moving a table there changes its placement, not total RAM consumption.

Successful build checks do not prove that every runtime configuration will boot. Hardware validation must still cover startup allocations and the enabled Wi-Fi/Bluetooth services, recording minimum free internal heap and the largest free internal block. Available heap changes during initialization; for example, the Arduino 2.x repeater releases unused Bluetooth controller RAM in initArduino(), after global constructors have run.

"},{"location":"research/firmware_choice_followup_2026-10-06/","title":"Firmware choice followup 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"research/firmware_choice_followup_2026-10-06/#further-firmware-choice-consolidation-audit-6-october-2026","title":"Further firmware choice consolidation audit - 6 October 2026","text":"

Baseline: pulled commit 08bf174ea30e2e075cdd9fc74111580662b461bb. The user requested fewer choices after the ESP32 Full-only release change. This follow-up combines menu/picker changes with a source, resolved-configuration, and retained-artifact audit. It did not build a matched nRF52 Full/Reduced pair or run hardware tests. Image-removal candidates below remain unqualified; the implemented changes simplify selection without changing firmware sources, partition layouts, or deployed update identities.

"},{"location":"research/firmware_choice_followup_2026-10-06/#implemented-selection-changes","title":"Implemented selection changes","text":"

The interactive board menu now uses canonical Full ESP32 targets and omits transport/default aliases, compact recovery recipes, and development utilities. On nRF52, normal names and aliases that already build the exact same qualified Full/Reduced pair appear once. Distinct external-storage and deployed OTA IDs remain selectable. Exact hidden names remain directly buildable.

Platform Supported exact names before Ordinary menu choices after ESP32 638 217 nRF52 403 209 RP2040 25 19 STM32 16 16 Total 1,082 461

These are menu targets, including KISS; they are not firmware-file counts. The current ordinary ESP32 release still has 174 Full node images (52 Companion, 58 Repeater, 47 Room Server, 17 Sensor). The pull added two Station G3 Sensor recipes to the previous 172-image plan. No Full image was retired in this follow-up. The selector evidence records exact menu/release agreement, zero duplicate menu names, and the remaining OTA identity constraints.

After selecting an exact board and role, the web picker skips logging, connection, and profile filters when they do not distinguish firmware builds. Logging and independent bridge controls remain in the result's setup directions; linked logging/bridge settings still select their corresponding commands. A sole install format is inferred; multiple install operations remain explicit. Real alternatives stay visible even after mutually narrowing filters are selected. The measured T-LoRa split and nRF52 sensor/storage alternatives remain available.

The complete build-profile suite, 28 ordinary release-selection tests, and two picker-package tests passed. Picker tests cover skipped requirements, real alternatives, install formats, existing share links, and runtime command rendering. The complete form was also exercised in the in-app browser using local replayed release metadata: a sole app-only Full download, explicit merged versus app-only installation, nRF52 Full/Reduced UF2 versus ZIP choices, runtime logging changes, and a partial share link with WiFi logging and ESP-NOW enabled. The correct files and commands remained selected. This used fixture metadata and did not download or install firmware, contact a device, or publish the website.

"},{"location":"research/firmware_choice_followup_2026-10-06/#nrf52-sensor-pairs-policy-versus-demonstrated-need","title":"nRF52 sensor pairs: policy versus demonstrated need","text":"

At this baseline, is_nrf52_sensor_ota_pair_target() in build_legacy.sh includes every nRF52 repeater, room-server, and sensor recipe, including external-QSPI and SD recipes. run_nrf52_sensor_ota_pair() requires Full to pass, then builds Reduced; a successful Reduced build cannot replace a failed Full build. Thus the pair is a blanket publication policy, not evidence that each board needs two images.

Classification Evidence Defensible action Same declared sensor functionality The 31 concrete recipes below enable none of the drivers removed by Reduced. GPS is outside the omitted set. Present Full as the ordinary choice. Treat image deduplication as a candidate until packaged applications are compared. Actual optional-driver difference Other recipes can enable environmental/ranging drivers; non-RAK recipes can also lose INA monitors. RAK3401/4631 Reduced keeps the four INA drivers and compatible GPS. Full for normal installs; Reduced can remain an advanced/recovery choice with its missing drivers stated. Disabling a sensor at runtime does not remove its compiled flash cost. Demonstrated internal-OTA headroom constraint The historical RAK3401 compact chain has several exact transitions with zero staging margin. Current staging capacity depends on the installed image and exact delta, not just the board's nominal flash. Preserve qualified reduced endpoints and exact-base update routes. This is not proof that every current Full/Reduced pair is necessary. Current measured mandatory pair No current matched Full/Reduced application and delta-package comparison was found in the retained local evidence. Do not claim a current pair is required by measured capacity, or claim Reduced can safely be deleted everywhere.

The baseline CLI matrix explicitly acknowledges that boards without optional sensor drivers still receive two named choices, with small or zero savings. This audit found no qualified artifact pair proving byte-identical firmware for those boards. The retained RAK3401 1.17.1.8 Reduced RAM report passes with 67,660 available bytes versus 66,904 required, a 756-byte margin; it is historical, not a current matched-pair comparison. The retained T1000-E reports concern Full Companion, outside this infrastructure pair policy.

Full already includes targeted RAM capacity tuning: T096/T1 repeaters use four flood rules, and RAK3401 unified uses 47. The baseline script records these as measured internal-RAM reductions while retaining the Full sensor support. They justify tuning within Full, not a second sensor profile by themselves.

"},{"location":"research/firmware_choice_followup_2026-10-06/#31-concrete-recipes-with-no-enabled-removable-sensor-driver","title":"31 concrete recipes with no enabled removable sensor driver","text":"

Source: out/esp32-full-audit-2026-10-06/platformio-current-resolved.json, the current resolved configuration retained for the baseline audit. Definitions and undefinitions were processed in order, and definitions equal to zero were excluded. The tested omitted set is the 12 environmental/ranging macros in apply_lora_ota_no_external_sensors_profile(), plus four INA macros except on the RAK recipes which retain them. This is a recipe-level result, not an image hash result or a count of canonical published outputs.

Heltec_mesh_solar_repeater\nHeltec_mesh_solar_room_server\nHeltec_tower_v2_repeater\nHeltec_tower_v2_room_server\nKeepteenLT1_repeater\nKeepteenLT1_room_server\nLilyGo_T-Echo_Card_repeater\nLilyGo_T-Echo_Card_room_server\nLilyGo_T-Echo-Lite_repeater\nLilyGo_T-Echo-Lite_room_server\nMeshTracker_X1_repeater\nMeshTracker_X1_room_server\nMesh_pocket_repeater\nMesh_pocket_room_server\nmuzi_base_duo_repeater\nmuzi_base_duo_room_server\nmuzi_base_uno_repeater\nmuzi_base_uno_room_server\nNano_G2_Ultra_repeater\nNano_G2_Ultra_room_server\nt1000e_repeater\nt1000e_room_server\nt1000e_sensor\nThinkNode_M1_repeater\nThinkNode_M1_room_server\nThinkNode_M3_repeater\nThinkNode_M3_room_server\nThinkNode_M4_repeater\nThinkNode_M4_room_server\nThinkNode_M8_repeater\nThinkNode_M8_room_server\n

ThinkNode_M1_repeater already declares external QSPI OTA. The plain MeshTower repeater name is an alias for the SD primary, so this list must not be blindly converted into 31 release removals. Aliases, full source options, board headers, storage layout, and deployed target identities must be resolved first.

"},{"location":"research/firmware_choice_followup_2026-10-06/#qualification-before-removing-an-image","title":"Qualification before removing an image","text":"

Start by showing Full for ordinary installation and moving Reduced to advanced or recovery selection. Preserve existing artifacts and update identities while qualifying actual duplicate removal. For each candidate:

  1. Build both profiles sequentially from the same source/toolchain, with the existing flash, RAM, capability, packaged-application, and OTA checks.
  2. Extract the DFU application's bin_file from its ZIP manifest and compare its complete bytes, EndF identity/body hash, image length, and layout. Outer UF2/ZIP hashes alone can differ because of packaging and are insufficient.
  3. If applications differ, measure the driver/capacity difference. For internal OTA, create exact-base deltas from representative installed Full and Reduced images and verify staging geometry, reconstruction, and the matched deployed bootloader. A passing local-install image does not prove a delta fits.
  4. Change publication validation and fixtures deliberately before emitting one artifact. validate_nrf52_sensor_pairs() currently requires exactly one qualified Full and one Reduced with matching target, hardware, version, source, and layout; it does not prove either member saves space or is needed. Preserve legacy endpoints needed by qualified chains.

Focused existing checks include test/test_build_profiles.sh, test/test_nrf52_ota_profiles.sh, test/test_ota_store_flash_nrf52_hybrid.py, test/test_rak_storage_policy.py, and the release-packaging tests. These check policy and boundary behavior; they do not substitute for matched image and exact-delta evidence. No PlatformIO process was started for this audit.

"},{"location":"research/firmware_choice_followup_2026-10-06/#storage-and-bootloader-choices-that-need-real-distinctions","title":"Storage and bootloader choices that need real distinctions","text":"

OtaFlashLayout_nrf52.h derives internal staging space from the trusted live EndF application extent, rounded to a 4 KiB page, and the effective bootloader ceiling. OtaStoreFlashNrf52.cpp adds 64 KiB of retained RAM only with the matching MOTARAMA capability and at least one available flash page. Its hybrid application path accepts deltas, not full application containers. A Full sensor firmware can still be the destination of a delta; the two uses of \"Full\" must not be confused.

The historical RAK3401 chain proves tight exact-package geometry under an older bootloader: multiple transitions have zero margin; steps 1-9 were physically passed, while the replacement final step has offline reconstruction and exact-bootloader simulation only. It does not compare the current sensor pair and must not be generalized to all nRF52 boards.

External NOR/SD removes the internal container-staging bottleneck, but does not increase executable internal flash or RAM. Existing runtime consolidation is already available for qualified RAK unified storage and MeshTower SD/internal fallback. Keep the exact NOR wiring, bootloader device/storage records, target IDs, and per-transfer store selection. MeshTower's old internal-only identity needs local migration; an alias does not authorize same-target LoRa replacement. Its internal fallback can reject a delta that fits on SD.

The source explicitly rejects raw OTA QSPI sharing a chip with the QSPIFLASH filesystem, and WisBlock OTA SPI sharing incompatible Ethernet/SD wiring. RAK3401 flash and the 1 W radio need distinct chip selects and the explicit bus handoff contract. Combining those recipes requires hardware/layout evidence, not just runtime sensor switches. Extra-filesystem and bootloader-scratch layouts likewise cannot be relabeled as the same OTA target.

"},{"location":"research/firmware_choice_followup_2026-10-06/#other-possible-menu-simplifications","title":"Other possible menu simplifications","text":"

The current ESP32 replay reports 174 Full choices, including the two newly pulled Station G3 sensor recipes. Its evidence file out/esp32-full-audit-2026-10-06/wifi_review_current_consolidation.json finds two equivalent source-option pairs: Heltec V4 base/Expansion Kit OLED repeaters, and their room-server recipes. Repeated identical I2C pin definitions are the only semantic option difference, but their mOTA target IDs differ. Simplify selection first; retain deployed update artifacts until an explicit migration is reviewed. The Expansion Kit TFT Companion has different display/sensor wiring.

Pico W already has a combined USB/WiFi/BLE Companion recipe; it is a candidate to replace transport-specific ordinary choices after fresh capacity and transport qualification. This audit has no current qualifying build result. STM32WL's retained flash reports support capacity tuning of its existing 256 KiB applications; they do not show a removable duplicate role. Repeater, room-server, sensor, Companion/Terminal Chat, and KISS remain different applications unless a separate combined application is designed and tested.

"},{"location":"research/firmware_memory_budget/","title":"Firmware memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"research/firmware_memory_budget/#firmware-memory-checks","title":"Firmware memory checks","text":"

Every firmware environment runs scripts/check_firmware_ram.py against its linked ELF before producing or uploading an image. build.sh, including option 3, also requires a passing report before collecting release files. Native host tests do not use a microcontroller RAM budget.

The check reserves room for enabled runtime allocations as well as static data. A firmware image fitting its board's reported RAM total is insufficient: the display, packet pool, USB, Bluetooth workers and WiFi can allocate after startup. The T096 Full 1.17.1.5 report exposed this distinction. MQTT uses an explicit startup minimum plus runtime connection admission. A passing build report does not qualify its maximum TLS/WSS/outbox workload.

"},{"location":"research/firmware_memory_budget/#what-is-counted","title":"What is counted","text":"Platform Source of available runtime RAM nRF52 Actual __HeapBase and __HeapLimit; excludes SoftDevice, retained state, ISR stack and the dedicated 64 KiB mOTA arena where present ESP32, S3, C3, C6 Linked ESP-IDF memory-region, capability and reservation tables; only internal, byte-addressable heap counts RP2040/RP2350 __end__ to __HeapLimit, according to the selected linker STM32 _end to _estack, minus _Min_Stack_Size

ESP32 PSRAM, instruction-only RAM and RTC RAM never increase the internal budget. On chips other than classic ESP32, the late-reclaimed ROM stack region is excluded because its silicon-specific reservations are only known at boot. Classic ESP32 additionally retains its existing 8 KiB static DRAM check.

The policy adds allowances for task stacks, radio packet pools, screen objects and pixel buffers, filesystem/sensor allocations, enabled wireless stacks, MQTT connections, OTA scratch and transient allocations. ESP32 images with MQTT reserve the same 49,152-byte WiFi allowance even without WIFI_SSID or WIFI_OTA_SEEDER: saved runtime credentials can activate WiFi. Multiple WiFi features count that allowance once; PSRAM-backed TLS does not remove it. ESP32 images with RS-232 reserve another 7,168 bytes even when UART defaults off: 3,072 bytes for the source-bounded bridge object and allocation metadata, plus an unchanged 4 KiB for the UART driver and buffers. This keeps runtime UART enablement in the budget beside MQTT and ESP-NOW. T-LoRa V2.1-1.6 cannot fit all three transports: the qualified linked triple image had 190,488 available internal bytes against 196,984 required bytes under the earlier allocation estimates. The corrected equivalent requirement was 193,400 bytes with the earlier flat MQTT allowance, still 2,912 bytes above capacity. Its configured 16 KiB MQTT worker stack adds another 8 KiB under the current startup policy; these figures describe historical linked checks. Its ordinary release retains separate UART/ESP-NOW and MQTT/ESP-NOW Full images; routing capacities remain unchanged. A 160x80 ST7735 RGB565 framebuffer needs 25,602 bytes. T096 recipes instead select ST7735_BUFFER_COLOR_DEPTH=8: the RGB332 canvas needs 12,801 bytes, plus a source-bounded 1 KiB allowance for the complete sprite object and allocator metadata. Each frame expands one 160-pixel scanline into a 320-byte stack buffer and sends the same RGB565 wire format. The current primary UI colors remain exact; resolution, fonts, all 32 message previews, 350 contacts, 40 channels and the 256-frame offline queue are unchanged. Other ST7735 recipes retain the 16-bit default. A future sprite-object increase beyond the allowance fails a C++ assertion rather than silently reducing available heap. The existing loop stack and transient margin remain additive.

The ordinary T096 BLE FEM-on/off policy includes its receiving OTA source scratch and full-length previews: the reported 81,474-byte requirement becomes 69,697 bytes with the smaller canvas and explicit sprite-object allowance. The old 75,396-byte linked capacity therefore failed correctly; the check is not bypassed or relaxed to accept it. Full Companion's independent 72 KiB floor remains in place.

ESP32 and nRF52 SSD1306 use a source-bounded 2 KiB allowance for the same fixed 1,024-byte framebuffer, embedded driver and allocation metadata. The OLED auto-detection backend reuses that one buffer across SSD1306/SH1106 selection and retries. Other display allowances remain unchanged. nRF52 Full with that color framebuffer must have at least 72 KiB available before startup allocations. Headless and OLED devices use their own smaller totals. The JSON lists each component and checks the largest available region against the largest planned single allocation.

"},{"location":"research/firmware_memory_budget/#mqtt-startup-minimum-and-runtime-qualification","title":"MQTT startup minimum and runtime qualification","text":"

The mqtt_startup_allowance component keeps a 24,576-byte internal startup minimum for the default 8,192-byte MQTTBridge worker stack and two active clients. Each SDK client task is explicitly configured with a 6,144-byte stack and priority 5 on IDF 4 and 5; its maximum configured receive/transmit buffers total 1,792 bytes. These stacks and buffers total 24,064 bytes for two clients, leaving 512 bytes within the existing allowance. This is a minimum allowance, not a complete allocation bound for the clients, transport objects, TLS records, certificate processing, WebSockets or outboxes. The independent 16 KiB transient margin remains in the policy.

MQTT_TASK_STACK_SIZE above 8,192 bytes raises the startup requirement by its actual increase and raises the required contiguous allocation when needed. Smaller stacks and fewer runtime slots do not lower the existing allowance. The default runtime slot array has six entries on PSRAM builds and three on internal-only builds. The bridge can expose five active connections when PSRAM is present and two otherwise; MQTT_RUNTIME_SLOT_COUNT can restrict that capacity. A PSRAM board with unavailable PSRAM uses the two-connection cap.

The report's separate mqtt.maximum_slot_stack_buffer_allowance_bytes records 48,384 bytes for five exposed slots or 24,576 bytes for two, with larger worker stacks added to either. The five-slot figure extends the minimum by 7,936 bytes for each additional client task and buffer pair. It is not added to the startup gate: extra clients allocate lazily, and every connection attempt must pass runtime admission. It still excludes the maximum TLS/WSS/outbox workload. maximum_runtime_load_qualified and physical_validation_performed are false in these build reports. A passed startup check must not be presented as qualification of five simultaneous PSRAM connections or two internal TLS connections.

The mqtt report also names the production runtime admission thresholds. A cold SDK start requires at least 32 KiB of DMA-capable internal free heap and an 8 KiB contiguous DMA block. This includes a separate 16 KiB allowance for its SDK task stack/TCB, receive/transmit/reassembly buffers and transport or configuration objects, alongside the 16 KiB AES reserve. Those ordinary SDK allocations occur after admission and do not use the TLS allocation guard. A retained initialized SDK handle with a stopped task requires 24 KiB free and an 8 KiB contiguous DMA block. Its separate 8 KiB restart allowance covers the pinned 6 KiB task stack plus TCB and allocator overhead. SDK stop retains its receive/transmit buffers, configuration, transport objects and wrapper reassembly buffer, so restarting that handle does not need to reserve their first-initialization allocation again. A wrapper whose SDK initialization has not completed still receives the full cold allowance. This distinction also applies after WiFi loss stops the SDK tasks but keeps their handles.

All ESP32 clients use 896-byte receive, transmit and wrapper reassembly buffers from first initialization. Applying configuration cannot grow the retained SDK buffers or wrapper reassembly capacity, so uniform sizing permits later JWT reconfiguration without replacing the client. This adds 1,152 bytes to a non-JWT client's three buffers on an internal-only board compared with 512-byte buffers. The startup allowance already uses 896-byte receive/transmit buffers, and cold admission covers the ordinary allocations. WiFi recovery and explicit reconfiguration continue to retain clients.

A warm reconnect keeps its running task and buffers; it requires at least 16 KiB free and a 4 KiB contiguous DMA block. The per-client SDK stack is independent of MQTT_TASK_STACK_SIZE, which sizes the existing bridge worker. A TLS attempt without PSRAM requires at least 60 KiB free and a 17,408-byte contiguous block. PSRAM allocation fallback must leave the 16 KiB DMA reserve available. These thresholds describe free heap at the time of admission; they are not additional linked capacity and the TLS contiguous threshold is not a startup allocation requirement. Available linked internal heap cannot establish how much DMA-capable heap remains or how it is fragmented after boot. Only runtime observations can establish that.

Maximum-load qualification still needs physical boot and sustained reconnect, publish and configuration-change tests with the selected number of brokers, transport schemes, certificates and enabled UART/browser/OTA features. Record DMA free heap, largest DMA block and allocation failures throughout that workload. Host admission tests and passing ELF checks provide engineering evidence without claiming that hardware validation has been performed.

The earlier W12 Full MQTT RAM failure (182,768 available against 198,432 required) and standard portable-slot overflow (1,322,304 against 1,310,720 bytes) are historical measurements, not the current expanded Full result. The 2026-10-18 UTC recheck at 060119af8 passes with 200,640 internal bytes available against 198,432 required, retaining the 16 KiB transient margin. Its largest region is 167,872 bytes against 18,464 required; the packaged 2,280,072-byte application fits the expanded 6,553,600-byte OTA slots. MQTT is enabled and all ten declared capability checks pass, without declared reductions. This establishes linked capacity and packaged capabilities, not physical boot/soak or maximum TLS/WSS/outbox-load qualification. It also does not qualify an independently built compact portable-slot recipe.

The experimental RAK4631 combined Ethernet/UART2/adaptive-storage config adds an explicit 3,072-byte allowance for the UART bridge and its TX semaphore. Its Ethernet worker reserves 1,024 stack words (4,096 bytes) plus 256 bytes for task metadata and allocator overhead. Its bounded TCP transmit queue allocates 512 bytes only while Ethernet is enabled; another 64 bytes cover allocator overhead. The DHCP object, socket state, queue indices and SPI objects are static and already reduce the linked heap capacity.

Ethernet and external OTA share an exclusive ownership contract. Enabling Ethernet rejects a live or pinned transfer, releases idle external OTA buffers, and blocks encoder, self-source, manual-staging and folder-source allocation while Ethernet owns SPI. Disabling Ethernet parks the worker after socket/SPI cleanup; the main loop deletes that parked task synchronously and frees its TX queue before releasing ownership. The worker stack therefore cannot remain queued for idle-task reclamation while OTA allocates again. The budget counts the larger of the 4,928-byte Ethernet worker/queue or 11,344-byte external OTA workspaces, with the UART allowance alongside either mode. It keeps the 8 KiB loop stack, 63 flood rules, and reserved 64 KiB OTA handoff arena.

The shared RAM changes now reserve the lazy folder-source buffers as well: up to 4,096 bytes of source leaves and one 2,048-byte encoded output block, plus allocation overhead, for the RAK4631. These can coexist with own-image metadata and the encoder. The combined budget therefore reserves 11,344 bytes for external OTA, or 10,304 with the device encoder disabled, against the 4,928-byte Ethernet worker/queue. Ethernet and external OTA remain exclusive. Freeing static buffers increases available heap; their active allocation cost still appears in the memory check.

This combined config uses a source-bounded 2 KiB SSD1306 allowance. Its named 128x64 geometry needs one 1,024-byte framebuffer, reused on later starts; the driver itself is embedded in a linked global, measured at 156 bytes in the ARM test ELF. Including that already-counted object again plus 32 bytes for allocation overhead still leaves 836 bytes within the allowance. A compile-time assertion covers framebuffer, complete driver object and allocator overhead within that bound. This corrects a padded allocation estimate; it does not free actual RAM. The same source assertion and bound now cover ordinary nRF52 SSD1306 images as well; other display drivers keep their existing budgets. These are linked capacity checks, not qualification of a published or physically tested image.

"},{"location":"research/firmware_memory_budget/#shared-ram-reductions","title":"Shared RAM reductions","text":"

These changes apply to every board that compiles the corresponding feature. They retain stack sizes, radio packet pools, USB buffering, table capacities, stored settings, OTA block sizes and source-image limits.

Change Scope RAM reduction Repeater ACL session state Pure repeater builds on all platforms 36 bytes per client on 32-bit targets; 1,152 bytes for 32 clients Compact recent-repeater records Repeaters with recent history enabled 12 to 9 bytes per record; 1,536 bytes for 512 records Compact flood-rule flags and aligned counters Generalized repeater and room rule engines 200 to 192 bytes per rule; 504 bytes for 63 repeater rules or 248 bytes for 31 room rules One tagged flood-rule target name Generalized repeater engine on all platforms 332 to 300 bytes per extended rule; 2,016 constructor-time heap bytes for all 63 slots Streaming Merkle proof generation All OTA roles and platforms Removes full-leaf-table proof scratch; compressed output needs only one logical block Source buffers allocated on demand All OTA roles and platforms Leaves allocate only the selected image's digest count; unused leaves/output return to the heap

The ACL source-role pre-script contributes one common compiler definition for the entire build. Room/sensor and unknown roles keep the complete session layout. The persisted four-byte room synchronization field stays in repeater records, so existing ACL files and their checksums remain compatible.

Recent-history timestamps retain all 32 bits using explicit byte operations, avoiding unaligned multi-byte accesses on ARM. The table belongs to the mesh loop; network command handlers marshal their work to that loop. These byte operations do not provide synchronization for hypothetical concurrent users. Flood flags remain internal; FPF files serialize each field explicitly and never expose compiler bitfield layout. Multibyte counters remain aligned. Repeater rules store either the direct scope name or target region name in one 32-byte array because the CLI and stored-policy validator already reject both actions together. A spare flag bit identifies the name. FPF6 retains its single scope field. FPF7/8 retain both fixed-width name fields; the inactive field is emitted as 32 zero bytes. Load reads and validates both fields before selecting the shared name, retaining maximum name lengths and refusing a malformed two-target policy. Room-engine rows and on-disk formats are unchanged.

Rule deduplication compares configured fields explicitly, so neither live rate-window counters nor struct padding can make different rules share a slot.

OTA leaf capacity remains controlled by OTA_PROOFGEN_SCRATCH, independently of output-buffer size. The maximum source buffers are 4 KiB plus one block on ordinary nRF52, 8 KiB plus one block with SD storage, and 16 KiB plus one block on legacy ESP32 paths. Non-PSRAM ESP32 Full uses grouped source proofs instead: the complete workspace is bounded to 1,536 bytes, while retaining the same 4,096-block source limit. The check still adds one output block and allocation metadata; safety reserves and stacks are unchanged. PSRAM and other platforms retain the legacy policy. Explicit larger custom leaf limits also keep the legacy implementation. Generic folder sources need no output allocation for raw DATA or proofs; own-image raw serving retains its caller-owned compatibility buffer. Its nRF52 allowance remains in the budget even when the device encoder is off. Queued DATA, PROOF and MANIFEST responses pin their buffers through radio backpressure. Explicit cancellation or source detach releases the reloadable cache; an unpinned cache also expires after 30 seconds. Attached source descriptors remain advertised and reload on the next request. Allocation failure refuses source loading safely, or falls back to raw DATA if only compression output is unavailable. Receive reassembly and serving/inflate buffers remain separate.

On RAK4631, the OTA context's permanent allocation falls from 28,816 to 20,640 bytes, an 8,176-byte reduction before table savings. This is idle heap capacity, not an additional 8 KiB saving on top of the active-operation savings. The RAM policy reserves lazy buffers when active and separately checks the largest contiguous allocation. ESP32 history uses its actual default capacities: 256 records on classic ESP32 and 2,048 on S2/S3/C-series, unless overridden.

The matched RAK4631 Full combined trial on 8 October 2026 retained all sensor drivers, 32 ACL clients, 63 flood rules, 512 recent repeater records, the 8 KiB loop stack and the reserved 64 KiB OTA arena:

Linked measurement Before shared RAM changes After all five changes Available heap capacity before startup 73,580 bytes 83,300 bytes Reserved startup/active allocations 72,584 bytes 77,104 bytes Capacity beyond those reservations 996 bytes 6,196 bytes Application flash 585,700 bytes 586,260 bytes

The permanent allocations shrink by 9,720 bytes. The smaller ACL and flood tables reduce startup allowances by another 1,656 bytes, while the new 6,176-byte folder-source allowance counts buffers when active. The resulting capacity beyond reservations improves by 5,200 bytes; flash grows by 560 bytes. The reports are bound to ELF hashes a16c02e43c12d7dadd94b92b9b80cdd22eced7f67e085b1fffb62ea39f09fcce and f78110d1609334621098cb6db2039227c2f8fc9ac7f9e6ef7dc3cc63eb8d502f, respectively. These are linked capacity checks, not measured free heap after boot or physical Ethernet/OTA soak results.

The five changes pass representative repeater builds for RAK4631 combined Full, Heltec V4 (ESP32-S3), Heltec V2 (classic ESP32), and Seeed XIAO RP2040. RAK4631 room-server and Heltec V4 sensor builds also pass while retaining their complete ACL session layouts. At that measurement, Wio E5 Mini repeater compilation passed but linking exceeded flash capacity: the baseline already overflowed by 27,200 bytes, and those changes added 224 bytes for a 27,424-byte overflow. The newer 1.18.0.1 check below still fails; this cable-only STM32 image remains unqualified and is not release-ready.

Regression coverage includes unchanged ACL/FPF file bytes, full capacities, timestamp rollover, flag combinations, ARM access widths/alignment, streamed proofs against the existing reference algorithm, allocation failures, source reload, queued-response lifetimes, and concurrent compressed serving/receiving. Synthetic and representative build results are distinct from physical soak qualification of every board.

Generate matched Full/Reduced baseline and combined configs outside the repository without starting PlatformIO:

python3 scripts/generate_rak4631_combined_test.py\n

Run its printed commands one at a time and preserve each ELF and memory report before the next build reuses the environment directory.

The combined prototype starts with Ethernet off after every restart. Use the USB CLI commands eth on, eth.status, and eth off; get eth reports the requested mode. UART2 remains independently controlled by set bridge.uart 2 and set rs232.enabled on|off. Full retains every sensor driver; absent sensors do not require a different image.

With Ethernet on, OTA uses eligible internal-flash deltas. External full-image staging, self-serving, manual serving buffers and OTA folders are unavailable until Ethernet is off. Active or staged OTA work prevents a mode change. RAK15001 and Ethernet share chip-select 26 and cannot be used together; the prototype refuses that combination. External storage also needs a matching bootloader that permits internal OTA while Ethernet is enabled.

TCP sends and receives use bounded register operations; a stalled peer cannot hold the mesh loop waiting for an acknowledgement. DHCP and listener management run in the Ethernet worker. This is synthetic coverage and a build experiment, not a physical Ethernet/PoE soak qualification or a replacement release image.

RAK13800 Companion transport also uses a 1024-word worker by default. Its existing four-frame queue stays in the linked object; it adds no raw CLI TX ring. Reserve the worker's 4096-byte stack plus 256 bytes for its TCB and allocation metadata in addition to OTA source workspaces: USB/BLE folder sources can coexist with Companion Ethernet. Stack overrides raise both the total and contiguous allocation requirements. Worker shutdown must complete before its stack is reclaimed or the shared SPI/storage owner is released.

Full RAK Companion keeps every OTA target name while storing an eight-bit candidate fingerprint instead of a four-byte ID for each name. A lookup reconstructs candidate names in the existing 4096-byte heap workspace and checks their complete SHA-256:4 IDs before returning a match. Other targets retain their existing index. This trades bounded, infrequent display lookup work for flash space; no target names or firmware capabilities are removed.

The small-screen message layout retains complete 160-byte messages. Its expanded preview records add 2,816 bytes to the startup allowance and increase the contiguous history allocation budget. The V4 can allocate that history in PSRAM; the guard conservatively reserves internal capacity so that PSRAM availability cannot hide a RAM shortage. A target may replace the generic UI allowance only with MESH_COMPANION_SCREEN_STARTUP_BYTES; a source-side assertion then covers its actual startup screens plus ESP32 allocator overhead.

These are engineering allowances for supported configurations, not measured free heap after boot or a guarantee against every future allocation failure. Unknown platforms, unknown display drivers and missing linker metadata fail closed. MESH_MIN_RUNTIME_HEAP can raise a profile's requirement; it cannot lower the calculated requirement. Add an allocation allowance when adding a display, transport or other substantial feature.

Wireless Paper Full keeps 350 contacts and 256 offline frames by lending the upper 128 queue slots to its mOTA workspace during a session. An idle WiFi mOTA listener leaves all 256 slots available. More than 128 unread frames refuses the loan; sync messages with an app first. USB/TCP source detach or disconnect returns all 256 slots and releases the ESP32 proof/leaf scratch buffers. The display and simultaneous USB, Bluetooth and WiFi remain enabled.

"},{"location":"research/firmware_memory_budget/#nrf52-companion-flash-savings","title":"nRF52 Companion flash savings","text":"

The common nRF52 Companion policy now retains a 960-byte Ed25519 fixed-base table in place of the original 30,720-byte table. It evaluates the same signed radix-16 digits with constant-time table selection, using more doublings instead of storing 32 shifted rows. Compact SHA-512 and scalar arithmetic keep the same outputs and wire format. The larger RAM-built table used by infrastructure profiles is not enabled for Companions.

The policy also omits unused USB device MSC/HID/MIDI/vendor/video classes, SparkFun's unused automatic NMEA cache and the invisible SSD1306 vendor splash. For the exact hash-qualified CC310 archive, the domain wrapper retains P-256 for BLE Secure Connections and removes unused legacy curves. CDC, DFU, USB host, GPS navigation, all Companion fonts, Meshtext, contacts, channels, the Reader corpus and the 8 KiB loop stack remain available. Explicit board/user overrides and MESH_NRF52_COMPANION_TRIM=0 retain their requested values. Other platforms do not receive this Companion policy.

The RAK4631 Full comparison uses the released 1.17.1.10 preview3 ELF at 9ce2a076 and the same target with these changes and Reader chapter/range support. Flash is initialized linked code/data; static RAM is .data plus ordinary .bss, excluding separately reserved arenas.

Measurement Preview3 With Companion trims and Reader ranges Difference Linked flash bytes 710,404 652,908 57,496 fewer (56.1 KiB) Static RAM bytes 157,028 153,876 3,152 fewer (3.1 KiB) Linked heap capacity bytes 78,484 81,636 3,152 more UF2 space below the application ceiling 2,048 59,648 57,600 more

Physical RAK signing averaged 14.4 ms with the original table and 35.9 ms with the smaller implementation. All 24 signatures per image, including SHA-512 block-boundary message lengths, matched byte for byte and verified independently. The linked ARM emulator measures 1,928 bytes in signing versus 1,528 before; these are crypto function frames only, excluding callers, interrupts and other tasks. The loop stack stays at 8 KiB. A full firmware RAM check still applies.

CI runs scripts/check_nrf52_companion_crypto.py on the actual Full RAK ELF. It checks the 960-byte table and retained Reader corpus, executes two RFC 8032 vectors, and rejects crypto function stack use above 2 KiB. Host tests compare 1,791 scalars and 128 keypair/signature/shared-secret cases against the original implementation, plus scalar boundary/random and sanitizer tests. Clean Full builds also cover T1000-E and T096. These builds are synthetic coverage for those boards; the physical Reader/signing tests use the RAK4631.

Reader commands support get reader 2 and get reader 2:2-12. Text terminals stream at most 96 bytes per mesh loop, preserving progress under zero/short writes. The TCP console uses one nonblocking socket-send attempt for the stream, so a peer that stops reading cannot hold the mesh loop in WiFiClient retries. App CLI replies remain bounded and supply a continuation command. The physical RAK passed all 21 chapters, ranges, app-style continuation replies, a paused USB reader exceeding the 4 KiB queue, and disconnect cancellation.

"},{"location":"research/firmware_memory_budget/#empty-contact-path-cache-initialization","title":"Empty contact-path cache initialization","text":"

The contact-path pool now has an explicit runtime constructor. Its existing member defaults still initialize every empty handle and resident entry, and Slot() still resets released handles. This moves the local static pool from initialized .data to zero-initialized .bss: the vtable pointer and repeated NONE/255 slot markers no longer require a 5,780-byte flash load image. The pool is constructed on first use by the existing main-loop contact owner. Its layout, 390 handle slots, 16 resident paths, reference/snapshot behavior and persistent contact records are unchanged.

The RAK4631 Full release recipe at c0672b90, with the full v1.17.1.10-halo-keymind-cascade-dev version and commit suffix, measured:

Measurement Before Runtime pool construction Linked initialized flash bytes 716,448 710,908 Application image including EndF 716,528 710,988 Linked available heap bytes 79,388 79,388 Required runtime heap bytes 74,576 74,576

The net flash saving is 5,540 bytes after constructor/guard code. The image passes the existing 712,704-byte application ceiling, capability checks, RAM budget and DFU handoff checks. Its padded UF2 ends at 0xD3A00, below the unchanged 0xD4000 boundary. Sensors, BLE/USB/Ethernet, SD, logging, Meshtext, OTA, Reader data and the 8 KiB loop stack remain enabled.

The contact-cache test compiles a real static pool and checks its zero-initialized storage, alongside the existing sanitized eviction, snapshot, migration and transaction tests. The compiled ARM Companion check also rejects a pool in an initialized ELF section, so compiler promotion cannot silently restore the flash image. Its normal linked crypto vectors and Reader retention checks remain required.

"},{"location":"research/firmware_memory_budget/#release-evidence-and-regression-tests","title":"Release evidence and regression tests","text":"

Full Companions without PSRAM also use 16-entry path and shared-secret caches. The optional NimBLE capacity trials retain the same RAM guards at 350 contacts and 256 normal offline frames.

Each newly built firmware has a matching .memory.json report. It records the linked ELF SHA-256, available internal RAM, required RAM, largest region, and SHA-256 hashes for the actual firmware files and capability manifest. Packaging and resumed builds reject absent reports, failures, stale ELFs, missing files and changed firmware. Do not reuse a report for another build.

Run PlatformIO commands sequentially in this checkout:

python3 -B test/test_firmware_ram.py\npython3 -B test/test_sh1106_memory.py\npython3 -B test/test_t096_full_memory.py\npython3 -B test/test_st7735_framebuffer.py\npython3 -B test/test_nrf52_ble_startup.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_cascade_release_package.py\npio test -e native -f test_ota\n

Tests cover all resolved firmware environments' hooks, real ELF parsing, allocator table formats, excluded memory, allocation failure, package/report binding and the published T096 failing budget. Shared mOTA tests exercise complete transfers, queue wraparound, unread-message order, source ownership, stop/disconnect and repeated reuse. Bluetooth tests inject task and service startup failures. The manual staging buffer also has allocation-failure and repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source framing through complete transfers, idle polling, queue-full refusal, network loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers. The ST7735 tests check all 256 RGB332 values, exact UI primary colors, full 160x80 scanout and byte order on both SPI APIs, the unchanged 16-bit path, allocation failures, and retained-buffer wake/retry behavior. CI also compiles both ordinary T096 BLE FEM variants with their real runtime-RAM gates, not only the distinct Full Companion overlay.

For older releases without saved ELFs, an audit can compare their ESP allocator tables against a matching pinned SDK ELF and read reservations from the published application itself using scripts/audit_esp32_image_ram.py. Unrecognized layouts require another matching reference or a historical rebuild. An audit must identify original-log/linker calculations separately from new ELF checks and verify the published firmware hashes.

Physical validation remains necessary: boot with and without USB, pair and exchange Bluetooth messages, visit every screen, wake with the button, enable logging/MQTT, transfer mOTA, and monitor heap during a sustained workload. See memory monitoring for runtime diagnostics.

"},{"location":"research/firmware_memory_budget/#11801-known-risk-matrix-2026-10-18-utc","title":"1.18.0.1 known-risk matrix, 2026-10-18 UTC","text":"

The targeted recheck used v1.18.0.1-halo-keymind-cascade-dev, usa-cascade-fixed, the Cascade profile, and release-default debug/logging policy (DISABLE_DEBUG=0). PlatformIO ran sequentially; new artifacts and logs were isolated from existing out/ builds. Exact-identity expanded Full profiles were selected for ESP32 infrastructure. Required nRF52 receiver pairs built both Full and Reduced; a Reduced pass did not replace a Full pass.

Twenty-five selections passed, producing 31 verified profiles:

Selections Passing profiles RAK3401 and RAK4631 Full Companion 2 Wio L1 Full/BLE; T096 Full/BLE, each FEM-on/off 6 RAK4631 canonical and unified Repeater, Full/Reduced 4 RAK3401 unified and RAK13302/W25Q16 Repeater, Full/Reduced 4 MeshTower SD and T096 Repeater, Full/Reduced 4 RAK3x72, Wio-E5 Mini, Wio-E5 Dev and Tiny Relay USB Companion 4 W12 expanded Full Repeater, Room and MQTT Repeater 3 G2 Full Companion/MQTT Repeater, G3 r2 MQTT Repeater, V4 Full FEM-on 4

The separate 26th selection, wio-e5-mini_repeater, was an explicitly cable-only control using --allow-no-ota, not an eligible wireless release target. Its baseline at 3059d7994 overflows the existing 245,760-byte app region by 38,040 bytes. A private compact-crypto/unused-timer trial recovered 18,544 bytes but still overflowed by 19,496. That unqualified trial was not retained. The 16 KiB filesystem, host CLI and existing features were not cut to manufacture a pass; no failing image was published.

Three reproduced release-target failures were fixed:

The STM32 changes reuse equivalent fixed-loop crypto and omit unused Arduino PWM/tone wrappers while retaining the low-level TIM HAL, 100 contacts, eight channels, GPIO/ADC interfaces and 32 KiB filesystems. CI now links all four small STM32 Companions serially at release-version length (2ae109fea) and the exact G3 Full MQTT profile. Display tests cover repeated successful and failed initialization, recovery, destruction, addressing and rendering under ASan/UBSan, including a missing-cleanup negative control.

Passes retain their individual source SHAs (060119af8, 3059d7994, 2ae109fea, 29787c0b2) and file-bound memory/capability reports. The affected G2/Wio L1 selections in this matrix and all four STM32 Companions were rebuilt at 29787c0b2. The final 95 related host tests and artifact-hash checks pass.

"},{"location":"research/firmware_memory_budget/#pr21-contact-recovery-size-recheck-2026-10-18-utc","title":"PR21 contact-recovery size recheck, 2026-10-18 UTC","text":"

The combined PR17\u201321 merge tree a40b1f662 was linked with v1.18.0.1-halo-keymind-cascade-dev, usa-cascade-fixed, and the Cascade profile. All four unchanged 229,376-byte application limits and runtime RAM gates passed. These are build results, not on-device qualification.

USB Companion Application bytes Free application bytes Static RAM bytes RAK3x72 228,744 632 40,220 Wio-E5 Mini 228,760 616 40,252 Wio-E5 Dev 228,884 492 40,212 Tiny Relay 228,940 436 40,220

The earlier 508/452-byte margins above are historical checkpoints. Continue linking every small STM32 Companion at full release-version length: these margins do not justify relaxing the application, filesystem or runtime-RAM gates. Physical storage and boot checks remain in the hardware checklist.

Margins remain narrow: RAK4631 Full Companion has 932 flash bytes free; canonical/unified RAK4631 Full Repeater have 548/540 bytes above their RAM budgets. W12/G2/G3 Full MQTT have 2,208/2,176/1,816 bytes above their budgets. These are linked-capacity margins, not measured running free heap.

No new feature reductions were introduced. Existing declarations remain: RAK3401 unified has 47 flood rules, T096 Repeater has four, Reduced profiles omit selected optional sensor drivers, and nRF52 Full Companions loan half their 256-frame offline queue while mOTA is active. STM32 Companions remain cable-only; nRF52 Full Companion source support is not LoRa self-update receiver qualification. Exact storage/bootloader requirements still apply.

This is a targeted build/capacity pass, not the complete canonical release matrix at one frozen commit, physical hardware/upgrade/soak qualification, or publication of a 1.18.0.1 release.

"},{"location":"research/firmware_memory_budget/#wio-e5-mini-repeater-gcc-15-experiment-2026-10-18-utc","title":"Wio-E5 Mini Repeater GCC 15 experiment, 2026-10-18 UTC","text":"

The separate compiler experiment used source 6035f6631 and the same release recipe above. For the reported comparisons, firmware timestamps were fixed at 18-Oct-2026 and epoch 1792281600. The actual compiler executables reported Arm GNU 14.2.Rel1 / GCC 14.2.1 and Arm GNU 15.3.Rel1 / GCC 15.3.1; a private platform_packages override selected the latter. Production pins were not changed. Each PlatformIO build ran alone, with a fresh private build/output directory; no existing out/ artifacts were replaced.

These numbers are linker overflows beyond the 245,760-byte app region, not successful firmware sizes. The 16,384-byte filesystem, host CLI, logging policy, radio preset and existing feature declarations were retained.

Matched recipe GCC 14 overflow GCC 15 overflow Original Mini Repeater recipe 38,040 37,700 Compact crypto and unused timer wrappers 19,496 19,060 Compact plus best tested five-flag bundle 18,800 18,468

Here, compact means adding ED25519_COMPACT_SHA512=1, ED25519_COMPACT_SCALAR=1, ED25519_COMPACT_FIELD=1, RADIOLIB_TONE_UNSUPPORTED and HAL_TIM_MODULE_ONLY. CompactBase was already enabled in the original recipe. These private additions were not retained in the production Mini Repeater configuration. Their combined savings must not be attributed to the compiler upgrade alone.

The four-flag bundle was -fno-caller-saves -fno-tree-dominator-opts -fno-reorder-blocks -fno-reorder-functions. Adding -fno-tree-switch-conversion produced the five-flag winner. Full-source GCC 15 trials, all with the compact additions, measured:

Additional tuning beyond the four-flag bundle Overflow bytes None 18,500 --param=max-inline-insns-single=70 18,556 --param=max-inline-insns-single=30 18,556 --param=max-inline-insns-single=15 18,588 -fno-inline-functions-called-once 27,108 -fno-jump-tables 18,588 -fno-tree-switch-conversion 18,468 -Os instead of -Oz, compilation and link 18,500 -fno-inline 38,116 --param=max-inline-insns-size=0 --param=inline-unit-growth=0 18,500 -fno-tree-loop-optimize -fno-tree-scev-cprop -fno-tree-loop-distribute-patterns 19,228 -mslow-flash-data 27,628

The winning GCC 15 recipe reproduced the same 18,468-byte overflow in a second clean build. Its compiler-only improvement over the matched GCC 14 recipe is 332 bytes; the original recipe improves by 340 bytes. The 19,572-byte recovery relative to the original GCC 14 build includes the compact/peripheral and tuning changes, not just GCC 15. The winning linked flash extent is 264,228 bytes, still 2,084 bytes beyond the chip's entire 256 KiB flash even before reserving filesystem space.

Actual verbose commands were checked across application, crypto library, Arduino framework and final LTO link. Experimental compile options were installed through the framework's late build.framework_extra_flags.arduino hook, preserving board defaults and overriding its inline threshold before library/application environments were cloned. A post-script forwarded the same tuning to the LTO driver. The true -Os trial also changed build_unflags to remove -Oz, so the inherited removal of -Os could not silently invalidate that comparison.

Earlier mixed-scope post-script trials were preserved but excluded from the full-source comparison: those options reached framework/link stages, not the application/crypto compilation. One later link-only -Os control also measured 18,500 bytes; it is not the true full-source -Os trial above. No ABI-changing struct/enum options, fast-math assumptions, filesystem cuts, or capacity-check overrides were used. Every full-firmware attempt still failed at the flash link; none qualifies as a runtime-RAM pass, usable image, or hardware validation. GCC 15 was not adopted and no failing image was published.

A private Unicorn 2.1.4 harness also checked generated Cortex-M4 code for eight standalone crypto profiles: GCC 14 compact, GCC 15 compact, the GCC 15 four-flag bundle, its five-flag winner, and the four more aggressive trials in the last four table rows. All passed 432 positive function calls: 40 key generations, 40 signatures and 352 SHA-512 checks. Each profile used two RFC 8032 signing vectors, three additional reference signing vectors, and 11 SHA-512 boundary lengths in one-shot and three streaming modes. All 16 corrupted-expected-signature/digest negative controls were rejected.

The harness checked explicit Cortex-M4 CPU-model readback, allocated-section bounds within physical 256 KiB flash/64 KiB SRAM, read-only executable flash, non-executable SRAM, buffer canaries, unchanged inputs, per-instruction stack bounds, restored SP/callee-saved registers and bounded execution. Source and ELF hashes were verified. Maximum observed standalone call frames were 3,376 bytes for GCC 14 compact, 3,368 for the GCC 15 winner and most other GCC 15 profiles, and 2,960 with no inlining. These are not whole-application stack/heap budgets.

This is CPU instruction emulation, not a Wio-E5 Mini board model. The harness independently links repository keypair/sign/SHA-512 code through stable exported wrappers, with explicit -Oz at link; it is not the exact full firmware LTO image. It does not qualify signature verification, boot, interrupts, MMIO, radio, USB, flash/filesystem drivers, sleep, concurrent traffic, physical timing or cryptographic side channels. A passing standalone harness does not make any overflowing full-firmware image usable.

"},{"location":"research/firmware_memory_budget/#mini-repeater-approved-cut-experiment-2026-10-18-utc","title":"Mini Repeater approved-cut experiment, 2026-10-18 UTC","text":"

The follow-up used an isolated source worktree at 9b616f4d1, with the same fixed timestamps, release recipe, real GCC 15 executable, compact additions and five-flag bundle described above. Only Morse/CW tools and automated management reporting were approved feature cuts. No experimental firmware source/configuration changes were adopted in the production branch.

Each comparison retained live USB packet logging for MQTT (MESH_PACKET_LOGGING=1, compact and merged USB logging), persistent /packet_log logging, the host-service bridge, normal authenticated CLI, 50 neighbours, radio2 and the 16 KiB filesystem. The existing cable-only limitation was unchanged. The management cut also omits its shared data.tx routing implementation, not ordinary request/reply statistics. The Morse/CW cut removes carrier tools and their timer/state/direct-carrier hooks; Morse RX was already disabled for this target.

These are conservative linked flash extents from the final .data load address plus its size, measured from flash base 0x08000000, rather than PlatformIO's smaller sum of selected section sizes:

Private GCC 15 recipe Linked extent App overflow Saving from control Compact/tuned control, all existing features 264,228 18,468 0 Morse/CW tools off 256,832 11,072 7,396 Management reporting off 254,280 8,520 9,948 Both approved cuts 246,908 1,148 17,320 Both cuts and shared numeric GET handlers 246,748 988 17,480

The two-cut result reproduced in fresh control builds. The proposed GET helper groups 27 simple scalar keys using typed member pointers and NUL-separated key lists; SET handlers, capability/Observer checks, matching rules, replies and persisted layouts are unchanged. It defaults off in the prototype and was not adopted merely for its 160-byte saving.

Further full-source tuning did not close the remaining gap:

Additional option Without GET helper: overflow With GET helper: overflow None 1,148 988 -mword-relocations 1,148 988 -fno-ipa-sra 1,844 Not tested -fno-tree-pre 1,148 Not tested -fno-ipa-ra 1,772 Not tested -fno-tree-sink 1,348 Not tested -fno-ipa-pta Not tested 2,640 -fno-ipa-cp Not tested 1,744 -fno-tree-forwprop Not tested 1,404 -fno-split-wide-types Not tested 3,244

Separate, unapproved diagnostic controls establish the tradeoffs but are not candidates: additionally removing the host-service bridge leaves 1,620 bytes free on GCC 15 and 1,240 on GCC 14; additionally removing only persistent packet-file logging instead leaves 828 and 384 bytes free, respectively. Both controls keep live USB/MQTT packet logging enabled. Neither extra feature cut was adopted.

Focused host tests exercised the actual raw RX/TX logging emitters and packet codec, exact line framing, maximum packets, short/non-draining serial writes, drop recovery, persistent logging callbacks and failed-TX handling across all eight carrier/management/file-logging combinations. Disabled-management tests verify fail-closed routing, password erasure, no management filesystem access and retained shared Fleet/beacon context. The full legacy/compact GET-body oracle passes 80,406 comparisons plus fallback checks under ASan/UBSan, including distinct fields, NaN/Inf, limits, whitespace and capability cases; four negative controls fail as expected. All ten CLI contract tests pass. Independent source/evidence review found no blocker, and git diff --check passes.

These tests use host boundary stand-ins; they do not establish physical USB/MQTT broker integration, ARM runtime behaviour, board startup, radio timing, sleep or sustained load. No approved configuration fits yet, so there is no approved runtime-RAM pass or usable Mini Repeater image from this experiment. No hardware was flashed, no failing image was published, and existing build outputs were not overwritten. Production compiler pins, features and capacity checks remain unchanged; the private source/tests, configs, verbose logs and linker maps were retained for further work.

"},{"location":"research/firmware_memory_budget/#mini-repeater-production-recipe-after-file-logging-approval","title":"Mini Repeater production recipe after file-logging approval","text":"

After approval to also omit persistent packet-file logging, the three gates were adopted only for wio-e5-mini_repeater: MESH_ENABLE_CARRIER_TOOLS=0, MESH_ENABLE_MANAGEMENT_REPORTING=0 and MESH_ENABLE_PACKET_FILE_LOGGING=0. All three default to 1 for other boards/roles. No compact numeric GET handler was adopted.

The production recipe retains the existing GCC 14.2 package pin, adds the equivalent compact SHA-512/scalar/field implementations and unused Arduino timer-wrapper trims, and applies the five measured compiler flags to both compilation and the final LTO driver. The target-specific link hook is inert for other environments. Existing flash and runtime-RAM checks remain active.

Canonical build.sh build-firmware links at release-version length (v1.18.0.1-halo-keymind-cascade-dev, Cascade, USA preset, DISABLE_DEBUG=0) pass:

Canonical target Linked flash extent App limit Flash headroom Runtime RAM available / required Wio-E5 Mini Repeater 245,376 245,760 384 44,560 / 32,288 Wio-E5 Mini USB Companion regression control 229,096 229,376 280 24,256 / 17,408

These are conservative complete flash extents, not PlatformIO's selected section sums. The Repeater still reserves its original 16 KiB filesystem, 50 neighbours, radio2, authenticated administration, host-service bridge and runtime-controlled live USB packet logging for an external MQTT bridge. The Companion keeps its 32 KiB filesystem and existing features. GCC 15 is not required and no toolchain upgrade was adopted.

The omitted file feature is only node-local /packet_log capture and log start|stop|erase / local log dumping. Live raw RX/TX framing, USB logging controls, statistics, watchdogs and bridge callbacks remain. Old saved log files are left untouched rather than automatically erased. Management-disabled routing fails closed and retains shared initialization needed by independently enabled Fleet/beacon features.

Focused coverage now runs in CI alongside an exact cable-only Mini Repeater build at release-version length. It covers the feature gates, default-on behavior, raw logging and backpressure under all eight flag combinations, shared cooperative serial output, crypto equivalence, CLI permissions and Morse/radio recovery. Production-source and host-test review found no blocker.

Both flash margins are narrow: future changes must re-link, not assume these images fit. These results qualify linked capacity and host behavior, not physical USB/RF/MQTT integration, board boot/sleep or a sustained-load soak. The Mini Repeater remains cable-only (--allow-no-ota); no hardware was flashed, no release was published and existing outputs were not overwritten.

"},{"location":"research/firmware_memory_budget/#shared-contact-ram-and-rak4631-full-recheck-2026-10-19-utc","title":"Shared contact RAM and RAK4631 Full recheck, 2026-10-19 UTC","text":"

Companion and simple_secure_chat contact sort/match indexes now use uint16_t when the complete table has at most 65,535 slots; larger configurations retain int. Static 350-contact targets save 764 internal bytes; PSRAM targets save 96 internal and 716 external bytes; static 100-contact targets save 264 internal bytes. Cached nRF52 record placement also saves four bytes per runtime contact. Capacities, serialization, queues, stacks, features and fallback behavior remain.

At aed4c22e5, matched against 31e693ec2, RAK4631 Full saves 2,200 static RAM bytes and 488 actual DFU application bytes. Its application is 712,112 of 712,704 bytes (592 free); modeled RAM admission margin rises from 4,516 to 6,716. CRC sharing and Reader packing preserve formats/text; Full features and existing SD support remain enabled. These are linked/build capacities, not running-heap or physical storage measurements.

Five final-source canonical packages pass across nRF52, ESP32 PSRAM, RP2040 and STM32. RAK3x72 USB has four actual BIN bytes free, requiring unchanged hard gates. At that revision, V3 Full fails RAM admission on both baseline and final source; its 100-contact wrapper gains 264 bytes, reducing the deficit from 21,288 to 21,024. The direct 350-contact V3 terminal default build passes separately; it is not a canonical Full or release-version/preset package. No hardware was flashed. See the qualification report and its source-bound artifacts, software coverage and limits.

"},{"location":"research/firmware_memory_budget/#heltec-v3-full-bounded-source-repair-2026-10-19-utc","title":"Heltec V3 Full bounded-source repair, 2026-10-19 UTC","text":"

At eb9259f2a, canonical V3 Full passes with 189,616 internal bytes available against 183,584 required: 6,032 bytes of modeled margin. The pinned NimBLE adapter preserves 15 saved phones and 30 CCCDs. Existing Bluedroid pairings need one-time re-pairing. Its 100 contacts, 40 channels, 256 offline frames, MQTT slot, Full features and stack sizes remain unchanged.

Grouped source proofs retain the same 4,096-block capacity with a lazy workspace of 1,004 bytes, statically bounded to 1,536. The guard replaces only the old 16,384-byte leaf allocation with this bound; the output block, metadata, transient margin and all other allowances remain additive. Legacy/PSRAM paths and larger custom leaf limits retain the existing policy.

The clean V3 package and constrained RAK regressions pass; physical V3 testing still awaits USB attachment. See the V3 qualification report for artifact hashes, tests, upgrade behavior and hardware limits.

"},{"location":"research/linux_chromium_web_serial_tty/","title":"Linux chromium web serial tty","text":"

View this page on MeshCore Docs.

"},{"location":"research/linux_chromium_web_serial_tty/#linux-chromium-web-serial-after-pyserial","title":"Linux Chromium Web Serial after pyserial","text":"

A Station G2 browser timeout on firmware source 1e421ff4fc6a3b56d43db7a18aab94a72129b527 was traced to the Linux host's inherited TTY read settings. Chromium 139.0.7258.127 closed its receive stream with NetworkError / device lost before the first ver request. The radio was still running and an exclusive pyserial query answered in 21.330 ms without a reset or replug. This finding explains this controlled host failure; it does not establish that every reported USB hang has the same cause.

"},{"location":"research/linux_chromium_web_serial_tty/#why-an-idle-port-looked-disconnected","title":"Why an idle port looked disconnected","text":"

pyserial normally sets VMIN=0, VTIME=0 and performs its timeout handling with select. These kernel TTY attributes survive closing the port. With those settings, an idle nonblocking Linux read() can return zero bytes. pyserial's POSIX implementation sets these values and closes without restoring the previous attributes.

In the exact Chromium 139 serial implementation, port configuration preserves the prior VMIN and VTIME. Its receive handler interprets a zero-byte read as device loss. Setting VMIN=1, VTIME=0 makes an idle nonblocking read return EAGAIN, so the browser waits for data instead.

Upstream Chromium fixed this in commit 45ccdf6e on 2026-09-18, under bug 559872992. The change explicitly configures both read settings and adds a real Linux pseudoterminal regression for inherited settings. The presence of this fix upstream does not prove a particular installed browser contains it; the Pi's tested Chromium 139 predates the fix.

"},{"location":"research/linux_chromium_web_serial_tty/#controlled-g2-comparison","title":"Controlled G2 comparison","text":"

The same firmware, native Pi Chromium, and unmodified official flasher console were used for both attempts. Console source SHA-256 was 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9. Instrumentation observed and forwarded existing native API calls.

Attempt Host settings and observation Result 203 VMIN=0, VTIME=0. The receive stream was already lost before Enter. The native five-byte ver\\r\\n write nevertheless completed in 28.1 ms. FAIL. No displayed reply by the unchanged 5,000 ms deadline. 204 Only host VMIN changed to 1; VTIME stayed 0. The receive stream remained open. PASS. First ordinary ver reply was displayed in 154.1 ms.

No firmware or website patch, modem-signal change, reset, replug, added command retry, keepalive, or longer deadline was used to obtain the passing comparison. Before this comparison, stats-core reported uptime 1,728 seconds and error bitmask 2; this is evidence of continued operation, not an error-free radio qualification. The comparison does not qualify a fresh flash, a long idle window, another host OS/browser, or every USB transport.

Safe receipt names and hashes are retained for the comparison:

Receipt SHA-256 stock-browser-diagnostic-203-safe.json e41b0f009b8dce42ad9ee23ad843032b97a47fc2cfc5385aefbf093ac46a9788 controlled-browser-host-setting-safe.json 88527690ca8d8740fd415831123c45ac3249134aec3929f7fd2b0224ddbe8fce stock-browser-diagnostic-204-safe.json 6ba3249a23364a589653a0a1a4abb7e453a1cb2f095cec6138f3cebc171d5f39"},{"location":"research/linux_chromium_web_serial_tty/#hardware-test-preflight","title":"Hardware-test preflight","text":"

The tracked Linux Web Serial helper changes and verifies only VMIN=1, VTIME=0 immediately before a native browser test. It requires an explicit absolute TTY path and restores the complete original termios attributes after the browser closes, including on a Python exception or interrupt. Failed preparation also restores the original state. It sends no serial data and makes no DTR/RTS or USB-reset calls.

Pause other serial clients and finish all pyserial preflight queries before entering the context. Another opener can overwrite the read settings, and a second reader can consume the radio's reply. The helper closes its preparation descriptor before the trial, so it does not keep the port open across the stock browser's Disconnect action. Controllers must close their native reader, writer and port in their own finally cleanup before the context exits, and route termination into that cleanup. If the endpoint is replaced or disappears, restoration fails explicitly rather than applying old attributes to a different device.

Record browser version, TTY settings, native stream state/error, native write completion, raw receive-byte count and rendered reply separately. A zero-byte console display alone does not prove that the USB endpoint received no bytes. For example, the stock console buffers input until a complete CRLF line arrives. See the HIL instructions for the context-manager example and the hardware-free pseudoterminal test.

"},{"location":"research/nrf52_bluetooth_debug/","title":"Nrf52 bluetooth debug","text":"

View this page on MeshCore Docs.

"},{"location":"research/nrf52_bluetooth_debug/#nrf52-companion-bluetooth-diagnostic-firmware","title":"nRF52 Companion Bluetooth diagnostic firmware","text":"

The T1000-E diagnostic image is Full Companion firmware with the normal radio, sensor, pairing and recovery settings. Diagnostics are enabled with MESH_NRF52_BLE_TRACE=1. Ordinary builds compile them out. Diagnostic builds reserve about 6.3 KB of RAM for a 256-event ring and counters.

The trace records connection timing, security results, UART notification subscriptions, app receive/transmit lengths, queue overflow, local disconnect requests and each recovery trigger. It brackets Bluefruit's real event dispatcher so a handler that stalls can be distinguished from an event that never reached the framework. Maximum dispatch duration and Companion loop gaps are also reported. Callbacks write bounded RAM records without serial printing or allocation. PINs, keys, peer addresses and message contents are not stored.

"},{"location":"research/nrf52_bluetooth_debug/#capture-a-phone-failure","title":"Capture a phone failure","text":"
  1. Install the diagnostic UF2 for your exact board, or use its application DFU ZIP. This does not require replacing the bootloader.
  2. Connect with the phone normally and reproduce the failure. Keep the node powered. Do not forget the Bluetooth device, reboot, or factory reset before collecting the trace.
  3. Connect the node's USB cable to a computer and close other serial clients. Install Python and pyserial if needed.
  4. Export the diagnostic report:

~~~sh python3 scripts/capture_nrf52_ble_debug.py --port /dev/ttyACM0 --output bluetooth-report.json ~~~

On Windows use the correct COM port instead of /dev/ttyACM0. The exporter enters the Full Companion USB terminal at 115200 baud, reads diagnostics, then returns it to binary mode. It does not reset or change settings. 5. Send the JSON report with the approximate failure time and what the phone displayed. Export soon after failure because old events roll out of the ring. Reboot or loss of power clears the RAM history.

If the Pi's attached T1000-E is used, its serial identity is 34A9141999729D5D. Identify the physical board before flashing; an upstream application may have the misleading USB product name T1000-E-BOOT.

"},{"location":"research/nrf52_bluetooth_debug/#manual-usb-commands","title":"Manual USB commands","text":"

The debug commands are restricted to local Companion transport requests and are not exposed through remote mesh management. Replies and raw records are decoded by the exporter. Sequence gaps and overwritten records are reported. Diagnostic contention is counted and dropped rather than blocking BLE. The exporter retains unsupported-command replies from older format-2 images; missing newer readbacks do not prevent export of their event rings.

"},{"location":"research/nrf52_bluetooth_debug/#reading-the-result","title":"Reading the result","text":"Evidence Stage identified No stack connection event; advertising remains active Connection has not reached the application's Bluetooth event layer. Connection but security error or timeout Pairing or encryption setup. Secured link but UART notifications disabled Notification subscription or restoration. Secured link with no app RX; secured_no_app_watchdog App never opened its Companion UART session; the 15-second recovery fired. App RX followed by blocked/partial TX Reply delivery; the report identifies the queue/watchdog result. Dispatch entry with no matching exit and increasing age Software stack handler may be stuck; correlate with a later snapshot.

Reason 0x08 is connection timeout, 0x16 is local termination and 0x3e is connection establishment failure. Do not confuse decimal 22 (0x16) with hexadecimal 0x22 (link-layer response timeout).

The report can identify the failing software stage, but cannot guarantee the physical cause. No connection event alone cannot separate a central controller problem, RF interference and a peripheral controller problem. Dispatch timing starts after the SoftDevice delivers an event; it does not measure radio interrupt latency. A bench failure is not proof of the same failure on an iPhone. Use the diagnostic image on the affected node.

"},{"location":"research/nrf52_bluetooth_debug/#build","title":"Build","text":"

Run one PlatformIO command at a time in this checkout:

PLATFORMIO_BUILD_FLAGS='-DMESH_NRF52_BLE_TRACE=1 -DBLE_DEBUG_LOGGING=0 -g3' \\\nOUTPUT_DIR=out/t1000e-ble-debug \\\nbash build.sh build-firmware t1000e_companion_radio_full \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev-ble-debug --full\n

The Bluefruit dispatch hooks are applied to private build copies of framework sources. The shared PlatformIO framework cache is not modified. Retain the ELF alongside the UF2/ZIP for future SWD inspection if hardware access becomes available. Do not halt the CPU during a Bluetooth connection: that disrupts SoftDevice radio timing and invalidates the test.

"},{"location":"research/rak3401_1.17.1.7_legacy24/","title":"Rak3401 1.17.1.7 legacy24","text":"

View this page on MeshCore Docs.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#rak3401-11607-to-11717-without-a-bootloader-update","title":"RAK3401: 1.16.07 to 1.17.1.7 without a bootloader update","text":"

Qualified on MercerWoodMesh on 27 September 2026: all 15 application updates passed over LoRa, from the exact supplied 1.16.07 image, without a bootloader update. Use RAK3401-1.16.07-to-1.17.1.7-legacy24-mOTA.zip and verify it against the accompanying .zip.sha256 file before extracting. Follow the exact before/after hashes in its CHAIN.csv; this is not a single-step stock update.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#exact-compatibility-boundary","title":"Exact compatibility boundary","text":"

This is an application-only migration for target 2FA509C1, hardware RAK_3401_repeater_lora_ota_no_external_sensors. It is not a generic RAK4631 package. The installed application must have body hash 71F4026CBE4B8B74 (1.16.07, full image SHA-256 4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb).

The bootloader remains 0.9.2-OTAFIX2.4, with ABI 2 and codec mask 0x4. Its application staging ceiling is 0xD4000. Every package is a codec-2 CRLE in-place delta. DEFLATE is used only for transport by compatible intermediate applications; the old bootloader still applies CRLE.

The destination is built from source commit 2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1 with the explicit MESHCORE_NRF52_LEGACY_FLASH_OTA=1 build selection. This preserves the ordinary S140 linker and disables hybrid flash/RAM staging and bootloader self-update. It is deliberately different from the stock hybrid 1.17.1.7 release. The endpoint has:

Do not replace this endpoint with the stock ZIP. Do not send a bootloader or SoftDevice update. The ZIP/UF2 files used to construct and qualify the chain are not themselves LoRa update containers.

Future application updates for this node must also be qualified against the unchanged OTAFIX2.4 contract. Keep the legacy build selection; do not assume that a later stock release is an interchangeable target for this chain.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#what-smallest-means","title":"What \"smallest\" means","text":"

The search compares an explicit, hash-pinned set of application images and every allowed page-aligned in-place workspace for each increasing-version transition. The starting bootstrap is fixed: its 89,844-byte package is the only admitted first edge. Synthetic bridge version numbers are real compiled versions below 1.17.1.7; their binaries are not relabeled after compilation.

The bytes objective minimizes ideal serialized MeshCore traffic first, then install count. The packets objective minimizes ideal originated packets first, then serialized traffic. The older transport objective minimizes install count before traffic and is not equivalent to either.

Accounting includes manifests, block requests, proof requests/responses, DATA fragments, and MeshCore framing. It uses the actual pinned encoder for compressed blocks. It excludes discovery, retries, admin/control commands, other mesh traffic, flood fan-out, and LoRa preamble/coding overhead. A fixed linear relay path can be accounted for separately. These are finite-search minima, not a proof that no possible custom firmware or protocol could ever use fewer bytes. The ideal compressed counts also assume negotiation remains in that mode. An older receiver can fall back to raw DATA after stalls; the physical-test transmission totals include that extra traffic.

Best lossless byte count is not a guarantee of the fastest real radio link. Use the fastest preset that remains reliable over the actual route. SF5 / 500 kHz is a direct bench measurement, not a recommendation to switch an unreachable field node or unconfigured relay chain to that preset.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#selected-route","title":"Selected route","text":"

The completed search covers 52 images and 47,920 flash geometries. The packet-first route has 15 packages, totaling 2,340,160 container bytes. Its ideal direct-link traffic is 12,621 packets / 1,808,961 serialized MeshCore bytes. These totals include traffic in both directions, not just DATA. Fifteen is also the fewest installs among the searched candidates.

The byte-first route uses the same sequence of application images, with four different patch workspaces: 12,625 packets / 1,808,787 bytes. Saving 174 bytes costs four packets. The delivered packages use the packet-first choice; do not substitute packages from the comparison route.

Step Resulting version Container bytes Ideal packets Ideal MeshCore bytes 1 1.16.7.9 89,844 878 101,203 2 1.16.7.10 62,838 551 70,031 3 1.16.8.7 106,125 496 70,134 4 1.16.9.105 149,542 728 104,992 5 1.16.9.110 112,540 489 67,887 6 1.16.9.111 71,070 268 35,453 7 1.16.9.116 138,545 692 101,308 8 1.16.9.122 177,536 995 148,671 9 1.16.10.1 258,398 1,477 223,042 10 1.16.11.0 245,426 1,450 219,953 11 1.16.12.1 217,608 1,167 172,794 12 1.16.13.1 199,395 1,024 150,005 13 1.16.14.0 199,527 989 143,160 14 1.16.15.0 139,188 618 86,721 15 1.17.1.7 172,578 799 113,607

Some bridge versions also exist as different binaries. CHAIN.csv's exact body hashes and package checksums, not this version-only table, determine which package is valid at each step. The first two transfers use legacy 1 KiB blocks; subsequent transfers use negotiated 2 KiB blocks with transport DEFLATE and the unchanged CRLE bootloader format. The second transfer also omits explicit proof requests: that historical receiver waits for the current sender's proactive Merkle proofs. The original 1.16.07 receiver does not have this optimization.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#measured-mercerwoodmesh-replay","title":"Measured MercerWoodMesh replay","text":"

All 15 selected packages passed in order on the Pi's debug-wired RAK3401, starting with the exact 1.16.07 image and OTAFIX2.4. Every application payload crossed LoRa from the Heltec T096. There were no debug writes, application substitutions, failed installs, or resumed downloads between the 15 steps.

The direct bench used 909.950 MHz, BW 500 kHz, SF5, CR5, continuous receive, and a 128-symbol source preamble. Observations for both radios totaled:

These are bench measurements, not a field-link completion-time promise. The control commands were sent over USB; a remote field console adds LoRa control traffic. BENCHMARK.csv separates each step's ideal cost from its observed frame count and timing. Actual serialized radio bytes were not measured.

Independent debug-port readback matched the complete final application, unchanged bootloader, and unchanged MBR/SoftDevice. UICR matched the normal post-startup baseline throughout this replay. The initial pre-first-boot UICR dump differs because normal Nordic/RAK startup configures the reset pins and 3.3 V regulator; this is documented in the evidence, not a bootloader upgrade.

After a fresh reboot, the watchdog reported on. Both radios returned to 910.525 MHz / BW 62.5 kHz / SF7 / CR5, and a nonce-matched normal-channel trace through the exact target key prefix completed in 423 ms. The inaccessible field node has not been touched. The lab RAK remains on this repeater image; the Pi's mctomqtt.service remains stopped because it previously owned that serial port. Radios other than the RAK3401 and sending Heltec T096 were left untouched.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#field-preparation","title":"Field preparation","text":"

All destination commands below can be sent through the authenticated remote repeater console; USB/Bluetooth at the destination is not required. Keep a separate control connection available while the sender owns its companion's serial port. A local Full Companion radio attached to the sender runs the bundled motatool seeder. Resolve ports by device identity, not a remembered ttyACM number.

The qualified source is the Pi's Heltec T096, USB serial 651F8E496197F882, running Full Companion v1.17.1.8-halo-keymind-cascade-dev-d4b3bb56. It supports the extended 2 KiB transport and host-backed DEFLATE. The destination is RAK3401 USB serial 0B81C9C68D8D01B4. Older companion firmware or an older motatool is not an interchangeable sender for this bundle.

Before any change, record ver, ota self, ota status, get bootloader.ver, get system.watchdog, get radio.rxps, get powersaving, get rxdelay, get af, and ota config. Match the exact starting body, hardware, bootloader, and signing-key policy. An unsigned bundle is not suitable for a node requiring signed packages. Choose a stable-power maintenance window long enough for all 15 installs; do not begin an install while the destination is at risk of losing power.

Disable the destination system watchdog with set system.watchdog off. Allow its one-time hardware reset and wait at least 90 seconds; reconnect and prove that it reports off. A quick software reboot is not a substitute.

Use continuous receive on the destination and sender throughout the bridge chain (set radio.rxps off). Record and disable a second scanning radio profile on the sender if it would take the radio away from the OTA channel. Record rxdelay, af, power-saving, reach, and relay settings before changing them. Use hops 0 only when there are no intermediate relays. Obey applicable airtime/duty restrictions and restore all saved settings when finished.

The direct bench also uses set rxdelay 0 and set af 0 on both radios, ota speed 1 on the sender, and set powersaving off where supported. The earliest applications do not recognize the last command; continuous receive is still selected through set radio.rxps off. The source has set radio2 off throughout the transfer. Wait for configuration replies to finish before handing its serial connection to the seeder.

For the qualified direct bench configuration only, the destination uses tempradio 909.950,500,5,5,120; the tested current Full Companion accepts tempradio 909.950,500,5,5,120,128 for an explicit 128-symbol preamble. Historical destination parsers do not accept the sixth argument. The sender and destination must be on the same live radio preset. Do not assume the temporary preset survives a bridge reboot.

The newer historical receivers performed poorly with a 16-symbol source preamble in this test. Use the qualified longer setting. Also avoid frequent ota status or ota stats polling: these versions rehash the complete running application while answering, which can interrupt receive processing. The lab runner observes the sender's local log and checks the receiver near completion or at a 60-second interval. Set a field maintenance lease appropriate to the actual preset and route; the 120-minute value above is a bench setting.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#applying-a-qualified-bundle","title":"Applying a qualified bundle","text":"

Verify the outer archive hash and every entry in SHA256SUMS.txt. Verify each .mota using the included, hash-pinned motatool. CHAIN.csv is the authoritative ordered list of packages, manifest IDs, and before/after hashes. On the Pi, use tools/motatool-linux-aarch64 (make it executable after extracting if necessary). The x86-64 binary is for Linux/WSL, not native Windows. The provenance images, tools, and evidence are not sent over LoRa; only the selected .mota in the current step's serving directory is sent.

For every row, including after an interrupted run:

  1. Prove that ota self matches that row's exact before hash and that the bootloader reports apply OK. Do not select a row by version alone.
  2. Reconcile ota status. Resume only that row's known partial MID. If it is already ready, do not redownload it. Stop on an unrelated or ambiguous session. Never send a blind ota cancel.
  3. Reapply the agreed radio/receive guardrails after reboot. Start the sender with only this row's package in its serving directory: motatool serve --dir STEP_DIRECTORY --serial COMPANION_PORT --baud 115200 -v.
  4. Run ota ls until the expected version is advertised. The old listing shows a version, not a MID. Use ota pull EXPECTED_MID flash and poll ota status at a restrained interval.
  5. Require ready to install with the exact MID. Stop the seeder gracefully with Ctrl-C. Recheck watchdog off, then send ota install.
  6. Allow up to five minutes for the application to return. Require the exact after hash, expected version, and bootloader: apply OK before continuing. Stop if any check fails; do not try the next package against another base.

A retained previous container may be re-discovered after boot. If the exact previous MID is verifying, wait for it to become ready or idle. Only after re-proving the current running hash may that attached, completed previous MID be cancelled. An idle ota cancel does not prove that flash was erased.

After the final hash is proved, restore the destination, sender, and relays to their recorded normal settings and confirm normal-channel reachability. Re-enable the system watchdog and require it to report on. The current endpoint supports normalradio; coordinate channel restoration so that the remote node does not become unreachable during the handoff. The endpoint requires a temporary radio channel for OTA operations, so a normal-channel ota ls is not a reachability test. Use an ordinary mesh trace or authenticated remote command after restoring normal radio.

The existing rak3401_mota_chain.py pins older release archives. It must not be pointed at this new bundle or have its safety checks bypassed.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#evidence-and-reproducibility","title":"Evidence and reproducibility","text":"

build_rak3401_legacy24_bundle.py checks every base/target hash, hardware identity, increasing version, block capability, flash workspace, and staging margin. It reconstructs every target with both zero- and erased-flash-filled scratch space, and applies every package with the actual OTAFIX2.4 release implementation compiled as a flash simulator. It remeasures transport with the pinned encoder. Its output remains explicitly offline-only until a separate physical qualification record is attached.

ACCOUNTING-CORRECTION.json records the proactive-proof accounting correction. It changed the ideal totals only; all package checksums and the selected hardware-test sequence remained unchanged.

The archive includes the 52 frozen search images, all 47,920 measured geometries, encoder binaries, per-step simulator logs, and raw hardware-test records. Only the selected .mota containers are served over LoRa. Lab scripts in evidence/ are tied to the Pi's USB/debug fixture and are not a remote field deployment controller.

Regression checks: 534 LoRa OTA tests ran successfully with one opt-in dependency-install test skipped; all 11 linker-selection tests passed.

All PlatformIO builds must be serialized, including builds in separate checkout directories that share the same PlatformIO package installation.

"},{"location":"research/rak3401_mota_chain/","title":"Rak3401 mota chain","text":"

View this page on MeshCore Docs.

"},{"location":"research/rak3401_mota_chain/#rak3401-1w-repeater-compact-lora-update-chain","title":"RAK3401 1W repeater compact LoRa update chain","text":"

Status: published prerelease for controlled, recoverable lab use. On 29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte identical to the corresponding transitions in the physically passed fd98bc90 chain. The replacement step 10 installs the current 3f6eddd5 endpoint. That replacement package and the hardened current runner have passed offline reconstruction plus the exact deployed bootloader simulator, but have not had a new physical end-to-end run. Use the lab gate and keep local DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates remain planning data, not physical qualification.

The prerelease is rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5. Use this asset:

RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\n

--verify-only remains the safe default. Live preflight and installation of this prerelease require --accept-test-candidate. That switch bypasses only the prerelease-status gate; it does not bypass the archive, identity, bootloader, route, watchdog, reachability, or post-boot checks.

"},{"location":"research/rak3401_mota_chain/#exact-destination","title":"Exact destination","text":"

This chain is intentionally specific to one firmware lineage and hardware target:

The exact starting application is the lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf RAK3401 asset. Its outer ZIP SHA-256 is 58ead2ea18efafda186bc9d983fe08cdf65f69b0e5a3a1c72c18fe35948a254c; its 566,912-byte firmware.bin SHA-256 is 4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb. The endpoint build ZIP SHA-256 is a9fca9be145a96feb650b57c43b5ed173d74d2f3194da12e31b1c54352635b56.

"},{"location":"research/rak3401_mota_chain/#retained-and-omitted-hardware-support","title":"Retained and omitted hardware support","text":"

Despite the legacy no_external_sensors target name, the endpoint retains the RAK12501 GPS provider and the common INA3221, INA219, INA226, and INA260 I2C voltage/current monitors. Install RAK12501 in sensor slot A. Do not use slot D with the RAK13302 1 W radio because the GPS reset/PPS signals overlap the radio's BUSY/DIO1 wiring.

The reduced profile omits optional external environmental and ranging devices: AHTX0, BME280, BMP280, SHTC3, SHT4x, LPS22HB, MLX90614, VL53L0X, BME680, BMP085, RAK12035, and BME680 BSEC. Board radio, display, buttons, battery telemetry, GPS, and the INA monitor family remain. No additional \u201cremove I2C temperature\u201d image is needed: the optional I2C temperature/environment drivers are already in the omitted set, while removing the remaining I2C/INA support would discard desired voltage/current monitoring and create another firmware identity without helping this route.

Do not use the chain on another target ID, hardware family, starting image, or firmware body hash. The runner checks all four.

"},{"location":"research/rak3401_mota_chain/#why-the-deployed-otafix24-and-old-staging-ceiling-work","title":"Why the deployed OTAFIX2.4 and old staging ceiling work","text":"

The deployed bootloader is not changed and no package uses the newer expanded 0xED000 ceiling. Each package remains bottom-aligned below the old bootloader's 0xD4000 scan ceiling. Its detools workspace is page-aligned and must satisfy:

0x26000 + inplace_memory <= staged_package_start < 0xD4000\n

The old bootloader already reads and validates that encoded detools geometry before its first destructive write. The previous application receiver was the limiting part: it reserved a fixed 0x98000 workspace before accepting a download. Step 1 therefore remains byte-for-byte identical to the physically passed 89,844-byte package. Step 2 is a 46,364-byte fixed-workspace package that installs the compatibility receiver. Later receivers stage above the real EndF image extent and let the bootloader enforce each package's selected workspace.

Every package in this candidate is an ordinary application container using format ABI 2 and in-place codec 2. The historical bridge applications scan the bootloader capability marker byte-by-byte, so they recognize the released 0.9.2-OTAFIX2.4 marker at absolute address 0xFCD16. That immutable marker advertises ABI 2, codec mask 0x0004, and internal storage profile 0. The 3f6eddd5 endpoint retains the matching legacy application-update fallback. Its separately compiled internal bootloader-update feature remains unavailable because privileged self-update requires a current ABI 3 marker with the exact storage profile. Installing this chain therefore does not replace the bootloader, and the final application can still accept a future valid ABI-2/codec-2 application update.

The exact installed bootloader is the 0.9.2-OTAFIX2.4 RAK4631 asset. Its outer ZIP SHA-256 is 5e29e7a8982cc2ed5f8868435556eb09387250fe5b9a7e8443bac325247dfed1; its 191,040-byte sd_bl.bin SHA-256 is a97f834388db0f1da6c29f6d8f458ff6d3e6a26034709044566384838919bb23. The exact simulator was compiled from release commit d73de8372e89b8ef352747c8bc7a1aaeab80fbfe; its executable SHA-256 is 9dacf24b1023fe2f4c620419417649ccc9538dd4a611b47bc19b7fedb97ceba6.

The schema-2 exhaustive search checked 1,799 candidate geometries and found 12 feasible forward edges in the declared exact 12-node inventory. It found one shortest ten-package route and selected the smallest total container bytes as the secondary objective. This minimum applies only to that declared inventory and geometry table, not every conceivable new bridge image. The selected route is:

Step From To Workspace Package Margin 1 1.16.7.0 1.16.7.9 0x98000 89,844 0 2 1.16.7.9 1.16.7.10 0x98000 46,364 40,960 3 1.16.7.10 1.16.8.7 0x91000 106,029 12,288 4 1.16.8.7 1.16.9.105 0x89000 149,927 0 5 1.16.9.105 1.16.9.110 0x8E000 111,312 16,384 6 1.16.9.110 1.16.9.113 0x8F000 120,624 4,096 7 1.16.9.113 1.16.9.117 0x8B000 139,637 0 8 1.16.9.117 1.16.10.0 0x7E000 174,202 20,480 9 1.16.10.0 1.17.1.3 0x6B000 271,569 0 10 1.17.1.3 1.17.1.6 0x76000 197,825 28,672

Total mOTA transfer data is 1,407,333 bytes. ROUTE.json, CHAIN.csv, and validation-results.json in the bundle pin the exact geometry and image hash for every transition.

Steps 1-9 are byte-for-byte identical to the transitions that passed on the physical RAK3401. The replacement step 10, from the physically reached FE65A6135A1E7B3F v1.17.1.3 body to 9BD7CF682EE065AE, has offline qualification only. All ten packages passed independent container checks, zero-filled and erased-workspace reconstruction, and the exact deployed 0.9.2-OTAFIX2.4 simulator. Those results prove the package bytes and apply geometry; they are not a physical claim for the replacement step 10 or the hardened runner.

The 171-byte transport DEFLATE support lands only in the final 3f6eddd5 application. The starting receiver and every receiver that accepts steps 1-10 lack that inflate path, so all ten legacy-chain transfers remain raw and the 1,407,333-byte total above is not automatically reduced over LoRa. A later update can negotiate transport compression after this endpoint is running; the application inflates before staging, and the immutable bootloader still applies an ordinary ABI-2/codec-2 container.

"},{"location":"research/rak3401_mota_chain/#host-requirements","title":"Host requirements","text":"

Install Python 3.9 or newer, meshcli 1.6.0 or newer, the current motatool, and an OTA-enabled Full Companion or repeater that can seed mOTA files.

"},{"location":"research/rak3401_mota_chain/#verify-offline","title":"Verify offline","text":"

No password or device is needed:

python3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --motatool /path/to/motatool \\\n  --verify-only\n

When given the ZIP, the runner checks its pinned outer hash and binds the extraction cache to that exact archive. An explicitly supplied extracted root starts at the pinned inner checksum list. In either case it checks complete inner coverage, all ten manifests, continuity, image anchors, both recovery images, and every container with motatool. Passing this command means offline and simulator qualification only; it does not unlock live use.

"},{"location":"research/rak3401_mota_chain/#guarded-prerelease-lab-command","title":"Guarded prerelease lab command","text":"

These direct-link arguments match the physical transition setup, but the current package replaces step 10 and the current host runner contains later cleanup and recovery fixes. Neither has been cleanly rerun through the whole chain. Keep local USB/SWD recovery available. The candidate switch bypasses only the prerelease-status block; all identity, checksum, bootloader, route, watchdog, reachability, and post-boot gates still run.

Restore the test start locally with recovery/test-start/RAK3401-test-start-v1.16.7-c1caa5ad.uf2, then run:

export MESHCORE_ADMIN_PASSWORD='password'\n\npython3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.51:5001 \\\n  --source-cli-tcp 192.168.1.51:5002 \\\n  --source-shares-controller \\\n  --target-key 63d8df63 \\\n  --temp-radio 909.950,500,5,5,120 \\\n  --ota-hops 0 \\\n  --legacy-full-airtime \\\n  --motatool /path/to/motatool \\\n  --accept-test-candidate \\\n  --yes\n

--target-key accepts a full contact public key or a unique hexadecimal prefix of at least eight characters, such as the 12-character prefix shown by meshcli lc. The chain resolves it against the controller's saved contacts and uses the full key for every remote command and transfer step. Emoji or a later change in the repeater's display name does not affect selection. Each --relay can likewise use a full key or unique prefix instead of a name. The target still must be a repeater contact with the exact hardware, firmware, and bootloader required by this chain.

Keep the work directory. Rerunning the same command resumes only when the live EndF body hash matches an exact chain node. Never manually skip a package. --legacy-full-airtime temporarily sets the destination airtime factor to zero and restores it at the endpoint. Use that option only where the selected frequency and local duty-cycle rules permit a full transmit budget; omit it otherwise.

"},{"location":"research/rak3401_mota_chain/#superseded-b40d2e6c-prerelease","title":"Superseded b40d2e6c prerelease","text":"

The immediately preceding prerelease remains pinned for offline provenance:

Its first nine packages are byte-for-byte identical to the current release and the physical fd98bc90 evidence. Its different step 10 had offline qualification only. The runner recognizes this exact archive separately and keeps live use gated; it is not interchangeable with the current 3f6eddd5 asset.

"},{"location":"research/rak3401_mota_chain/#legacy-fd98bc90-physical-and-swd-qualification","title":"Legacy fd98bc90 physical and SWD qualification","text":"

The earlier local candidate remains the exact physical evidence source for this route:

RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip\n

All ten exact fd98bc90 transitions completed on the RAK3401 with its deployed 0.9.2-OTAFIX2.4 bootloader and a Heltec V4 source at 909.950 MHz / 500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD readback then matched the endpoint application, original bootloader, SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine packages in the b40d2e6c and 3f6eddd5 prereleases. All three candidates have different step 10 packages or endpoints, so the physical result and step-10 timing must not be attributed to either later prerelease.

"},{"location":"research/rak3401_mota_chain/#measured-direct-link-timing-for-fd98bc90","title":"Measured direct-link timing for fd98bc90","text":"

The first ready to install transfer ages were:

Step Blocks Transfer 1 88 598 s 2 45 42 s 3 103 81 s 4 146 105 s 5 109 76 s 6 118 94 s 7 136 102 s 8 170 127 s 9 264 187 s 10 169 193 s

The physically observed bulk-transfer total for those exact legacy packages is 1,605 seconds (26m45s). The log came from the pre-cleanup-fix runner revision. Step 1 was measured before the host began forcing source RXPS off and is therefore a conservative outlier. A new complete run with that fix is projected at roughly 18\u201319 minutes of bulk transfer, but that projection is not a substitute for the measured total. Allow about 70\u201375 minutes direct at BW500 for validation, ten transfers, installs/reboots, retained-store checks, and final restoration.

The legacy step 10's longer per-block time was not a V4 TempRadio delay: the source RXPS readback was off. Its v1.17.1.3 requester uses non-overlapping adaptive flights and a five-second recovery floor and logged more block reloads than step 8. The V4 itself intentionally schedules its local radio handoff after 1.5 seconds so the normal-channel command reply can drain; the host waits three seconds and then checks the live TempRadio state.

"},{"location":"research/rak3401_mota_chain/#live-progress-and-speed-sanity-checks","title":"Live progress and speed sanity checks","text":"

The runner watches motatool serve -v locally, so its quarter-progress reports do not consume LoRa airtime. At BW500 direct, physically measured steps 2\u20139 averaged about 1.1\u20131.4 newly requested payload blocks per second; the legacy step 10 averaged about 0.88 blocks/s, and step 1's 0.15 blocks/s was the known pre-RXPS-fix outlier. Use those only as bench sanity ranges, not guarantees. Relays, narrower bandwidths, interference, and recovery reads lower the rate.

Healthy output keeps advancing from [download] passive source progress x/y to source read all y payload blocks, followed by destination status ready to install y/y (100%) for the same manifest ID. Aggregate reads above the unique block count indicate recovery/retry traffic. A radio-scaled no-progress window causes one destination status check; the transfer timeout leaves the partial download staged for an exact-package resume. Stop and diagnose instead of installing if the manifest changes, progress remains flat through those checks, the final receiver count is not y/y, or the measured rate is far below the matching bandwidth/hop estimate without an understood RF cause.

For planning only, scale the conservative measured fd98bc90 26m45s bulk baseline by the qualified adaptive-preamble airtime and by each additional relay transmission:

Bandwidth, SF5/CR5 Direct / 0 relays 1 relay 2 relays 500 kHz 26m45s 53m30s 1h20m15s 250 kHz 46m51s 1h33m42s 2h20m33s 125 kHz 1h27m03s 2h54m07s 4h21m11s 62.5 kHz 2h54m07s 5h48m15s 8h42m23s

These are transfer-only estimates, not measured alternate-bandwidth or relay results. Packet loss, relay contention, discovery, administrative commands, staged verification, and ten reboot cycles add time. The bandwidth factors are not simple powers of two because the adaptive wire preamble is 128 symbols at 500 kHz, 64 at 250 kHz, and 32 at 125/62.5 kHz.

"},{"location":"research/rak3401_mota_chain/#manual-operation","title":"Manual operation","text":"

The automated runner is preferred because it binds every transition to the expected MID and body hash. The manual details below document the checks used during qualification and recovery; they are not permission to skip the runner's candidate gate. Keep a written copy of each original setting and never skip a step even when a later package appears in ota ls.

"},{"location":"research/rak3401_mota_chain/#1-verify-and-extract-the-asset","title":"1. Verify and extract the asset","text":"
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\nunzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\ncd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5\nsha256sum -c SHA256SUMS.txt\nfor package in motas/*.mota; do motatool verify \"$package\" || exit 1; done\n

The outer hash must be 135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4, and the SHA-256 of the extracted SHA256SUMS.txt must be 93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4. Do not continue after any checksum or verification error.

"},{"location":"research/rak3401_mota_chain/#2-record-and-prepare-the-destination","title":"2. Record and prepare the destination","text":"

Before changing anything, save the complete replies from:

ver\nota self\nget bootloader.ver\nget system.watchdog\nget radio.rxps\nget powersaving\nget rxdelay\nget af\nota config\n

The starting ota self hash must be 71F4026CBE4B8B74, the target must be 2FA509C1, and the hardware must be RAK_3401. Disable the nRF52 system watchdog before the chain:

set system.watchdog off\n

The hardware watchdog cannot stop immediately. The RAK will reset once within about 60 seconds. Reconnect, wait at least 90 seconds from the command, and require get system.watchdog to report > off before continuing.

For a direct bench link, apply these temporary transfer guardrails to the RAK after every bridge reboot:

set powersaving off\nset radio.rxps off\nset rxdelay 0\nota config hops 0\ntempradio 909.950,500,5,5,120\n

Where local duty-cycle rules permit it, set af 0 can also remove the legacy firmware's saved airtime wait during this bounded maintenance window. Record get af first and restore that exact value after the final step. Current firmware grants the bounded TempRadio transfer budget without overwriting the saved airtime factor.

set powersaving off is an isolation guardrail, not the fix for the observed failure. The actual source failure was an oversized USB CDC reply; current firmware splits those reads. Some historical bridge builds may report RXPS as unsupported while already operating continuously. Verify the reported state and do not substitute a normal reboot for the one-time watchdog reset. Direct tests use hops 0: hops 1 makes the target echo each source response despite there being no intermediate relay, increasing half-duplex loss and legacy three-second retries.

The exact candidate cannot use fast RXPS throughout because its historical bridge applications predate the v1.17.1.5 adaptive-preamble contract. Keep RXPS off for all ten steps whether using SF5/BW250 or the faster direct-bench SF5/BW500 tuple. After the final endpoint is proven, a later update whose target, controller, source, and relays are all v1.17.1.5 or newer can use the generic LoRa OTA runner and its qualified adaptive-preamble boundaries.

Put the source on the identical TempRadio tuple. A current ASCII-first Full Companion recognizes motatool's initial ota folder on line directly:

motatool serve \\\n  --dir ./motas \\\n  --serial /dev/ttyACM1 \\\n  --baud 115200 \\\n  -v\n

The same command also works with older Full Companion firmware in binary mode, where the idle parser recognizes the identical preamble. Restart motatool for every step so the source emits a fresh catalog advert. Leave it running during the download and stop it with Ctrl-C only after the destination reports ready to install.

"},{"location":"research/rak3401_mota_chain/#3-install-all-ten-packages-in-order","title":"3. Install all ten packages in order","text":"Step Manifest ID Before hash Expected version After hash 1 C147BCEF 71F4026CBE4B8B74 1.16.7.9 42BC53A64288E845 2 BEB578FC 42BC53A64288E845 1.16.7.10 1AD2AA8B6C478DA6 3 6D8EF814 1AD2AA8B6C478DA6 1.16.8.7 D709A25308BC1831 4 40AC4CEB D709A25308BC1831 1.16.9.105 457BEDA5E6406C81 5 F6C8890B 457BEDA5E6406C81 1.16.9.110 E76BFA205634CAB8 6 82405E89 E76BFA205634CAB8 1.16.9.113 65328FC8A1FBED2D 7 80F40DB6 65328FC8A1FBED2D 1.16.9.117 AF7532E13337FADD 8 DBF6310D AF7532E13337FADD 1.16.10.0 22002359BBDFA76E 9 C2979E08 22002359BBDFA76E 1.17.1.3 FE65A6135A1E7B3F 10 B5011420 FE65A6135A1E7B3F 1.17.1.5 1FA6AAA3C28D8BD8

For each row, first prove that ota self exactly matches its Before hash, then inspect the manager before changing it:

ota status\n

Proceed to discovery only after that status is reconciled. When the manager is idle, run:

ota ls\nota pull MANIFEST_ID flash\nota status\n

If the first status reports this row's MID as partial, use ota pull with that same MID to resume it. If this row's MID is already ready, do not pull or cancel it; continue with the install checks below. If status reports the immediately previous row's MID as verifying staged blocks, wait through the bounded discovery window; require either that exact MID to become ready to install or the manager to become no download, and re-prove the current Before hash. Cancel only the same previous MID while it is visibly attached and ready. If it has become idle, send no cancel. Stop on any other MID, failed or incomplete state, ambiguous reply, or timeout. Never issue a blind ota cancel: it can discard a resumable or unrelated session.

Repeat ota status at a restrained interval until it says ready to install. If the manifest is initially absent, wait for a fresh source advert, run ota ls again, or restart motatool; do not pull a different ID. Stop the seeder and install:

get system.watchdog\nota install\n

The watchdog reply must still be > off. Allow the current default five-minute readiness window for the USB/LoRa identity to return; automation probes every 10 seconds and returns early as soon as the exact body is visible. Then run ver, ota self, and ota help. Require the row's exact After hash, require bootloader: apply OK, and require rescue install <hash16> in the help before exposing that bridge to the next package. This rescue-help gate applies to steps 1-9; step 10 is the endpoint and is not exposed to another package in this chain. A normal-radio ota status response of no download proves only that the manager is idle; legacy internal-flash firmware does not durably erase a retained container by accepting an IDLE ota cancel. Do not claim otherwise. When the same expected manifest reports verifying staged blocks after the next TempRadio starts, wait only for the bounded discovery window and require that it become ready to install or no download; stop on a changed ID, failed/incomplete state, or timeout. Prove the exact running body before cancelling an attached completed session. OTAFIX consumes the approval word during a successful install, so a retained endpoint container is inert and a later valid pull replaces it. Reapply the transfer guardrails and TempRadio tuple, restart motatool, and continue with the next row.

If the board boots but ota self says the EndF is invalid, stop. Only when ota status still identifies that row's complete staged package may the guarded ota rescue install BEFORE_HASH command be used. It is not a force option and must use that row's exact Before hash. If the board does not boot, recover the documented start/recovery UF2 locally over USB. Step 10 is the endpoint and has no successor package in this chain. If its EndF is invalid, use local DFU/SWD rather than attempting an unqualified LoRa package.

"},{"location":"research/rak3401_mota_chain/#4-intermediate-relays-and-restoration","title":"4. Intermediate relays and restoration","text":"

On every managed intermediate relay, save get rxdelay and get txdelay, then use the airtime-scaled collision window during the maintenance session:

set rxdelay 0\nset txdelay 0.3\n

Current OTA transfer packets honor that configured relay txdelay; setting 0.3 gives competing relays a small randomized, airtime-scaled forwarding window. The source's manifest-fragment gap independently follows active packet airtime and duty spacing, clamped to 100-1000 ms. These two delays solve different problems.

After step 10 is proven, restore each saved destination and relay value exactly, including rxdelay, RXPS, CPU power saving, af, ota config hops, and relay timing. Current full-parser repeater firmware accepts normalradio and restores the saved tuple after replying on the temporary channel:

normalradio\n

The b40d2e6c endpoint supports that command. If a recovery build unexpectedly replies Unknown command, shorten its lease to one minute with the same tuple:

tempradio 909.950,500,5,5,1\n

Wait for the lease to expire, return the controller and ordinary source to their saved normal channels, and then re-enable the RAK system watchdog:

set system.watchdog on\nget system.watchdog\n

A Full Companion source is the exception: its local TCP console supports normalradio and the automated runner uses it before restoring the shared Binary API radio tuple.

Final lab success requires version v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c, body hash 1FA6AAA3C28D8BD8, target 2FA509C1, hardware RAK_3401, and get system.watchdog reporting > on. If a relay cannot be restored before its TempRadio lease ends, wait for it to return to the normal channel and restore its saved rxdelay and txdelay there.

"},{"location":"research/rak3401_mota_chain/#two-relay-deployment","title":"Two-relay deployment","text":"

The first nine direct package transitions above were physically observed as the byte-identical first nine transitions of the legacy chain. Replacement step 10, this hardened host-runner revision, and multi-hop operation are not cleanly end-to-end physically qualified. A controlled, recoverable multi-hop lab can exercise two intermediate relays by listing them farthest-to-nearest and using three OTA hops. Run a non-mutating preflight first; even preflight requires the prerelease candidate override because the command connects to live devices:

python3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp SOURCE_IP:5001 \\\n  --source-cli-tcp SOURCE_IP:5002 \\\n  --source-shares-controller \\\n  --target-key 63d8df63 \\\n  --relay 'Far Repeater' \\\n  --relay 'Near Repeater' \\\n  --temp-radio 909.950,250,5,5,120 \\\n  --ota-hops 3 \\\n  --motatool /path/to/motatool \\\n  --accept-test-candidate \\\n  --preflight-only\n

After preflight succeeds, rerun with --yes in place of --preflight-only. Confirm that the selected frequency and bandwidth are legal at the deployment location.

"},{"location":"research/rak3401_mota_chain/#rescue-and-completion","title":"Rescue and completion","text":"

The guarded ota rescue install <base_body_hash> command is present after step 1 and remains available through the step-9 bridge. It is not a force command: it refuses a valid normal EndF, a base mismatch, wrong target/hardware, or invalid payload. A failure before step 1 completes still requires local USB recovery because the deployed start image predates the rescue command. The final b40d2e6c endpoint uses the stricter shared-slot profile; its invalid-EndF recovery is local DFU/SWD, not the rescue command.

Before the first mutation, the runner saves the destination's RXPS periods, CPU power-saving state, RX flood delay, airtime factor, and OTA hop reach in the persistent work directory, flushing both the file and its directory entry before changing a guardrail. It also saves the managed source's exact RXPS preference in the chain-root source-rxps-settings.json before disabling it. Every step reuses that protected record, so a rerun after a killed host process restores the chain-start preference instead of treating the temporary off state as original. The record is accepted only for the same CLI endpoint. The runner retires that chain-root record only after it has proved the source is back on its normal radio and restored the exact preference at the verified endpoint. The runner reads the destination, controller, source, and relay versions before applying the same RXPS policy as the generic runner. The mixed historical chain therefore verifies radio.rxps off after every bridge reboot; a future all-v1.17.1.5-or-newer SF5/BW250 chain would instead keep RXPS on under the qualified level-8/preamble-64 boundary. It also verifies set powersaving off and rxdelay 0, plus af 0 when --legacy-full-airtime was explicitly selected. It restores every original value only after the exact endpoint is proven. An interrupted run deliberately leaves those transfer guardrails active; rerun the same command with the same work directory to resume and restore them. CPU power saving was not the cause of the observed folder-source failure: that was a USB CDC receive-ring overrun fixed by bounded serial reads. The runtime settings remain conservative isolation and fast-link reliability guardrails.

For every named --relay, the per-step runner also captures rxdelay and txdelay, writes them to relay-timing-settings.json, applies rxdelay 0 and txdelay 0.3 during transfer, verifies the readback, and restores both before the relay leaves TempRadio.

The runner also keeps the watchdog off across the chain, rechecks identity and OTA reach before every package, requires the exact post-boot EndF hash, and only re-enables the watchdog after step 10. Lab success requires endpoint body hash 1FA6AAA3C28D8BD8, target 2FA509C1, hardware RAK_3401, normal radio 910.525 MHz / 62.5 kHz / SF7 / CR5, and the watchdog verified on.

"},{"location":"research/rak3401_mota_chain/#external-radio-startup-and-manual-usb-recovery","title":"External-radio startup and manual USB recovery","text":"

RAK3401 is a distinct target from RAK4631. Its RAK13300/RAK13302 radio is on the WisBlock SPI bus (SPI1 in the Adafruit nRF52 core), with BUSY and DIO1 on P0.09/P0.10. Current builds explicitly enable those NFC-capable pads as GPIO and do not advertise the radio bus as an on-board QSPI flash device.

The RAK13302's FEM and boost supply use switched 3V3_S, but the SX1262 core uses unswitched 3V3. An MCU reset, a 1200-baud DFU touch, or disconnecting only USB may therefore leave a battery-powered SX1262 in its previous state. At startup, current firmware quiesces the FEM, reinitializes the dedicated SPI instance, asserts NRESET, and, if BUSY remains high, sends the Semtech NSS/GET_STATUS wake sequence without waiting on RadioLib first. The one-time board startup cold-starts 3V3_S; later radio retries leave that shared rail enabled so they do not reset GPS or sensor modules.

If BUSY still cannot be released, firmware must not enter a reset loop:

A permanently high BUSY after NRESET and the direct wake transaction indicates an electrically unavailable radio, loose module, or power-domain fault; an MCU reboot cannot manufacture a response. If local access is possible, disconnect the battery as well as USB and reseat the WisBlock module. Remote firmware will continue retrying without requiring that physical intervention or churning the USB device.

For an existing deployed tower using Preview 5, do not change the bootloader as part of this application chain. Its exact local recovery package is the Preview 5 RAK4631 ZIP. For a new RAK3401 installation or local recovery, use the OTAFIX 2.4.3 RAK3401 ZIP from the OTAFIX 2.4.3 release. These are local DFU/SWD recovery assets, not mOTA chain steps.

For a local Serial DFU recovery with the repository helper:

cd /path/to/meshfirmware\nMCFIRMWARE_NO_SUDO=1 ./mcfirmware.sh\n

Select the serial entry whose USB identity is WisCore RAK3401 Board, then choose Custom, nrf52, and the exact RAK3401 .zip. Use flash-update to preserve InternalFS preferences and identity. Use flash-wipe + flash only when erasing application data is intentional. The helper follows the same USB device into bootloader mode, so do not substitute another /dev/ttyACM* merely because its number appears first.

"},{"location":"test-results/","title":"Test Results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/#test-results","title":"Test Results","text":"

Completed validation reports, experiments, and test guides are collected here. Older page addresses redirect to these copies. Results are tied to the hardware and firmware versions named in each report; they do not automatically qualify a newer build.

"},{"location":"test-results/#radio-and-rf","title":"Radio and RF","text":""},{"location":"test-results/#companion-usb-ble-and-display","title":"Companion, USB, BLE, and display","text":""},{"location":"test-results/#build-storage-memory-and-checklists","title":"Build, storage, memory, and checklists","text":""},{"location":"test-results/battery_calibration_2026-10-22/","title":"Battery calibration 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/battery_calibration_2026-10-22/#shared-battery-adc-calibration-validation-2026-10-22","title":"Shared battery ADC calibration validation - 2026-10-22","text":"

Final integration baseline: 94a3b7e568781533510721670af24c162804a5ca on keymindCascade, plus the calibration changes. Earlier builds and link failures below used af106a0ee9edd8ee0113b7b364290e0944245b86; the final integration section supersedes those results for the targets it names. These checks do not constitute an installed-device or voltmeter acceptance test.

"},{"location":"test-results/battery_calibration_2026-10-22/#behavior-and-compatibility","title":"Behavior and compatibility","text":""},{"location":"test-results/battery_calibration_2026-10-22/#host-checks","title":"Host checks","text":"

All following checks passed using TMPDIR=/var/tmp (the machine's /tmp user quota prevented compiler temporary-file creation):

Check Coverage test/test_battery_calibration.py - 3 tests Production parsing, meter correction, existing absolute coefficients, pack references, effective multiplier, reset, unsupported/zero/saturated readings, bounds, actual MainBoard scaling and nRF52 policy precedence. test/test_battery_calibration_store.py - 1 test across four platform fixtures ESP32, RP2040, nRF52 and STM32 transaction implementations; reboot restore, every single-bit record corruption, truncation, write failure, uncertain publication, reset, missing persistent storage and backup recovery. test/test_battery_forwarding.py - 19 tests Actual infrastructure setter, failed-save rollback, nRF52 policy fallback, and refreshed battery-based forwarding decisions. test/test_heltec_r8_external_power.py - 1 test Existing R8 power estimate still uses the calibrated voltage. test/test_stm32_companion_profile.py - 21 tests Existing ADC conversion and sampling remain exact at default calibration; memory/profile constraints retained. test/test_rp2040_application_capabilities.py - 11 tests Existing upload artifact and capability validation.

The production calibration, store and forwarding harnesses use AddressSanitizer and UndefinedBehaviorSanitizer. git diff --check also passed.

"},{"location":"test-results/battery_calibration_2026-10-22/#earlier-firmware-builds","title":"Earlier firmware builds","text":"

Before integration, PlatformIO processes were run serially. Successful targets:

That earlier Mini Companion occupied 228,832 of 229,376 application bytes: 544 bytes remained. This validates the current build recipe, not every possible version string, optional feature combination or firmware target.

"},{"location":"test-results/battery_calibration_2026-10-22/#earlier-stm32-flash-blockers","title":"Earlier STM32 flash blockers","text":"

The following targets compile but fail the linker flash limit. Both failures were reproduced from an isolated, unchanged checkout of the baseline with the same build recipes and compiler versions:

Target Baseline overflow Calibration build overflow Additional bytes wio-e5-mini_repeater 1,168 2,852 1,684 wio-e5-mini_sensor 25,584 27,368 1,784

The calibration increase consists of code and constants; it did not introduce additional floating-point runtime helpers in the Repeater. Filesystem and application boundaries have not been moved, and no unrelated feature was removed to conceal these build failures. These results apply to the earlier source baseline above, before the Mini Repeater feature gates in 94a3b7e56. The integration checks below supersede that earlier Repeater result when a newer build succeeds.

"},{"location":"test-results/battery_calibration_2026-10-22/#final-integration-checks","title":"Final integration checks","text":"

After merging 94a3b7e56, 69 distinct focused tests passed: 42 calibration, platform-storage and related regressions, 19 battery-forwarding tests, and 8 Mini Repeater profile checks. The 23 calibration/store/forwarding tests were rerun successfully after the final reply compaction. Both new calibration suites are included in the unit-test workflow.

The first integrated Mini Repeater link exceeded flash by 432 bytes. Disabling guessed branch probabilities in that target's existing size recipe, sharing the existing save-failure replies, keeping the command key in constant storage and shortening calibration replies recovered 808 bytes. Calibration, routing, logging, 50 neighbours and the existing 16 KiB filesystem are retained. No extra feature was disabled to make this image fit.

The final merged-source builds passed their flash and runtime-memory checks:

Target Application bytes used Application limit Free flash bytes Heltec_v3_repeater 1,890,881 3,342,336 1,451,455 RAK_3401_repeater 559,616 815,104 255,488 wio-e5-mini_repeater 245,384 245,760 376 wio-e5-mini_companion_radio_usb 228,664 229,376 712

These small Mini margins qualify the stated recipes, not arbitrary optional features or longer build metadata. The earlier Mini Sensor overflow remains recorded above; that target was not rebuilt in this final integration campaign. No physical ADC, voltmeter, charging or installed-device test was performed.

The later unit-test workflow follow-up records release-wrapper failures that these direct recipe builds did not cover.

"},{"location":"test-results/bootloader_version_stock_audit/","title":"Bootloader version stock audit","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/bootloader_version_stock_audit/#stock-nrf52-bootloader-version-audit","title":"Stock nRF52 bootloader version audit","text":"

Checked 2026-09-29, against the 40 nRF52 variant families resolving to 26 board configurations in this checkout. This concerns get bootloader.ver, not bootloader installation or OTA eligibility. No devices were flashed, and no features, storage sizes, or partitions were changed.

"},{"location":"test-results/bootloader_version_stock_audit/#result","title":"Result","text":"

The real firmware reader successfully reads 26 distinct manufacturer-published bootloader images from 32 artifact records, including older RAK revisions, current and historical LilyGO images, and vendor HEX, UF2 and DFU ZIP packages. Repeated copies and identical normalized bootloader images are counted once. These are publicly supplied stock/recovery images, not proof that every manufacturing batch shipped identical bytes. No untouched live unit was read in this audit.

One format gap was fixed: Seeed's T1000-E LoRaWAN development-board bootloader declares UF2 Bootloader 1.00. The reader now accepts that two-component version and returns 1.00 unchanged. It does not invent 1.00.0. That development image is separate from Seeed's T1000-E recovery image, which reports 0.9.1-5-g488711a and already worked.

All other tested images already expose a readable UF2 bootloader record. Generic bare dotted-number searches remain deliberately unsupported: TinyUSB, nrfx, and SoftDevice can embed their own versions inside the same bootloader. The bounded standalone fallback still requires the explicit OTAFIX version shape. Ambiguous records, invalid vectors, and corrupt canonical OTAFIX metadata retain the existing rejection rules.

"},{"location":"test-results/bootloader_version_stock_audit/#published-images-actually-tested","title":"Published images actually tested","text":"

The exact source URLs, download hashes, normalized-image hashes, extraction layout and expected strings are pinned in stock_images.json.

Manufacturer image family Version captured from the image RAK4631/RAK4630 Arduino bootloader, historical through current 0.3.2-109-gd6b28e6, 0.4.1-13-g5e6690e-dirty, 0.4.2, 0.4.3, 0.4.4 Heltec HT-n5262, SDK and T114 vendor recovery HEX 0.9.0-2-g836c8dc-dirty LilyGO T-Echo 0.6.1-2-g1224915 LilyGO T-Echo Lite, Card, T-Impulse Plus (including historical builds) 0.9.2-dirty Seeed XIAO nRF52840 and Sense 0.6.1 Seeed XIAO nRF52840 Plus and Sense Plus 0.9.2-29-g6a9a6a3 Seeed Wio Tracker 1110 SDK/conversion bootloader 0.6.2-26-g949425a-dirty Seeed Wio Tracker L1 vendor recovery ZIP 0.7.0-22-g277a0c8 Seeed T1000-E recovery ZIP 0.9.1-5-g488711a Seeed T1000-E LoRaWAN development-board HEX 1.00 (newly supported) Seeed MeshTracker X1 recovery ZIP 0.10.0-18-gb93789f MTools Tec MeshTiny vendor bootloader UF2 0.4.5 BQ/Unit Engineering Nano G2 Ultra vendor HEX ZIP 0.6.4-dirty Muzi Works R1 Neo release HEX 0.9.2-31-g990aa7f-dirty

Use the embedded version, not the filename. For example, Seeed's XIAO file named ...bootloader-0.6.2...hex reports 0.6.1, and its L1 ZIP named ...bootloader-0.10.0...zip reports 0.7.0-22-g277a0c8. These are actual tested bytes, not renamed or inferred release numbers.

All these normalized images use the Adafruit nRF52840 F4000..FE000 bootloader window. HEX/UF2 address records identify the bytes. The boot-only Seeed DFU packages omit MBR/UICR address words; their bootloader binary is extracted using the manifest's sd_size/bl_size and the Adafruit nRF52840 linker layout. The runtime reader independently selects the installed MBR/UICR boot address; this audit does not hardcode that layout into the firmware getter.

"},{"location":"test-results/bootloader_version_stock_audit/#coverage-of-every-variant-family","title":"Coverage of every variant family","text":" Variant family Status Evidence or remaining gap gat562_30s_mesh_kit Unverified GAT documentation/firmware repository checked; no product-specific stock bootloader found. gat562_mesh_evb_pro Unverified Same GAT gap; RAK board configuration is not factory-image proof. gat562_mesh_tracker_pro Unverified Same GAT gap; custom vendor/OTAFIX builds also exist. gat562_mesh_watch13 Unverified Same GAT gap. heltec_mesh_solar Representative only HT-n5262 SDK image passes; Solar downloads found application images, not a board-specific stock bootloader. heltec_t096 Representative only HT-n5262 SDK image passes; T096 factory revision not established. heltec_t1 Representative only SDK image passes; vendor F&T archive contains application-only UF2s. heltec_t114 Image verified Both SDK HEX and T114-specific vendor recovery HEX pass. heltec_tower_v2 Representative only SDK image passes; vendor matching-firmware archive contains applications, not the stock bootloader. ikoka_handheld_nrf Representative only XIAO board configuration; XIAO module images pass, exact installed factory image not obtained. ikoka_nano_nrf Representative only Same XIAO representative coverage. ikoka_stick_nrf Representative only Same XIAO representative coverage. keepteen_lt1 Unverified Manufacturer product/download search did not yield a factory bootloader binary. lilygo_t_impulse_plus Image verified Both historical and newer vendor bootloader HEX images pass. lilygo_techo Image verified Vendor T-Echo bootloader passes. lilygo_techo_card Image verified Three distinct vendor historical/current bootloader images pass. NFC UICR repair application excluded. lilygo_techo_lite Image verified Vendor bootloader HEX passes. mesh_pocket Representative only HT-n5262 SDK image passes; exact Pocket factory revision not established. meshtiny Image verified Direct manufacturer bootloader UF2 passes; newer ADV/factory revisions require their actual image. meshtracker_x1 Image verified Seeed product-specific recovery ZIP passes. minewsemi_me25ls01 Unverified Manufacturer module page lists firmware as none; no stock bootloader binary found. OEM/dev-kit installations can differ. muzi_base Unverified Manufacturer publishes bootloader source, but its release assets found were for R1 Neo, not BASE. muziworks_r1_neo Image verified Manufacturer R1 Neo release HEX passes; do not substitute a generic RAK version. nano_g2_ultra Image verified Manufacturer's preinstalled-bootloader recovery HEX ZIP passes. promicro Unverified Nologo documents a Nice!Nano UF2 drive; its linked download requires Baidu access. Generic clones need separate factory images. rak3401 Representative only RAK4631 Arduino images pass; RAK3401-specific untouched image not obtained. rak4631 Image verified Five distinct published stock Arduino bootloader revisions pass. RAK4631-R/RUI3 is a different bootloader family. rak_wismesh_tag Representative only RAK core images pass; exact Tag factory image not obtained. sensecap_solar Representative only Seeed documentation identifies XIAO DFU; XIAO images pass, exact Solar factory image not obtained. t1000-e Image verified Both vendor recovery ZIP and separate LoRaWAN development-board HEX pass. thinknode_m1 Unverified Manufacturer repository checked; no factory bootloader binary found. thinknode_m3 Unverified Published factory UF2 is application-only (26000..52BFF), not a bootloader image. thinknode_m4 Unverified Published factory UF2 is application-only (26000..521FF). thinknode_m6 Unverified Published factory UF2 is application-only (26000..46CFF). thinknode_m8 Unverified No accessible product-specific factory bootloader binary found. wio-tracker-l1 Image verified Seeed's L1 recovery ZIP passes. wio-tracker-l1-1w Representative only L1 recovery image passes; exact 1W/ADV factory revision not established. wio-tracker-l1-eink Representative only L1 recovery image passes; exact e-ink factory revision not established. wio_wm1110 Representative only Published Wio Tracker 1110 and XIAO SDK images pass; the variant's XIAO config does not prove the shipped module's image. xiao_nrf52 Image verified XIAO, Sense, Plus and Sense Plus vendor images pass.

Representative/unverified products use the same shared reader and should report a version if their installed bootloader exposes one of the recognized records. There is no evidence yet for claiming an exact version or success on every untouched unit. ESP32, RP2040 and STM32 are outside this nRF52 flash reader; their getters were not changed and this report does not claim stock-version capture on those architectures.

"},{"location":"test-results/bootloader_version_stock_audit/#reproduce-the-artifact-tests","title":"Reproduce the artifact tests","text":"

Normal tests are offline and include all observed version strings, malformed and conflicting versions, dependency-version false positives, region/vector bounds, and the existing OTAFIX metadata/CRC/fallback cases:

python3 test/test_nrf52_bootloader_version.py -v\n

To replay the real images, download each url in stock_images.json into a cache directory using its file name, then run:

MESHCORE_STOCK_BOOTLOADER_CACHE=/absolute/path/to/cache \\\n  python3 test/test_nrf52_bootloader_version.py -v\n

This checks the exact download SHA-256, extracts only the bootloader window, checks the normalized-image SHA-256, and invokes the actual C++ reader with an exact expected version. It never downloads automatically or flashes a device. The ordinary regression cases cover every captured format even without the optional cached artifacts. RAK4631 and T1000-E repeater firmware builds passed.

"},{"location":"test-results/bootloader_version_stock_audit/#what-is-needed-to-close-the-remaining-gaps","title":"What is needed to close the remaining gaps","text":"

For each untouched unit, collect its DFU drive's INFO_UF2.TXT if present. To prove the application's flash reader can capture that exact revision, also obtain a read-only bootloader dump plus the MBR word at 0xFF8, UICR NRFFW[0], and flash geometry (or a full vendor HEX including those address records). Keep the bootloader unchanged while collecting this evidence. Only the identified bootloader region is needed; private application keys/configuration are not. If no version bytes or startup register value exist, report unknown; a regex cannot recover a version that was never embedded.

"},{"location":"test-results/bootloader_version_stock_audit/#manufacturer-sources","title":"Manufacturer sources","text":""},{"location":"test-results/cad_scan_validation/","title":"Cad scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/cad_scan_validation/#sx1262-channel-scanning-hardware-results","title":"SX1262 channel-scanning hardware results","text":"

Hardware test on September 12, 2026 (America/Los_Angeles). Heltec V4 transmitter; XIAO ESP32-S3 with Wio SX1262 receiver. Both used standalone diagnostic firmware.

The best tested scan setting was four channels with four-symbol RX-preamble windows. Three-symbol windows were unreliable even in the single-channel re-arm control. In the longer four-symbol runs, four channels received 80/80 packets, five received 79/80, and six received 52/60. These are measured limits of this implementation and test setup, not universal SX1262 channel-count limits.

"},{"location":"test-results/cad_scan_validation/#configuration-and-counting","title":"Configuration and counting","text":"

SF5, 125 kHz, coding rate 4/5, explicit header, payload CRC, and 32 preamble symbols. The programmed preamble is 8.192 ms. Channels start at 909.5 MHz and increase in 1 MHz steps; tests use the first N channels, up to 920.5 MHz. Test packets are transmitted individually with varied timing relative to the scanner. There are no overlapping transmitters in this experiment.

A success requires a CRC-valid packet with the expected sequence, channel, length, and test payload. Every included transmission has its own successful transmitter acknowledgement; delayed serial acknowledgements are recovered by sequence, without retransmitting. The one exploratory RX scan trial with shifted acknowledgements is excluded.

"},{"location":"test-results/cad_scan_validation/#completed-trials","title":"Completed trials","text":"Trial Method/window Channels Payload bytes Received / sent Mean complete scan baseline-fixed Fixed-channel RX 4 64 16 / 16 \u2014 cad-standard CAD + software RX 4 64 7 / 32 14.469 ms cad-auto CAD + automatic RX 4 64 16 / 32 14.468 ms rxfast-4 rxfast 4 4 64 32 / 32 6.100 ms rxfast-4-maxpayload rxfast 4 4 255 35 / 35 6.098 ms matrix-cad1-1 cad 1 1 64 7 / 8 2.643 ms matrix-cad1-10 cad 1 10 64 2 / 20 28.389 ms matrix-cad1-11 cad 1 11 64 5 / 22 31.297 ms matrix-cad1-12 cad 1 12 64 3 / 24 34.145 ms matrix-cad2-10 cad 2 10 64 3 / 40 31.040 ms matrix-fixed-12 Fixed-channel RX 12 64 24 / 24 \u2014 matrix-rx1-10 rxfast 1 10 64 0 / 20 7.555 ms matrix-rx1-11 rxfast 1 11 64 0 / 22 8.316 ms matrix-rx1-12 rxfast 1 12 64 0 / 24 9.072 ms matrix-rx2-10 rxfast 2 10 64 0 / 40 10.133 ms matrix-rx3-7 rxfast 3 7 64 2 / 35 8.892 ms matrix-rx3-8 rxfast 3 8 64 1 / 40 10.160 ms matrix-rx4-5 rxfast 4 5 64 39 / 40 7.630 ms boundary-rx3-1 rxfast 3 1 64 4 / 24 1.268 ms boundary-rx3-4 rxfast 3 4 64 2 / 24 5.080 ms boundary-rx3-5 rxfast 3 5 64 0 / 25 6.350 ms boundary-rx3-6 rxfast 3 6 64 2 / 30 7.621 ms boundary-rx4-4 rxfast 4 4 64 80 / 80 6.104 ms boundary-rx4-5 rxfast 4 5 64 79 / 80 7.630 ms boundary-rx4-6 rxfast 4 6 64 52 / 60 9.158 ms

rxfast N means a continuous-RX preamble observation window of N symbols, with minimal reconfiguration between frequencies. It is not CAD. cad N uses the SX1262 CAD detector with N symbols and automatic handoff to RX. CAD has no three-symbol mode.

Scan times describe uninterrupted sweeps. When reception starts, the radio stays on that frequency through the packet and cannot hear packets on the other frequencies. Serial status requests can add occasional timing outliers.

The maximum-payload trial ended after 35 confirmed transmissions when a serial acknowledgement was delayed. Its last TX acknowledgement and matching RX were subsequently recovered from the serial logs; all 35 were received. It is not a completed 64-packet trial.

"},{"location":"test-results/cad_scan_validation/#interpretation-and-limits","title":"Interpretation and limits","text":"

The initial four-channel fast-RX test received 32/32 64-byte packets and 35/35 255-byte packets. That is a successful strong-signal bench result, not a guarantee of zero loss. Ordinary four-symbol CAD took approximately 14.47 ms to cover four frequencies and was unreliable with an 8.192 ms preamble.

Reducing the observation window also reduces the time available for the modem to recognize a preamble. Dividing preamble duration by scan duration alone overestimates the number of usable channels. One- and two-symbol RX windows produced no preamble detections in the tested high-channel-count cases.

The three-symbol single-channel control repeatedly re-arms RX, just as a scan would, and received only 4/24 packets. It is distinct from continuous fixed-channel reception, which passed its baseline. This isolates a detection-window limitation even without a long multi-channel sweep.

CAD detection peak 18 and minimum 10 were held constant. These are the datasheet four-symbol SF5 thresholds; the one- and two-symbol CAD tests do not establish optimized thresholds or weak-signal sensitivity. CAD activity followed by no decoded packet can be a late packet detection, not necessarily a noise false positive.

Received signals were strong. These tests do not establish weak-signal range, adjacent-channel rejection, simultaneous-packet capacity, production firmware scheduling performance, power consumption, FCC compliance, or months of uptime.

"},{"location":"test-results/cad_scan_validation/#reproduction-and-evidence","title":"Reproduction and evidence","text":"

See the test harness. Compact validated results, timing counters, and final firmware/source hashes are in cad_scan_results.json. Detailed sequence-level logs and private lab transport helpers are retained locally under out/cad-scan/.

"},{"location":"test-results/carrier_wave_validation/","title":"Carrier wave validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/carrier_wave_validation/#cw-and-cw2-validation","title":"CW and CW2 validation","text":"

Tested on the MercerWoodMesh lab Pi on September 14, 2026 (Pacific), using base commit 9241ccc253ad2f8ac2c9a2fe756f59ba2f52505d plus the CW changes. Test images are marked v1.17.1.6-cw-test-9241ccc2; they are development images.

A separate RAK4631 sampled live received signal strength (RSSI) about every 10 ms, without needing to decode a packet. The transmitting boards ran the Full V4 repeater and Full T1000-E Companion builds. Both were configured for \u22129 dBm chip TX power, with profiles at 909.5 and 919.5 MHz, BW125, SF7, CR5, and 32-symbol preambles.

Check V4 / SX1262 T1000-E / LR1110 cw on 0.25 measured duration 0.25 s 0.25 s cw on 1.25 measured duration 1.25 s 1.25 s Manual stop approximately 1 s into a 5 s request 1.01 s 1.01 s cw on 1.25 after enabling RXPS 1.26 s 1.25 s cw2 on 1.25 on the second frequency 1.25 s 1.25 s Primary-frequency RSSI change while CW2 transmits 0 dB 0 dB Paired RX and TX recovery checks 5/5 passed 5/5 passed

The observer saw roughly \u221256 to \u221257 dBm during V4 carriers and \u221267 to \u221271 dBm during T1000-E carriers, against a \u2212112 to \u2212113 dBm baseline. Readings returned to baseline after timeout and manual stop. These are received signal readings, not calibrated transmitter output measurements.

RX recovery used CRC-valid LoRa test payloads and the DUT's receive counter. TX recovery used each DUT's MeshCore advertisement, received with valid CRC by the observer. Both DUTs finished in RX, with CW and radio2 disabled and no outbound packet pending. The observer has no autonomous transmission.

"},{"location":"test-results/carrier_wave_validation/#software-checks","title":"Software checks","text":""},{"location":"test-results/carrier_wave_validation/#sx1276-follow-up","title":"SX1276 follow-up","text":"

The SX1276 carrier feature from PR #11 is included alongside SX1262 and LR1110 support. It switches to FSK with zero deviation, applies the board's calibrated TX power, and rebuilds the runtime LoRa configuration when stopped. External-PA settings pass through the same DAC/drive-power mapping used for packets, including RFO selection.

The normal TX power command can adjust a running carrier for meter sweeps. RadioLib's SX1276 power setter enters standby, so the carrier is restarted afterward without extending the deadline. Failed power changes stop the carrier and restore the previous cached power.

Follow-up checks use base 9dd9881d520b4b791600a00fff1f8e1436921054 plus the SX1276 changes. Production-method regression tests cover both profiles, zero deviation, external-PA calibration, RFO, power sweeps, timeout, failed FSK entry, failed restoration, retry guards, and return to LoRa. The native CW paths and existing radio/profile/sleep/PA checks pass as well. Both GEPRC_Linkflow_900_repeater (external PA) and Heltec_v2_companion_radio_usb firmware builds pass, including size checks.

SX1276 RF output has not been measured in this follow-up. The physical RF results above apply to SX1262 and LR1110.

"},{"location":"test-results/carrier_wave_validation/#bench-qualifications","title":"Bench qualifications","text":"

The first CW2 request arrived before radio2's existing delayed activation. The harness now polls get radio2.status for the active mode before testing.

An initial V4 dual-profile packet probe used a 120-symbol preamble against 32-symbol profiles. A comparison around another CW2 burst received 0/8 before and 1/8 afterward with that mismatched probe, versus 8/8 before and 8/8 afterward with a matching 32-symbol probe. The configured preamble also sets the driver's preamble-detection hold timeout, so a much longer incoming preamble can outlast that hold and be interrupted by scanning. The final dual-profile recovery checks use matched preambles. This finding does not establish general packet-loss performance for every scan combination.

These checks establish RF activity, selected-frequency behavior, and recovery on the tested boards. They do not measure spectral purity, frequency error, SWR, calibrated power, or months of uptime. Carrier timeouts are serviced by the main loop and depend on that loop remaining responsive.

"},{"location":"test-results/carrier_wave_validation/#reproduction","title":"Reproduction","text":"

Append tools/hil/cw_observer.ini to extra_configs in platformio.local.ini, preserving existing entries, and build pio run -e cw_observer_rak4631. Flash that application onto a separate lab RAK4631. Do not run PlatformIO concurrently with another build or upload in the checkout.

Configure the lab DUT's primary profile as above, disable radio2, set TX power to \u22129 dBm, and reboot to apply saved primary settings. Then run:

python3 tools/hil/carrier_wave.py --dut /dev/serial/by-id/DUT \\\n  --observer /dev/serial/by-id/OBSERVER --role repeater --output cw-results.json\n

Use --role companion for the T1000-E. The harness changes lab radio2/RXPS settings and emits short RF bursts and packet probes. It is for test boards.

Compact evidence: carrier_wave_results.json. Command guide: carrier-wave RF checks.

"},{"location":"test-results/cli_settings_audit/","title":"Cli settings audit","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/cli_settings_audit/#cli-setting-dispatch-audit","title":"CLI setting dispatch audit","text":"

The 1.17.1.5 source retained get path.hash.mode in CommonCLI but lost its setter when radio preferences moved into CommonRadioPrefs. Infrastructure roles call CommonCLI directly, so the shared parser's setter was unreachable. The correction restores set path.hash.mode 0|1|2 there, validates the value, and saves accepted changes. Missing/invalid values report a value error and leave the setting unchanged.

The other radio setters removed by the original refactor had already been restored. A feature-guarded check found a second mismatch: get extra.sf exposed stored data on non-LR2021 radios while its setter was compiled out. Both commands now return Error: extra.sf requires an LR2021 radio there. Supported LR2021 set/get behavior is preserved. These are source corrections; existing release binaries need rebuilding/updating to receive them.

"},{"location":"test-results/cli_settings_audit/#coverage","title":"Coverage","text":"Surface Check Common infrastructure CLI 72 literal setter keys and 84 query keys; every query has a setter or an explicitly reviewed read-only/alternate-command exception MQTT/observer CLI 33 literal setter keys and 39 query keys, including Wi-Fi, timezone, alerts, display and watchdog settings MQTT slots All nine writable slot subkeys have query/set coverage; diag is intentionally read-only Repeater, Room Server, Sensor Shared CLI delegation and role-specific query/set coverage Companion Shared-radio allowlist and parser agreement, persistence wiring, terminal-specific setters, and role query exceptions Radio settings moved by the refactor All 13 shared setters remain present in infrastructure dispatch Literal command comparisons Checks key lengths so a setter cannot accidentally compare its value against the key's terminating NUL Feature guards Preprocesses common and observer handlers for minimal, nRF52 GPS/SD, ESP32 WebConfig, ESP32 MQTT, RS232/GPS, ESP-NOW, and LR2021 profiles before comparing query/set coverage

Read-only exceptions include runtime connection status, diagnostics, firmware role, bootloader identity and reset information. Alternate-command exceptions include the password command and set battery.alert on <region>. These exceptions are explicit in the test, rather than silently allowing all missing setters. Dynamic/delegated command families and hardware-specific features continue to use their dedicated tests; the inventory is not a claim that every build has every feature enabled.

"},{"location":"test-results/cli_settings_audit/#executed-behavior-tests","title":"Executed behavior tests","text":"

The native C++ fixture compiles the production get/set dispatch branches and the real setting bodies for 14 radio-related settings: radio, freq, af, dutycycle, int.thresh, cad, radio.rxgain, tx, rxdelay, agc.reset.interval, path.hash.mode, multi.acks, txdelay, and direct.txdelay. It runs local and authenticated on-air caller timestamps, checks round-trip values and save calls, preserves AGC interval rounding, and checks that rejected hardware applies do not save or change preferences. Hardware callbacks are mocked; these are not live-radio or flash-durability tests. The fixture also runs with and without USE_LR2021, checking supported extra.sf set/query/clear behavior, invalid values, and the unsupported-radio error.

The focused hash-mode test checks all valid modes, missing values, negative and out-of-range values, overflow, malformed numbers, whitespace, and similarly named unknown keys. Existing transport tests exercise Companion local/framed and on-air dispatch, local-only secret reads, and browser/stream routing under AddressSanitizer and UndefinedBehaviorSanitizer on Linux.

Run the CI checks locally with a host C++ compiler (Linux is required for the existing sanitizer-enabled transport suite):

python3 -B test/test_path_hash_cli.py\npython3 -B test/test_cli_settings_contract.py\npython3 -B test/test_local_cli_access.py\n

All 14 tests passed on the Linux VM. The workflow runs these commands on future pushes and pull requests. The new inventory and native radio tests also passed with the Windows host compiler.

pio run -e heltec_v4_repeater passed after both corrections, including the firmware's flash-size and runtime-RAM checks. The initial audit used host tests and build checks; subsequent hardware checks are recorded in the dual-profile validation.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/","title":"Companion ble refresh 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#companion-bluetooth-contact-refresh-2026-10-19","title":"Companion Bluetooth contact refresh, 2026-10-19","text":"

The production Heltec V3 Full Companion refreshed 92 contacts in a median 3.347882 seconds at 32.560 kbit/s, compared with 6.191396 seconds at 17.606 kbit/s for the earlier 60 ms firmware. All five complete refreshes and the 46-contact incremental refresh passed exact-record validation in each run. The new firmware reduced median complete-refresh time by 45.9% on this Mercerwood Pi connection.

Measured production source: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6, version v1.18.0.1-halo-keymind-cascade-dev-4e4f27a2, target Heltec_v3_companion_radio_full. This was the normal Full build, with no BLE benchmark override. These results measure contact synchronization on one V3 and one Linux central; phone performance and complete application refreshes remain untested.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#production-behavior","title":"Production behavior","text":"

The modern NimBLE-Arduino adapter now spaces notification attempts by at least 7 ms. Legacy NimBLE and Bluedroid retain their 60 ms setting. After the first successful encrypted, authenticated and bonded connection callback for a link, the modern adapter requests a 15-30 ms connection interval, zero peripheral latency and a four-second supervision timeout. It makes this request once per authenticated link. The central chooses the accepted connection parameters.

Required replies retain the existing bounded queues, notification-subscription and negotiated-MTU gates, retry behavior and stalled-link recovery. Each notification carries one complete protocol frame. Failed notification attempts retain the frame; successful queue acceptance removes it without waiting for a later callback. Contact-stream production retains its existing 5 ms pacing. The change preserves the Full feature configuration and contact capacities.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#hardware-method-and-results","title":"Hardware method and results","text":"

The same physical V3, Pi controller and 92 owned synthetic contacts were used for both firmware revisions. V3 Full has capacity for 100 contacts; eight slots were reserved. The fixture used zero path bytes and two modification timestamps so the incremental filter selected exactly 46 records. The complete fixture record-set hashes agree across both runs. Application-only upgrades preserved the contact fixture and node identity.

After the comparison, all 92 owned test contacts were removed. A fresh USB session verified their absence and that the other contact records were unchanged.

The earlier firmware was fcc4714e2b1294a0d76c0aab4fa3b95b1609b3d0. Its comparison run followed a controlled warm boot, recorded separately from the BLE measurement. That preparatory check verified the same node, all 92 persisted contacts and a reset uptime without core errors. The result measures the combined firmware revisions, including the intervening contact-storage fixes; it does not isolate the contribution of each individual change.

Measurement Earlier 60 ms firmware Production 4e4f27a2 Complete refreshes passed 5/5 5/5 Exact contacts per complete refresh 92 92 Median request-to-END time 6.191396 s 3.347882 s Complete-refresh time range 6.137166-6.331489 s 3.342054-3.435741 s Median protocol throughput 17.606 kbit/s 32.560 kbit/s Incremental contacts verified 46 46 Incremental request-to-END time 3.259745 s 1.696429 s Incremental protocol throughput 16.733 kbit/s 32.152 kbit/s

Each stream had to return a valid start header, the declared complete count, unique complete records, every expected owned record byte-for-byte, and a valid END watermark. Incremental results had to equal the timestamp-filtered complete inventory. All streams had zero duplicate contacts or unexpected restarts. The qualifier also checked the complete firmware version and public identity against USB, authenticated NUS access, and monotonic device uptime.

Throughput is application-protocol bytes divided by host-observed time from the GET_CONTACTS write through the validated END notification. One complete refresh contains 92 records of 148 bytes plus ten header/END bytes: 13,626 bytes. The figures include request latency and protocol framing. Unsolicited pushes are excluded from the byte total. These values are not BLE PHY bit rates.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#observed-connection-and-controller-limits","title":"Observed connection and controller limits","text":"

Metadata collected during the normal production run showed the connection start at 48.75 ms, latency zero and a 420 ms supervision timeout, then accept 30 ms, latency zero and a 4,000 ms supervision timeout. The complete-refresh contact callback gap medians were 30.79-30.87 ms; per-run p95 gaps were 56.97-58.40 ms. Callback gaps include host/controller scheduling and are not direct radio airtime measurements.

The Pi controller returned LE feature bytes [31, 0, 0, 0, 0, 0, 0, 0] from HCI opcode 0x2003: its Data Length Extension and LE 2M PHY feature bits were both zero. HCI LE Read Maximum Data Length (0x202f) returned status 0x01, unsupported command. This hardware result establishes the controller's reported capabilities; no DLE support was inferred from its Bluetooth version. A newer controller or phone may behave differently and requires separate measurement.

The monitor collector emitted only numeric connection/update, data-length and PHY fields. Raw monitor captures, addresses, keys, passkeys and contact records were not saved in the shareable evidence.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#native-and-build-evidence","title":"Native and build evidence","text":"

All six cases in test_nimble_companion.py passed. Four real-adapter configurations, including the production default and explicit 7/15/60 ms lab settings, execute a 128-record stream under controller rejection, queue pressure, notification-subscription changes and small-MTU pauses. They verify exact ordered delivery, retry retention, bounded queues, interval boundaries, timer wrap and connection-parameter request gating. The other two cases verify modern/legacy pacing policy and reject unauthorized or out-of-range benchmark overrides. These are native tests, separate from the 92-contact hardware measurements.

All 13 campaign targets compiled and passed their memory admission checks at the measured production source. The 11 Full Companions were V3, T1000-E, RAK4631, T096 FEM on, Mesh Pocket, XIAO nRF52, WisMesh Tag, V4.3 FEM off, T-Deck, XIAO S3 WIO and SenseCAP Indicator. RAK3401 and MeshTower SD repeater targets also passed, with full/reduced OTA packages. All 15 resulting capability manifests were verified. Compilation covers this build set; it does not qualify every board or every Full feature on hardware.

The retained V3 ELF SHA-256 is a05c0cd6d70a8146296d9b6c3edbe3e8602fe05901b4d05759454cf61ba7e902. It matches the passed Full memory manifest, whose application, merged image, capability manifest and license hashes also matched. Disassembly confirms the 7 ms comparisons in isWriteBusy() and checkRecvFrame(), and the authenticated updateConnParams call with interval units 12/24, latency 0, timeout 400 and connection-event length hints 0/0. This establishes the linked code; the monitor evidence above establishes the parameters accepted in this run.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#retained-safe-evidence","title":"Retained safe evidence","text":"

These artifact names and SHA-256 hashes identify the evidence used for this page. The page contains no private device identifiers or raw contact records.

Artifact SHA-256 v3-equivalent-warmboot.safe.json 6d816656a4ff5d2dc22f2175026078bb6b487445acbcc51a70fb3cd51cf87838 v3-60ms-92-after-warmboot-retry.safe.json 41a06fdb63cb63c08d63ca09be78c12e5997bec7fccc2a4a164acda56fd0fabf v3-production-fast-92.safe.json d0633f82862322d48194ccdb99a030a600365f1ecd3da729f32614aa80beb510 v3-production-fast-link.safe.jsonl fdac4c5d0ed73d2c1e3d1e37c2ef02ecc5831d7cb791099da1e3c97493825e64 pi-hci-capabilities.safe.json 7c103253e305b2345dfcbb9170faad4b67c91df5a475536798e1bf48d60b7980 v3-linked-ble-proof.safe.json cc04c5b0547a9911b9fb3a41fa33140c4647565b27d32c4b51c3d266c6ef7fbb v3-owned-cleanup.safe.json c1c7e59c30bf0c2e8bd3097007a2ddb457b5272d6c4b598564d8ac65059ef2c4 BUILD-RESULTS.json 03eb95513c6111804b59e6175892929bd4346be92c9b8942bed925442114cb7f"},{"location":"test-results/companion_contact_cache_results/","title":"Companion contact cache results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_contact_cache_results/#contact-cache-and-nimble-ram-qualification","title":"Contact-cache and NimBLE RAM qualification","text":"

Test date: 2026-09-08. Firmware source: b68aa1c6.

All six constrained ESP32 Full Companion trials pass the linked RAM budget at 350 contacts, 40 channels and 256 normal offline frames. Their final capability manifests confirm compiled Bluetooth/mOTA support and valid WiFi OTA layouts. These are the optional environments in the contact-cache guide. Ordinary release recipes retain their established contact limits pending qualification on those boards.

"},{"location":"test-results/companion_contact_cache_results/#linked-internal-ram","title":"Linked internal RAM","text":"

Every column below uses 350 contacts, 40 channels and 256 normal offline frames. Values are bytes available for runtime allocations after linking. The final margin subtracts the existing conservative runtime reservation; it is not a measurement of live free heap.

Hardware Bluedroid NimBLE NimBLE + mOTA queue loan Plus contact caches Final margin Heltec V3 143,776 158,896 170,152 196,168 23,112 Wireless Tracker 148,432 163,536 174,792 200,824 30,838 Tracker V2, FEM on 133,064 148,168 159,416 185,456 15,470 Heltec CT62 99,040 114,096 125,312 151,344 15,152 XIAO ESP32-C3 98,816 113,888 125,120 151,152 14,960 Generic ESP-NOW 99,776 114,800 126,032 152,064 15,872

NimBLE recovers about 14.7 KiB. Contact caches add about 25.4 KiB on top of NimBLE and the shared mOTA workspace. NimBLE alone fails the unchanged budget at this capacity on all six; adding the queue loan passes only on the original Wireless Tracker. Adding the contact caches passes all six, with 14.6-30.1 KiB of margin. The same Bluetooth runtime allowance is used in every comparison.

The shared mOTA queue stays at 256 frames normally and retains 128 while mOTA borrows its workspace, then returns to 256. A queue above 128 must be synchronized before that workspace can be borrowed; unread frames are not discarded to start mOTA. PSRAM-backed queues retain their existing policy.

The baseline builds use b041fc7f; the final builds use b68aa1c6, with the same ESP32 Arduino 2.0.17 toolchain and NimBLE-Arduino 2.5.1 where applicable.

The CT62, XIAO C3 and Generic ESP-NOW applications fit two 1.5 MiB OTA slots on 4 MiB flash. Their explicit trial layouts retain 896 KiB SPIFFS and the existing SPIFFS/coredump addresses. Install the merged image over USB when changing from the ordinary single-slot Full layout. The other three trials also retain verified dual WiFi OTA slots. All six remain mOTA senders.

Additional builds check the platform and default-policy branches:

Build Available bytes Reserved runtime bytes Margin bytes Policy T096 Full, FEM on 100,076 73,728 26,348 nRF52 path/secret caches RAK3401 Full 99,900 50,976 48,924 nRF52 path/secret caches Wireless Paper Full 187,360 152,576 34,784 ESP32 caches, existing Bluedroid V4 Full NimBLE 263,944 173,056 90,888 Complete contacts in PSRAM XIAO nRF52 legacy BLE Companion 84,368 38,688 45,680 Existing inline contact storage

The V4 test-only forced-cache build also passes its RAM budget. The V4 normally keeps complete contacts in PSRAM; forcing caches adds internal RAM overhead. The six-board savings above apply to boards without PSRAM.

"},{"location":"test-results/companion_contact_cache_results/#physical-checks","title":"Physical checks","text":"

The VM V4 used the test-only cache override. USB tests verified all 350 contacts and all 64 saved path bytes per contact, cold individual lookups, replacement of the complete path set, and persistence after reboot. A secret workload contacted 21 distinct synthetic peers, then the first peer twice: 22 calculations, one RAM hit, and no secret-file access under the ESP32 policy.

A sustained send test exposed a false receive watchdog recovery. RX could occur inside the main loop between two successful transmissions, while the watchdog observed TX at both loop boundaries. It counted from the start of the whole burst and could reset a working radio during a later CAD pause. 468cc3cf restarts the receive recovery allowance after each successful TX. The native regression fails before that change and passes afterward; it also checks that a stuck receiver still gets recovered.

The corrected V4 passed the same 23-datagram workload with zero radio error flags. It also passed a complete 350-path replacement, another send workload and a post-reboot path check with zero radio error flags. The replacement took about 127 seconds: dirty-cache eviction still performs synchronous SPIFFS contact-file saves, so bulk path updates remain a latency workload to qualify on the other ESP32 boards. After that run it reported 133,828 bytes free internal heap, a minimum of 130,976 bytes, and a largest free block of 122,868 bytes. These readings belong to the forced-cache V4 workload, not the six other boards.

After qualification, the VM V4 was erased to remove synthetic contacts and installed with the normal PSRAM-backed V4 NimBLE trial from b68aa1c6. It is named NimBLE-V4-VM, uses the PIN shown on its display, and has powersaving off for USB use. That clean setup reported 143,612 bytes of free internal heap and passed 201 local USB requests with zero radio error flags. The Mercer V4 and XIAO S3 were left on their previously qualified NimBLE builds.

The Mercer RAK3401 Full Companion verified 350 paths, eviction and persistence. Its first secret workload produced 21 calculations, one flash hit and one RAM hit with no skipped saves. After reboot, the same workload produced 22 flash hits, one RAM hit and zero calculations. Both completed with zero radio error flags. The final b68aa1c6 RAK image repeated the 350-path and saved-secret checks with zero radio errors and zero calculations. The RAK was returned to the published 26303793 repeater application with its original repeater name and USA Cascade settings; the Pi's MQTT logger was restarted after the exclusive USB tests.

"},{"location":"test-results/companion_contact_cache_results/#shared-secret-timing","title":"Shared-secret timing","text":"Hardware/filesystem Successful saved-secret lookup Key exchange Selected miss policy V4 / SPIFFS 132,427 us 21,932 us Recalculate RAK3401 / LittleFS 7,812-8,789 us 31,250 us Read saved entry, calculate on miss

The SPIFFS timing comes from the earlier forced flash-cache experiment. The final ESP32 policy does no secret-file I/O. Both platforms reuse the 16 most recent secrets from RAM. These are elapsed-time measurements, not energy measurements; timing on other ESP32/nRF52 boards remains to be measured.

"},{"location":"test-results/companion_contact_cache_results/#regression-checks-and-evidence","title":"Regression checks and evidence","text":"

The contact-cache suite exercises the production cache and persistence code under address/undefined-behavior sanitizers, with the real Ed25519 key-exchange implementation. Its cases include 350 paths, identity changes, snapshots, legacy migration, storage faults, handle exhaustion and packet release when a cold path cannot be read. The related contract suites passed 62 checks. The native queue/store suites passed 27 cases, the radio-liveness suite passed eight, and the partition-selection suite passed 11.

Hardware reproduction commands are in the contact-cache guide. The tracked harness is tools/hil/contact_cache_serial_stress.py. It requires exclusive access to the specified Companion data port.

Local evidence in the qualification workspace:

Each final RAM report is checked against its ELF hash. All 12 final builds pass their RAM and OTA capability checks. Physical testing here covers the V4 and RAK3401; the six constrained ESP32 boards still need pairing, WiFi/BLE/LoRa coexistence and sustained-load testing on their own hardware. No GitHub release assets or firmware-picker entries were replaced by these experimental builds.

"},{"location":"test-results/companion_lost_reply/","title":"Companion lost reply","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_lost_reply/#companion-lost-status-response-validation","title":"Companion lost-status response validation","text":"

Validated on 2026-10-10. These are production-path host tests, browser tests and firmware builds. No lab radio was flashed for this feature; this report does not qualify a sleeping tracker client or a live LoRa exchange.

"},{"location":"test-results/companion_lost_reply/#automated-checks","title":"Automated checks","text":"

The responder test executes the actual private receive callback, message composition/send functions, ACK handling and local CLI policy. Seven compiled variants cover normal and shared one-key DM queues, a text terminal, explicit STM32 enablement, default STM32 exclusion and explicit exclusion elsewhere. AddressSanitizer and UndefinedBehaviorSanitizer checks passed.

Coverage includes exact question matching, Yes/No loop avoidance, known full identities, temporary/unknown contacts, room/channel/CLI exclusions, a full offline inbox, duplicate requests, cooldowns, clock wrap, busy packet/ACK capacity, send failures and preservation of pending user messages/deadlines. Queueing the response preserves the shared send timer. ACK reception retains the existing shared-timer behavior; independent per-message expiry continues to protect pending user deliveries. Excluded builds have no responder state, automatic send, successful setting command, save or WebConfig capability.

Saved preference tests cover all three values, all 256 stored byte values, legacy images loading off, reboot, atomic save failures, recovery copies and runtime adapter preservation. Existing CLI, delivery, inbox, notifications and preference regression suites passed after updating adapters for the new append-only preference byte.

WebConfig tests execute the actual JSON and settings handlers with ArduinoJson. Chromium exercises Off -> No -> Yes -> Off, Companion-only visibility and the absence of the control on infrastructure roles. Existing browser, authorization, secret masking and generated asset checks passed. Both new suites are included in the unit-test workflow.

"},{"location":"test-results/companion_lost_reply/#capacity-and-reliability","title":"Capacity and reliability","text":"

The responder cache is 184 bytes on ARM and retains four complete public keys. The saved preference uses an existing padding byte in the tested ARM preference object and one new byte in its explicit storage image. No heap allocation is introduced by the responder.

Compact STM32 Companion images exceeded their existing application boundary when the responder was included. Those default to MESH_ENABLE_LOST_REPLY=0, preserving their existing features and filesystem layout. Explicit opt-in is tested synthetically and remains subject to the normal capacity checks.

The ordinary RAK WisMesh Tag BLE queue uses 240 frames, retaining 350 contacts and 40 channels and saving 2,832 bytes. Source comparison showed that its prior 256-frame recipe was already below the current runtime RAM allowance before the new 184-byte responder cache. Full retains its 256-frame shared queue. Sanitized tests exercise real FIFO wrapping, downloads and held-DM isolation at 240 slots, plus the production Full override.

Queueing an answer suppresses that request even if later delivery fails. A tracker needs a fresh timestamp for a later poll, must stay awake for the private answer, and must authenticate its configured Companion. Plain Yes/No answers do not contain a poll ID, so delayed replies cannot be precisely correlated with a newer poll. An unanswered poll means unknown.

"},{"location":"test-results/companion_lost_reply/#firmware-build-qualification","title":"Firmware build qualification","text":"

All ten final builds passed flash and internal RAM policy checks. Compact STM32 and both Full targets were also packaged using the normal v1.17.1.9-halo-keymind-cascade-dev label. Ordinary targets used their normal PlatformIO recipes. Full nRF52 UF2 boundaries and applicable Bluetooth DFU handoff checks passed. These are local qualification artifacts, not new published release assets.

Companion target Responder Flash used / limit (bytes) Link-time internal RAM available / required (bytes) RAK 3x72 USB Excluded 228,924 / 229,376 24,728 / 17,408 Tiny Relay USB Excluded 228,944 / 229,376 24,728 / 17,408 Wio-E5 Mini USB Excluded 228,888 / 229,376 24,704 / 17,408 Wio-E5 USB Excluded 229,008 / 229,376 24,736 / 17,408 T1000-E Full Included 607,480 / 708,608 87,064 / 44,864 RAK WisMesh Tag Full Included 649,408 / 712,704 82,112 / 44,864 T1000-E USB Included 426,756 / 708,608 48,248 / 38,944 Pico W USB Included 640,180 / 1,568,768 101,712 / 17,408 Heltec V4 BLE Included 1,550,217 / 6,553,600 263,448 / 118,560 RAK WisMesh Tag BLE Included 459,564 / 712,704 46,316 / 44,864

The STM32 historical-size table uses byte entries while retaining a 32-bit input file size; exhaustive tests reject oversized aliases and partial images. Tiny Relay and Wio-E5 use the size option already used by neighboring STM32 recipes. Existing terminal help was combined into fewer writes while retaining its text, order and 256-byte maximum per write. Supported images also show the new lost-reply commands. Contacts, channels and filesystem boundaries are unchanged on those compact boards.

"},{"location":"test-results/companion_qualification_f807_followup_2026-10-19/","title":"Companion qualification f807 followup 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_qualification_f807_followup_2026-10-19/#mercerwood-companion-diagnosis-follow-up-2026-10-19","title":"Mercerwood Companion diagnosis follow-up, 2026-10-19","text":"

Overall status: FAIL, incomplete. Across the selected measured sources, 1/2 Companions PASS, 1 FAIL. RAK 4631 passed USB, BLE and two-way LoRa at f807. T1000-E passed normal Full USB and BLE on battery, wall power and suspended Pi USB, but active Pi USB reproduced its BLE failure. It remains FAIL pending resolution of that operating condition. XIAO nRF52 remains a carried-forward failure.

Both selected boards first returned to Full f80751354ffc530461e87109ced4f575d3950a52. T1000 subsequently received normal Full 42f7df082fc4c7362ecd5c18c8644ab25b51b9e9, version v1.18.0.1-halo-keymind-cascade-dev-42f7df08, after the corrected bootloader. Its ZIP SHA-256 is 7fac141e40183ada315f70b296583eaf854fd819c76fbd896a1f0f2e4a405e5b. Checks ran on October 20 UTC across October 19-20 local time. Normal Full 72a74c784c38af72518bec7517e691ce9abccaa3 is now restored, with unchanged identity, zero core errors and MCU auto/DCDC. Its restoration smoke passed three USB opens/closes and 19 responses with clean framing. This is not new BLE, RF or full feature qualification; the earlier Full 42 results retain their original source binding.

Companion Source USB BLE LoRa Overall RAK 4631 f8075135 PASS PASS PASS PASS Seeed T1000-E 42f7df08 PASS battery/wall power/suspended USB PASS; active Pi USB FAIL not requalified; f807 passed FAIL Seeed XIAO nRF52 prior failure retained FAIL not qualified not qualified FAIL retained

This is a selected-board follow-up, not a 13-radio fleet qualification. Repeaters were outside scope. Earlier mixed-source results remain in the previous follow-up.

Before the bootloader comparison, each f807 USB run passed 100 query cycles, 10 reopens, radio2 checks, owned-contact CRUD, persistence through warm reboot and contact cleanup. Each closed 18/18 ports with clean final core flags and zero stable binary prefix discards or invalid lengths. ASCII setup and intentional reboot recovery remain separate transition evidence.

RAK BLE passed three 30-second sessions with USB-bound identity, board, full version, NUS services and getters verified. Uptime advanced, flags stayed clean and each session disconnected. LoRa passed an exact zero-hop DEV datagram in each direction between RAK and T1000, with packet-counter checks, owned-channel cleanup and restored radio leases. These finite checks do not establish phone interoperability, range or text delivery.

T1000's original f807 BLE run failed current-advertisement identity binding with zero sessions. A later 90-second Pi follow-up also completed none despite Bluetooth enabled, stealth off and clean USB core flags. The unpaired BlueZ cache retained T1000-BLE-PROBE while USB verified normal f807 and its configured MeshCore name. Cached naming may affect filtering without a current scan response; it proves neither the BLE cause nor a duplicate address or working connection.

Minimal BLE-only, payload/timing, clock-request, transmit-power and LFRC images were diagnostic probes, not Full acceptance. The LFRC probe used an explicit RC configuration and valid vectors; this does not establish a defective crystal or justify changing production clocks. Advertising/HFXO readbacks do not measure RF accuracy or prove over-air packets.

The official upstream companion-v1.18.0 T1000 BLE reference had matching ZIP/UF2 application bytes, base 0x27000 and SoftDevice requirement 0x0123. Its bounded Pi scan did not find the required current advertisement. Phone visibility was insufficiently bound to name/time. This application-only experiment retained the same bootloader/SoftDevice and cannot isolate them. Normal Full was restored. Reference ZIP SHA-256: 79e53c2ce13521ccda5db3621a68f79728d2f241c3a8345e4107b7ae6b5a374d.

Installed S140 7.3.0 readback CRC 733639BD matched the package bytes, also identical to Seeed's recovery package SoftDevice. That checks the covered image, not RF operation. C2 installation preceded a historical secured diagnostic link that never completed full BLE qualification.

A separate DFU defect was corrected in bootloader c0c421184a3cf1ca66b40b055101f20b8c587e26: active-high P0.06 had been configured pull-up/active-low, with RESET/P0.18 treated as another button. The correction adds qualified three-second hold/USB-first recovery while retaining explicit-request latency and other boards' two-button behavior. Eleven actual-function host tests, 13 DFU-entry checks and T1000/T096/T114 cross-builds passed. See candidate PR #1 and button-polarity evidence.

The reviewed combined bootloader transfer returned 0, but its installer stopped when the MSC disk was unavailable. That failed workflow remains failed. Later exact-device public INFO verified model Seeed T1000-E for Meshtastic, Board-ID nRF52840-T1000-E-v1 and version TEST_0x02040C01. Package CRC 6BA4D206 was validated offline; installed CRC was not exposed. Combined ZIP SHA-256: 8db09e6b7f253b1e54aae754220f0d1053d369c07bff299a26b384233de6e234.

Separate f807 application programming and its adapter command both returned 0. An independent USB check passed 15 queries/two reopens, closed 5/5 ports and verified normal f807, the same public identity, clean framing and zero core errors. With the corrected bootloader and same f807, an explicit-address 90-second BLE attempt still failed advertisement binding with zero sessions. No BLE fix or bootloader cause is established.

Final normal Full 42f7df08 installation succeeded. Its fresh USB run passed 100 queries, 10 reopens, owned-contact CRUD/cleanup, warm reboot and radio2 checks, with 18/18 ports closed, zero stable framing rejects and clean core flags. Its normal 25-second BLE discovery still failed with zero sessions. LoRa was not requalified at 42f7df08; the earlier f807 RF pass remains source-bound.

Combined serial delivery avoided legacy UF2 scratch 0xE0000..0xEA000, which overlaps internal ExtraFS. Its staging and f807 restore remain below ExtraFS. No filesystem-preservation claim replaces runtime checks.

A subsequent 90-second scan by the independently verified Bluetooth address also failed to reach a connection on normal Full 42f7df08.

After the user reported a reset, USB was present at preflight. Fresh USB passed 15 queries/two reopens on the same normal Full image. A 90-second Pi scan completed zero BLE sessions and captured zero target advertisements among 643 advertising event blocks. A separate 30-second passive scan captured one target advertisement among 3,375 blocks; direct public-service discovery timed out. A temporary saved random address produced two primary advertisements but no usable connection. Factory-default addressing was restored and verified over USB; Bluetooth remained enabled with clean core flags. No host bonds were removed. These are post-reported-reset observations, not a verified cold or battery-only test.

Fresh normal RAK and T096 USB/BLE controls passed on the Pi. An independent RAK nRF52 central diagnostic, version v4.1, then passed a single public ATT/NUS metadata comparison against the USB-bound T096. It read the expected public GAP name, recorded no security activity, and disconnected cleanly. The unchanged T1000 Full image then supplied a current primary advertisement at -63 dBm, but its one attempted connection ended with 0x3E before any NUS metadata or public read succeeded. The observer's uptime and watchdog counters advanced, with no late connections or security events; scanning and connections stopped cleanly. A central connected event does not prove the peer received its connection request; Nordic describes this initial-exchange failure.

This comparison strengthens a T1000-specific link finding across two central implementations. It does not identify a faulty component or establish a bootloader cause. That comparison did not reach pairing or password exchange. The temporary central is not Companion qualification. Its initial v4 scan-unit error was corrected before either trial; live v4.1 status verified a 3,000-unit, 30-second scan configuration. The RAK was restored to the exact normal Full f807 ZIP and passed fresh 15-query/two-reopen USB and one 10-second BLE session with the same public identity. T1000 remains FAIL. Its pin assignments match Seeed's T1000-E hardware list; no further pin change is justified by this evidence.

A later controlled Bluetooth off/on test kept USB management open and had the Pi client scanning before enable. Unlike the earlier fixed-duration discovery, this client attempted pairing 49 ms after its first current target advertisement, which arrived 84.331 seconds after the on request. This time the trace proves bidirectional SMP and ATT exchanges and a passkey request. The supplied PIN matched the active USB-reported PIN and the user's expected PIN. The link then ended with HCI Connection Timeout 0x08; BlueZ reported Authentication Canceled. No completed bond or Companion session, explicit PIN rejection, or stable-link pass is established. The parent evidence exporter subsequently failed to parse a traceback as JSON; that workflow failure remains recorded, separate from the independently analyzed radio trace. A further exact-address ATT connection was requested while Bluetooth was off, with its kernel scan already pending before enable. It failed initial establishment with 0x3E and read no NUS service. Bluetooth was restored on, with the same factory-address policy, stealth off, monotonic uptime and clean core flags. Firmware and host bonds were unchanged. These tests refine the failure to an unstable link that can sometimes reach pairing; T1000 remains FAIL.

A subsequent application-only isolation test installed two small public-NUS echo diagnostics built against source context 56b4d72e6. They omit Companion, LoRa, GNSS-driver and sensor processing, retain the board/framework/SoftDevice/bootloader, and feed every active watchdog channel. Both builds passed memory/vector/package checks and exact-device serial DFU/runtime verification.

Diagnostic Observation Result Minimal external LF crystal, 20 ppm No target advertisement in 60 seconds; uptime advanced 62.215 seconds with watchdog feeds advancing No connection or echo Same minimal app, internal LF RC, 250 ppm One target advertisement after 42.138 seconds at -60 dBm; connection attempt timed out; uptime advanced 59.209 seconds with watchdog feeds advancing No connection or echo Normal Full with four-second initial supervision timeout from RAK central v4.2 Current target advertisement at -62 dBm; initial connection establishment failed with 0x3E before public GATT reads or security activity FAIL T096 control with the same central parameters Public NUS service/characteristics and expected GAP name read successfully Diagnostic control PASS

The controller reported the configured 48.75 ms interval, zero latency and four-second initial timeout for both central trials. A central connected callback does not prove T1000 receipt of its connection request; the failed initial exchange does not test pairing on an established four-second link. The Pi's earlier attempt to extend a live connection saw no target advertisement in 90 seconds, so no update was applied. A host-only HCI-filter padding error was corrected before that valid trial and remains recorded separately.

These results make it less likely that the Companion workload or the external low-frequency crystal alone explains the failure. They do not distinguish common framework/SoftDevice behavior, the high-frequency radio clock, power or RF hardware. Both minimal images were removed. Normal T1000 Full 42f7df08 was restored and passed 15 USB query cycles/two reopens with the same public identity. The test RAK returned to its exact normal Full f807 image and passed 15 USB query cycles/two reopens and one 10-second secure BLE Companion session with the same identity. The four-second comparison had T1000 USB present before and after; it was not a battery-only test.

Physical battery-only testing subsequently passed on the unchanged normal Full image. The user confirmed unplugging USB, and the Pi verified the exact T1000 USB identity remained absent throughout each run. The first run completed fresh PIN pairing, authenticated bonding, encryption and a 30-second Companion session. Three subsequent one-minute sessions passed normal discovery by name and saved-bond reconnects. All four verified the same USB-bound identity, complete firmware version and Companion getters, negotiated MTU 247, advanced uptime with zero core error flags, and disconnected cleanly. The optional four-second timeout helper aborted at its live-handle guard before sending any command; it did not cause this success. The first connection negotiated normally from 420 ms to two-second supervision, and the reconnects used two-second supervision.

The user then reconnected USB after being asked to keep the radio in place without resetting it. The exact runtime endpoint remained present throughout both comparison trials, which opened no USB serial port, changed no firmware/preferences and retained the same bond.

USB-connected comparison Observation Result Normal name discovery, 25 seconds Zero target advertisements among 202 advertising event blocks Zero Companion sessions Immediate connection by independently verified address Ten target advertising event blocks, no target name scan response; one BlueZ Connect operation produced nine controller attempts, seven ending with 0x3E and two with 0x08; no successful encryption Zero Companion sessions Read-only USB check after BLE trials Same normal Full version and public identity, uptime 2,903 seconds without a decrease from the battery test, Bluetooth on, stealth off and zero core error flags Runtime check PASS

The USB comparison used the same normal two-second supervision parameters that worked on battery. After the user unplugged the hub end, with instructions to leave the magnetic end attached and the radio in place, three further 30-second sessions passed normal name discovery, encryption and Companion queries. Uptime advanced from the USB check, all sessions disconnected cleanly, and the Pi verified USB absence throughout. That makes seven successful battery sessions in total, with no firmware change, reset request or bond removal during this comparison.

The user then connected the same cable to wall power only. Three further 30-second sessions passed normal name discovery, saved-bond encryption, the same Companion identity/version/getters and clean disconnects. Pi USB remained absent throughout. Charging from that supply alone did not reproduce the failure. Supply voltage was not instrument-measured.

The battery/Pi USB/battery/wall-power sequence associates the failure with the Pi/hub connection; it does not identify cable/ground/RF effects, USB software activity or a particular faulty component. Position and cable attachment were requested but not independently measured. The MQTT gateway remained active before and after the comparison trials.

On the next Pi reconnection, a guarded Linux USB device authorization test deconfigured only the exact T1000, then restored its configuration. The physical cable remained connected as requested; no hub or port-power command was sent. Before and after deauthorization, 25-second scans captured no target advertisement. While deauthorized, the Pi captured three target advertising event blocks and briefly encrypted a link, but it ended with 0x08 before any Companion session completed. This did not restore stable BLE. Deconfiguration does not physically disconnect USB or establish that bus signalling and the peripheral USB block have stopped.

USB authorization returned to its original state. The first USB runtime read failed before opening a port because the recreated device node was not yet accessible to the normal user; this failed workflow is retained separately from the BLE results. After normal user access returned without a permission change, a separate readback passed on unchanged Full firmware with the same public identity, Bluetooth on, zero core errors and uptime 3,987 seconds. The gateway remained active.

The user moved the T1000 to another powered-hub port. The Pi verified the same USB serial identity at a different physical USB path, continuously present during both BLE trials. Normal 25-second discovery captured zero target advertisements among 358 advertising event blocks. A 90-second address-based scan captured zero among 580 and timed out before a connection attempt. Moving ports did not restore BLE. A read-only USB check between these trials passed seven queries with the same firmware/identity, Bluetooth enabled, zero core errors and uptime 4,635 seconds.

A guarded per-device USB runtime suspend comparison then changed only this T1000's idle policy. Linux reported the device suspended while its hub port remained active with port power-off prohibited. Authorization, configuration and device number stayed unchanged. No serial port was opened during either BLE phase, and no firmware, bond or reset change was requested.

USB runtime state BLE observation Result Suspended, cable still connected as requested Three normal-name, saved-bond sessions; same Full identity/version/getters, MTU 247, advancing uptime and clean core flags; three encrypted links and clean local disconnects 3/3 thirty-second Companion sessions PASS Resumed to original active policy Three target advertising event blocks; two controller attempts, one ending with 0x3E, the other briefly encrypted before 0x08 Zero completed Companion sessions

The original USB policy and idle delay were restored and verified, and the gateway remained active. This comparison makes active USB operation a stronger discriminator than charging alone or the individual hub socket. It does not distinguish USB software/peripheral behavior from electrical or RF effects of active signalling, and no waveform or supply-voltage measurement was made. Firmware remains unchanged.

Three subsequent handoff cycles temporarily allowed this device to autosuspend after two seconds idle. Each serial open woke USB and passed seven read-only queries. After serial close, Linux reported USB suspended, and a normal-name, saved-bond 30-second Companion session passed with the same identity/version, MTU 247 and clean core flags. All three cycles passed. Together with the direct suspend trial, six secure sessions passed with USB suspended.

The original active USB policy was restored again. A final seven-query USB readback passed, with uptime 5,259 seconds, Bluetooth on, unchanged identity/firmware and zero core errors. No persistent host configuration was installed. Automatic idle suspend is a tested candidate workaround for sequential USB/BLE use; it does not qualify simultaneous active-USB/BLE operation or establish a production firmware fix. A second computer with USB data active is the next comparison needed to separate the Pi/hub path from behavior common to the T1000's USB connection.

Further firmware isolation did not establish a reliable simultaneous active-USB/BLE fix. All native SDK runs below are public DIS/NUS nonce-echo diagnostics, not secure Companion qualification; they add zero qualified secure sessions. Results remain bound to their runtime builds and safe evidence digests.

Probe Source or build Completed observation Assessment MCU LDO, active USB Normal Full 42f7df08 Zero of three BLE sessions; MCU auto/DCDC restored, same identity and clean core flags FAIL Minimal USB IRQ 2/6/2 T1000-USBIRQ-v1 Zero echoes in each corrected trial; IRQ count-rate upper bounds 0.866/0.744/0.728 Hz including probe traffic No reliable fix; interrupt durations not validated Explicit HFCLK claim T1000-USBIRQ-v2 Ten echoes, then GATT protocol failure FAIL Modified Full clock prototype Base 72a74c78 Zero of three Companion sessions; current-advertisement identity gate failed FAIL; not retained as a production fix Active USB / suspended USB / active USB Native SDK v1 Zero echoes / three sessions with 90 nonce echoes / zero echoes Only suspended phase PASS Advertising channels 37/38/39 Native SDK v2 Target discovered on each channel; zero echoes in every trial No reliable fix Default/constant-latency power; LF RC/SYNTH Native SDK v3/v4/v5 Zero echoes in all four trials No reliable fix PHY and link policies Native SDK v6 Two isolated 30-echo harness passes, including an explicit 1M PHY request and default policy with longer connect allowance; both matching HCI traces recorded LL Response Timeout 0x22. Repeat and other policy trials failed No clean, repeatable fix TX +8 dBm and regulator selection Native SDK v7 Baseline and TX8/LDO: zero echoes. TX8/DCDC: one 30-echo PASS with clean HCI local-host close, then repeat FAIL after two echoes; ten-second echo-deadline retry FAIL after nine echoes with HCI/peripheral timeout 0x08 Intermittent success; not a solution CPU sleep versus busy idle Native SDK v8 Sleep: zero echoes and HCI timeout 0x08. Busy: one 30-echo PASS with HCI local-host close; busy repeat: FAIL after twelve echoes with HCI/peripheral timeout 0x08 despite a ten-second echo deadline Intermittent success; not a reliable fix Short actual connection interval, TX8/DCDC/sleep idle Native SDK v9 Channel37: 30 echoes and clean local close after update to 7.5 ms. All advertising channels: three sessions/90 echoes at actual 7.5 ms, but HCI recorded two teardown completions 0x22 about four seconds after host Disconnect commands, two failed-establishment attempts 0x3E and one local close. USB queries remained functional Diagnostic candidate; not three clean reconnect passes Same-image slow-interval control Native SDK v9 Actual interval updated to 75 ms before service-discovery failure; zero echoes. HCI: four initial failures 0x3E and one 0x08; final SDK readback still connected FAIL; controller/SDK observations retained separately TX 0 dBm recovery with fast interval Native SDK v9 Three payload sessions/90 echoes, but strict lifecycle: eight initial failures 0x3E, one 0x08, two 0x22 and one local close Strict lifecycle FAIL Quiet GATT initiation with fast interval Native SDK v10 Three payload sessions/90 echoes; data_exchange_passed=true, passed=false. Strict lifecycle: five initial failures 0x3E, two 0x22 and one local close Strict lifecycle FAIL; no reliable fix

A final native SDK v11 active/suspended/active comparison retained the same image and RAM phone-interval policy [12,12,0,400] (15 ms, zero latency, four-second supervision), TX +8 dBm, MCU REG1 DCDC, sleep idle and low-power mode, with advancing uptime and no reset observed. HCI verified actual 15 ms operation for all 270 nonce notifications. This is a connection-interval diagnostic, not phone interoperability testing.

V11 USB condition Nonce echoes Strict HCI lifecycle Median echo RTT Active, with application USB serial queries 90 across three payload sessions FAIL: two initial failures 0x3E, two 0x22, one local close 0x16 452.8 ms Runtime suspended, same image/policy 90 across three sessions PASS: three clean local closes 0x16 33.7 ms Active, no application USB serial I/O during BLE 90 across three payload sessions FAIL: two initial failures 0x3E, one 0x22, two local closes 0x16 394.0 ms

The active phase recorded 344 USB telemetry query replies, including 154 link replies reporting BLE connected at actual 15 ms. The suspended and active-idle phases performed no application USB serial I/O during BLE; configuration readbacks bracketed those trials. Active-idle failure strengthens active USB as the discriminator even without application serial payloads. It does not establish an electrical, RF, peripheral or host implementation cause. The exact harness source 19703941... is preserved and digest-matched for all three runs.

Across the v9 runs, 168 USB link-query replies explicitly reported BLE connected at actual parameters [6,6,0,400] (7.5 ms interval, zero latency, four-second supervision): 41 replies over 39.576 seconds in the first run and 127 across the three repeat-session windows. The complete runs recorded 196 and 334 USB rssi/link query replies respectively, including discovery and disconnect periods. Linux samples reported active USB throughout, with configuration retained before and after. These are concurrent diagnostic command-response checks, not Full USB or secure Companion acceptance. The 0x22 events occurred after explicit host Disconnect commands; payload completion and clean teardown remain separate results. The same-image slow control subsequently observed actual 75 ms before failing with zero echoes; production verification remains pending.

Controller and SDK counters remain distinct. TX0 recorded twelve controller connection-complete events versus SDK connection/disconnect deltas 5/5; quiet v10 recorded eight versus SDK deltas 3/3. No cause is assigned to those differences. The exact original v9 and slow-control harness snapshots were recovered with matching SHA-256 values 83be8cf2... and 3295d776..., and retained as reproducibility evidence. The completed v11 comparison is reported above.

The two-echo repeat ended in a harness timeout without a captured HCI disconnect, so that failure alone does not prove link loss. The separate nine-echo trial did record link timeout despite its longer deadline. Early missing-characteristic and D-Bus failures remain host-workflow evidence and are excluded from clean BLE comparisons. T1000 remains FAIL for simultaneous active USB and reliable BLE. Normal Full 72a74c78 was subsequently restored from unmodified source using application-only ZIP SHA-256 ba092c8e29b5b752a3d1667a0980910d4bec5815dfd0f07277b2ba3b8ce4e25a. Restoration and two additional USB opens returned 19 responses across three ports, all closed, with unchanged identity, advancing post-restoration uptime, zero framing rejects/core errors and MCU auto/DCDC with zero recorded crashes. The gateway remained active. These are restoration and USB runtime smoke results; historical Full 42 qualification remains source-bound, and BLE/RF were not requalified at Full 72.

Cold boot, battery-only restart, complete phone/feature testing and fleet qualification remain untested. Unplugging USB alone does not prove a restart. Safe JSON retains source bindings, workflow distinctions and evidence digests without device secrets or raw addresses.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/companion_qualification_followup_2026-10-19/","title":"Companion qualification followup 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_qualification_followup_2026-10-19/#mercerwood-companion-follow-up-2026-10-19","title":"Mercerwood Companion follow-up, 2026-10-19","text":"

Across the retained baseline and selected normal HEAD tests, 9/11 Companions PASS and 2 FAIL. Overall campaign status: FAIL, incomplete. A nonworking required axis counts as a board failure.

Baseline: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6. Selected normal HEAD refresh: 51538f0379076111c14d50b29618c5792e0490dc (v1.18.0.1-halo-keymind-cascade-dev-51538f03). This report covers 11 Companions, with no new fleet-wide inventory or repeater qualification claim.

The branch advanced to 811b2d7232aff86fd13075cee9ff6ac6471d2b6c during the campaign. Its four subsequent radio changes are outside the measured sources and have not been hardware-qualified by this report.

Later baseline RF tests closed the V4.3 and XIAO ESP32-S3 gaps: each exchanged an exact zero-hop DEV datagram in both directions with RAK 4631, packet counters advanced, owned channels were cleared and radio leases restored. A bounded transition drain preceded the unchanged strict handshake; it was not itself a qualification pass. Earlier setup failures remain in the record.

The retained 4e campaign now has 9/11 Companions passing all three required axes: 10 USB passes, 9 BLE passes and 10 RF boards through 9 two-way links. T1000-E BLE and XIAO nRF52 remain failures.

Companion Qualification source USB BLE RF Overall Heltec V4.3 51538f03 PASS PASS PASS PASS LilyGo T-Deck 4e4f27a2 retained PASS PASS PASS PASS RAK WisMesh Tag 4e4f27a2 retained PASS PASS PASS PASS Heltec MeshPocket 4e4f27a2 retained PASS PASS PASS PASS SenseCAP Indicator 4e4f27a2 retained PASS PASS PASS PASS RAK 4631 51538f03 PASS PASS PASS PASS Heltec V3 4e4f27a2 retained PASS PASS PASS PASS Seeed XIAO ESP32-S3 51538f03 PASS PASS PASS PASS Seeed XIAO nRF52 4e attempted; runtime unverified FAIL not runnable not runnable FAIL Heltec T096 4e4f27a2 retained PASS PASS PASS PASS Seeed T1000-E 51538f03 PASS FAIL PASS FAIL

The six retained baseline boards have not been requalified at the new source. Of the four refreshed boards, three passed every required axis; T1000-E failed BLE. Selected normal HEAD USB passed 100 query cycles and 10 reopen checks on V4.3, XIAO ESP32-S3, RAK 4631 and T1000-E. Each also passed owned-contact cleanup, warm reboot and radio2 checks, with 18/18 ports closed, zero stable binary framing rejects and clean final core flags. ASCII-to-binary setup and deliberate reboot transitions are recorded separately.

HEAD RF passed all three RAK hub links (V4.3, XIAO ESP32-S3 and T1000-E), one exact DEV datagram in each direction, with clean channel/lease cleanup. RAK also passed three 30-second normal BLE sessions, matched its USB identity/full version and disconnected cleanly.

V4.3 and XIAO ESP32-S3 initially failed normal HEAD BLE. Both traces show a successful exact-target connection followed by HCI encryption status 0x06, PIN or Key Missing, consistent with obsolete host keys after full ESP erase. A separately gated repair removed one proven target host bond each; it changed no other bond, reset no adapter and did not qualify BLE. V4.3 subsequently passed all three normal BLE sessions with USB identity/full-version matches, clean core flags and final disconnects. XIAO ESP32-S3 also subsequently passed all three normal BLE sessions with the same identity, version, core-state and disconnect checks.

The original V4.3, XIAO ESP32-S3 and RAK flasher commands returned 1 and remain failed workflow records. The ESP logs contain successful image-hash verification before native USB reset waits expired. Later exact-source clean USB qualification is separate evidence of working application runtime; it does not rewrite those return codes.

T1000-E BLE remains FAIL. Two normal HEAD discovery attempts, an MCU LDO trial, restored auto/DC-DC policy, a HEAD diagnostic image and the older cdacf670 reference all failed to produce the required current matching advertisement. The older Page Timeout pairing failure remains historical evidence rather than the reason for these discovery failures. No direct-link failure is counted where no connection was attempted.

The diagnostic firmware reported SoftDevice/interface readiness and advertising activity, with advancing loop timestamps, but those readbacks do not prove an over-air advertisement. The regulator comparison establishes no DC-DC cause. Normal HEAD was restored after the older-reference test; fresh USB evidence matched the same public identity, normal full version and original MCU/radio auto policy (both active DC-DC). Diagnostic collection does not count as normal BLE qualification.

A final read-only check at 2026-10-20 02:13:35 UTC confirmed normal HEAD on T1000-E, Bluetooth enabled, clean core flags and the original regulator policies. The active MQTT gateway still owned its original RAK 3401 primary USB interface; that port was not opened by the check.

C2 bootloader installation was verified at 2026-10-19 19:20:57 UTC. The same T1000-E old-reference trace at 20:04:22 UTC subsequently recorded one secured connection, with clean core flags; that trace did not qualify the full BLE test. The same older reference now fails advertisement discovery, so the failure is not isolated to the latest application build. All eight required C2 installation baselines remain separate from application qualification. Package manifest CRC validation is not an installed-flash CRC readback.

XIAO nRF52 is FAIL. The normal application transfer succeeded, but normal USB/runtime never qualified; BLE and RF cannot currently run. Its latest passive candidate-port capture returned protocol error -71 and zero bytes for all 24 device-descriptor reads and all eight address-setting attempts. No configuration or CDC request occurred, and no original UID or firmware mode was verified in that capture. This establishes failure before USB configuration, without assigning a cable, power, host, bootloader or application cause.

The passive observer issued no deliberate reset, serial open or image write. The kernel automatically issued 35 port-reset requests and two port-power off/on request sequences; accepted requests do not measure downstream power. The diagnostic image and staged 40b source hardening were not installed, so neither is claimed as a hardware fix.

These are finite campaign checks. USB covers the specified query/reopen/CRUD/warm-reboot/radio2 checks; BLE covers three 30-second sessions; RF covers zero-hop DEV datagrams and cleanup. They do not establish phone interoperability, cold boot, RF range, text delivery or every firmware feature.

Original histories remain in the earlier campaign report: Tag recovery, V3 discovery retry, Pocket watchdog observations, earlier setup aborts and failed XIAO runtime recovery. No earlier trial or physical-presence history is deleted.

Machine-readable results contain source-bound board records and SHA-256 evidence digests.

The unit-test workflow passed all 12 jobs for source 51538f03: GitHub run.

Later selected-board evidence, the T1000 bootloader button correction and the final Full Companion USB/BLE results are recorded in the diagnosis follow-up. The T1000 BLE and XIAO nRF52 failures remain open.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/companion_usb_ascii_validation/","title":"Companion usb ascii validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_usb_ascii_validation/#usb-companion-ascii-default-validation","title":"USB Companion ASCII default validation","text":"

The updated 1.17.1.6 Companion release covers 103 USB-enabled profiles: 52 ESP32 Full, 43 nRF52 Full, four RP2040 USB, and four STM32 USB. The BLE-only WM1110 image has no USB interface and is outside this change.

"},{"location":"test-results/companion_usb_ascii_validation/#behavior-and-cause","title":"Behavior and cause","text":"

Ordinary USB Companions previously started in Binary mode. Full Companions started in ASCII, but host-session cleanup could select or retain Binary mode for the next connection.

All USB Companions now select ASCII before framed dispatch begins. An observable USB session boundary cancels old USB operations and clears partial input, queued protocol output, frame ownership, and mOTA attachment before restoring ASCII. A network terminal can defer that restoration; a new USB binary client that connects during the deferral keeps its chosen mode. BLE and network Companion transports keep their existing binary protocol.

A complete framed app command still selects Binary mode automatically. An incomplete initial probe returns to ASCII after one second. Explicit terminal start/stop controls remain available.

Session detection depends on the hardware. Native USB with DTR can detect a terminal closing. ESP32 hardware USB Serial/JTAG observes bus resets and host loss, but cannot reliably observe a program closing its handle. UART bridges usually cannot observe that close either. Idle time never forces an active binary app back into ASCII; use the start token on a port that remains Binary. See the USB mode guide.

"},{"location":"test-results/companion_usb_ascii_validation/#automated-validation","title":"Automated validation","text":"

The 99 ESP32/nRF52/RP2040 profiles use d4a641ff. The four STM32 profiles additionally use the compatible Arduino 2.12 platform pin and build test in d2c52e4d. Both source revisions passed GitHub checks: main repair and STM32 repair.

All 103 builds passed capability and RAM qualification. Packaging checks bind each application to its capability and memory manifests, retain the original profile capabilities and download formats, and verify ESP32 application/merged image consistency. Replacement publication verifies public SHA-256 digests before deleting old images and checks that unrelated firmware assets retain their original hashes. Release manifests, checksum lists, picker links, and capacity-note source references follow the new binaries.

Publication completed with all 430 uploaded files verified through their public download URLs. The 412 superseded firmware/manifest files were removed. The remaining 1,837 assets kept their original hashes, and all five release checksum lists matched the final GitHub asset digests.

No USB Companion was attached to the local computer for this repair. These results are compilation and automated protocol tests, not physical USB tests across the board matrix.

"},{"location":"test-results/companion_usb_client_validation/","title":"Companion usb client validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_usb_client_validation/#usb-companion-client-compatibility","title":"USB Companion client compatibility","text":"

The RAK4631 report was reproduced on the MercerWoodMesh Pi with the published RAK_4631_companion_radio_full-v1.17.1.6-halo-keymind-cascade-dev-d4a641ff image. The fixes described here are in the source and test firmware; the existing GitHub release assets have not yet been replaced with this fix. Hardware validation was completed on 2026-09-14 with test builds labeled v1.17.1.6-usb-client-test-0e5955f8 (base commit plus these USB fixes).

"},{"location":"test-results/companion_usb_client_validation/#port-selection","title":"Port selection","text":"Hardware Companion/ASCII port USB logging RAK4631 and other nRF52 Full Companions Primary CDC, Linux *-if00 Optional second CDC, *-if02 ESP32 Full Companions Primary USB serial port Shares that port; must be off for Binary Companion

+++MESHCORE-TERM-STOP changes the primary port to Binary Companion. Its OK - Binary mode reply does not move the client to a second port. Close the terminal before opening that same port with meshcli -s PORT infos. Do not use MeshCLI's repeater-mode -r option for a binary Companion connection.

"},{"location":"test-results/companion_usb_client_validation/#two-reproduced-failures","title":"Two reproduced failures","text":"
  1. Stock meshcore-cli 1.6.3 was tested with meshcore 2.3.10, whose serial client deliberately deasserts DTR. The nRF52 primary USB gate and response route both required DTR. A raw battery query returned a valid 14-byte response with DTR high and no bytes with DTR low.
  2. The automatic ASCII banner ended in >. MeshCLI recognized that > as a binary frame marker and consumed the real response's leading > as a length byte. Even forcing DTR high in a diagnostic client did not recover the first APP_START response. Stock MeshCLI exited without node information.

The nRF52 transport now accepts real USB input as evidence of a client, tagged with the current session generation. Close, line-coding, and device-reset events invalidate that evidence; an old reader racing cleanup cannot grant access to a newer session. The write path remains bounded and does not wait for an unread host. CDC1 logging retains its own DTR requirement.

The default ASCII terminal now suppresses its banner and unsolicited terminal events until ASCII input arrives. Enter or a command reveals the terminal. A binary probe receives a clean first response. Explicit START/STOP remains available, and an incomplete binary probe still times out back to ASCII.

On a shared logging port, STOP now reports ERROR: set usb.logging off before Binary mode while logging is enabled. It no longer acknowledges Binary mode and immediately reclaims the port for ASCII logging.

"},{"location":"test-results/companion_usb_client_validation/#hardware-validation","title":"Hardware validation","text":"

The RAK4631 Full test firmware passed with an unmodified installed MeshCLI and its normal DTR-low serial library:

The Heltec V4 Full Companion also passed using its ESP32-S3 USB Serial/JTAG interface, with USB logging disabled:

The shared-port logging policy was exercised on the V4 as well. With logging on, STOP returned the explicit error, a binary probe could not claim the port, and stock MeshCLI could not connect. After set usb.logging off, STOP worked, 10 binary replies were clean, and three stock-client reconnects succeeded.

After testing, the V4's complete original flash snapshot was restored and verified by the flasher. Its original v1.17.1-soak-v4-B firmware answered again, and the previously active memory-soak collector was restarted. This test interrupted that board's uptime; the restored firmware starts a new run. Fresh successful V4 telemetry was verified at 2026-09-14T19:46:01Z. The RAK4631 remains on the patched Full Companion test build with its separate logging interface enabled. The Pi's previously active services were restored.

The nRF52 DTR stress sequence is deliberately not used on ESP32. Changing those control lines during immediate close/reopen triggered the V4's hardware bootloader and produced an ESP boot message. Preselecting RTS before opening did not prevent that transition on this host. ESP32 reconnect results above use the unmodified client's normal connection sequence instead.

The test issues local information queries only. It does not transmit test chat messages over LoRa. Firmware installation and logging configuration are separate from the reusable test runner:

python3 tools/hil/companion_usb_client.py \\\n  --port /dev/serial/by-id/PRIMARY-if00 \\\n  --logging-port /dev/serial/by-id/LOGGING-if02 \\\n  --test-dtr --cycles 10 --output usb-client-results.json\n

Replace the example paths with the device's actual stable paths. Install meshcore-cli in the Python environment used for the test. Start after a fresh reboot so the default-ASCII check has a new session. On ESP32, omit both --logging-port and --test-dtr, and disable USB logging first.

"},{"location":"test-results/companion_usb_client_validation/#automated-checks","title":"Automated checks","text":"

The USB workflow runs the new compiled regression tests on subsequent changes.

"},{"location":"test-results/contact_memory_2026-10-19/","title":"Contact memory 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/contact_memory_2026-10-19/#contact-ram-savings-and-rak4631-full-capacity-2026-10-19","title":"Contact RAM savings and RAK4631 Full capacity, 2026-10-19","text":"

Shared contact indexes save RAM across Companion profiles and terminal chat. RAK4631 Full additionally saves 2,200 static RAM bytes and 488 application bytes, preserving its existing features and SD support. Five canonical packages and a direct terminal build pass. Heltec V3 Full failed its preexisting RAM-admission limit at this revision; the subsequent V3 Full repair is measured separately. No hardware was flashed for this contact-memory campaign.

Baseline source: 31e693ec209109a930dcf8aeef89dacbe9116a63. Final source: aed4c22e51201f0ee3586e26e1007194a74b015d. The machine-readable results bind package, ELF, memory-manifest and linked-check hashes to these revisions.

"},{"location":"test-results/contact_memory_2026-10-19/#shared-contact-savings","title":"Shared contact savings","text":"

BaseChatMesh uses uint16_t sort/match indexes when the complete contact table has at most 65,535 slots; larger configurations keep int. API counts retain their existing types. All Companion profiles and simple_secure_chat share this implementation across nRF52, ESP32, RP2040 and STM32. Infrastructure client/ACL tables use different record types and are outside this contact-table change.

Existing configuration Internal RAM saved by indexes PSRAM saved by indexes Static 350-contact table 764 bytes None PSRAM 350-contact table with internal fallback 96 bytes 716 bytes Static 100-contact table 264 bytes None

These totals include eight anonymous slots and 24 match results. PSRAM retains its existing 24-slot internal fallback and allocation-failure behavior. Cached nRF52 ContactInfo records also shrink from 96 to 92 bytes by moving the runtime-only storage_slot field into an alignment gap; other record layouts retain their sizes. Field-based serialization, contact/channel capacities, features, queues and stack allocations remain unchanged.

"},{"location":"test-results/contact_memory_2026-10-19/#rak4631-full-comparison","title":"RAK4631 Full comparison","text":"Measurement Baseline Final Improvement Actual application BIN extracted from DFU ZIP 712,600 712,112 488 fewer Application-region capacity 712,704 712,704 Unchanged Actual flash margin 104 592 488 more Ordinary static RAM (.data + .bss) 156,284 154,084 2,200 fewer Linked heap capacity 79,092 81,292 2,200 more Modeled heap requirement 74,576 74,576 Unchanged Modeled admission margin 4,516 6,716 2,200 more

.data stays at 1,660 bytes; ordinary .bss falls from 154,624 to 152,424. Separately reserved linker arenas are excluded from this section sum. The measured reduction includes final object alignment. Linked capacities and admission models do not measure running free heap or physical stack high-water.

Four duplicate CRC loops share the existing implementation without changing initial values, byte ranges or stored formats. Reader packing increases offline Zopfli iterations from 15 to 1,000 and selects 16-bit block offsets only when every block start fits. General 32-bit types/fallback, 4,096-byte blocks, scratch policy and all canonical text remain unchanged.

Reader flash data Baseline Final Saving Compressed payload 38,366 38,293 73 bytes Twenty-four block indexes 288 192 96 bytes Payload plus index 38,654 38,485 169 bytes

The complete image saves 488 bytes; Reader payload plus index accounts for 169. The Full marker and thirteen feature-verification entries are unchanged. Existing SD support, its 2,048-byte SD/FAT runtime reserve and protected authentication range remain intact. No physical SD operation was tested.

"},{"location":"test-results/contact_memory_2026-10-19/#source-bound-artifacts","title":"Source-bound artifacts","text":"Artifact SHA-256 Baseline DFU application 520cf40ec5ada891d3fe8081b23546ce777c712383f0479292bb8fac0017d4d7 Final DFU application c2080afe8874c71eb9a5d1fb253fec06463cac8a774fc776ff46453abf2da420 Baseline DFU ZIP ef641d49dc91db61f0a49912f191f6d81c2887a69950fa66a612f81b31f0e142 Final DFU ZIP d99d904430de75dd5e507a530267d933b8f0bda762afe86cdb150209f4df4499 Final ELF f002455df52a9cdf4278461685eaee3082cc14752f741428f6dfe2dcdc2bb2a7

Every admitted package's memory-bound file hashes were recomputed. Nordic UF2 payloads match the DFU applications, and initialized ELF flash-load bytes agree. The JSON records the individual hashes and actual packaged selectors/profiles.

"},{"location":"test-results/contact_memory_2026-10-19/#software-verification","title":"Software verification","text":"

Contact coverage totals 26 Python unittest methods: three cache/persistence harness methods, eleven persistence-contract methods, seven stream-contract methods, two index/layout methods, one lifetime matrix, and two RP2040/STM32 uncached-storage methods. They execute 24 ASan/UBSan C++ configurations (three cache, eleven index, eight lifetime, two uncached) and separately compile eight actual cached/uncached record layouts across the four MCU families. The lifetime matrix checks 284 assertions. Index execution uses a small stand-in record; separate layout checks compile the real header. Contact code is unchanged since b0f141615; results are retained in the tool transcript, with no separate contact log claimed.

Adjacent checks pass: 16 Reader tests, 19 Beacon tests, a radio-profile matrix with 32 scenarios, seven Morse-codec tests and sixteen persistent-format tests. These focused counts do not establish a complete hosted CI run.

Final linked ARM checks all bind to the ELF above:

The partitioned campaign invokes the unchanged linked Reader checker with all-page reconstruction; partitioning is not an additional release gate. Equivalent complete reproduction against the matching ELF uses Unicorn 2.1.4:

python scripts/check_nrf52_companion_crypto.py path/to/firmware.elf\npython scripts/check_nrf52_companion_ota_workspace.py path/to/firmware.elf\npython scripts/check_nrf52_companion_reader.py path/to/firmware.elf --every-page\n
"},{"location":"test-results/contact_memory_2026-10-19/#final-source-build-matrix","title":"Final-source build matrix","text":"

Canonical packages used v1.18.0.1-halo-keymind-cascade-dev, radio preset usa-cascade-fixed and profile cascade; Full selections required OTA. PlatformIO commands ran one at a time. For example:

bash build.sh build-firmware RAK_4631_companion_radio_full --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota\n
Requested target Actual application bytes Modeled RAM margin Result RAK_4631_companion_radio_full 712,112 6,716 PASS RAK_3401_companion_radio_usb 523,864 50,940 PASS heltec_v4_2_v4_3_companion_radio_full_femon 2,558,856 34,536 PASS, Full PSRAM profile Xiao_rp2040_companion_radio_usb 472,204 144,340 PASS RAK_3x72_companion_radio_usb 229,372 7,112 PASS; four actual flash bytes free Heltec_v3_companion_radio_full No admitted package -21,024 Verified preexisting RAM-gate failure

Matched baseline V3 Full also fails: 177,144 available against 198,432 required (21,288 short), versus final 177,408 (21,024 short). This wrapper preserves 100 contacts, gaining 264 bytes. The final failed ELF was cleaned by a later build; the failure is bound to its build log; that ELF hash is not recorded.

The direct pio run -e Heltec_v3_terminal_chat default build passes with 350 contacts: 263,800 available, 77,856 required and 185,944 margin. ELF SHA-256 is 1a193ae1bee829d0bf7d140310b8487d2a4671f0c85715d16cfb764850e06cb6. This qualifies a no-PSRAM/shared-terminal compile path; it is not a Full package or a firmware-picker release-version/preset qualification.

"},{"location":"test-results/contact_memory_2026-10-19/#retained-failures-and-limits","title":"Retained failures and limits","text":"

Intermediate b0f141615 failed the STM32 size check. Shared little-endian Morse serialization and an equivalent branchless CRC loop restore the packaged fit. STM32 static RAM falls from 40,256 to 39,992 bytes; baseline actual BIN was 229,284/229,376 bytes (92 free), although PlatformIO's section summary showed 444 free. Acceptance uses actual BIN/ELF extent. The final four-byte margin and RAK4631 Full's 592-byte margin require the unchanged hard gates for subsequent versions.

The separate legacy V3 BLE target has the same five unresolved WiFi setup/seeder symbols on matched baseline and intermediate source. Initial disk-quota failures and an unsupported V4 command flag remain separate failed attempts; successful retries establish the results above. No features, capacities or budgets were reduced to obtain these passes. There is no six-canonical-pass claim.

These are software/build/linked-code results. No board was flashed. Physical contact/client operation, PSRAM pressure/fallback, storage persistence, SD/radio operation, power, timing and full-firmware ISR/SoftDevice stack high-water remain outside this qualification.

"},{"location":"test-results/double_frequency_v4_validation/","title":"Double frequency v4 validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/double_frequency_v4_validation/#double-center-frequency-write-v4-transmitter-xiao-receiver","title":"Double center-frequency write: V4 transmitter / XIAO receiver","text":"

2026-09-14. Completed bounded comparison: neither single nor duplicate frequency writes reached 400/400. The duplicate adds about 86 us per hop. No production default was changed. Both radios are idle with HIL installed; the duplicate-write toggle is off. The earlier descending-SF sweep and its follow-up remain paused; no new exhaustive settling sweep was launched.

"},{"location":"test-results/double_frequency_v4_validation/#hardware-and-control","title":"Hardware and control","text":"

The V4 FEM uses the repository's production detection and TX/RX control. HIL restores RX after success or error on each direct transmit path. The GC1109 branch (not detected here) uses bypass according to GC1109 table 4. The detected KCT8103L uses its amplified path; antenna-port TX power was not measured. This is not a power-matched Indicator/V4 comparison.

"},{"location":"test-results/double_frequency_v4_validation/#exact-experiment","title":"Exact experiment","text":"

scanfreqrepeat 1 makes the HIL ExperimentalSX1262 repeat the same SetRfFrequency (0x86) immediately after the first successful frequency call, before modulation, packet parameters and RX restart. The second call skips the image-calibration check, but keeps normal BUSY/status handling. Only an identified RX-to-RX hop may repeat; ordinary setup/TX does not. Either write's failure propagates through the existing failed profile-apply path. The toggle defaults off at reboot and cannot change during an active scan.

Both modes use the same XIAO binary. Actual SPI command counts, not only software policy labels, are checked against one or two 0x86 writes per hop. The modulation cache remains on in both modes.

"},{"location":"test-results/double_frequency_v4_validation/#rx-only-timing","title":"RX-only timing","text":"

Six-second windows in single/double/double/single order, with reboot/clean setup between windows. No reference packets transmitted during this phase.

Frequency writes Hops Mean switch Actual 0x86 Actual 0x8B Single, first 142 451.965 us 142 0 Double, first 141 538.213 us 282 0 Double, second 141 538.170 us 282 0 Single, second 142 451.930 us 142 0

Weighted means: 451.948 us single, 538.192 us double: +86.244 us / +19.1%. All 566 hops had zero retune, RX-mode and cache errors. Maximum observed switch costs were 594 us single and 674 us double; these are samples, not hard latency guarantees. Unheld full-cycle means were about 168.942 ms versus 169.285 ms.

"},{"location":"test-results/double_frequency_v4_validation/#packet-checks","title":"Packet checks","text":"

Single first, then double, each up to 100 packets/channel and stopping on its first miss; a 400/400 result would have stopped all RF testing. Neither passed.

Frequency writes Valid before first miss Failed packet RF-run mean switch Hops / 0x86 writes Single 1 2, channel 2 452.719 us 235 / 235 Double 4 5, channel 2 542.063 us 255 / 510

Both failed packets were receiver timeouts, with TX success acknowledged, zero device-error flags and RX mode still active. Both settings reported zero retune, RX-mode, cache and RX errors. Ordinary processing after received packets caused one/four modulation rewrites respectively; pure RX-only timing above is the cleaner comparison of the duplicate cost.

All five good V4 packets reported -19 dBm RSSI. SNR was +9.0 dB in the single-write sample and +8.2..+9.2 dB (mean +8.675 dB) for four double-write samples. The previous Indicator run had 547 good packets at average -40.547 dBm RSSI, +8.311 dB SNR. Thus this V4 setup was about 21.5 dB stronger as seen by the receiver. Hardware/antenna/placement/power differences remain confounded; neither the Indicator nor close-range effects are proven causes.

One versus four successes before a miss is too small and stop-conditioned to establish a reliability improvement or packet-error rate. Repeating center frequency did not remove the observed failure at this setting. No results for lower SFs or added settling delays are claimed for the V4. A weaker received signal is needed before treating a transmitter swap as a clean comparison.

"},{"location":"test-results/double_frequency_v4_validation/#artifacts-and-checks","title":"Artifacts and checks","text":"

Base checkout 0e5955f889788a4317863e731b38ffe09e3d5441, branch keymindCascade, plus local uncommitted changes. RadioLib 7.7.1 pinned at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both builds/uploads succeeded and uploader verified written data. The 59 selected host/native tests passed (39 collectors, ten profile, four SX1262, five radio and one receive-mode), including FEM restore on error and frequency-repeat scope/error propagation. Temporary build overlay removed.

Actually loaded firmware SHA-256:

Raw evidence and byte-preserving SHA-256:

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/","title":"Esp32 ble radio2 sleep 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#esp32-bluetooth-shutdown-and-radio2-cpu-idle-2026-10-21","title":"ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21","text":"

The ESP32 changes cover two gaps identified while qualifying nRF52 CPU sleep: reversibly stopping the Bluetooth host/controller, and allowing bounded MCU light-sleep during a settled secondary-radio receive visit. This report separates host/build coverage, SDK wait measurements and physical packet tests.

Bluetooth transitions and quiet-window SDK wait checks pass. Three earlier strict RF trials with power saving on and matched preambles received five of six packets, including the trial after the first IRQ fix. They remain failures; radio2 is not fully qualified.

The first V3 CPU-clock sweep stopped during setup, before any of its 20-packet-per-frequency cases transmitted. Switching Bluetooth off exposed a NimBLE-Arduino 2.5.1 shutdown crash: PC zero in the host task, with the event address resolving to ble_hs_timer + 8. This matches upstream issue #1184 and its merged fix #1185. The backport removes premature timer deinitialization while leaving final cleanup intact. The earlier V4 Bluedroid checks do not qualify V3 NimBLE. The regression reproduces the original null callback, then passes 1,000 shutdown/cleanup cycles under ASan/UBSan for each timer backend. Library version, source hash and build-local middleware checks also pass.

Full Companion source 6236eec3 includes that repair and a preferences-reader fix for a truncated timezone record colliding with the new CPU footer length. Both timezone and CPU suites pass; damaged CPU overrides still fall back to Auto. V3 and V4.3 OLED FEM-off builds pass all 24 capability markers and their memory gates, at 2,118,680 and 2,556,504 application bytes respectively.

The V3 received one application-only update after its old application hash was verified. Complete new-image readback passed, with its partition table and OTA metadata unchanged. Starting with Bluetooth saved off, it restored Bluetooth on and completed three off/on cycles without a reboot or core error. Uptime advanced from 32 to 58 seconds across 20 health samples. Every Binary handoff revalidated board identity and source version. The final checked configuration and credential hashes matched the baseline, and the active PIN stayed unchanged. This physical check covers Bluetooth power transitions, USB control and checked preference preservation.

The recovery also exposed an already-ROM ESP32-S3 UART finish gap. The flasher now explicitly releases the boot strap before its final reset; its regression passes with both esptool command spellings. That separate fix is published in meshfirmware f9d03d1.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#behavior-and-safeguards","title":"Behavior and safeguards","text":"

Bluetooth off disconnects clients and completes host/controller teardown in the main loop. Bluetooth on rebuilds the service without releasing controller memory permanently or clearing successful bonds. Address, name, PIN and saved stealth policy remain intact. Pending or failed teardown prevents CPU sleep. The Bluedroid characteristics have deterministic TX/RX registration order; heap fragmentation must not change a paired client's cached handles. On SDKs using automatic Service Changed handling, a small asynchronous service change requests client rediscovery after authentication. A local SDK completion is not treated as proof that the client refreshed its cache. Only actual use of the current notification and receive handles permits skipping this request on a later connection from the same peer within that host session.

Companion, Repeater, Room Server and Sensor can idle during an eligible radio2 visit. The decision requires a settled, validated receive profile, working radio GPIO wake, no pending reception and physically stopped wireless drivers. An absolute scanner deadline is checked again at sleep entry. The timer budget is at most 10 ms, reserves a 4 ms early-wake scheduling guard and requires at least 2 ms remaining. Calibration, repair, ownership, queue, network, USB and role deadlines retain their existing guards. The receiver stays on. This does not enable sleep through an active Bluetooth/WiFi link.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#host-tests-and-builds","title":"Host tests and builds","text":"

The scheduling, board wake, dispatcher and real role integration suites pass. Coverage includes expired and wrapping deadlines, stale profile proofs, pending interrupts, failed wake setup and SDK errors. The BLE adapter tests exercise actual production code with ASan/UBSan, ownership differences, partial initialization/teardown failures and repeated off/on cycles. Existing nRF52 event/profile tests also pass; nRF52 hardware was not touched.

All 22 scripts in the affected Sensor/tracker/sleep CI step and all 57 in the Companion contact/channel step pass locally. The latter initially encountered a missing Python platformio module in one read-only metadata case; that script's six cases passed with the existing PlatformIO Python environment. No firmware build or hardware test was repeated for this environment correction. The diagnostic fixtures exercise the current production sleep branches rather than relying on obsolete source anchors.

The initial sleep/teardown implementation, before the cache fix, has Full V4.3 FEM-off Companion, V3 Companion, V4 Repeater, Room and Sensor builds that pass their capability and memory gates. ESP32-C6 BLE (Arduino 3.1.3), the NimBLE-Arduino V4 trial and RC32 Full (Arduino 3.3.11) also compile. RC32 exposed SDK service ownership and array-deletion defects; a version/hash-bound build-local repair fixes those sources without editing a shared SDK install.

The cache fix is published in 1087643ae. Its V4.3 FEM-off Full and RC32 Full builds pass all 24 capability markers and their application size gates: 2,551,592 and 2,152,840 bytes respectively, each within a 6,553,600-byte application slot. The other earlier builds do not constitute a full build matrix of this later source revision.

d34e65175 removes unnecessary delays between successful cache-recovery phases while retaining bounded failure backoff. Its V4 Full build passes, at 2,551,608 bytes. The same-size V4 build of f8b5b8301 adds only the channel-response padding fix described below. Bluetooth, radio, sleep-policy and role code are unchanged between those last two revisions.

At this checkpoint, Classic Heltec V2 BLE was below its required 8,192-byte static DRAM reserve. Historical checks left 2,552 bytes free on the baseline and 1,224 bytes on the initial sleep candidate. Removing an unnecessary WiFi-driver reference recovered 1,208 bytes, raising the later candidate to 2,432: 120 below baseline and 5,760 short of the reserve. Final cache/padding revisions were not remeasured on V2. The gate was retained. The later 22 October V2 cache repair closes the RAM build failure with 12,440 static bytes free and unchanged capacities; physical V2 operation was not measured by that repair.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#physical-v4-observations","title":"Physical V4 observations","text":"

The test board is a Heltec V4.3 OLED using its matching FEM-off Full Companion image. The first candidate reports source suffix 278c9ffb; its published equivalent after history cleanup is 61eecbd97. An application-only write and complete readback preserved the partition table, OTA metadata, identity, settings and credential digests. No whole-flash backup, bootloader write or other-device update was performed.

With Bluetooth/WiFi off and power saving on, only this V4's USB data was suspended. After the host-loss grace period, a 90-second diagnostic window recorded 4,635 SDK light-sleep calls and returns, zero SDK errors, zero short waits, zero vetoes and zero pending-wake returns. Cumulative call time was 35.523299 seconds; the longest call was 9,869 microseconds. The mixed profiles were 910.525 MHz / BW62.5 / SF7 / CR5 and 916 MHz / BW125 / SF9 / CR5. USB authorization and runtime policy were restored. Core errors remained zero. These are SDK wait measurements, not electrical current or residency readings.

Two initial saved-bond Bluetooth off/on sessions passed. Further testing exposed both a host-side notification-acquisition problem and a real firmware GATT-handle ordering defect. The host driver now uses ordinary StartNotify. The original failures are retained; clearing a client cache is not accepted as proof that firmware handles remain stable or an existing cache migrates.

On d34e65175, both authenticated NUS connections passed with the old opposite handle cache retained. The test independently checked board, exact version, role, public identity, node name, address and advertising policy. Switching off while connected disconnected the client; the test required completed off before re-enabling and reconnecting. No cache was cleared and no new pairing was made. The cache changed automatically to TX value 0x002A, RX value 0x002D; saved pairing-key digests were unchanged. The image also started successfully with Bluetooth saved off. No electrical power measurement was made.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#two-frequency-packet-controls","title":"Two-frequency packet controls","text":"

The peer is an independently identified ESP32 XIAO S3 WIO. Every transmitted packet uses a new encrypted nonce, full sender/destination identities and a zero-hop direct route. The receiver's USB data is absent during the sleeping RF window; queued packets are checked only after USB resumes. WiFi and Bluetooth are stopped, crossover is off, and physical RX counters must advance on both frequencies. No failed packet is resent automatically.

Earlier runs used a transmitter preamble of 128 symbols while the receiver reported 32. An awake two-packet control received zero packets with zero SDK sleep calls. Disabling radio2 then received the one primary test packet. Source inspection found that the receiver's 32-symbol preamble guards expire after about 99/198 ms, while a 128-symbol transmitted preamble alone lasts 262/524 ms on these profiles. The guard can clear preamble detection and permit a scan hop before the header arrives. These mismatched trials remain recorded; they do not establish a CPU-sleep regression.

With both sides at the verified 32-symbol preamble and peer TX at 2 dBm:

Control Unique packets received SDK waits Result USB active, CPU awake 2/2; one on each profile 0 Pass, including exact cleanup USB suspended after 121-second host-loss grace, power saving on 5/6; all three primary and two secondary 777 calls/returns; zero SDK errors, short waits, vetoes or pending returns Strict RF check failed: final secondary nonce missing

The sleeping trial's measured profile RX deltas were 100/2, including ambient traffic. Both frequencies carried verified private packets while the MCU idle path ran, but the missing sixth packet prevents an all-pass result. These finite samples do not establish a loss rate or attribute that loss to CPU sleep.

A predeclared paired comparison then used the same d34e65175 image, settings, 32-symbol preambles, TX power and suspended USB in both modes. Each interval sent six new nonces once. A first preparation attempt was refused before any RF transmission; its clean restoration and zero-transmission receipt remain preserved. Bounded admission of the power setting was moved before USB suspension; no packet retry was introduced. Power saving also caps the CPU at 80 MHz; switching it off restores the nominal 160 MHz clock on this V4 Full build. This comparison measures the whole power-saving policy, so it does not isolate light sleep from foreground CPU speed.

Same-image control Unique packets received SDK calls/returns Result Power saving off 6/6; three on each frequency 0/0; all diagnostic error/veto counters zero Pass, cleanup and PS-on restoration verified Power saving on 5/6; primary sequence 2 missing, all three secondary packets received 820/820; zero errors, short waits or pending returns; five vetoes Strict RF check failed; cleanup and PS-on restoration verified

The second sleeping trial had profile RX deltas 103/3, including ambient traffic, and seven receive errors. The different missing packet rules out a deterministic final message-slot loss. This small comparison warrants a wake-path investigation; it does not isolate interference, scanner timing or an individual GPIO event as the cause. No unchanged RF trial was repeated after it.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#gpio-wake-and-packet-ownership","title":"GPIO wake and packet ownership","text":"

ESP-IDF 4.4.7 documents that GPIO interrupts are not generated during light sleep. GPIO wake is a separate mechanism. The board temporarily uses a level wake source and then restores a rising-edge interrupt; the SDK's GPIO driver and S3 wake implementation do not replay a software callback when that interrupt type is restored.

The wrapper previously checked only its software event. If a completed packet left DIO high without a callback, the loop could fail to claim its FIFO before a later rearm or retune. This is a concrete ownership gap, but the aggregate packet results do not prove which missing nonce encountered it.

1ba4e2830 adds an ESP32-only physical IRQ fallback to both pending and claim checks. It uses that registered radio module's own pin and HAL, validates ownership, null pointers, NC, pin range and the SoC validity mask, and adds no RAM fields. Claiming consumes any software event too; the unchanged hardware RX_DONE check still gates FIFO reads. It changes no frequency, preamble, wake mask or sleep budget. Other platforms retain their software-event behavior.

All 12 interrupt-recovery host tests pass with ASan/UBSan. They execute the production receive and reconfiguration paths with an asserted DIO and no ISR, combined and delayed callbacks, independent radio pins, invalid pins and non-ESP32 variants. Removing either fallback fails an executed regression assertion. The V4 Full build passes all 24 capability markers and its memory/size gates, at 2,551,736 bytes within the same 6,553,600-byte application slot. The RC32 Full build on Arduino 3.3.11 also passes all 24 markers and its memory/size gates at 2,152,968 bytes. These are build checks, not physical RC32 qualification.

The application-only installation and full readback of this image passed, including unchanged partition table, OTA metadata, identity, saved settings and credential digests. One new six-packet qualification trial then exercised the changed IRQ code with the same radio settings, USB suspension and power-saving policy. It received 5/6, missing secondary sequence 6: another strict RF failure. All six sends completed once; none was resent. Profile RX deltas were 79/2, including ambient traffic, with five direct packets and two receive errors. The interval recorded 741 SDK calls/returns, zero SDK errors, short waits or pending returns, and two safe vetoes. Cumulative call time was 5.710857 seconds; the longest was 9,900 microseconds. Cleanup and PS-on restoration passed.

Independent source audits found no additional concrete clock/deadline defect: ESP32 micros() and the scanner use the ESP Timer domain; the deadline is anchored before the late query and checked again after wake setup. Pending packets and latched preamble/header activity guard the next retune. The current 4.6-symbol scanning model is not a zero-loss guarantee. The observed losses remain unresolved; neither interference nor the IRQ gap is established as their cause. No further unchanged RF trial or arbitrary timing/preamble change was used to obtain a pass. Further qualification needs a control that holds CPU speed fixed and correlates scan/preamble/IRQ timing with individual packets.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#channel-cleanup-and-response-padding","title":"Channel cleanup and response padding","text":"

The single-profile and sleeping reports also recorded cleanup failures. The single-profile power restore was refused while the radio was busy; a separate bounded cleanup restored 22 dBm and removed only the hash-matching test channel. The later sleeping run restored power and cleared its channel but failed a raw channel-response digest comparison.

A separate read-only audit compared every meaningful channel name and every key byte twice on both devices. All matched, and both temporary slots were empty with zero keys. Raw differences occurred only after the terminating NUL in the fixed-width channel-name field: one channel differed between repeated reads on each device and all 40 differed between devices. The getter used strcpy without clearing the unused name bytes, exposing stale reply-buffer padding. The original failed receipts were retained; no channel data was rewritten to force a digest match.

The shared Companion fix zero-pads and bounds that field while retaining key offset 34 and the existing 50/66-byte frame layouts. All nine affected host tests pass. The actual-handler regression covers empty, short, maximum and unterminated names, dirty output buffers, Meshtext on/off and 16/32-byte keys; the original implementation fails its negative control.

A read-only hardware audit on 1ba4e2830 also passed: both devices' meaningful channel names and every key byte matched across two reads, both temporary slots were empty with zero keys, and every V4 name field had zero padding. The peer's firmware was not changed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#v3-fixed-clock-comparison-with-20-packets-per-frequency","title":"V3 fixed-clock comparison with 20 packets per frequency","text":"

After the NimBLE repair, the V3 completed all four predeclared cases on 6236eec3. Every case sent 20 unique packets per frequency once, with the same 32-symbol preambles, 2 dBm peer TX power and default 10/4/2 ms sleep dials. Power saving stayed on; CPU speed and the temporary manual sleep switch were controlled independently. RX power saving was saved off for these comparisons; the profile scanner itself uses continuous RX during each visit.

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns over the whole diagnostic window 80 MHz Off 18/20 17/20 0/0 80 MHz On 20/20 19/20 270/270 160 MHz On 18/20 14/20 18/18 160 MHz Off 19/20 16/20 0/0

All 160 transmissions reached TX_DONE, with no packet retries or duplicates. All four strict delivery checks failed. Settings, channels and credential hashes were restored; both radios finished with zero core error flags. Losses with manual sleep off show that sleep is not required for the observed failure. Ambient traffic differed between cases, and these samples do not establish a CPU-speed relationship or a packet-loss cause.

The On rows do not yet prove sleep during packet traffic. The UART quiet control stopped the driver but left its Companion session logically connected. Incoming messages then queued push notifications to that stopped transport; the existing pending-output guard correctly prevented sleep. Whole-window SDK calls could have occurred before traffic began. The delivery counts remain valid, but sleep comparison qualification requires retiring that UART session and timing evidence inside the RF sequence. No pending-work guard is bypassed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#uart-session-repair-and-timed-v3-comparison","title":"UART session repair and timed V3 comparison","text":"

The campaign continued on 2026-10-22 with 7d0709bf0. UART quiet now retires only that USB protocol session after draining the ACK, including its pending reply owners and USB Reader output. The offline inbox and other transports remain active. A successful UART restart restores a fresh silent session; failed restarts keep the interface parked and retry. The integration regression reproduces the original stuck notification, then retains and downloads 40 actual receive frames without weakening sleep guards.

ESP32 diagnostics now retain sixteen 16-second SDK-entry buckets, with explicit outside-window, saturation and missing-clock indications. The collector stays within its 256-byte budget; nRF52 layout and behavior are unchanged. The physical comparison requires a positive bucket wholly after the first confirmed TX_DONE and before the last TX request, including host/device clock uncertainty. This establishes SDK entries during the packet sequence, not sleep through a particular packet or electrical residency.

Both Full Companion builds passed 24 capability markers and their memory gates. V3 and V4 application sizes are 2,120,088 and 2,557,400 bytes. Application-only updates passed full readback, unchanged partition/OTA metadata and checked configuration/credential preservation on both boards. No Bluetooth lifecycle or earlier quiet-only test was repeated solely for documentation changes.

The new V3 comparison again sent 20 unique packets per frequency per case, with no RF retries, the same 32-symbol preambles and default sleep dials:

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns SDK calls in wholly contained RF-time buckets 80 MHz Off 17/20 15/20 0/0 0 80 MHz On 18/20 19/20 10,925/10,925 4,625 160 MHz On 18/20 16/20 10,164/10,164 4,585 160 MHz Off 20/20 16/20 0/0 0

All 160 sends reached TX_DONE once; there were no duplicate deliveries. All four strict delivery checks failed. On cases have positive SDK activity during the RF sequence, with zero SDK errors, short returns or pending waits. In the 80 MHz On comparison, serial blocking fell from 109,265 ms on the old image to 1,368 ms after session parking. This supports the console-fix mechanism; varied ambient traffic still prevents a causal delivery comparison. Neither CPU speed nor manual sleep consistently removed packet loss. Original settings, channels, identity and checked credential hashes were restored, with clean core flags.

V4's first new comparison stopped during setup, before Bluetooth-off was sent and before any case or RF transmission. Its strict stable-Binary check rejected one unexpected prefix byte after a valid health response. Seven valid push frames were accepted; the rejected byte was not retained, so its origin was unknown at the time. That original failure remains recorded.

Two diagnostics-only attempts on unchanged 7d0709bf0 also stopped before Bluetooth-off was sent, with zero RF transmissions. Bounded raw capture retained the late SDK line E (2905571) wifi:timeout when WiFi un-init, type=4. Source inspection identified an SDK timer firing after shutdown; this message does not establish that WiFi deinitialization timed out. The raw SDK log bypassed disabled USB logging and broke strict Binary Companion framing. All three attempts restored checked settings/channels and verified a fresh MQTT PUBACK increase. The latest immutable diagnostic receipt is v4-7d0709bf-suffix-completed.private.json. These setup failures supply no new V4 packet-delivery evidence.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#sdk-logging-repair-and-v4-scanning-controls","title":"SDK logging repair and V4 scanning controls","text":"

The SDK logging fix in d73599bc6 shipped in 3fd9b392. It prevents raw SDK logs from bypassing the disabled USB-logging policy and corrupting the shared Binary Companion stream. All 101 USB host tests pass. The V4 application is 2,558,856 bytes, with SHA-256 ddfd4f40a97da40bdeda2c00fb18ebc2d4d365590d4bc63496a425d16975906d. Its installation passed full readback and checked configuration preservation.

Two new scanning cases completed on this V4 image, with 20 new nonces per profile, matched 32-symbol preambles and 2 dBm peer TX:

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns SDK calls in wholly contained RF-time buckets 80 MHz Off 19/20 18/20 0/0 0 80 MHz On 19/20 15/20 4,121/4,121 3,511

Both strict delivery checks failed. All 80 sends reached TX_DONE once, with zero retries or duplicate deliveries. SDK errors, short returns and pending waits were zero. The awake losses establish that CPU sleep is not required for this symptom; these samples do not identify the cause of individual misses.

The user redirected testing toward the scanner. The 160 MHz On case was intentionally stopped during its USB host-loss grace period, before any RF transmission; 160 MHz Off was not entered. This is not a completed four-case comparison. The interrupt receipt remains recorded in v4-3fd9b392-user-steered-completed.private.json (SHA-256 e102200af7e2503bb0362e65afad0d947d5c3aeb220425a589d0588032530c52). Full restoration and a fresh MQTT PUBACK increase passed after that stop.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#fixed-tuple-controls-and-receive-path-defect","title":"Fixed-tuple controls and receive-path defect","text":"

Two fixed-tuple controls on the same 3fd9b392 image each received 20/20 unique packets with radio2 explicitly off. The primary physical radio was temporarily tuned first to 910.525 MHz / BW62.5 / SF7 / CR5, then separately to 916 MHz / BW125 / SF9 / CR5. Both runs used 80 MHz, manual CPU sleep off, saved power-saving intent on, RX power saving off, 32-symbol preambles and 2 dBm peer TX. The target's USB data was absent after the same 121-second host-loss grace. Each run retained the original seeded 40-slot cadence, leaving the opposite profile's 20 slots silent. SDK calls, duplicate deliveries and RF retries were zero. The immutable control report remains failed because the peer's original 22 dBm setting was not restored within three bounded busy-response attempts; MQTT recovery passed. A separate recovery then restored 22 dBm and verified full peer settings, credential hashes and channel semantics without another RF transmission. The original failure is retained in fixed-tuples-v4-3fd9b392-completed.private.json; successful recovery is in peer-tx22-restoration-verified.private.json.

These finite controls narrow the investigation toward scanning or overlapping traffic. Radio2 time-shares one transceiver: a valid primary reception can occupy it throughout a secondary packet's preamble. The idle-scan timing model does not guarantee zero loss under simultaneous traffic. The controls do not distinguish that limitation from a receive-path defect for a particular nonce.

A separate host regression executing the real receive functions with ASan/UBSan proved a stale-IRQ defect. Servicing a packet through its asserted physical DIO could leave a late software event. Subsequent empty RX_DONE probes could then restart continuous RX over a newer packet's preamble or valid header. The narrow ESP32 SX126x continuous-RX guard in a5293fa50 preserves that new reception while retaining normal activity timeouts. All 15 interrupt-recovery, five receive-contract and six RX-retry tests pass. The V4 build passes all 24 capability markers and its memory gate at 2,558,936 application bytes. Its application SHA-256 is cc16e44ccd24e64724a2bd17f59f684117e411077e4c0a7d04611be328b7ccfc. The application-only installation passed complete readback, with the partition table, OTA metadata and checked configuration preserved. The immutable receipt is flash-v4-a5293fa5-completed.private.json (SHA-256 ef61cf8dfc77f8907d621f7f0493949ff6888b017c277eefb309247ccf88b278).

One new awake 80 MHz scanning run then sent 20 unique packets per profile on this changed image, with manual CPU sleep off and the same matched preambles, TX power and USB host-loss grace. The V4 received 19/20 primary and 20/20 secondary packets. All 40 sends reached TX_DONE once, with zero retries or duplicate deliveries. SDK calls, returns and all time buckets were zero; core error flags remained clear. The missing primary nonce keeps the strict RF result failed. Full restoration passed, including a fresh MQTT PUBACK increase. The immutable receipt is v4-a5293fa5-awake-completed.private.json (SHA-256 466bdf0bb08f10665007d156e1ec37c074e1c576e6ac5fc401bcd449179ee774).

The stale-IRQ regression proves a receive-path bug and the new guard repairs that case. This physical sample does not establish which historical misses encountered it or show that all scanning losses are fixed. A controlled two-transmitter overlap diagnostic is recorded below; it does not establish a cause for these historical misses.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#receive-activity-across-rx-restarts","title":"Receive activity across RX restarts","text":"

A further host regression executing the production receive methods exposed a separate scan hazard. Reading a completed packet and successfully rearming RX cleared the hardware IRQs but retained the previous packet's _activityAt and _headerSeen latches. At the next due scan, the detector could mistake the new packet's preamble without a header for the old packet's consumed header. It reported idle before processing that preamble, permitting a retune over the new packet. The regression reproduces that sequence and shows that resetting the latches on successful RX entry protects the next preamble.

The change resets those existing fields only after successful receive starts in CustomSX1262, CustomSX1268, CustomLLCC68, CustomSTM32WLx, CustomLR1110 and CustomLR1121. It covers normal starts and, where supported, fast resumes and duty-cycle entry. Failed starts retain their prior state; the change adds no fields and leaves existing activity timeouts and receive/retune guards unchanged. Independent source review passed. Five new regression methods cover 24 ASan/UBSan variants, including reset-removal controls that reproduce the retune over a new packet for all six handlers. All 19 affected host-fixture tests and 26 interrupt-recovery, receive-contract and receive-retry checks pass. The new suite runs in the required unit-test workflow. Physical RF results on the new image are recorded below. The host proof does not attribute historical missing nonces or establish a complete RF pass.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#rx-restart-installation-and-overlap-diagnostic","title":"RX-restart installation and overlap diagnostic","text":"

The V4 image built from pushed source e4119a120 passes its capability and memory gates at 2,558,984 application bytes. Its application SHA-256 is fc1a835c45ebc5e7a9fec95c757d8606e145bbc92fa8d365ba62d96a7930286d. The application-only installation passed full readback and checked configuration preservation. The receipt is flash-v4-e4119a12-completed.private.json (SHA-256 b06d8b6ef8d9df9aa881d6d1ca2f6096d19c8bcc6d4cbb2873fb20d47a9f85c5).

Two overlap conditions completed on this image at 80 MHz with manual CPU sleep off. Each condition sent 20 unique primary/secondary pairs; all 40 sends reached TX_DONE once, with zero retries or duplicate deliveries and zero SDK counters in both conditions.

Condition Primary received Secondary received Radio2 scanning 16/20 2/20 Fixed 916 MHz, radio2 off 0/20 (outside receive frequency) 20/20

All 20 scanner timing bounds were inconclusive. The nominal 140 ms secondary write occurred 227.239-263.257 ms after the primary write (median 244.363 ms), while the first counter reply confirming primary TX_DONE arrived 523.533-903.982 ms after that write. That reply bounds completion; it does not timestamp the RF event. The controller synchronously saved the report before the secondary write and after each counter reply, widening these bounds. The analysis uses 101-byte wire airtimes of 397.824/652.288 ms, a complete 148.480 ms secondary preamble and a 2 ms margin. No pair proves that the whole secondary preamble fell inside the primary payload reception. The timings do not exclude desense or attribute historical misses. The ordinary alternating-profile check is recorded below.

Three earlier setup attempts sent no RF: the first two stopped before mutation; the third encountered a pending lease, with separate V3 automatic cleanup recovery passing. The overlap experiment completed with an inconclusive status: neither condition has a qualified timing pair, so masking was not demonstrated. Full restoration of all three boards passed without errors, and a fresh MQTT PUBACK verified recovery. The final host receipt is /home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/overlap-v4-e4119a12-completed.private.json (SHA-256 c1aee5132b447734df3fe9c663f4f3ae7a8b980f3165de43f96c2653d05200e1). No firmware qualification pass is claimed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#ordinary-awake-check-after-both-receive-fixes","title":"Ordinary awake check after both receive fixes","text":"

The predeclared e4119a120 check sent 20 new packets per profile, once each, with the ordinary alternating schedule and no deliberate overlapping sender. It received 17/20 primary and 18/20 secondary packets. All 40 sends reached TX_DONE once; duplicates, RF retries and SDK sleep calls were zero. The strict RF result remains failed. The physical counters recorded 120 successful raw receives: 35 owned direct packets and 85 ambient floods, with seven separate receive errors. These totals do not identify the cause of each missing nonce. The immutable receipt is v4-e4119a12-awake-completed.private.json (SHA-256 9f2f8acecc634ebaf7b9e9aa21e51079b9872316e571a886d3866bfedabab558).

That receipt also retains a cleanup failure: restoring automatic radio2 permission was rejected while the temporary primary restore was still pending. Fresh MQTT recovery passed. A separate guarded operation subsequently restored automatic permission with one setter and no RF transmissions, then verified the checked settings, credentials and channel semantics against the baseline. Its receipt is v4-auto-restoration-after-e411-awake.private.json (SHA-256 4dbf10eeac81e8897c8a4f9fa4d7b62ac20e74c91b467a501f31fa3a1d8bd0ec).

Both receive defects remain proven by executed host regressions, but the physical results show that additional losses remain. The following diagnostic records bounded activity and transmission times on the radios themselves. It distinguishes other-profile ownership, unresolved preamble activity, read/rearm gaps and delays between scanner calls without treating host counter replies as RF timestamps. No arbitrary preamble or CPU-sleep setting change or replacement packet was used to turn a failed result into a pass.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#on-radio-capture-after-the-receive-fixes","title":"On-radio capture after the receive fixes","text":"

Source 9b70827d0 adds an opt-in, bounded RAM trace for this diagnosis. It is compiled out of normal firmware. The private V3 sender and V4 receiver images record physical interrupt bounds and receive activity without additional SPI reads or asynchronous USB output. Trace capture does not qualify a release image by itself.

Two controller failures are retained separately. The first rejected a valid pending-frequency response before sending any RF. After correcting that parser, the absolute-slot attempt sent and received one primary packet, then stopped with 39 packets unsent. Its next frequency change took 2.847 seconds, exceeding the 2.553 seconds available after transmit confirmation. Both attempts restored the original checked settings and verified a fresh MQTT PUBACK. Neither is an RF qualification pass, and no original packet was resent.

A separate, predeclared minimum-gap-v1 measurement keeps the seeded packet order and minimum gaps, allowing slow frequency changes to stretch the cadence. It uses 40 fresh unique packets, 20 per profile, each sent once. The entire transmission phase completed in 114.162 seconds with zero retries or duplicate deliveries. Its changed cadence must not be treated as a passing replacement for the failed absolute-slot attempt or directly compared as the same loss-rate experiment.

V4 awake, radio2 scanning Sent once Received Primary 20 20 Secondary 20 12

Both radios recorded zero SDK sleep calls, returns and errors. Both captures had zero overflow, SPI errors and context errors. All 32 shared packet-end checks agreed with the clock model. The timing model uses causal clock-query brackets, a conditional awake ESP32-S3 rate bound of 10 ppm per radio, and an explicit conditional 2 ms PHY allowance. Its full uncertainty envelope is retained; the predeclared 50 ms quality limit never narrows that envelope.

Three of the eight secondary losses have a positive ownership classification: the entire possible preamble interval fell inside a confirmed valid primary packet reception from ambient traffic, rather than one of the owned test packets. The single transceiver could not listen on the secondary frequency during that reception. Five losses remain unresolved. Three overlap a read/rearm window, which is only a timing association; the other two have no qualifying cause in this capture. These results do not establish desense, noise, a new scanner defect, or the cause of historical missing packets.

The strict RF result remains failed. The successful fixed-frequency controls and the new timing evidence establish frequency sharing as a cause of some losses while CPU sleep is disabled. Five losses still lack a qualifying cause. A longer preamble, different scan period, or dropping a valid primary reception is not silently applied as a general fix.

The completed capture receipt is radio-trace-minimum-gap-v1-completed.private.json (SHA-256 a2f34dc973692c9567ac7854db59446c590ab1c176375b06076fe6944ea7440a). It retains a cleanup failure: restoring the V4's automatic radio2 permission exceeded the controller's combined 15-second budget after a busy rejection. The primary temporary lease had already become inactive; automatic permission remained unrestored. Fresh MQTT PUBACK recovery passed. A separate guarded operation restored that permission with one setter and no RF, checked channel semantics and the PIN through a fresh handshake, and preserved the original failed receipt. Its receipt SHA-256 is 432b1104156e2b6c4802efa5af6089d1b2ccc6bcfe70ea7acb1596fcc3cfa89f. A subsequent read-only baseline verified both radios against all checked original settings and credential digests, with no transmissions or preference writes (SHA-256 f32cb54600142b2011fd9bcf51a186e95f41b70e590fabbf5e06516bb6506dc7).

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#final-handback-and-limits","title":"Final handback and limits","text":"

V3 and V4 now run normal Full Companion 9b70827d0, with the diagnostic trace compiled out. The peer remains on ebb59d48. Both application-only returns passed complete readback, partition-table and OTA-metadata preservation, and checked configuration/credential preservation. The normal application SHA-256 values are 39e15c4df674993ef1a3a809867a381a01b182af3e920ff208f28bef16178619 (V3) and dd714ad4e9f57f811647af889c8eab7e99a434f81da9541a4398624049b6361f (V4). Return receipts have SHA-256 28a5e075ee9154c2e67b1d4bc1fbec375d45abc1a41f40ac4797dc4a23bd9528 (V3) and 5894b59b1d3344cf88d3804a05022271c86f7b4aad29e5307be0f1af475237b9 (V4). A read-only post-boot observation confirms V4 MQTT running with its saved broker connected and zero core error flags.

The first V3 return invocation stopped before any ROM operation or firmware write because the existing diagnostic install used the same source-commit attempt marker. That failed receipt is retained (SHA-256 972e888b0d5c425676b2411ae6f7ca04cbd1b5e126fd1557667c474d0617c741). The normal return uses a separate node/source/application-hash marker while retaining exclusive creation and all original update checks; the diagnostic marker is unchanged. Later source commits only repair a host fixture and update this report. Unchanged RF tests were not repeated for the normal return.

The completed earlier V3 campaign and V4 scanning controls, including the a5293fa50 awake run, restored their checked radio, RX power-saving, Bluetooth, WiFi, MQTT, logging, diagnostics and USB-watchdog settings, including credential digests, original radio2 automatic permission and peer power/channel state. Fresh MQTT PUBACK increases verified recovery. The fixed-tuple cleanup power failure is retained, with the later peer recovery verified separately. Full overlap restoration is verified. The ordinary run's original cleanup failure is retained, with its separate automatic-permission recovery verified. No nRF52 board, hub power or other firmware was changed in this task.

The Bluetooth hardware checks pass. SDK light-sleep calls are observed without SDK errors. Strict sleeping two-frequency packet qualification remains open. The later V2 cache repair passes its existing RAM reserve gate. These results do not qualify every ESP32 board or the full thirteen-radio fleet, and do not measure electrical power savings. Unchanged Bluetooth and quiet-wait checks were not repeated for padding, version, documentation or fixture-only changes.

Private artifacts, receipts and traces are retained under /home/mesh/hwtest/esp32-ble-radio2-sleep-cd4ea053-20261021/ and /home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/. See power-saving diagnostics for the controls and the limits of interpreting wait counters as energy savings.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/","title":"Esp32 full power ap 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#esp32-full-idle-power-and-compact-ap-validation-2026-10-06","title":"ESP32 Full idle power and compact AP validation \u2014 2026-10-06","text":"

Tested firmware source: 219e936ef82d16539111f03bc7982dd5654412df, after pulling upstream through 5a7c07a6c. Version: v1.17.1.9-halo-keymind-cascade-dev-powerap3-219e936e. PlatformIO commands ran one at a time in a native WSL checkout. A later upstream test-only commit f8e04f090 was merged without changing the tested firmware sources; its 14 USB serial hygiene tests also passed.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#corrections","title":"Corrections","text":"

Fresh expanded ESP32 Full profiles default USB packet logging off. Existing saved logging and power-saving preferences remain authoritative. Repeater and Room Server automatic setup with no saved SSID suspends unused network bridges and their automatic retries. When the setup window ends, idle cleanup releases the actual WiFi driver before sleep becomes eligible. Failed shutdown remains a sleep blocker, and active/stopping WebConfig, MQTT, ESP-NOW, and OTA retain their ownership. OTA resumes only bridges that were actually running beforehand.

Native USB Companion idle handling now checks the actual WiFi and Bluetooth drivers, GPS UART, pending work, logging, host presence, and host-loss grace. An enabled Bluetooth controller still blocks manual light sleep. This does not change the deliberate raw-UART console or primary ESP-NOW sleep constraints.

Compact browser OTA and Companion setup use the shared scan-before-AP startup. They require an AP-start event and correct live SDK configuration, protocol, channel, and IP before reporting success. Scanner cancellation and stale Arduino versus SDK state have explicit regressions. Browser OTA preserves inherited LAN/ESP-NOW ownership, and repeated OTA start preserves joined clients on a healthy AP. WebConfig setup disconnects STA to isolate its open setup API while preserving an explicitly running ESP-NOW owner.

Physical testing caught an additional regression in the first candidate: manual WiFi setup suspended explicitly enabled ESP-NOW. The final implementation limits that suspension to automatic boot setup. Manual starts and master radio off/on now preserve the requested WiFi/ESP-NOW subset. The regression test executes the real infrastructure backend and wireless controller for both roles; reverting the manual wrapper to automatic suspension makes it fail.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#software-checks","title":"Software checks","text":"

All 98 focused host tests in 20 suites passed. They include extracted production startup, idle cleanup, sleep guards, USB host grace, saved preferences, OTA bridge restoration, AP fault injection, and real wireless controller subset restoration. The new suites are included in the unit-test workflow. A separate shared AP fixture compiles Arduino 2/3 and open/password setup variants. Negative controls demonstrate rejection of stale live SDK AP state and revived idle bridges.

All eight release builds passed linked capability, internal RAM, and app-slot checks:

Target/profile MCU/core App bytes App slot bytes Heltec V4 Repeater Full ESP32-S3 / Arduino 2.0.17 2,100,584 6,553,600 Heltec V4 compact LoRa OTA ESP32-S3 / Arduino 2.0.17 1,283,304 1,310,720 portable ceiling Heltec V4 Room Server Full ESP32-S3 / Arduino 2.0.17 2,035,864 6,553,600 Heltec V4.3 Companion Full FEM off ESP32-S3 / Arduino 2.0.17 2,362,280 6,553,600 Heltec V4 Sensor Full ESP32-S3 / Arduino 2.0.17 1,493,976 6,553,600 Heltec V2 Repeater Full classic ESP32 / Arduino 2.0.17 1,675,432 3,342,336 Xiao C3 Repeater Full ESP32-C3 / Arduino 2.0.17 1,826,728 2,031,616 Heltec RC32 without display Repeater Full ESP32-S3 / Arduino 3.3.11 2,277,304 6,553,600

Full V4 SHA-256: ceb901afaba5f8012c49094122e30612dbf08518ebeffa51d8f92375cb52bc02; running body hash 8D26967FCC1A4984; target ID E792A051. Compact V4 SHA-256: 7b00131b795b236ce42cf62a824fb5e30a3f934034a819bf5d7a5d71a55983c9; running body hash EF44A3B4FE567589; target ID 4D015420.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#physical-checks","title":"Physical checks","text":"

Mercerwood's Heltec V4.3 OLED was identified through its exact USB serial, board, Repeater role, hardware family, running image hash/size, target ID, and expanded two-slot partition layout. Only application slots were updated; partition table, bootloader, identity, and ACL were preserved.

Boundary Result Automatic boot with no SSID, temporarily saved WebUI/ESP-NOW enabled PASS: fresh boot, working discovered/joined setup AP and cached scan results; unused ESP-NOW stayed suspended. Temporary saved flags were restored before manual cases. Explicit ESP-NOW off/on, then manual WiFi on PASS: ESP-NOW remained on through WiFi startup, AP connection, and portal stop. Master 2.4 GHz off/on PASS: individual on rejected while master off; both independently requested WiFi and ESP-NOW restored afterward. Full AP after WiFi toggle, ESP-NOW toggle, master restore, and OTA stop PASS: fresh beacon, association, HTTP 200, setup UI/API, cached scan results, and requested ESP-NOW ownership. Full OTA AP after master off and repeated after stop PASS: fresh beacon, association, HTTP 200, usable update page. Compact explicit AP, restart after stop, and implicit start ota PASS in all three cycles: fresh beacon, association, HTTP 200, lightweight raw uploader. Full \u2192 compact \u2192 Full application uploads PASS: each exact qualified image/target and application slot switch verified; final Full body 8D26967FCC1A4984. Restoration PASS: original saved preferences, public identity, ACL, runtime radio flags, Pi WiFi, and wired route verified; temporary profiles removed, serial released, no cleanup errors.

The compact direct-build alias displays env:? because the optional target-name catalog has no entry for its ID. The harness stopped on that display after upload, then resumed only after exact qualified body hash, size, version, hardware, role, target ID, and slots were independently proven. Unknown bodies, wrong IDs, and wrong named environments still reject. That guard stop is retained in the sanitized evidence; it was not counted as a firmware pass.

The final node runs the new Full image with its original settings. It still has saved power saving off and USB logging enabled. USB is attached and no current meter is available, so actual sleep/current is unmeasured. The automatic setup window expiration and sleep eligibility are covered by production-code host tests; the physical check did not wait through the entire setup window.

Sanitized build, host-test, and physical evidence: esp32_full_power_ap_2026-10-06.safe.json. Actual ESP-NOW peer packet exchange, the reported iPhone client, saved-network LAN recovery, and other boards' physical current/AP behavior were not qualified by this transaction.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#board-scope-and-limits","title":"Board scope and limits","text":"

The fixes use shared ESP32 code: Repeater/Room lifecycle, CommonCLI Full defaults, ESP32Board browser OTA/sleep, and Companion setup. Other ESP32 boards inherit the applicable fixes when rebuilt through their matching release profile. No normal ESP32 board sleep override or alternate production AP path was found. Standalone partition migration/legacy seed utilities retain separate AP code. nRF52, RP2040, and STM32 do not use these ESP32 changes.

Build and host-test coverage does not establish each board's battery current. Raw UART consoles, active BLE/controller, GPS UART, Ethernet/serial Companion interfaces, native USB host grace, primary ESP-NOW, and intentionally enabled logging can still inhibit manual sleep. Saved power-saving off also remains off after updating. There is no claim of 11\u201314 mA consumption from this validation.

Future AP acceptance must verify fresh beacon discovery, association, working pages/API, scan results, owner preservation, and cleanup on physical hardware. CLI success and mocked SDK state alone are insufficient.

"},{"location":"test-results/esp32_management_password_2026-10-19/","title":"Esp32 management password 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_management_password_2026-10-19/#esp32-management-password-hardware-check-2026-10-19","title":"ESP32 management password hardware check, 2026-10-19","text":"

PASS on one physical XIAO S3 WIO Full Companion at Mercerwood. The same admin password worked for the setup and OTA WiFi hotspots, WebConfig login, and authenticated WiFi firmware upload at source 036fb477. Password changes through the production browser, automatic reauthentication, and persistence after reboot and OTA passed. The original firmware/data and the Pi's networking were restored and verified. This does not qualify every ESP32 board, firmware role, migration path, or the release.

A sanitized result record preserves the source/artifact hashes and check outcomes without credentials or device IDs.

"},{"location":"test-results/esp32_management_password_2026-10-19/#candidate-and-recovery-baseline","title":"Candidate and recovery baseline","text":"Item Recorded value Physical board XIAO S3 WIO Firmware target Xiao_S3_WIO_companion_radio_full Source 036fb47717774a1fb13e824175462ad4442d00c7 Candidate version v1.18.0.1-halo-keymind-cascade-dev-036fb477 Application BIN size 2,271,096 bytes Application BIN SHA-256 2419a991b7421b8444b0f39e4e941a60f9d4e5bcfb3a187ca77a0a2d41d31dcf Pretrial full-flash backup size 8,388,608 bytes Pretrial full-flash backup SHA-256 9517ff0caa61ae5ae2be95d4dfd2ab28a58e979eb1c7607b857055b9fb5f1445

The pretrial full-flash image and raw hardware evidence remain private. The independent restoration readback matched this full-flash hash before runtime handoff. Candidate application and full-flash backup hashes identify different artifacts.

"},{"location":"test-results/esp32_management_password_2026-10-19/#physical-results","title":"Physical results","text":"Check Observed result Eight-character printable ASCII password WPA-protected setup AP was usable with the admin password. Fifteen-character ASCII password containing spaces and punctuation, saved through the API Protected AP operation passed with the changed credential. Five-character password Setup AP became open; configuration still required the admin HTTP login. Return to a twelve-character printable ASCII password Protected AP operation passed again. Session rotation Old HTTP cookies were rejected; the originating operation retained its scoped save receipt. Warm reboot persistence The new password remained usable after an actual Binary Companion reboot. Production browser password form Login, exactly one password save, scoped receipt, automatic reauthentication, and actual protected-AP rejoin passed with the new password. OTA hotspot and HTTP authentication The same current admin password joined the protected OTA AP and authorized HTTP Basic as username admin. Anonymous, wrong and previous credentials returned HTTP 401 on /, /update and /log. Empty OTA requests Anonymous POST returned HTTP 401; authenticated empty POST returned HTTP 400 before image installation. Real WiFi application upload One raw application upload sent exactly 2,271,096 pinned bytes; HTTP 200 and the exact install/reboot response were received. Post-OTA USB Candidate full version and public identity matched; 350 contacts, 40 channel slots and zero core error flags were retained. Post-OTA password persistence The browser-saved password joined the protected setup AP and authenticated WebConfig after OTA; the previous password returned HTTP 401. Alternate OTA slot CRC-selected slot changed from original app0 at 0x10000 to app1 at 0x340000; its application bytes matched the candidate SHA-256.

These are real-board association, HTTP API and production browser password results. Chromium used the normal home-page loader and the decoded loader/UI bodies matched the candidate's embedded hashes; native fetch and production handlers were unchanged. One new-password login was observed, and the browser was closed after success. This does not establish browser firmware-upload completion behavior. The reboot was a software warm reboot; it does not establish power-removal persistence or a battery-only cold start.

The expected policy is documented in WiFi setup, Full Companion, and the password command. Password classes are recorded here without exposing the test secrets, network names, BSSIDs, cookies, or private device identity.

"},{"location":"test-results/esp32_management_password_2026-10-19/#original-device-and-host-state-restored","title":"Original device and host state restored","text":"

The original full app0 and app1 partitions, NVS and SPIFFS were restored, with complete readback verification after every write. Original OTA metadata was restored last. Bootloader, partition table and coredump bytes were unchanged. An independent 8,388,608-byte readback then matched every byte and the SHA-256 of the fresh pretrial backup, before the firmware was started again.

The separate normal-boot check passed on the original 652289ee build: complete SELF_INFO and DEVICE_INFO records, full CLI version, all 350 raw contact records, and canonical contents of all 40 channel slots matched the baseline. Core error flags were zero and the USB session closed normally. Whole-flash equality is a preboot observation; normal firmware may write flash after boot.

The Pi's original WLAN profile was restored by its recorded UUID. All 12 owned temporary profiles were removed. The original Ethernet connection, default routes and selected Internet route remained unchanged. No factory reset or bootloader/partition-table/coredump write was used.

The same candidate version was used for the initial USB installation and the WiFi upload. Installation is established by the success response and the independent alternate-slot application readback, rather than by a version string alone. The authenticated /log request returned HTTP 404 because no packet log was present; unauthenticated access was rejected before that check.

"},{"location":"test-results/esp32_management_password_2026-10-19/#retained-failed-samples-and-limits","title":"Retained failed samples and limits","text":"

Early attempts did not count as passes. An incorrect identity offset in the test manifest was rejected before HTTP mutation. The first browser attempts hit a startup deadline or requested the versioned UI asset without its required version token; the completed browser run used the normal production loader. A separate initial HTTP connection failed before browser startup; a fresh AP scan and guarded rejoin restored access.

A previous WiFi key did not associate within the 20-second wait, but NetworkManager reported a timeout rather than the test's expected wrong-key error. That sample establishes only bounded non-association. The HTTP old-key rejections and successful new-key associations above are separate observations. This campaign does not establish uninterrupted WiFi operation or cold-boot behavior.

A scripted raw upload does not by itself qualify the unchanged browser uploader. The broader ESP32 browser OTA release gates remain separate, as do two-source recovery-AP login throttling, infrastructure roles, partition migration, and other hardware families.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/","title":"Espnow bridge diagnostics 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#esp-now-bridge-diagnostics-22-october-2026","title":"ESP-NOW bridge diagnostics, 22 October 2026","text":"

Status: host tests and both Full repeater builds PASS. No radio was flashed or contacted in this work. The reported V4/V4 R8 TFT bridge delivery failure remains physically unverified; matching saved settings and espnow.running on do not establish peer receipt.

Clean firmware source 400878207 contains the diagnostics. Both canonical builds used the Full profile, Cascade runtime defaults, USA Cascade radio preset and version v1.18.0.1-halo-keymind-cascade-dev-40087820. The safe build receipt records source, artifact hashes and linked memory measurements. Later documentation-only changes do not alter these firmware artifacts.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#investigation-and-changes","title":"Investigation and changes","text":"

Both V4 variants use the same ESP-NOW bridge implementation. No display-specific forwarding difference was found. bridge.source rx is a valid choice: it exports LoRa receptions. If both boards directly hear the same LoRa message, normal mesh duplicate suppression can discard a later bridged copy. This is a condition to measure, not an established cause of the reported failure.

Normal builds previously compiled out the bridge's verbose debug path. Runtime USB logging/debug can now produce bounded ESP-NOW summaries without a special build flag. Status exposes the configured and actual 2.4 GHz channel, SDK state, queue depths and historical error stage/code. Separate counter pages locate RX decode/admission, TX queue/start/completion and filter/duplicate decisions. No secret is printed.

The local-only espnow test command sends a single bounded diagnostic probe. The peer consumes it before mesh allocation or LoRa forwarding. It has no echo or retry and does not modify saved settings. The probe checks 2.4 GHz reachability only; it is unauthenticated and does not validate the wrapped secret or normal packet format. Confirm receipt using the peer's increased rx count and matching rxid, then test the other direction. SDK send success alone is not application receipt; see the Espressif ESP-NOW documentation.

Use the diagnostic procedure and CLI reference for the counters and exact command grammar. The USB summary requires both usb.logging on and usb.debug on; record and restore both original values after a short capture. Counter pages and probes need neither logging gate.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#build-results","title":"Build results","text":"Canonical target Selected Full environment Available internal bytes Required heap bytes Result heltec_v4_repeater heltec_v4_repeater_observer_mqtt 202,576 192,656 PASS heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt 200,344 190,608 PASS

The largest linked internal regions are 169,808 and 167,576 bytes respectively, against a required contiguous allocation of 21,184 bytes. These are linked capacity estimates, not runtime free-heap, fragmentation or maximum-load measurements. Application and merged packages, memory gates, OTA packaging and linked capability verification pass for both targets. Both capability receipts report the Full profile with no feature reductions.

An initial attempt at source 14682de00 compiled but was rejected by the immutable-input packaging guard because generation changed the embedded web page header. That header was committed in 400878207; both qualifying builds then passed from clean source. The rejected attempt is retained privately and is excluded from the safe receipt.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#verification-and-limits","title":"Verification and limits","text":"

The host validation passed for:

Callback work remains bounded and does not print or allocate. Counter snapshots and resets use the existing RX/TX locks. Diagnostics are guarded by ESP-NOW bridge support, including the CLI callbacks, so unrelated firmware roles do not acquire the new callback interface.

No physical WiFi coexistence, MQTT coexistence, LoRa delivery, probe exchange or USB debug capture was performed. Actual pair qualification still requires the new readings and fresh LoRa traffic in both directions. Stop/restart can discard pending queue work without classifying delivery; lifetime counters are not a complete end-to-end delivery ledger.

"},{"location":"test-results/firmware_variant_consolidation/","title":"Firmware variant consolidation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/firmware_variant_consolidation/#firmware-variant-consolidation-validation","title":"Firmware variant consolidation validation","text":"

All ten packaged profiles passed. Firmware source: b31d942b231360386fdfb92d0561f36095fdf011. Version: v1.18.0.1-variants-b31d942b. PlatformIO builds ran one at a time. The packages were built locally; physical testing of these combinations remains pending.

"},{"location":"test-results/firmware_variant_consolidation/#qualified-behavior","title":"Qualified behavior","text":"

Future release selection retires an alternate choice only after its exact successor is qualified. Existing published binaries, filenames and update identities retain their original meaning. Switching between different target identities requires the documented local installation or an exact compatible update route.

"},{"location":"test-results/firmware_variant_consolidation/#packaged-applications","title":"Packaged applications","text":"Profile Application bytes RAM admission margin (bytes) Application SHA-256 prefix Pico W combined Companion 902,956 58,540 246b07c54f5c V4 R8 TFT Full Companion 2,287,528 66,528 19170cc8866b V4 R8 TFT Full Repeater 2,297,368 5,736 0837183ff083 V4 R8 TFT Full Room Server 2,302,648 7,784 28ffdb3bd325 V4 R8 TFT Full Sensor 1,590,952 131,472 064c4c98d2b6 V4 OLED Full Repeater 2,283,496 5,904 73ec156914e8 V4 TFT Full Companion 2,281,768 68,176 85c2dcaa00e8 Muzi Base Uno Full Companion 580,640 32,704 0d9fbe41f3ed Muzi Base Duo Full Companion 571,792 32,784 d5970c20227f T-Echo Lite Full Companion 595,896 28,928 91495097a524

The machine-readable evidence contains the full application SHA-256 values, hashes for every offered BIN/merged BIN/UF2/DFU ZIP, exact requested and packaged targets, source version, hardware/update identity where present, detection markers and RAM values. The application hash covers the ESP32 BIN, nRF52 DFU application's BIN, or decoded RP2040 flash application; container hashes are recorded separately.

RAM margins are linked capacity after the complete modeled heap requirement and transient reserve. They are admission results; runtime load and physical current were not measured.

The verifier checked every memory manifest's file bindings and RAM admission. It read the promised marker and exact firmware/source version from each uploadable application, compared ESP32 application bytes with their merged install, compared nRF52 DFU application bytes with decoded UF2, and required Pico W's decoded UF2 to match its BIN. UF2 headers, unused sector bytes and alignment padding cannot provide application capability evidence. Observer-named ESP32 packages retain their actual target and embedded observer version in the JSON.

"},{"location":"test-results/firmware_variant_consolidation/#software-and-browser-checks","title":"Software and browser checks","text":"Check Result Native R8 rotation, Muzi accessories and T-Echo Lite detection PASS, 11 methods across three suites; production paths, sanitizers and negative controls RP2040 application admission and existing capability checker PASS, 11 RP2040 methods and 18 existing methods; malformed UF2, wrong family/address/layout, BIN mismatch and metadata-only proof rejected Consolidation policy, release controls and legacy compatibility PASS, focused Python suites; packaged marker/config/source gating, paired sensor policies and unchanged historical downloads Picker JavaScript PASS, five suites covering consolidation, Full defaults, explicit Reduced links, historical metadata and runtime directions WebConfig source checks PASS, 13 methods WebConfig browser behavior PASS, 25 methods in the real Chromium headless browser, including saved rotation load/change Firmware picker browser behavior PASS, 14 DOM checks across Full default/Advanced interaction, explicit Reduced links and stale metadata Complete build-profile selection suite PASS Actual package controls and picker replay PASS, 10 qualified profiles; R8 rotation commands offered for all four roles Physical boards/accessories, sleep current and radio/transport load Not performed

The production controls generator and picker used release metadata assembled from these exact local files. Public firmware gains the new behavior when a matching qualified image is published. Full sensor support is the ordinary picker default; Advanced retains Reduced images and explicitly linked Reduced selections. Release policy keeps both Full and Reduced downloads and their exact update endpoints.

"},{"location":"test-results/firmware_variant_consolidation/#physical-acceptance-still-pending","title":"Physical acceptance still pending","text":"

These checks are required before describing the hardware combinations as physically qualified.

The earlier optional-screen record covers separate RC32 and Tracker smoke packages; those are not part of this ten-profile matrix.

"},{"location":"test-results/four_fixed_tx_dwell_validation/","title":"Four fixed tx dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#four-fixed-transmitters-longer-dwell-repeats","title":"Four fixed transmitters: longer dwell repeats","text":"

Same physical MercerMesh fixture and installed firmware as the completed 5.1-symbol run: RAK4631, Heltec T096, MeshTower V2 and T1000-E fixed transmitters; Heltec V4 RX. SF10 / BW125 kHz / CR4/5, four channels spaced 1 MHz apart, 32-symbol preamble, normal SX1262 RX gain, -9 dBm chip TX setting, zero added settling and one frequency write per hop. No firmware was rebuilt or reflashed for these repeats.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#61-symbol-run-user-stopped-incomplete-sample","title":"6.1-symbol run: user-stopped, incomplete sample","text":"

Started 2026-09-14 22:44:47 UTC. Dwell was 49,972 us per channel. All four stationary controls passed 100/100 again.

The user requested stopping this run and proceeding to 7.7 symbols. The collector was interrupted only after verifying its PID, command line and owner. At the last saved checkpoint, 342/348 scored scan packets were received (98.28%), with six timeouts:

TX / channel Scored attempts Received Timeouts RAK4631 / 0 87 87 0 Heltec T096 / 1 87 86 1 MeshTower V2 / 2 87 85 2 T1000-E / 3 87 84 3

This is not a completed 400-packet run or a validated full-run timing capture. An interrupted in-flight packet can exist beyond the last saved checkpoint; it is not scored as another success or failure. No final switch timing was collected before reboot. The raw capture remains incomplete, and the separate user-stop record documents why. All five radios were then independently verified idle, and ModemManager was restored. Gateway services stayed active; the V4 memory-soak service stayed inactive.

Partial raw capture SHA-256: ad199124930da19fe56c78a4ea8280a6d94b95ef408589b19a9e165155dbf7fd.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#77-symbol-run","title":"7.7-symbol run","text":"

Launched 2026-09-14 22:58:14 UTC; collector ran from 22:58:15 to 23:11:45 UTC with 63,079 us visits. All four fixed-channel controls passed 100/100. The full scan completed with 395/400 received (98.75%), five timeouts and zero wrong-channel or invalid payloads. Thus 7.7 symbols did not achieve a 400/400 pass.

TX / channel Scanning result Timeouts Mean received RSSI (dBm) Mean received SNR (dB) RAK4631 / 0 100/100 0 -21.05 +8.14 Heltec T096 / 1 99/100 1 -4.35 +8.98 MeshTower V2 / 2 97/100 3 -44.86 +8.63 T1000-E / 3 99/100 1 -18.56 +8.26

All 800 baseline/scan TX sequences were unique, all transmit calls succeeded, and every TX packet reported zero frequency and modulation configuration writes. There were zero packet-response transport replays. All four final TX lifetime checks reported 200 packets and a prepared, non-failed transmitter.

The five timeouts occurred at scan attempts 158 (T1000-E), 184 (T096), and 200, 293, 314 (MeshTower). Each reported RX mode 1 and zero device errors. Final RX failure, RX-mode-error, cache-error and receive-error counters were zero.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#measured-switch-and-sweep-timing","title":"Measured switch and sweep timing","text":"Hop population Count Minimum (us) Mean (us) Maximum (us) All single-pass switches 5,046 438 524.146 804 Cached modulation; no 0x8B 4,651 438 502.147 686 Post-reception modulation refresh 395 760 783.180 804

Exactly 5,046 RF-frequency commands and 5,046 optimized RX resumes were counted, with one retune pass and zero second passes/detours. The 395 modulation writes match the 395 post-reception cache refreshes. This is the same cache behavior as the earlier run, not double-setting the radio. The slightly higher overall mean than the 5.1 run partly reflects a larger fraction of refresh hops.

Across 860 measured idle cycles, the four-channel sweep averaged 254.351 ms, minimum 254.208 ms and maximum 255.482 ms. Relative to the 262.144 ms programmed preamble, that leaves 7.793 ms at the mean and 6.662 ms at the maximum. Nominal dwell was 63.079 ms; measured idle dwell averaged 63.084 ms. Packet-held visits include payload/guard time and are not comparable to idle cycles.

The 7.7 run received three more packets than the earlier 5.1 run (395 versus 392 out of 400). This small, sequential comparison does not establish a reliable improvement or a lossless dwell threshold. The 6.1 result is partial and must not be treated as an equal-size completed comparison.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#evidence-and-cleanup","title":"Evidence and cleanup","text":"

Unlike the original 5.1 run's overly strict host assertion, this collector completed normally with complete: true and exit code 0. Independent offline validation also passed. The same collector SHA-256 was used for both repeats: cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f. The copied deployment/manifest files are provenance for reused firmware, not records of a new flash operation. Host options and acknowledged dwell match.

Raw capture SHA-256: 5e3f9a80af1384c3072303428b75c8550ac42d2ac95bd1234bf92cb0aea625e8.

The cleanup wrapper retained three errors from its reboot phase: the T096 primary USB interface was temporarily missing/ambiguous, and attempts to open the MeshTower/T1000-E ports returned permission denied. Its subsequent final readbacks nevertheless succeeded for all five radios: each TX was unprepared, non-failed, packet count zero and autonomous TX disabled; RX scanning was inactive with zero channels. No further radio access or permission changes were attempted after those results returned. The wrapper's verified_idle flag remains false because it retains any earlier cleanup error, and the outer operation returned nonzero for that reason. These warnings happened after the complete RF sample; they are not counted as RF packet losses or silently removed from the record.

ModemManager was restored active; gateway services remained active. The V4 memory-soak service remains inactive with test firmware installed. No firmware restore, commit, push or pull was performed. Previous raw captures are unchanged.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#dwell-budget","title":"Dwell budget","text":"

One chirp at SF10/125 lasts 8.192 ms. For the experiment's nominal budget of one complete four-channel idle sweep inside 32 programmed preamble symbols:

maximum dwell symbols = 32 / 4 - switching_ms / 8.192.

The 5.1-symbol run's mean switch (0.513980 ms) gives about 7.937 symbols; its maximum switch (0.801 ms) gives about 7.902 symbols, before other loop overhead. These are timing ceilings, not proven lossless dwell limits or a complete acquisition model. Near the ceiling there is little timing margin. At 7.7 symbols, four nominal dwell-plus-average-switch intervals total about 254.4 ms, versus a 262.144 ms programmed preamble.

The host keeps the same seed and arrival-randomization rule, spanning about two nominal scan cycles; absolute arrival pauses therefore scale with dwell. The fixed-channel controls, sample counts, switching policy and firmware stay unchanged. As before, external PA/LNA paths and unrelated RF traffic mean this is a finite bench comparison, not a calibrated sensitivity or interference test.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/","title":"Four fixed tx single pass validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#four-fixed-transmitters-fast-single-pass-receiver","title":"Four fixed transmitters / fast single-pass receiver","text":""},{"location":"test-results/four_fixed_tx_single_pass_validation/#result","title":"Result","text":"

MercerMesh Pi bench run, started 2026-09-14 22:18:49 UTC (15:18:49 PDT): four physically separate, fixed-channel transmitters and the Heltec V4 receiver. The full stationary controls passed 400/400. The subsequent four-channel scan received 392/400 correctly (98%), with 8 timeouts (2%) and zero wrong-channel payloads. No RF retries were used.

TX / assigned channel Frequency (MHz) Fixed-RX control Scanning RX Scan mean RSSI (dBm) Scan mean SNR (dB) RAK4631 / 0 909.5 100/100 96/100 -20.69 +8.63 Heltec T096 / 1 910.5 100/100 98/100 -3.84 +8.45 Heltec MeshTower V2 / 2 911.5 100/100 100/100 -45.48 +8.41 Seeed T1000-E (LR1110) / 3 912.5 100/100 98/100 -18.27 +8.51

RSSI/SNR describe received packets only. Each transmitter sent 200 unique packets across its baseline and scan. All 800 transmit calls returned success, with zero frequency or modulation commands during those packet calls. There were zero recovered/replayed USB responses in the packet records.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#switching-timing-and-command-evidence","title":"Switching timing and command evidence","text":"Hop population Count Minimum (us) Mean (us) Maximum (us) All single-pass switches 7,206 429 513.980 801 Cached modulation; no 0x8B 6,814 429 498.602 686 Modulation cache refresh 392 763 781.291 801

The receiver recorded exactly 7,206 frequency commands and 7,206 optimized RX resumes: one frequency write and one resume per hop. Second-pass count was zero. Duplicate frequency writes, frequency/CR detours and added settling were disabled. Warm XOSC, bulk 8 MHz SPI and cached modulation were enabled.

The average switch was 0.514 ms, but not every switch was below 0.6 ms. After each received packet, ordinary RX staging invalidates the modulation cache; the next hop refreshes it. Subsequent unchanged hops omit 0x8B. The 392 refreshes match the 392 received packets. The source path is RadioLibWrapper::recvRaw() -> startRecv() / startReceiveMode() -> PhysicalLayer::startReceive() -> CustomSX1262::stageMode() -> SX1262ProfileSwitchState::invalidate(). No production cache-invalidation behavior was changed for this test.

Mean switch-plus-settling-instrumentation time was 515.698 us, maximum 803 us. The settling measurement averaged 1.718 us of timer/measurement overhead; the configured settling delay was zero. The idle four-channel sweep averaged 169.135 ms (maximum 170.470 ms), versus 262.144 ms for the 32-symbol preamble. Packet-held visits include reception/guard time and are not idle sweep timings. The deferred counter counts guard-loop deferrals, not lost packets.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#method-and-fixture","title":"Method and fixture","text":" Role Hardware serial / MAC Pi USB topology TX0 RAK4631 9AB3B64C641BA927 1-1.3.1 TX1 Heltec T096 651F8E496197F882 1-1.2.3 TX2 Heltec MeshTower V2 9352162A72082314 1-1.2.2 TX3 Seeed T1000-E LR1110 34A9141999729D5D 1-1.2.1 RX Heltec V4 44:1B:F6:69:CF:98 1-1.3.3

Board selection checked serial plus topology, not transient tty names or CDC interface count. The RAK3401 gateway, serial 0B81C9C68D8D01B4, was excluded. The earlier local XIAO radios were not modified during this run.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#remaining-losses-and-interpretation","title":"Remaining losses and interpretation","text":"

Timeouts occurred at overall scan attempts 10, 14, 55, 65, 145, 299, 304 and 384. Thus they were not confined to startup. Each timeout reported RX mode 1 and zero device errors; TX calls returned success and still reported no configuration writes. Final RX failure, RX-mode, cache and receive-error counters were all zero.

The wrong-channel behavior did not reproduce in this configuration. The remaining misses do not require a transmitter that retunes between packets. However, this is not a controlled A/B against the earlier XIAO setup: the RX board, TX boards/chips, antennas and geometry changed. These results do not prove that the earlier transmitter was faulty or establish the cause of the eight remaining losses.

Several received signals were very strong, particularly the T096 at about -4 dBm. The weakest link, MeshTower V2 at about -45 dBm, passed 100/100 while scanning. That makes a controlled spacing/attenuation comparison worth considering, but this sample does not prove receiver overload. The observed 98% is a finite bench result, not a reliability guarantee or sensitivity test.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#capture-integrity-and-post-run-validation","title":"Capture integrity and post-run validation","text":"

All 400 scan probes finished and final radio telemetry was returned. The original host checker then rejected that telemetry because it incorrectly required zero modulation writes over the entire run, including post-packet RX staging. Its raw capture therefore remains complete: false, with final status preserved verbatim inside its error string. The normal sender_final collector step did not run. The raw failure record was not rewritten.

Source review established the cache-refresh behavior above. The host checker now validates modulation writes against the measured refresh-hop count, checks that cached plus refresh hops equal total hops, and still requires single frequency writes, single fast resumes, zero added delay and the other policies. It also saves final telemetry before checking it, so future failures retain that structured record.

Separate offline validation parsed the saved telemetry, checked the full 400-probe scan and all four 100-probe controls, verified 800 unique sequences, and checked every transmitter's packet counter from 1 through 200 and its zero-retune acknowledgments. A capture regression additionally checks that each TX/RX response ID matches its probe and every accepted payload/channel matches the assigned channel. No radio was rerun to replace the failed host assertion. The derived summary explicitly reports original_collector_complete: false and post_run_validation_passed: true.

Evidence files:

Raw capture SHA-256: 42de3ac0c335b2c59f35c1cab217a01ef242c9f2e7b0010c8000a908a59232e6. Derived summary SHA-256: 316bd551eb7306dfc576f4b88ad23e82cb1d701481d1e022067c961806d31bfe.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#build-deployment-and-final-state","title":"Build, deployment and final state","text":"

Built from the dirty keymindCascade worktree based on 2b5a6f209835f0f67ed73fee78d36447a4566ce6. This is not a claim that HEAD alone reproduces the images. The manifest records image and HIL-source hashes. All five PlatformIO builds succeeded and ran sequentially.

After the host-checker correction, 25 focused unit/regression tests passed, including the preserved-capture check. git diff --check passed; its only warning concerned a pre-existing CRLF conversion in MyMesh.cpp.

RadioLib was pinned to 187ef24791c3d844939b2be13a68bd890bd04e4c (7.7.1). The nRF framework was d5413016 (1.10701.0); the ESP build used platform 6.11.0 / Arduino 2.0.17. The first three nRF targets retained S140 6.1.1 (FWID 0xB6, app base 0x26000); T1000-E retained S140 7.3.0 (FWID 0x123, app base 0x27000). nRF DFU packages were application-only, with their matching SoftDevice requirements intact. No nRF bootloader/SoftDevice replacement or cross-board override was used. Runtime FWID and application base were verified.

V4 received the normal ESP bootloader, partition table, boot_app0 and HIL app at 0x0, 0x8000, 0xE000 and 0x10000, respectively, with esptool hash checks. No whole-flash bulk erase was issued. No backup was requested for these authorized bench deployments.

The initial bundle's collector/deployer hashes are not the final scripts used: the actual run used profile_four_tx_v2.py with SHA-256 b762e412636076902174c76b71961cc8ab3059c2b6de3dd3b4123bafcb06545c (startup-greeting fix, original strict final assertion), and deployment used profile_four_tx_deploy_v3.py with SHA-256 8a660788e0215e6ac1aece0a5d033521f9f376c9e51c00e760e918ebfd72cca1. The current local collector includes the later validation correction; firmware images were not changed after the run. Remote evidence remains under /home/mikec/hwtest/runs/four-tx-sf10-20260914.

All five radios were subsequently rebooted and independently verified idle. All TXs reported unprepared, packet count zero, no failure and autonomous TX disabled; V4 reported scanning inactive and zero hops after reboot. HIL firmware remains installed. ModemManager was restored active, gateway services were left active, and the V4 memory-soak service remains inactive because its firmware is now the bench receiver. No Pi USB hub power cycle was performed.

No commit, push or pull was performed for this test. Unrelated worktree changes were preserved.

"},{"location":"test-results/full_usb_logging_validation/","title":"Full usb logging validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/full_usb_logging_validation/#full-usb-logging-repair-11716","title":"Full USB logging repair - 1.17.1.6","text":"

Audit date: 2026-09-13. Release firmware source: 306feebe6d648a247f925894f876be9d747fc8d2.

This is historical qualification of the 1.17.1.6 images, not an all-board runtime test of later firmware. Current source separates the saved USB output master (usb.logging) from opt-in diagnostic verbosity (usb.debug, default off), and adds full ASCII RAW records to Full Companion. See current USB logging controls for defaults, legacy preference handling, capture-loss behavior and dedicated CDC/control-port restrictions. These source changes do not imply new hardware validation of the published images listed below.

The original Station G2 Full Repeater and Full Room Server images omitted USB packet logging. Both inherited -UMESH_PACKET_LOGGING from their ordinary MQTT observer recipes. Full added -DMESH_PACKET_LOGGING=1, but PlatformIO's ProcessFlags moves undefines after defines in the compiler command. The resulting image compiled successfully with its packet logger removed.

The previous release gate checked OTA and other features without requiring USB logging in Full infrastructure. An old passing manifest could also be reused by --resume without the new logging evidence.

"},{"location":"test-results/full_usb_logging_validation/#published-image-audit","title":"Published-image audit","text":"

Each downloaded/cached artifact was matched to GitHub's published SHA-256. The check inspected application bytes, including the application inside nRF52 DFU ZIPs. It required the packet-log formatter and USB logging controls.

Release group Application images Missing USB packet logging ESP32 Full infrastructure/utility 157 2 ESP32 Full Companion 52 0 nRF52 Full Companion 43 0 Total 252 2

Affected targets:

V4/R8 Full builds and Station G2's Full ESP-NOW bridge passed. This audit does not count ordinary MQTT profiles that intentionally omit USB logging as broken. Artifact inspection proves compiled support, not hardware operation on every board.

"},{"location":"test-results/full_usb_logging_validation/#fix-and-regression-coverage","title":"Fix and regression coverage","text":"

Focused verification (after installing PlatformIO and its tool-scons package):

python3 -B test/test_full_build_flag_dedup.py\npython3 -B test/test_full_logging_compile.py\npython3 -B test/test_firmware_capabilities.py\npython3 -B test/test_full_logging_resume.py\nbash test/test_build_profiles.sh\npython3 -B test/test_cascade_release_package.py\n

Both Station G2 profiles were rebuilt through build.sh --full with the original observer recipes, including their inherited logging undefines. They passed the new application checks, runtime RAM budgets, and both OTA slot size checks. The new checker rejects both original published images.

On the Bellevue Station G2, the corrected Full repeater implementation also passed serial CLI and passive traffic checks: four RX and two TX packet log lines were captured. Its active temporary profile retained preamble 120 and the original schedule end time; the Pi relay services were restored.

Corrected application SHA-256:

Repeater:    81a0a2926d4e567c069d0b10f316047881bcedce062afaa42119dddb2baadf23\nRoom Server: 7ee81f1cb65fc274bd363397c3c80ce9006d575efae50fe952c8ca246613a18c\n
"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/","title":"Hardware validation bluetooth stealth 2026 09 07","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#xiao-bluetooth-stealth-hardware-validation-2026-09-07","title":"XIAO Bluetooth stealth hardware validation - 2026-09-07","text":"

Historical command syntax: the initial run used stealth as a MAC mode. Current firmware uses the independent bluetooth.stealth on|off flag instead and does not migrate that development mode. See current commands and the independent-flag follow-up below. Each observation applies only to the exact artifact listed in its section.

This is a hardware observation report, not an all-board or phone qualification. The test host was the MercerWoodMesh Raspberry Pi Zero 2 W, accessed remotely over Tailscale. Tests used its built-in Bluetooth adapter, not USB resets or hub power switching. Local timestamps below use America/Los_Angeles.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#exact-target-and-firmware","title":"Exact target and firmware","text":"

The application was installed with Nordic Legacy BLE DFU. The tool verified the exact ZIP hash, bootloader advertisement and live DIS model XIAO nRF52840 before transmitting 616,584 application bytes. Target image validation and target-initiated activation disconnect passed. The transfer reported about 2,223 bytes/second (about 277 seconds for payload transmission). Authenticated MeshCore device queries subsequently returned protocol 14, board Seeed Xiao-nrf52, and the expected unique test-version prefix.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#observed-results","title":"Observed results","text":"Check Result and evidence First-pair discovery set bluetooth.mac stealth saved a new random-static address; reboot returned that exact address with the normal name and Nordic UART advertisement. First authenticated pairing The Pi received a passkey request and completed pairing. BlueZ reported both Paired and Bonded. MITM-protected Nordic UART notifications and real command/reply exchanges worked at MTU 247. Pairing transition keeps the session alive The initial connection returned stealth; bonded-peer-only advertising, remained usable for five more seconds, and answered another command without disconnecting or pairing again. Advertising after disconnect A public-address passive scan captured ADV_DIRECT_IND from the exact target, with advertising data length zero. The capture contained no undirected advertisement or scan response from that address. Bonded reconnect Subsequent direct connections by saved BlueZ device identity succeeded without a new passkey, including live version, core-statistics and Bluetooth-mode replies. Warm reboot persistence Two commanded reboots retained the address and saved bond. Directed advertising was captured after reboot, followed by authenticated reconnect. Device uptime decreased from 61 seconds before the second reboot to 35 seconds at the next query; core error flags were zero. Slow advertising interval A successful bonded reconnect occurred about 61 seconds after reboot, beyond the 30-second fast-advertising interval. Reopen pairing Setting stealth again from an existing bonded connection generated a new address, and the next boot allowed a fresh first pairing. Restore default set bluetooth.mac default plus reboot restored E5:C3:A8:B0:60:66 and normal discovery. Fresh PIN pairing, version/core-statistics queries and repeated factory address (default) replies passed. The bench target was left in this discoverable mode.

The newly completed pairing reaches Bluefruit's secured callback before its bonded flag is necessarily ready. Recording the completed pairing in the pair-complete callback as well, waiting for the deferred bond write, and deferring identity-list reconfiguration until disconnect are covered by this fresh-pair hardware run. Static source contracts also check those paths.

The binary reboot command can reset the target before BlueZ receives its ATT write acknowledgement. One initial harness run reported Unlikely Error after successful pre-reboot commands. Later checks require an observed disconnect, a live post-reboot query and decreased device uptime; they do not treat the write exception or tool exit code alone as a successful reboot.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#discovery-and-dfu-cautions-found-during-testing","title":"Discovery and DFU cautions found during testing","text":"

An ordinary BlueZ discovery scan used a temporary random local address and did not show the directed target. A passive scan using the Pi's public identity did show it. Reconnection must use the saved peer/device identity; absence from an ordinary scan is not proof that the node stopped working. Conversely, BlueZ can retain a cached name after pairing, so a displayed name is not proof that the name is still being advertised. The packet-type and zero-length-payload capture is the relevant evidence here.

The existing DFU handoff helper assumed the application and bootloader shared one address. That assumption is false with a custom application address: the authenticated handoff worked, but the helper timed out looking for DFU at the application address. A fresh exact-address scan found the known factory-address XIAO_DFU instead. An old factory-address host bond also caused the first DFU service-discovery attempt to disconnect; clearing only that old lab record allowed DFU. No bootloader code was changed for this test.

An earlier application-address host pairing record was also removed before its saved stealth state was known. A direct, known-address connection from the same Pi identity allowed fresh PIN pairing and protected commands again. This does not prove that an arbitrary replacement phone can rediscover or recover a stealth node after forgetting its bond.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#remaining-hardware-gates","title":"Remaining hardware gates","text":""},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#repeatable-regression-procedure","title":"Repeatable regression procedure","text":"
  1. Record the exact board, application version, ZIP hash, local/remote BLE identities and original Bluetooth setting. Keep a working recovery path.
  2. Set the desired custom or random MAC, enable bluetooth.stealth on, reboot, and capture normal discovery before pairing.
  3. Pair once. Require a stored bond and successful protected MeshCore commands, then keep the original session open and issue another command.
  4. Disconnect. Capture the actual advertising type and payload with an appropriate receiving identity; do not use cached scan names as evidence.
  5. Reconnect by the saved peer without another PIN and exchange commands.
  6. For custom and saved-random addresses, repeat bluetooth.stealth on, reboot twice, and verify unchanged identity/bond, decreased uptime and live commands. Include a reconnect after the fast-advertising period ends.
  7. Separately remove all target power and repeat the persistence checks.
  8. Test bluetooth.stealth off, then on, as a pairing reset, and off as the exit from stealth. Verify live discovery, pairing, commands and an unchanged MAC policy. Changing the address to default is a separate step.
  9. With each rotating MAC policy, verify that its rotation trigger still works, stealth stays enabled, and the new address permits fresh pairing.
  10. Repeat relevant gates for a phone with address privacy and each BLE backend.

Raw VM-side logs, the exact artifact and bounded test helper are in /home/mesh/mercer-stealth-validation.jUDW0v/. Raw Bluetooth captures are local diagnostics and should not be published without privacy review.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#mercer-crash-capture","title":"Mercer crash capture","text":"

During this run the Pi used its existing dwc_otg.speed=1 full-speed USB configuration. Kernel and health logs were streamed to this VM. No new kernel errors or Pi lockup were observed during these tests; that does not establish the cause of the earlier lockup.

The live test streams and all temporary scans were stopped at approximately 22:56 PDT. Journald remained active, the restore service remained enabled, and the test XIAO was disconnected in default mode. Disk journals occupied about 4.9 MiB at the final check.

At 22:35 PDT, temporary persistent journaling was enabled with a 16 MiB configured disk-use limit, 2 MiB journal files, one-day retention and a 30-second normal sync interval. Recent buffered messages can still be lost in a sudden failure. The original Storage=volatile configuration is intact.

mercer-crash-capture-restore.service is enabled for the next Pi boot. It checks the armed boot ID, removes only the unchanged managed override, switches journald back to RAM and leaves the old disk journals for collection. Its same-boot no-op guard and unit syntax passed. The next-boot restoration has not yet been exercised; no Pi crash or reboot was deliberately induced.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#independent-flag-follow-up-dev-stflag","title":"Independent-flag follow-up - dev-stflag","text":"

The same XIAO and Pi were used later on 2026-09-07 to test the separate bluetooth.stealth on|off flag, with no development-mode migration. This run used Xiao_nrf52_companion_radio_ble, target radio preset, default profile, and firmware dev-stflag-4b400ea5 with the uncommitted flag changes.

Check Observed result Default and rejected input Initial mode was factory address with stealth off. set bluetooth.mac stealth and set bluetooth.stealth maybe returned errors, leaving both settings unchanged. Custom plus stealth D2:27:5A:40:06:99 advertised normally before first pairing. Fresh PIN pairing, stored bond, protected commands, and a five-second held session passed with stealth on. Repeated on and warm reboot Repeating set bluetooth.stealth on retained the paired state. After reboot, the same custom address accepted protected commands without another PIN. Uptime decreased from 84 to 67 seconds. Actual advertising after reboot A public-address passive capture showed ADV_DIRECT_IND for the custom address, data length zero, and no undirected advertisement or scan response from that address. The scan was explicitly disabled afterward. Off preserves custom address set ble.stealth off and reboot restored normal discovery at the same custom address. A live connection returned stealth off and the unchanged custom policy. Saved random plus stealth F2:2E:E5:65:18:5E passed first pairing and held commands. After reboot, it retained its address, saved-random policy and stealth bond; protected reconnect needed no PIN. Uptime decreased from 55 to 30 seconds. Random-after-connect plus stealth Selecting this policy kept stealth on and reopened pairing at C7:FD:29:44:4F:3E. Pairing produced both bonded-peer-only status and an armed next-boot rotation. Reboot changed the address to F5:F3:89:B7:B5:26, where normal discovery, fresh pairing and protected commands passed with stealth still on. Random-every-boot plus stealth The first boot used F9:B2:03:DD:CC:43, completed fresh pairing, and answered commands with stealth on. The next commanded reboot advertised F8:C7:C0:96:1E:00; fresh PIN pairing and protected commands passed again with the rotating policy and stealth on. Restore the bench target Setting the MAC to default kept stealth on until the separate set bluetooth.stealth off. After reboot, factory-address discovery, fresh PIN pairing and protected commands confirmed default MAC and stealth off. The obsolete factory-address host bond was replaced during this final pairing.

These checks used actual encrypted BLE connections at MTU 247, not scan sightings alone. Two first-pair harness attempts stopped before connecting because their unpaired BlueZ discovery object no longer existed. A fresh bounded scan immediately followed by pairing was used for retries; this was not a firmware GATT command failure. Reboot-write/disconnect races were accepted only with an observed disconnect and subsequent live device checks.

Local automated verification passed 39 native cases, 32 source/persistence/UI contracts, and 11 real-browser cases. Supported-wrapper builds passed for XIAO nRF52, XIAO C3, M5Stack Unit C6L and XIAO S3 WIO Full Companion, covering nRF52, ESP32 NimBLE and ESP32 Bluedroid integration. The contracts and browser suite are included in the unit-test workflow; a browser must be available for the optional browser suite to execute instead of skip.

Exact artifacts, build logs, command logs and the private packet capture are in /home/mesh/bluetooth-stealth-flag.ApLB8o/ on the VM. The remaining hardware gates above still apply; these results do not qualify every board or phone.

At 23:42 PDT the XIAO was disconnected in normal discoverable mode. The Pi retained the same boot ID throughout, remained responsive, and had no new kernel messages in the streamed log. Journald was active, the next-boot volatile-log restoration service was still enabled, and disk journals used about 5.9 MiB. No USB reset, Pi reboot, or target battery removal was used.

"},{"location":"test-results/hardware_validation_checklist/","title":"Hardware validation checklist","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_checklist/#hardware-validation-checklist","title":"Hardware validation checklist","text":"

This file records dated hardware observations and open validation tasks. Historical defaults, versions, and test frequencies below describe the tested revision, not necessarily the current release. For current ASCII USB defaults and runtime switches, use feature switches by role.

Use this checklist for release-candidate hardware runs. A check is complete only when its log identifies the physical device, firmware artifact, artifact hash, command result, and cold/warm boot outcome. Do not infer success from a tool's exit code when the tool has a documented false-success mode.

"},{"location":"test-results/hardware_validation_checklist/#known-failure-follow-up-2026-10-2021-utc","title":"Known-failure follow-up, 2026-10-20/21 UTC","text":"

The follow-up report binds builds, host tests and hardware observations to their actual sources. It preserves the earlier failures and the accepted fleet results.

The independent eight-board nRF52 test-bootloader deployment and original-app handoffs are complete with documented USB concurrency and RAK secondary-data preservation limits. This is separate from final-source Companion qualification. The nRF52 testing hold is released; MeshPocket remains excluded.

The current V4 differs from the physical board in the 18 October failure below. Its follow-up cannot establish that earlier USB disappearance's cause or erase the failed historical measurement. Remaining release gates keep their latest documented status.

"},{"location":"test-results/hardware_validation_checklist/#mercerwood-candidate-campaign-2026-10-18-utc","title":"Mercerwood candidate campaign, 2026-10-18 UTC","text":"

The current hardware campaign records actual candidate flashes, identity-bound USB/BLE and warm-contact checks, live Room authentication/browser results, and shipping Morse failures. It retains the original populated-board stack failures and the d52e4e93 repeated-read/browser retests that passed. The matching-profile trials established LoRa reception and paced live-session mailbox operation; durable replay, final-role testing and other release gates remain open. These observations do not constitute release approval.

The ec9ebc09 Full V4 logging-off native-console trial 03 passed 600.444 seconds with zero idle writes, complete replies/reopen and zero core error flags. The passed Room logging-on trial does not qualify Full logging-on.

Complete CI runs 39110179910 at 5fd33a230 and 39109551854 at 7a5a7993c are green. The native-console/TTY cleanup follow-up passed 35 offline checks. Both run 39115389840 at 76220d5d7 and run 39116894845 at 7b41d2893 completed successfully with all eleven jobs green. Those CI passes do not qualify the failed hardware case.

"},{"location":"test-results/hardware_validation_checklist/#pr1721-follow-up-qualification-2026-10-18-utc","title":"PR17\u201321 follow-up qualification, 2026-10-18 UTC","text":"

The merged source checkpoint is 0a24279a3. This audit adds host regressions, operator documentation, and a C6 GPIO capability fix; it is not a hardware qualification. No device was flashed or contacted during the audit.

Completed host coverage includes guest replay-store capacity and live-client eviction; per-address/shared WebConfig limits and room-password windows across millis() rollover; existing WebConfig sessions during login throttling; migration credential precedence and failed-identity upload denial; generated package instructions; and cached/uncached partial-contact recovery. The real Chromium room and WebConfig fixtures use mocked HTTP responses, so they do not prove the actual network/firmware interaction below.

The merge was compiled for Heltec_v3_room_server, heltec_v4_companion_radio_usb, esp32_s3_4mb_partition_migrator, and all four small STM32 USB Companions at release-version length. Follow-up builds cover Xiao_rp2040_companion_radio_usb and Arduino 3's LilyGo_Tlora_C6_room_server_. The C6 build initially exposed unavailable global deep-sleep GPIO hold APIs; the helper now follows the SDK's per-pad capability, with a fixture that omits the unavailable declarations. The CI cross-platform-smoke job retains both RP2040 and C6 build checks.

Additional migration builds passed for esp32_8mb_partition_migrator, esp32_c3_4mb_partition_migrator, heltec_v4_partition_migrator_lora_repeater, heltec_v4_partition_expander, and esp32_s3_4mb_partition_migrator_usb, including their application-size and runtime-RAM gates. This covers representative chip families and Wi-Fi, identity-only LoRa, Expander and USB code paths, rather than every board alias.

"},{"location":"test-results/hardware_validation_checklist/#physical-acceptance-checks-and-subsequent-campaign-results","title":"Physical acceptance checks and subsequent campaign results","text":"

Record the exact clean source revision, firmware SHA-256, device identity, observed result, and cold/warm boot outcome for each check. Record password classes and success/failure, never the passwords themselves. Checked entries below are later, candidate-specific campaign results; they do not change the host-only scope of the earlier 0a24279a3 audit.

The later d52e4e93 campaign completes the real-board browser throttle-error/retry/draft and reproduced repeated-read cases above. An executed configuration-restore-to-AP credential handoff remains open. The host AP fixture supplies pre/post-restore files; it does not run an entire migration. The current hardware campaign remains in progress and is not release qualification.

"},{"location":"test-results/hardware_validation_checklist/#esp32-web-console-and-browser-ota-release-gates","title":"ESP32 web console and browser OTA release gates","text":"

The 2026-10-05 V4/XIAO checkpoint records reproduced browser OTA failures, fixes, physical results, and remaining USB qualification limits. It is not a release approval.

The 19 October XIAO S3 WIO password campaign records Full Companion AP/password transitions, HTTP session rotation, production browser password saves, warm-reboot persistence and a real authenticated raw WiFi upload at 036fb477. Post-OTA password persistence, alternate-slot image readback, original device state and host networking all passed. This scoped result does not close the browser-upload gate below, two-source recovery-AP throttling, migration checks, or other board/role qualification.

Host tests cannot prove USB enumeration, WiFi discovery, or an HTTP upload on the physical board. Before qualifying an ESP32 release, record the following on each available hardware family. Mark unavailable hardware as untested; a successful build or simulated peripheral is not a hardware pass.

  1. Run the pinned, unmodified official web console, recording its source revision and hash. With power saving enabled, leave it idle for at least ten minutes without transmitted commands or keepalives, then require a complete CRLF-delimited command reply. Repeat with USB logging enabled and disabled. Close and reopen the console and verify that uptime did not reset.
  2. Flash through the official website's reset/handoff sequence. Record the delay until the first complete CLI reply and reconnect without unplugging or issuing another reset. A boot banner is not a command reply.
  3. Exercise start ota from WiFi off, connected LAN, WebConfig LAN, and WebConfig setup AP states. Require the reply to identify the reachable URL, required network, and WebConfig shutdown when applicable. Also exercise start ota ap while the LAN uploader is already running.
  4. Discover and connect to the actual board's hotspot. Measure its WiFi MAC; do not infer it from the USB serial number. Pin that measured identity before any firmware upload. Fetch the returned URL repeatedly across delayed WebConfig cleanup and background bridge retry intervals.
  5. With ESP-NOW configured on, exercise OTA across background bridge retry intervals. For builds that pause bridges during upload, require that pause to last until stop ota, then verify that the configured bridges resume.
  6. Upload a board-compatible, hash-verified application through the HTTP uploader's unchanged browser page. A scripted HTTP POST alone does not exercise the page's upload format or completion handling. Require the server's completion response, normal reboot, expected application version, and working USB console without an extra reset or unplug. Use a distinct target version so a reboot without an update cannot pass. Select the upload protocol from the compiled implementation: AsyncElegantOTA uses multipart data and an MD5 field; LightweightOTA uses a raw binary request. Confirm identity and settings were preserved, then restore temporary device settings and the test host's network connection.

Keep the host regressions (test_official_console_acceptance.py, test_official_web_console.py, test_wifi_ota_start.py, test_wifi_ota_lifecycle.py, and test_async_web_lifecycle.py) alongside these gates. They cover parsing, sleep policy, startup errors, network ownership, and deferred cleanup. A Started reply alone does not satisfy the physical OTA gate.

Record USB line states and reset reasons during these checks. An opener that briefly asserts the hardware reset combination invalidates a no-reset test. Keep its failed sample, correct the opener, and repeat under a new run name. Report startup readiness, uninterrupted idle, and close/reopen as separate results; a later successful command must not erase an earlier timeout.

"},{"location":"test-results/hardware_validation_checklist/#bluetooth-stealth-qualification-2026-09-07","title":"Bluetooth stealth qualification - 2026-09-07","text":"

The XIAO stealth report records exact artifacts for the initial implementation and independent-flag follow-up. The flag run covers custom/saved-random pairing and bonded reboot reconnect, idempotent on, off preserving the MAC, directed advertising, and both rotating policies with stealth still enabled. It also gives the repeatable hardware regression procedure. True battery-disconnected cold boot, phone address-privacy behavior, a second unbonded central, and ESP32 hardware remain separate uncompleted gates; do not infer them from the XIAO results.

"},{"location":"test-results/hardware_validation_checklist/#current-marathon-ledger-through-2026-09-04","title":"Current marathon ledger (through 2026-09-04)","text":"Hardware Stable identity Current state Next blocking check Seeed XIAO nRF52840 B35E71C1C3726CE7 OTAFIX candidate 0x02040405 passed exact serial install and corrected Legacy DIS model gates; the patched Bluetooth repeater-updater application was restored over BLE and returned as exact 2886:8044 USB Use the updater for an identity-gated Bluetooth transfer to another repeater, then prove current-build cold persistence Seeed Tracker T1000-E 34A9141999729D5D OTAFIX candidate 0x02040405 and the uf2reset-fixed Full Companion are installed. The real text command returned exact 2886:0057 boot USB, the same fixed app was restored, and refreshed bonded BLE services pass as MeshCore-09848C15 Restore the protected Repeater identity when Companion qualification is complete; cold boot, then force LR1110 reset RAK3401 0B81C9C68D8D01B4; FICR 8D8D01B4 0B81C9C6 OTAFIX test version 0x02040403 passes bidirectional application UF2, exact SWD application/bootloader readback, unchanged UICR, and a 74-block direct V4 LoRa apply from exact 3caf9dcf to current HEAD 9fd580c8. Post-apply USB and LoRa identity/hash checks pass and the temporary 0 dBm bench setting is restored to 22 dBm Add exact RF packet counters, then repeat the supported-bandwidth and controlled/passive/mixed routed-hop matrix Heltec T096 651F8E496197F882 OTAFIX candidate 0x02040405 and the uf2reset-fixed no-footer Full Companion are installed. The real text command returned exact boot USB, and final USB terminal/Binary mode, dual CDC, version, help, and BLE identity pass Perform a privileged or physical host-driven USB bus reset without resetting the MCU; complete the deferred multi-click/long-press physical matrix Heltec MeshTower V2 with SD 9352162A72082314 OTAFIX candidate 0x02040405 passed serial combined install, the historical cached DIS handles, BLE application restore, and exact application USB return. Card format/cooldown/forced-format/raw-erase/remount and the live SD apply contract passed earlier Signed interrupted-download resume, corruption/signature rejection, then full/delta apply Heltec V4 USB MAC 44:1B:F6:6A:E8:44; BLE 44:1B:F6:6A:E8:45 USB/BLE/Wi-Fi and all three TCP services pass on hardware. The final fresh-NTP-gated Full Companion image builds with 4.31 MB app space free Flash the exact final NTP build, prove NTP-before-TLS success/failure ordering, then OTA seeding SenseCAP Indicator LoRa CH340 plus USB MAC D8:3B:DA:75:23:AC; BLE D8:3B:DA:75:23:AD The current dark-layout application is SHA-256 81bebdc07b6a8349c1c975cb5a0e30c5f6019d35bcac641e5f3d4df951f7406b; identity-gated flash, USB ASCII/Binary switching, runtime logging, and separate configured-Wi-Fi, BLE, and TCP checks pass. Fresh-NTP HTTPS recovery, strict Range resume, STAGEV2 install, and RP USB readback passed on earlier exact artifacts; the final coordinator source still needs its blocked-UDP/123 hardware gate Physical display-wrap and four-mode render/heap checks, exact-final blocked-NTP recovery gate, LoRa/TempRadio, and non-empty OTA seeding

The current source checkpoint passes 1,132/1,132 native cases, 68/68 host broker/endpoint cases, all 318 LoRa-OTA chain/route/bundle cases, and 41/41 MOTA format/signature/delta cases under the documented pipx detools interpreter. Indicator geometry, dark-theme, and font-recovery contracts pass 4/4, 6/6, and 25/25. A serialized real Indicator ESP32-S3 Wi-Fi/USB profile also compiles at 95,944 bytes RAM and 1,589,433 bytes flash. The direct system-Python MOTA invocation predictably lacks detools; that environment boundary is not a firmware failure and must not be worked around by installing the package globally.

The RAK failure log is /home/mikec/hwtest/runs/rak3401-syncidle-clean-tSp7PLgA/result.txt on the Mercerwood Pi. The tested UF2 SHA-256 was 650c9fda456685cdf99c1f2996c60f1f551cc9216d2b746a22c8cb78464b0ff7. cp returned success in 95 ms, but sync -f failed with EIO after 157,188 ms. Post-failure SWD readback proved that all seven ordinary sectors whose bytes differed between the old and intended builds still contained the old values, and the settings page was still erased. The only available pre-copy SWD dump was taken several hours earlier, however, so it cannot prove that zero unchanged blocks were written during this attempt or establish when the stale S140-v7 tail in the first application page appeared. The next run must capture an immediate pre-copy dump as well as the post-copy dump. Serial DFU recovery then completed in 35,698 ms with the exact Device programmed. marker. All 1,830 UF2 payload blocks matched SWD readback after recovery, and the application passed a subsequent real power-removal boot.

The corrected 2.4.4-preview.2 candidate was installed through identity-gated SWD from combined-image SHA-256 9b1654baccd2a920f68651893e31d79170400d1a7982a3f854ae640f1d3513a8. An immediate pre-copy dump closed the earlier evidence gap. A deliberately old UF2 then copied and flushed successfully in 16,568 ms, and the exact formerly failing UF2 above copied and flushed successfully in 16,601 ms. After each direction, all 1,830 payload blocks matched a full SWD readback with zero header or payload mismatches. The return update changed 123 bytes across five expected application pages, UICR remained byte-identical, and a subsequent downstream USB power removal plus Pi reboot returned the RAK as the same stable identity. It reported the intended 3caf9dcf application, board, node name, USA radio tuple, and 22 dBm setting after that cold boot. The new bootloader's CLI source string is truncated before the injected preview number, so the independently hashed SWD artifact is the authoritative candidate identity for this run.

The later TinyUSB lifecycle/generation candidate identifies itself as test version 0x02040403. Its exact 40,960-byte bootloader readback SHA-256 is f609eb18754070c54f127325b1cb48547baf5c6b1c695d36fd0c8e0874713c13. With that candidate installed, a full CURRENT.UF2 read completed before each write. The 936,960-byte bf092b24 application UF2 (SHA-256 12477a9734cf12eb920361bd21024e39bf303953fcf79d0bf720e00847e2ae92) copied and flushed in 28,203 ms and returned to application mode four seconds later. The exact 3caf9dcf application UF2 (SHA-256 650c9fda456685cdf99c1f2996c60f1f551cc9216d2b746a22c8cb78464b0ff7) then copied and flushed in 26,479 ms and also returned in four seconds. Both boots retained RAK3401-OTA-BENCH and 910.525/62.5/SF7/CR5.

Identity-gated SWD then read the entire 468,480-byte padded UF2 application range. Its SHA-256 exactly matched the converted source on both sides at b2e37a7681027ca91d1b2bd8e39b55395d9e91d4863aed17131e50c1ba1d2227. The bootloader still matched the installed 0x02040403 image byte-for-byte, and UICR still matched the pre-test image at d029ea630c2f632a1b690cb52b2a96a28b6f870c656c3e1bd74ba3a571c53b9c. The authoritative logs are under /home/mikec/hwtest/runs/rak3401-uf2-02040403-20260830-170829 and /home/mikec/hwtest/runs/rak3401-uf2-return-02040403-sfHiGEqA on Mercerwood.

The subsequent direct LoRa qualification used the V4 as controller/seeder and the RAK's exact bf092b24 body 91CF4EC2882F23D6 as the base. Its mandatory fixed three-minute rehearsal passed on 909.95/250/SF5/CR5, including natural expiry and exact normal-radio recovery. The 2,132-byte, two-block in-place delta reached 2/2 and ready in six seconds; install was accepted, and the new body appeared at the 60-second reboot probe. The automation took 514 seconds including rehearsal, clock/radio safety checks, and post-install verification. The final body was 27A223DDFC3A8F47, version 1.17.1.5 at 3caf9dcf, and the bootloader reported blrc:B8, which is OTAFIX's successful application apply result rather than an error. Exact target TX power was wrapped from 22 dBm to 0 dBm for the co-located transfer and restored to 22 dBm afterward; the source RXPS configuration and both normal radio tuples were also restored.

Identity-gated SWD then read the live application at 0x26000. All 468,384 bytes matched the intended release image exactly (SHA-256 b496e3f4419c9f771564ed3c74b3407822f45acdf60cbd6c4625a54b181ca084), and the independently recomputed EndF hash was 27A223DDFC3A8F47. The target ID was 2FA509C1, hardware ID was RAK_3401, and version word decoded to 1.17.1.5. The 40,024-byte bootloader readback also matched the exact installed ffb1580/test-version-0x02040402 artifact byte-for-byte; UICR was unchanged. After SWD reset-halt/resume, USB again reported the expected application, identity, name, radio, 22 dBm, no TempRadio lease, and blrc:00 (the retained success diagnostic is intentionally cleared by the next normal boot).

The current 0x02040403 bootloader candidate then passed a second direct V4 LoRa qualification using a materially larger application change. The unsigned in-place package was 75,614 bytes with 75,108 payload bytes and 74 transfer blocks. Its mandatory fixed three-minute rehearsal passed, including natural TempRadio expiry and exact normal-channel recovery; one lost read-only ota status reply was reconciled by its bounded exact retry. The transfer reached 74/74 and READY in 122 seconds, the explicit unsigned install was accepted, and reboot produced exact HEAD 9fd580c8 with body 744BC4443891ED10. The complete safety-wrapped run took 589.487 seconds, including rehearsal, recovery, and final verification. The first postboot ver reply was lost and its exact read-only retry passed. Independent USB checks then confirmed target 2FA509C1, hardware RAK_3401, unchanged identity/name/radio/RXPS/watchdog state, bootloader CRC 6683C066, ABI 3, apply result blrc:B8, and no active TempRadio/download/seeder/key state. After the separate host-recovery exercise, the near-field TX setting was restored from 0 dBm to its exact original 22 dBm and read back over an authenticated LoRa session. The sanitized authoritative log is /home/mikec/hwtest/runs/rak3401-02040403-head-vzWOXVxT/qualification-redacted.log with SHA-256 3e53dcaa5922c7c18a7890963b8fc092311d2db01caa5603640f43f1482802f6.

A serialized VM build of the six connected-board qualification profiles then completed 6/6 in 136.122 seconds: XIAO nRF52 BLE Companion, T1000-E BLE Companion, RAK3401 LoRa-OTA Repeater, SenseCAP Indicator LoRa USB/Wi-Fi Companion, MeshTower V2 SD LoRa-OTA Repeater, and the consolidated Heltec V4 V4.2/V4.3 Full Companion. The earlier V4 selector typo was rejected before any compile began and is not a firmware build failure.

After the fresh-NTP download gate, shared SNTP coordinator, MQTT-to-MeshCore RTC handoff, and daily refresh policy were added, the complete native matrix passed 1,102/1,102 cases in 93.953 seconds. The scheduler is host-tested at 24-hours-minus-one-millisecond, the exact 24-hour boundary, normal and wrapped millis() values, a short retry that overrides the daily cadence, the zero deadline sentinel, and a Wi-Fi event latch. A reconnect after a successful sample preserves the existing deadline, so successful samples are requested after boot and then only once per day. All current Python and shell contract suites passed, and the expanded LoRa-OTA automation suite passed 317/317 cases.

Serialized real builds also passed after that change. The final event-safe implementation uses an atomic GOT_IP latch so even a short initial connection before the 10-second MQTT status sample cannot skip the required boot NTP request. The final Heltec V4 MQTT Observer application is 1,853,304 bytes with SHA-256 1938ae88fcdbe601cf8698df44b548656079562d899801886b808af913e5f9ac. The consolidated Full V4 image used 2,250,801/6,553,600 flash bytes and left 4,302,799 bytes of app-image space. Its 2,251,272-byte application image has SHA-256 05131eac29164be46d1bcceeb4f03455f5ea8d61c8d4f529b77d8adfe25fdb6a; the 2,316,808-byte merged image has SHA-256 c4c1fc639b6d0087c795a81aca79c07eba56dc4e071eb374dc70da0bec81815e. The focused daily-NTP policy suites passed all 42 cases after the latch was added, and the Full capability manifest verified all ten expected features. The reduced RAK3401 LoRa-OTA Repeater also compiled with the verified WisBlock I2C aliases. At that source state its link used 471,344/815,104 flash bytes; the reduced external-sensor contract retains INA219/INA226/INA260/INA3221 as the voltage/current entries in the optional environmental-telemetry table. These are not the image's only I2C consumers. The image separately retains its SSD1306 display, auto-discovered I2C RTCs, RAK12500 I2C GPS, and RAK12501/L76K UART GPS paths. The configured INA3221 and RAK12500 both default to I2C address 0x42; those two devices cannot coexist at those addresses. The supported combined arrangement leaves RAK12500 at 0x42, straps INA3221 A0 to SCL for 0x43, and uses a build with -DTELEM_INA3221_ADDRESS=0x43. The nRF52840 die-temperature and ADC battery-voltage paths are also retained. The 472,227-byte DFU ZIP SHA-256 is fdd369005a61635756efa9e38b217376a952f334b0864f927a46799b65264fa9, and the matching 943,104-byte UF2 SHA-256 is 2155632fde730e0ae2e4a72ce64f89910861f6c5105a4bd7ac96aaa538a33dca. These VM results do not replace the remaining exact-final hardware test with UDP/123 blocked.

The later serialized RAK3401 contract build, after the non-blocking nRF52 USB logging change, used 471,576 bytes before its 56-byte EndF trailer. Its EndF body hash is 026749684bf6ee8f, target ID is 2FA509C1, hardware ID is RAK_3401, and version word is 0x01110105. The 472,435-byte DFU ZIP has SHA-256 cf0880b26f2d6bd8157f8af76a6718fe1f998de8ea7df8cd952e734448201fb9; the 943,616-byte UF2 has SHA-256 f893a8cebf309af12a4a6f5483acb28f94158e18632f660e011658c846e8ca7f. The linked ELF contains GPS, SSD1306, RTC, and the four INA families, while an exact symbol audit finds none of AHT10/20, BME280, BMP280, SHTC3, SHT4X, LPS22HB, MLX90614, VL53L0X, BME680/BSEC, BMP085, or RAK12035. The complete native matrix passed 1,112/1,112 cases at that intermediate source state. These are VM gates only; flash the exact artifacts and repeat the physical peripheral and blocked-clock checks before marking any later source state hardware-qualified.

Subsequent source hardening changed RAK peripheral discovery: the shared 3V3_S rail remains enabled, UART GPS requires a complete checksum-valid NMEA sentence, an exact INA3221 identity blocks a conflicting u-blox probe, and a successful I2C GPS claim suppresses a sensor only on the same bus and address. The hashes and sizes above describe their recorded intermediate artifacts; they are not current-HEAD evidence and must not be reused for the hardened source.

The immutable font endpoint was re-probed only after the VM reported NTPSynchronized=yes. GitHub returned HTTP 206 for bytes 0-65535, an exact 65,536-byte match to the checked-in font (range SHA-256 8b0dd308eb01469d1f9e732fba260720a80cc86f6cb5385c28e8a16c76dcfbf3), with total length 1,302,608 and strong ETag \"39ff0cfbe37e36905507697d042869c05374c3202dd96d5a9f98812153f324c1\". This validates the current strict Range assumptions against the live service; the exact-final Indicator still needs the negative hardware run proving that blocked UDP/123 prevents every TLS request and RP2040 staging write.

Mercerwood subsequently recovered without a target write. Both its Tailscale and LAN addresses again accepted SSH, Bluetooth was idle, and the exact T1000-E stable serial 34A9141999729D5D was present as application USB 239a:8029. The RAK3401 simultaneously uses the same VID/PID, which confirms that every continuation must resolve the stable serial/by-id identity rather than VID/PID. The T1000-E remained in the exact bonded Full Companion application; no stale DFU, scanner, meshcli, or HCI-capture process was running.

The post-reboot seven-TTY inventory was also resolved without assuming that one board owned adjacent device names. ttyUSB0 is the SenseCAP Indicator's ESP32-S3 CH340 UART and ttyACM5 is its RP2040; their sibling physical USB paths and the Indicator's live TCP board reply agree. The separately attached ttyACM4 is the MeshTower V2 native HT-n5262 endpoint and returned the qualified MeshTower build over its own console. The remaining ACM endpoints independently identify the RAK3401, XIAO nRF52840, Heltec V4, and T1000-E.

ModemManager had restarted after the Pi reboot and could probe otherwise idle radio consoles. Six serial-bearing boards now use stable-serial-only ID_MM_DEVICE_IGNORE rules. Because the Indicator CH340 has no stable serial, its separate rule requires the exact current physical port chain and exact 1a86:7523 identity; there is no broad CH340, product, or VID/PID-only rule. A controlled ModemManager restart reported zero modems, displaced no TTY owner, caused no USB disconnect/re-enumeration, and left all seven endpoints ignored. The same mctomqtt process retained sole ownership of the RAK, while a subsequent Indicator TCP query still returned the expected board and build. Reversible root-only backups were retained on the Pi before each rule update.

"},{"location":"test-results/hardware_validation_checklist/#run-wide-gates","title":"Run-wide gates","text":""},{"location":"test-results/hardware_validation_checklist/#compute-placement-gate","title":"Compute-placement gate","text":""},{"location":"test-results/hardware_validation_checklist/#lora-controlled-host-recovery-gate","title":"LoRa-controlled host recovery gate","text":"

The sanitized host-recovery evidence bundle is under out/host-actions-live-20260831/. Its SHA256SUMS manifest has SHA-256 5acff71ed98f483bb769e8c99c577bf358d0491355ac72855ee7093cd658bf21, and sha256sum -c verifies every deployed-hash, pre/post-state, radio, OTA, network, and reboot record in that manifest.

"},{"location":"test-results/hardware_validation_checklist/#mercerwood-pi-usb-power-cycle-gate","title":"Mercerwood Pi USB power-cycle gate","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-xiao-nrf52840","title":"Seeed XIAO nRF52840","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-tracker-t1000-e","title":"Seeed Tracker T1000-E","text":""},{"location":"test-results/hardware_validation_checklist/#rak3401","title":"RAK3401","text":""},{"location":"test-results/hardware_validation_checklist/#rak4631-reduced-profiles","title":"RAK4631 reduced profiles","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-t096","title":"Heltec T096","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-meshtower-v2-with-sd","title":"Heltec MeshTower V2 with SD","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-v4","title":"Heltec V4","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-sensecap-indicator-lora","title":"Seeed SenseCAP Indicator LoRa","text":""},{"location":"test-results/hardware_validation_checklist/#cross-radio-lora-ota-acceptance","title":"Cross-radio LoRa OTA acceptance","text":""},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/","title":"Hardware validation esp32 web console ota 2026 10 05","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#esp32-web-console-and-browser-ota-2026-10-05","title":"ESP32 web console and browser OTA - 2026-10-05","text":"

This is a local qualification checkpoint, not a release approval. GitHub writes and publication of 1.17.1.9 remain on hold. Firmware source for the final runs is 7821329851f69b6b945f061bc4d15212d43c3198.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#coverage-gap-and-reproduced-failures","title":"Coverage gap and reproduced failures","text":"

Earlier component and simulated-peripheral tests did not establish the complete V4 browser flow. In particular, accepting a Started reply did not prove that a normal WiFi client could discover the hotspot or connect to its HTTP server. The actual embedded uploader also had to receive its completion response before the radio restarted. Those boundaries needed physical qualification.

The physical V4 runs found three failures:

Host regressions also cover background ESP-NOW retries undoing an OTA pause, WebConfig teardown retaining OTA's WiFi, upload ownership and interruption, failed binds, and complete CRLF replies through the stock web console client. Negative controls fail with the old AP protocol, TCP binding, callback reboot, USB sleep guard, or line delimiter behavior.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#final-physical-observations","title":"Final physical observations","text":"Check Result and boundary V4 actual browser upload PASS. Chromium ran the unchanged embedded AsyncElegantOTA page. Multipart upload returned HTTP 200 OK; normal reboot loaded a distinct target version. USB answered afterward without another reset or unplug. V4 OTA startup PASS for WiFi off -> OTA, WebConfig setup AP -> OTA, ESP-NOW coexistence, WebConfig LAN -> OTA, and an active LAN uploader -> OTA AP. The returned URLs served real pages across deferred cleanup. V4 USB idle PASS for 600 seconds with no transmitted commands or keepalives, power saving on, USB logging on, and DTR asserted. Subsequent query and close/reopen passed; uptime advanced from 111 to 717 seconds. V4 DTR deasserted PASS for immediate queries, 30 seconds idle, and reopen using an opener that established RTS low before deasserting DTR. No reset or startup delay was added. XIAO S3 WIO Companion Restored from the temporary Repeater test role using an application-only flash. Partition signature and identity were preserved. All 60 read-only replies met the two-second deadline, including 10 held query pairs and 10 reopen pairs; maximum latency was 22 ms.

The V4 console runs above use the unchanged official SerialConsole through Node Web Streams and a physical Linux serial transport on Mercerwood. They do not exercise Chromium's OS Web Serial backend on that remote V4. Earlier physical XIAO browser tests are separate evidence, not a substitute for it. The client is pinned to meshcore-dev/flasher.meshcore.io revision 45a5257bf792b11a83f4c51c18d4f312addfb677, with lib/console.js SHA-256 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9.

The first disposable LAN association failed. A separate run with a fresh SSID, explicit WPA2/RSN CCMP configuration, AP readiness, and credential readback passed on the same firmware. The first failure is retained; these observations do not identify which test-network difference caused it.

Two DTR-deasserted first-query runs also failed. Their original pyserial opener briefly presented the USB hardware reset combination; the boot log recorded USB_UART_CHIP_RESET. Correcting the opener's line-state order passed without a reset or settle delay. These are retained test-fixture failures, not evidence that the user's intermittent console hang has been fixed.

During the 600-second idle run, dispatcher flags changed from 0 to 8. The radio remained in RX with no receive errors, zero received packets, and a configured five-minute receive-silence watchdog. This is consistent with the existing radio recovery policy on a quiet channel; uptime proves the MCU did not reboot. It is not an error-free RF stress qualification.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#artifacts-and-host-checks","title":"Artifacts and host checks","text":"

V4 application installed by the successful browser run:

XIAO application restored after console testing:

Both firmware builds passed. On this source, focused host checks passed: OTA startup 7, OTA lifecycle 15, async web lifecycle 2, official web console 3, WebConfig 40, and V4 partition migrator 4. PlatformIO was run serially.

Private raw logs and safe reports are retained under /tmp/meshcore-hil-20261004/web-console-investigation, including the failed runs. The index 782-evidence-index-safe.json records evidence hashes. Temporary V4 settings and the Pi network are restored after testing. The G2, its recovery ACLs, and its running MQTT services were not changed by this run.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#remaining-limits","title":"Remaining limits","text":"

The subsequent current Full V4 native browser qualification adds actual Pi Chromium Web Serial, the unchanged setup GUI's five-second deadline, and measured fresh-flash and power-saving console idle results. The older source and boundaries above remain historical evidence. Neither report qualifies every host OS, physical USB adapter, cold power cycle, ESP32 board, firmware profile, or radio traffic load. USB-attached console checks do not establish CPU sleep, measured current or button wake. The subsequent V4 button-wake profile correction passed host/build validation and another native GUI fresh-flash plus 180-second open/closed console check, as recorded in that linked report. On 2026-10-06 the user separately confirmed V4 button screen wake; its firmware revision and CPU sleep state were not specified. The operator USB tests included no actual button press or sleep/current measurement.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#release-candidate-follow-up-recorded-2026-10-07","title":"Release candidate follow-up recorded 2026-10-07","text":"

The following source-scoped observations were originally recorded in the 1.17.1.9 candidate release notes. They are retained here as historical test results, including failed attempts and their separate recoveries. Build and publication status in this snapshot does not override the final release notes. Older passing images do not qualify a later firmware revision or turn an earlier failure into a pass. Latest-source V4 hardware was unavailable for a repeat; that is a physical coverage limitation, not a failed latest-source test.

The earlier c5 GitHub unit workflow passed all four jobs. Host regressions exercise production OTA callbacks, the official configuration GUI and app protocol behavior, including MD5 ordering, rejected second files, incomplete POSTs, cleanup and subsequent valid uploads. Sanitizer fixtures cover these OTA lifecycle cases. These checks use MeshCore's actual-code fixtures; MeshBench is not integrated.

For 1e421ff4, the coupled production WebConfig credential interception, NVS commit, CLI routing and MQTT consumer fixture passed four tests, including fresh portal-only setup, legacy fallback, reconnects, raw-PSK boundaries, failed and interrupted writes, and mutations that restore the old credential lookup. The broader WiFi and MQTT host suites passed 62 and 14 cases, respectively, with some overlap. The WebConfig host suite passed 53 cases, including the embedded page in an isolated Chromium headless browser. Seven infrastructure power-lifecycle cases and six portal-ownership cases passed. Five V4 setup-page cases include actual button/session handling under both signed and unsigned char. The new tests are explicitly included in the unit workflow. The current GitHub unit workflow at documentation and host-test commit 4c5f58f4 passed all four jobs; its firmware source remains 1e421ff4. The completed local release passed the strict package checks below; V4 OTA and G2/V4 portal-to-MQTT hardware checks remain pending.

The completed local all-board release has 463 qualified recipes and 926 firmware files. Its complete checksum inventory, capability and memory checks passed independent read-only revalidation. All 94 exact ESP32 board/role migration ZIPs and their combined archive passed validation. The 114 nRF52 infrastructure targets each have a qualified Full/Reduced pair with matching actual DFU identity, firmware and flash layout. These results qualify local packages from 1e421ff4; they do not qualify the pending physical upgrade paths or publish the release. The newer bd4cbf281 source protects ordinary setup/OTA APs from the SDK's shared long-range WiFi flag, defers conflicting ESP-NOW starts and keeps long-range protocol writes out of saved WiFi settings. It reports an active ESP-NOW conflict before starting an AP. The subsequent 8c4e09275 source also defers MQTT station maintenance and Full Companion station recovery while an AP owns WiFi. Final builds and hardware qualification of this candidate remain pending. Final public assets and catalogs still require their separate inventory and digest checks.

The physical G2 and V4 tests used the unchanged embedded browser uploader and the normal host console, retaining the stock 5-second first-command deadline. The earlier c5fe8a0e applications passed AP uploads with HTTP 200/OK, automatic restart, and preserved identities, ACLs and saved settings. The G2's initial upload completed in 19.053 seconds; the V4's completed in 16.392 seconds. First ordinary replies took 1.385 and 1.010 seconds, respectively, without a cable replug or reset rescue.

On the G2, a full valid first file followed by a second file returned HTTP 409 without an observed MCU restart. A full-file request with an incomplete closing multipart boundary also left uptime continuous. A valid upload then passed through that same running listener in 18.767 seconds. Full-image wrong-MD5 requests had already returned HTTP 400 without restart on parent 70bf4bdf. The tests do not claim that rejected requests leave the inactive flash slot untouched. A fresh 385-second sample of unmodified Cisien meshcoretomqtt had all four brokers connected, zero publish failures and no service restarts; both the original and recovery Admin ACLs were preserved. The earlier G2 LAN upload stall is treated as a local slow-WiFi limitation, not a release blocker. LAN upload was not qualified by these AP tests.

The new 1e421ff4 G2 Full Observer application also passed the unchanged native AP uploader: HTTP 200/OK in 18.437 seconds, firmware-owned restart, and first ordinary ver reply in 1.304 seconds. Live EndF body identity, the exact G2 target and source were verified, with the original 15 settings, both Admin ACLs, services and the Pi's exact SlowFi connection preserved. This qualifies that AP update, not fresh WebConfig provisioning into the on-device MQTT consumer; that check and the new-source V4 checks remain pending.

On parent 70bf4bdf, both G2 and V4 passed four 600-second quiet intervals: port open and closed with logging off, then open and closed with USB logging, all with power saving enabled. Uptime remained continuous, with no host keepalive writes during the intervals. The V4 reported radio-watchdog event bit 8; this was not an MCU reset or an all-zero-error result. Its USB-logging open interval received 26 bytes. The V4 also passed the first unchanged native setup-GUI connection immediately after a fresh flash, with the first clock reply in 73.3 ms. These quiet and fresh-flash checks were not repeated on c5fe8a0e, whose change was confined to OTA completion behavior. The newer canonical-WiFi source 1e421ff4 has the narrower G2 results below.

On 1e421ff4, the G2 passed one 600.022-second open-port quiet interval with USB logging and power saving enabled, with no host writes and continuous uptime. The following 600-second closed-port interval ended in a stock-browser reopen failure at the test helper's 5-second acquisition gate, before ver was sent. The host then had VMIN=1/VTIME=0; the ordinary exclusive serial client still replied in 21 ms, with continuous uptime and unchanged core error flags. This is an unresolved browser/native-port acquisition result, not a qualified closed-port pass or evidence of an MCU crash. A cleanup-overlay bug in that test helper was also found and fixed for the instrumented repeat; the earlier failed receipt is retained.

The instrumented G2 repeat on the same 1e421ff4 source passed both 600-second open and closed quiet intervals with USB logging and power saving enabled, continuous uptime and no host keepalive writes. Original settings and the host were restored. The maximum stock Enter-to-reply time was 3.584 seconds. The second native port open took 5.532 seconds; click-to-connected took 8.201 seconds. That acquisition time is separate from the unchanged 5-second command deadline, and the helper's connected wait started after its click RPC returned. This repeat does not reclassify the earlier failure or establish its cause. The separate logging-off run also passed both 600-second open and closed quiet intervals with power saving enabled, 85 successful queries, continuous uptime and restored original settings. Together the completed runs qualify all four quiet intervals on this G2 and source.

The subsequent 100-cycle acceptance attempt stopped after 32 completed cycles. All 86 stock commands passed, with a maximum Enter-to-reply time of 450 ms and no observed MCU reset. The failure was an automation timeout while clicking the stock close-overlay button; this is separate from the stock command deadline. At the failed-trial snapshot, the UI had marked itself disconnected while native close had not started. Native close subsequently fulfilled in 105.5 ms, and the serial port and page closed. The cause of the earlier disconnect or click delay remains unproven. The failed receipt is retained; 100-cycle qualification and follow-up diagnosis remain pending.

A separate Linux Chromium 139 comparison isolated an inherited host TTY setting: VMIN=0/VTIME=0 produced device-lost stream termination, while changing only VMIN to 1 gave a stock ver reply in 154.1 ms without reset or replug. That finding does not establish the cause of the later reopen failure above. See the Linux Web Serial report for primary browser-source evidence and the bounded host preflight.

The physical RAK3401 Companion on 70bf4bdf passed 21 malformed-request cases with 217 request/reply frames. Its Full Repeater passed the official native setup GUI and 600-second open/closed quiet intervals with power saving on, continuous uptime and zero core error flags. It was returned to the then-current c5fe8a0e Full Companion; the first stock SDK reply took 25.9 ms without an initial retry or settling delay. Original identity, contacts, channels, custom variables, RX power-saving settings and actual packet-stream mode were restored. The planned reboot needed to restore stream mode was separate from the stability measurements. An initial SWD programming failure was recovered and verified; this was not a first-attempt programming pass. These tests did not qualify RAK3401 BLE DFU, LoRa OTA or bootloader updates.

The actual published 9053038f 1.17.1.8 Full V4 installed newer applications over both AP and a controlled shared WiFi LAN, but its old page did not receive HTTP completion. Check ver before retrying a stalled old page. These trials used already expanded application slots; actual phone-hotspot uploads remain untested. The requested legacy small-slot G2 trial now has a verified existing 16 MiB backup and genuinely programmed and verified the official 1.14.1 stock components and small-slot table while preserving NVS. Its first stock USB ver request reached the five-second host command limit; a later read-only probe replied without reset. A separately recorded stock setup restored and verified the original identity, 22 common settings and two Admin ACLs after normal reboots. Password setters were confirmed and the guest password persisted; stock has no admin-password getter. The final start ota returned its expected URL, but the test's AP scan timed out. A subsequent completed native scan found no MeshCore-OTA AP at any BSSID. A later read-only NVS audit identified the test starting-state problem: earlier preconditioning had saved the WiFi long-range protocol flag, which stock inherited. Its long-range AP beacons cannot be received by the ordinary WiFi clients used here. This does not establish a stock AP startup bug. A separately recorded correction changed only the fresh live NVS protocol flag and its entry CRC, with exact readback and unchanged stock components and small-slot table. After stock identity, common settings, ACL and guest/ private-identity checks, a completed fresh scan found the exact G2 OTA AP. This establishes the genuine stock WiFi-only starting boundary. The earlier failed receipts remain failed. The first native migration controller then failed on a missing host iw executable before any HTTP or image-upload step. It may already have associated with the AP; that outcome was not verified. Its return to SlowFi and owned guard cleanup passed. A separately reviewed retry retained the firmware/controller inputs, separately pinned the old runtime log and checked host executables before using fresh guards. It associated with the exact stock AP and observed one native bridge-image POST with the exact bytes, SHA256 and MD5, but the old embedded page did not complete its HTTP response before the uploader deadline. Browser closure, return to SlowFi and guard cleanup passed; that transaction remains failed. A later scan found the exact migration AP. A separately recorded stage2 continuation then verified the actual expanded-layout-ready page and uploaded the exact 1e421ff4 ordinary Full application through the unchanged native page, with matching MD5 and HTTP 200/OK in 14.526 seconds. That upload phase also passed its network return and guard cleanup. The subsequent controlled Pi AP phase failed on a host FileNotFoundError before any final HTTP request. The host's scheduled WiFi refresh also interrupted that AP phase. Its later guard-file cleanup refused an unproven network state; a separate read-only qualification and owned-file cleanup then verified the original network, dead controller and removal of all prior guard profiles and files. A new proof-only attempt stopped before AP activation because reloading services moved the host hold timer's relative deadline. Its failed receipt is retained, and the held host script was restored. The next proof used a fixed absolute hold deadline, but stopped before AP activation when a host-file permission check rejected the actual private service file; its failed receipt and host restoration are preserved. A third proof verified and ran the isolated Pi AP with the independent return timer armed, then reached the five-minute client/DHCP discovery deadline without a verified G2 client or any HTTP request. It returned to SlowFi, removed its owned network resources and restored the host's scheduled WiFi recovery script. Final running-image/layout/identity/settings HTTP verification remains unproven; the successful firmware upload does not establish that final verification. After that completed failed WiFi check, separate USB maintenance installed the 8c4e09275 ordinary Full Observer application. Full application readback and the normal running version matched, while the bootloader, partition table, NVS, OTA selection and checked filesystem region stayed unchanged. This maintenance does not qualify the WiFi-only upgrade. The saved-settings check found RX boosted gain off instead of its original on; the other 21 common settings, identity, passwords and two Admin ACLs matched. A separate checked CLI repair restored gain on and verified all 22 settings. The first extras restoration then missed the five-second CLI deadline while changing bridge source. A later read-only probe found continuous uptime, zero core error flags and the requested source value; it does not establish the missing command acknowledgment or its save duration. After checking that state, a separate restoration completed with the original WiFi pair, saved extras, all 22 common settings, identity, passwords and both Admin ACLs verified. The MQTT worker was running with valid effective configuration; broker connectivity was not tested. Original TTY settings, all three host services and SlowFi were restored, and the exact temporary host executable alias was removed. The earlier timeout and WiFi proof failure remain recorded. The loader correction is included in 88c851080. Its G2 ordinary Full Observer application was independently read back in full after installation, and normal boot matched the exact new runtime and EndF footer. The checked lower 64 KiB, OTA selection and first 64 KiB of the filesystem stayed unchanged. A separate read-only post-boot check verified the original identity, passwords, two Admin ACLs, all 22 common settings, seven supported bridge fields and saved extras. Core error flags were zero; the MQTT worker was running with valid configuration, without a broker-connectivity claim. Original TTY settings, all three host services and SlowFi were restored. This USB maintenance does not qualify the failed WiFi-only final verification. The fresh all-board build and V4 hardware checks remain pending. These are separate outcomes; the initial failed transactions remain failed. The earlier c5 AP upload success does not turn those old HTTP-completion failures into passes. The upgrade guide keeps the old and new updater behavior separate.

Original host services, temporary network changes and owned test resources were restored after the historical completed hardware checks. The current new-source V4 native AP OTA trial has not yet produced a verified result; its return to SlowFi and original host-service state also remain unverified. The legacy G2 check established the stock starting boundary with its original identity, common settings and ACLs verified, then completed the expansion and Full application upload over WiFi. It has not yet qualified the final running image through HTTP. Post-verdict 88c851080 G2 firmware readback, normal boot, original-settings acceptance and host restoration passed separately over USB. The all-board build and current V4 hardware qualification remain pending. These checks keep publication pending. Build qualification does not establish physical compatibility on every board. Quiet USB trials do not measure MCU sleep/current draw, stack high-water, sustained log backpressure or long-term fleet reliability. Linux native-browser tests required host TTY preparation; they do not qualify every browser or operating system. Hardware CDC has finite buffers, and closing a Linux TTY does not necessarily produce a hardware session reset. See the historical V4 console report and USB backpressure results for their exact source revisions and measured scope.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/","title":"Hardware validation esp32 web console startup 2026 10 05","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#esp32-native-browser-console-startup-2026-10-05","title":"ESP32 native browser console startup - 2026-10-05","text":"

This is a local validation report, not a release approval. No GitHub upload, website deployment, or release publication is part of these results. The GUI time-sync connection timeout was reproduced on native USB. After the boot filesystem correction, the unchanged GUI's first fresh-flash connection passed on both the Heltec V4 and XIAO ESP32-S3. The final V4 600-second idle run also passed with power saving on, logging off, and no transmitted command during idle. Earlier terminal runs establish different boundaries and are recorded separately. The V4 core error flags changed during idle and are retained below; this is not a claim of error-free radio operation.

A later user screenshot identifies the affected surface as the Repeater / Room server USB setup GUI at config.meshcore.io, with Cannot connect: Command timeout: time <epoch>. The initial physical observations used the terminal at flasher.meshcore.io/console. Native tests of the actual GUI were added after identifying that mismatch and are recorded separately below.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#native-test-boundary","title":"Native test boundary","text":"

The physical target was a Heltec V4 ESP32-S3 Repeater attached to Mercerwood. Chromium 154.0.8037.92 ran on the Pi and used its actual OS Web Serial backend, native SerialPort objects, and native readable/writable streams. Playwright drove the full Vue website and recorded the UI and loader lifecycle. This is additional coverage beyond the physical Linux serial/Node Web Streams tests in the earlier OTA report.

The baseline firmware source was 7821329851f69b6b945f061bc4d15212d43c3198. The unchanged website was pinned to upstream commit 45a5257bf792b11a83f4c51c18d4f312addfb677. Source hashes were checked before flashing, including these production files:

File SHA-256 lib/console.js 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9 js/flash.js e9762a29f6a91f8e33736e02ff41a81cd49e7b96fe253a5d71cba7daefd46d5b lib/esp32.js 2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6

Only the unavailable headless permission chooser was replaced with selection of the single, already granted V4. Before each application-only flash, native ROM reads checked the 4096-byte partition table and OTA metadata. The table hash was 9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60; valid OTA sequences 3 and 4 selected app1 at 0x650000. The UI's application address was changed to that verified active slot. Full erase was disabled. The actual stock loader write, native reset sequence, and disconnect remained unchanged. No filesystem erase or additional backup was needed.

In the initial terminal experiments, the first command was sent after native open resolved with a 30-second deadline. The actual setup GUI kept its stock 5000 ms command deadline. No arbitrary startup sleep, extra reset, unplug/replug, keepalive, or automatic command retry was added to turn a failure into a pass. A first failure would remain a failure even if a separately recorded reopen recovered. USB-open readiness and firmware reply latency are separate measurements.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#completed-physical-observations","title":"Completed physical observations","text":"Check Observation Baseline fresh application flash, same native port handle PASS. The first ver reply took 9398 ms. Subsequent power-saving and core-stat queries passed. No unplug or additional reset was needed. Progress 100 preceded flashEsp32 completion by 604 ms. Baseline power saving on, logging output off, 600 seconds idle PASS with no transmitted command or keepalive during the idle interval. The post-idle ver reply took 5544 ms. Uptime advanced from 119 to 741 seconds, so the MCU did not reboot. Power saving remained on and logging remained off; temporary settings were restored afterward. Stock website Close clicked immediately at the success heading, live site PASS after reload; first ver took 1558 ms. The heading appeared 61.8 ms after progress 100, while both native streams were still locked. The original document continued through reset and disconnect; disconnect finished 987.3 ms after progress 100. Same Close test with identical pinned HTML served locally at the official origin PASS after reload; first ver took 1418 ms. The heading appeared 33.5 ms after progress 100 with both streams locked. Reset/disconnect still completed before navigation replaced the original document. The served index, app, and console hashes matched the unchanged pinned source. Actual browser tab closed at the stock success heading, new tab in the same profile PASS; first ver took 1575 ms. The tab-close request was emitted 126.5 ms after progress 100 with both streams locked. Reset and disconnect had not completed before that request. This used normal Page.close(run_before_unload=False), not the website Close/reload handler, browser shutdown, or a renderer kill. Local fixed terminal UI 452e6e7, final startup firmware 163364e52, actual tab close and 600 seconds idle PASS. Completion was exposed only after reset/disconnect, with native readable/writable streams already absent. First ver took 1408 ms; the post-idle reply took 1064 ms with power saving on and logging output off. Core-stat queries and preference restoration passed. This is terminal qualification, not GUI qualification.

The stock completion display is demonstrably premature: progress 100 does not mean the loader has finished its tail, reset, and port release. However, both site Close tests and the actual tab-close test passed on this physical V4. The mock's canceled-tail ordering is not evidence that the physical device was left permanently hung. In particular, the tab-close request time is not the same as the time Chromium finished closing the target.

The qualified fresh-flash application SHA-256 was 912c1c5d80497de04a4ebd5d3a81b9275181ab2bb3bfb7f22e74c692ef0c5d8a. The three completion/close experiments used the 2,093,336-byte application ebee70b05528b21aaaeddf8bc80f2762b7ad767ff5b6f29eadf8e2a78e90859d. Those terminal-only baseline passes did not establish a failing-baseline / passing-fix comparison for the affected GUI. The later unchanged-GUI comparison below provides that first-connect evidence.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#firmware-startup-correction-and-synthetic-coverage","title":"Firmware startup correction and synthetic coverage","text":"

The startup changes are local firmware commits:

The startup predicate is captured when the native reset interrupt occurs, before delayed framework delivery can reclassify an initial enumeration as an active-session reset. Boot diagnostics and early RX do not end startup. The first application command-service loop ends startup; resets afterward retain the existing quarantine, queue purge, and session cleanup. Early command RX is retained until that loop can service it.

Delaying hardware USB data attachment through application setup is not a safe way to hide this startup boundary from the stock flasher. After progress 100, the unchanged loader still needs the native interface for RTS/reset and port cleanup. Withdrawing that interface during its tail can break the handoff. The selected correction keeps the transport available and retains first RX until command service is ready instead of forcing another physical reconnect.

Synthetic tests execute the pinned real driver ISR, production owner predicate, event handling, and session cleanup against host peripheral/RTOS stubs. They cover early and delayed enumeration, boot TX, boot RX, first-command retention, and a real reset after application readiness. Negative controls remove the startup gate, remove capture-time classification, arm on boot traffic, or leave startup suppression active forever. The removed protections cause the expected failures. Driver source pins, mode/version scope, unchanged SDK files, and idempotent build-local transforms are also checked.

The startup synthetic suites and V4/C6 builds passed. The earlier V4 full Repeater build at 163364e52 passed its linked capability and memory checks. Its application hash is 41ed8598aa2fb1b7aa5d320157249dc03a6e543051257c8b1d7d34d8e4c26df6. The 3.x transforms only add startup reset classification; they do not import the 2.0.17 TX/PHY backport or claim its guarantees. Unreviewed framework versions keep their existing driver. C6 ISR tests and compilation do not substitute for physical C6 qualification.

Five sequential qualified builds passed at source base 631f2ad97, with the local test version v1.17.1.9-hil-bootfs-631f2ad9-631f2ad9. Their generated capability/memory reports and application artifacts were retained privately. This test version is not a published release.

Requested build Resolved environment and scope Full heltec_v4_repeater heltec_v4_repeater_observer_mqtt, PASS Full Xiao_S3_WIO_repeater Xiao_S3_WIO_repeater_observer_mqtt, PASS Full heltec_v4_room_server heltec_v4_room_server_observer_mqtt, PASS Ordinary Xiao_C6_repeater_ Complete supported profile with pinned Arduino-ESP32 3.1.3, PASS Explicit Full XIAO S3 Companion Xiao_S3_WIO_companion_radio_full, PASS

The two physical unchanged-GUI first-connect passes below used the S3 Repeater images. The Room server and C6 results in this table qualify compilation and packaged capabilities, not physical GUI operation on those targets.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#local-website-correction-and-browser-tests","title":"Local website correction and browser tests","text":"

Website commit 452e6e7 is local in a separate flasher checkout; the pinned baseline source remains unchanged and the website has not been deployed. Its three production changes are limited to index.html, js/app.js, and lib/console.js:

All 16 full Vue/Chromium lifecycle tests passed, including five negative controls. They run the production UI, console, Web Streams, and flash/reset wrappers with mocked USB peripherals and loader data transfer. The immediate Close test uses a DOM MutationObserver so locator delay cannot miss the early completion race. Coverage includes delayed/failed open, nonfatal RX recovery, fatal RX, rejected writes, cancelable and uncancelable pending writes, slow reader cancellation, close errors, and flash-tail failure. The five controls restore progress-only completion, early editable input, missing RX recovery, missing writer cleanup, and the old fixed 50 ms close delay. Each fails for the intended missing protection. These tests establish client lifecycle behavior; they do not reproduce the user's physical hang.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#actual-setup-gui-handshake-and-regression-comparison","title":"Actual setup GUI handshake and regression comparison","text":"

The live setup GUI matches upstream meshcore-dev/config.meshcore.io commit aff42ad8ef2b332f24d644ff6a783f8272a88ff2. The initial path is GUI Connect -> native port open at 115200 -> held reader/writer -> time <epoch> -> getData. There is no explicit DTR/RTS call on GUI open. The command is ASCII followed by CR only. Its 5000 ms timer is armed before awaiting native writer.write, so both write completion and response parsing must fit that deadline. The firmware Repeater parser accepts CR and ignores LF; CR-only is a supported input path.

GUI source SHA-256 index.html efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d src/gui.js 919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800 lib/serial-cli.js fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870

The actual GUI page is preloaded before a separate tab runs the unchanged stock flash backend. After that backend returns, the GUI Connect button is clicked without reloading the GUI, inserting a settle delay, or changing its timeout. Each official origin is granted only the single intended native device. Raw GUI debug output and app.device are not retained: getData reads private keys and passwords. Lifecycle observations record command categories, byte counts, locks, timeouts, and safe numeric statistics without secret values.

Completed GUI configuration reads mean the unchanged getData flow returned and the GUI became connected. They do not exhaustively validate every field: the stock GUI can catch optional private-key, region, and ACL errors or skip an unsupported variable. The final XIAO run recorded no command-error events and completed its radio, region/default, and ACL commands as well as initial time.

Actual GUI run First attempt and separate recovery V4 163364e52, fresh flash FAIL. Initial time timed out at 5022 ms, and explicit stock cli.disconnect followed by GUI reconnect timed out at 5007.3 ms. Writes themselves completed in 4.7 and 29.3 ms. Only 147 boot RX bytes arrived; no command echo or reply marker was observed. Both locks remained held and GUI connected remained false. Same V4 source, separate settled no-flash control PASS. Initial time completed in 50.2 ms and the full GUI configuration read completed. This was a separate control, not a successful first fresh-flash attempt. V4 cooperative GPS source c8c5f87ab, fresh flash FAIL. Initial time timed out at 5024 ms. A separately recorded clean reopen completed time in 3336.2 ms and the full GUI read passed. Recovery does not change the first-attempt result. Local XIAO ESP32-S3 Repeater, fresh flash FAIL. Initial time timed out at 5001 ms. Separate clean reopen completed time in 3983.4 ms and the full GUI read passed. Native local Chromium and the target's own verified partition/active slot were used. Same local XIAO, separate settled no-flash GUI control with 600 seconds idle PASS. The initial GUI time completed in 9.9 ms and the full configuration read passed. With power saving on and logging off, no native writes occurred during the 600-second idle interval. Uptime advanced from 177 to 777 seconds, errors stayed at 0, and a visible GUI disconnect/reconnect reread passed. Original preferences were restored. This does not change the failed fresh-flash first attempt. V4 normal, untraced firmware 631f2ad97, fresh flash and 600 seconds idle PASS. Initial time completed in 420.7 ms and the full unchanged GUI read flow completed. With power saving on and logging off, zero native writes occurred during 600 seconds idle and uptime advanced from 18 to 618 seconds. Core err_flags changed from 0 to 8, setting the radio-watchdog bit. Visible GUI Disconnect/Connect completed a full reread, original preferences were restored, and final awaited Disconnect left port/reader/writer absent. XIAO normal, untraced firmware 631f2ad97, fresh flash PASS. Initial time completed in 3110.6 ms and the full unchanged GUI read passed. Core statistics reported uptime 4 seconds, errors 0, and queue length 1. No diagnostic reopen, extra reset, unplug/replug or timeout change was used. Final visible Disconnect completed with port, reader and writer absent.

The native V4 failure is not just a pending write: both writes completed quickly. The second attempt began about 10.48 seconds after the stock flash backend had returned and still failed on 163364e52. A five-second GPS wait alone therefore does not explain every observation. Cooperative GPS discovery removes that real startup block, but its first GUI attempt still failed physically. That remaining gap required the direct startup-phase diagnosis below. The short GUI deadline and the terminal's 30-second test deadline are material differences in coverage.

The final driver preserved the original command order and native calls. Source checks, permission selection, and ROM preflight occurred before application flashing; no settle sleep or additional USB operation was inserted after the stock flash backend returned. Automation and native-open latency are separate from the time command's elapsed response measurement: the final XIAO time began about 71 ms after backend return, while the V4 time began about 3586 ms later. Their 3110.6 ms and 420.7 ms reply times begin at command entry. Passing these cases does not claim that native port open or first command always occurs at zero elapsed time after a flash.

The final V4 application SHA-256 was ffb0a00b97b382038db758954a38af464efe6b00bb88784936f9cc7c4b54db39. Its final run recorded no client command-error events and no diagnostic reopen. The final XIAO application SHA-256 was b00ffbd0b7154f29741c3d93110705b33d3d34dcfa392ba7bd0e8e03d8cac9ce. Its verified partition table hash was f4e3b6cfe370c81dca210d57f3c463f924a539842a19fff6eda4615278ee97a2, with parsed active app0 at 0x10000; the image fit that slot. Native Chromium 140.0.7339.186 ran locally on the VM. The V4 used the Pi's native Chromium.

In the stock GUI, a failed handshake alerts without disconnecting. The visible connected state stays false, while the native reader and writer remain owned. Retrying Connect can therefore encounter its own still-open port. The explicit cli.disconnect/reopen in these experiments is separately labeled diagnostic recovery, not an automatic retry added to the user's first handshake.

The hardware-free full Vue reproducer also shows the exact time alert when the mock initial write is delayed 9400 ms. The stock 5000 ms deadline fails while the native-style writer is pending; a separate runtime 15000 ms deadline control completes the same time command. That control is an ordering proof, not an unchanged-client fix or a reproduction of the native cause.

New CI coverage in test/test_official_config_gui.py executes the full SHA-pinned stock SerialCLI and the exact GUI connect/disconnect functions in Node. Actual production GPS init elapsed time controls command admission, and the production Repeater command pump/CommonCLI time branch generate byte-fragmented replies. The actual boot filesystem inventory and production recovery/presence paths also supply a deterministic metadata-cost workload. Its absent native scans cost 129 ms each plus 1000 ms setup settle: one inventory scan yields 1129 ms, while the same workload without inventory performs 40 scans and yields 6160 ms. These are modeled readiness costs, not predicted full physical boot times. All six tests passed in local commit b259b1055. The four negative controls restore blocking GPS, omit inventory, replace the CRLF reply with LF, or regress CR command recognition. Each fails at the stock client-visible 5000 ms initial-time boundary. getData is a callback witness there; full Vue fields, enumeration, and other physical setup latency still require the native tests. No complete third-party bundle is vendored.

The diagnostic V4 phase run retained its first time failure at 5026.1 ms and separate recovery at 2136.8 ms. Recovery core statistics reported uptime 16 seconds, errors 0, and queue length 1. The firmware's own millisecond phase timestamps locate setup work before the first CLI service:

Measured startup phase MCU interval and elapsed time Sensors 2815 -> 2829 ms, 14 ms Dispatcher 2829 -> 3714 ms, 885 ms Preferences 3714 -> 8261 ms, 4547 ms Management state 8261 -> 9941 ms, 1680 ms ACL 9941 -> 11104 ms, 1163 ms Regions 11104 -> 11362 ms, 258 ms Filters 11362 -> 12781 ms, 1419 ms First CLI entry, ready admission, and available RX All at 12796 ms

No blocked CLI admission was reported before that first readiness; the first blocked-gate marker was later, at 14147 ms. The measured setup duration, rather than a new reset requirement or the GPS block alone, was the baseline first-time deadline problem. These trace intervals identify where to investigate; they do not by themselves prove a particular filesystem or CPU cause, or qualify an faster startup path. Qualification of that correction came from the subsequent unchanged-GUI first passes, rather than from trace intervals alone.

A qualified second run of the same granular trace application recorded the inner preference and management phases. It retained the initial time failure at 5024.1 ms and a separate recovery at 1787 ms; recovery core statistics were uptime 17 seconds, errors 0, and queue length 1. Its first CLI service was at 12805 ms. The earlier trace does not contain these inner markers.

Qualified inner phase MCU interval and elapsed time Radio profile lookup 3714 -> 4232 ms, 518 ms OTA speed lookup 4233 -> 4492 ms, 259 ms Display settings lookup 4492 -> 5010 ms, 518 ms Common preference recovery 5010 -> 6571 ms, 1561 ms Existing common preference image, open through completed load 6572 -> 6576 ms, 4 ms MQTT preference recovery/read 6577 -> 8266 ms, 1689 ms Management route lookup 8267 -> 9043 ms, 776 ms Absent management reporter probes 9043 -> 9561 ms, 518 ms

These measurements separate slow missing-path metadata scans from the quick read of an existing preference image. No management reporter was allocated. The correction is a boot-only validated absence inventory, so known absent files can avoid repeated scans while existing-file reads, recovery, and mutation invalidation keep their native behavior. A failed or incomplete inventory falls back to native probes; existing-file errors remain delegated. Known absence is witnessed by the completed enumeration, so those cached negative lookups intentionally perform no later per-path metadata operation. The actual inventory negative controls and both normal-firmware unchanged-GUI first fresh-flash passes qualify the tested startup boundary.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#final-target-state","title":"Final target state","text":"

Final V4 readback reported uptime 718 seconds, active radio state 1(RX), zero received packets, one sent packet, physical RX errors 0, err_flags 8, and no outbound packet. This confirms active RX after the idle test without identifying the exact past watchdog trigger. Power saving was restored off, logging to USB, USB debug off, and MQTT and WebUI off/inactive. A manual get bridge probe was unsupported and is retained as such, not counted as a pass; the correct get bridge.enabled and get bridge.running readbacks both returned off.

The local XIAO was restored to the latest tested Full Companion image at source 631f2ad97, application SHA-256 cf77ff2824f8359ab5e690424e5d6181911f401f0fe7b9a827550f13c0610e2c. Native ROM preflight checked the target, partition table and OTA CRC selection of app0. The filesystem and partition table were preserved.

Settled USB verification passed 60 requests with zero missed 2-second deadlines and maximum reply latency 7.152 ms. It covered ten held app/core pairs, ten reopen pairs, and ten seconds idle. The identity hash matched the recorded original, settings hashes remained stable, no input flush was used, and all serial handles closed. This restore check includes the helper's existing 250 ms open-handshake wait; it is not the fresh GUI's 5000 ms startup proof.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#qualification-and-limits","title":"Qualification and limits","text":"

The affected first-connect timeout has a failing baseline and passing corrected normal-firmware result on both tested S3 boards. Diagnostic HIL_BOOT tracing was off in those passing images. The GUI timer and reset/reconnect behavior remained unchanged.

The final V4 fresh-flash 600-second idle test completed its uptime checks, configuration reread, preference restoration, and awaited final disconnect. The JSON field stats-core.errors is the err_flags bitmask serialized by StatsFormatHelper.h, not an event counter. Its change from 0 to 8 sets ERR_EVENT_RADIO_WATCHDOG (bit 3 in Dispatcher.h). It does not mean eight errors. USB remained responsive, but this evidence alone does not identify which radio-watchdog trigger path set the flag. The XIAO Full Companion restoration is complete.

The local XIAO unchanged-GUI 600-second no-transmit idle control completed successfully with power saving on and logging off. Its successful initial settled handshake, uptime/error checks, full GUI reread, and preference restoration are separate from the failed first fresh-flash handshake. The already completed fixed-terminal 600-second run is also separate evidence.

These results do not qualify every host OS, USB adapter, cold power cycle, firmware role, high logging volume, or physical ESP32 board. A matching initial GUI timeout was reproduced and the tested first fresh-flash boundary now passes; idle results apply only to their measured configurations and intervals.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#evidence-retention","title":"Evidence retention","text":"

The completed sanitized reports are retained privately under /tmp/meshcore-hil-20261004/web-console-investigation. This document contains no credentials, device identities, or raw console logs. Completed native reports and their SHA-256 hashes are:

Safe report SHA-256 pi-chromium-baseline-fresh-flash-qualified-safe.json d01b18ce6a467f704605832d7a8adc1653999718f7cb4e23d11a9dd9d7bbc414 pi-chromium-baseline-power-idle-600-safe.json 100d609d7a448ee223a86ec4ac6b2e14362f86d44ee2670b407815eff7c1b66d pi-chromium-stock-ui-immediate-close-safe.json 32bcc0a67ea7716e2a0a0f60bf9081c4d2800bac3a8fe61f3595ee5d34b077c0 pi-chromium-stock-ui-fast-reload-close-qualified-safe.json b44e5d4885bb598596d021342692292c55c3f6f7a426c532845742caf8cb7e18 pi-chromium-stock-ui-tab-close-safe.json 13557189ba0b68bb28837cc88b108d2df1536e583b96199027edeef307a7cadc pi-chromium-fixed-ui-final-firmware-idle-600-safe.json e1b3f14e5ae6d0762fc44ab4dc28b4b41a231415cae54b4cc5332f75bd46d926 pi-chromium-config-gui-baseline-fresh-flash-qualified-safe.json 7abf519696c55d5af14d59c6781c644d7016d36f13d7c05833c23ce82b16f041 pi-chromium-config-gui-baseline-settled-control-safe.json da85499cebfddac700696cdfdf9ad6d96970932801af469552a2d91899c3a637 pi-chromium-config-gui-gps-fixed-fresh-flash-safe.json 06c5525d66134cbbb460bf9e4e37e7ec74275559f1727f52cef889f074829e40 local-chromium-xiao-config-gui-fresh-flash-safe.json aa6c945f3d2c40b0db3e368a905ba38ab1a4d966f0d15b4d5848fccc88ff4f2b local-chromium-xiao-config-gui-settled-idle-600-safe.json 3e7b996f13978574c77819ab80132d5c4452f8c7490ebb55333c124887f903bb pi-chromium-config-gui-startup-trace-fresh-flash-safe.json 8062dfcdb202a2320c430e9f013f9510392f090a0883dc8fadbc296c92a46eed pi-chromium-config-gui-startup-granular-qualified-fresh-flash-safe.json 10b269c68ef1e56a6104376d6c0b4f671bbe230ec005425a09cc7805953e07b0 local-chromium-xiao-config-gui-bootfs-fixed-fresh-flash-safe.json b94a53a4d017e95688c0231474c0aa0c67bc091b5738e0100173441d108db262 pi-chromium-config-gui-bootfs-fixed-fresh-flash-idle-600-safe.json 8419f294aeeed087a43c1f062a109da2770f2dcfa79c52ac8aae72d82250cb05 bootfs-xiao-companion-restoration/flash-safe.json ae72ae2e3645735e44d46cb4d4a21920b7f669edb5571151e3a28ebafb89848c bootfs-xiao-companion-restoration/strict-USB-safe.json a8c56c8b7030ca5b5d18eed55f012e7227c9d234427e9e7df7e261b9cabb4dfa v4-bootfs-final-readback-safe.jsonl 6186871dfed0f0a62bc35dc004c8eb971a5cd4ab3b81c0e11ab2aa9db00afc38 v4-bootfs-final-bridge-readback-safe.jsonl b26b9d104cd853208aff685f528659d2b1aa35cd2c802c1dd31c8288a04700ae

The host reproductions are browser-console-lifecycle-mock-safe.json and browser-flash-ui-early-completion-safe.json. Their mocked peripheral and write-tail boundaries are recorded separately from native physical evidence. The GUI timing control is config-gui-investigation/gui-startup-timeout-mock-safe.json. Earlier GUI safe reports used an overly strict observational time_reply_ok equality check for plain OK; production returns OK - clock set: .... That metadata flag was corrected to accept the prefix and never controlled first-connect PASS/FAIL.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/","title":"Hardware validation heltec v4 native console 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#heltec-v4-full-native-browser-console-2026-10-06","title":"Heltec V4 Full native browser console - 2026-10-06","text":"

The normal Full Heltec V4 Repeater from source c88885a315208908e050c57fc0ec0a71ff10bd6f passed fresh-flash connection, 600 seconds with the console open, and 600 seconds with it closed through the unchanged config.meshcore.io setup GUI. Power saving was on and logging output was off during those idle intervals. A separate fresh-flash and 600-second open-console trial also passed with USB logging on. Both trials restored the original settings and closed their streams. Final operator readback and owned browser/host-service cleanup also passed. Release 1.17.1.9 remains held and unpublished.

The subsequent button-profile correction at ebf2f082 passed host/build checks and another native fresh-flash GUI connection with 180-second open/closed console intervals. That updated-source checkpoint is recorded separately below; it does not extend the older source's 600-second measurements to the new image.

On 2026-10-06, the user separately confirmed that a Heltec V4 button press wakes the screen. This is a user-reported manual confirmation; its firmware revision and CPU sleep state were not specified. It does not supply a current measurement or a V4-R8 manual result.

These results extend the earlier startup investigation to the current normal Full image and native hardware CDC. They are physical console stability checks, not a measurement of sleep current, radio traffic capacity, or every ESP32/host combination.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#image-and-build-gates","title":"Image and build gates","text":"

The requested target was heltec_v4_repeater, Full profile. Its resolved heltec_v4_repeater_observer_mqtt feature base retains the normal repeater update identity. Runtime readback had to match the complete version v1.17.1.9-halo-keymind-cascade-dev-c88885a3 (Build: 06-Oct-2026). The source includes the OTA upload correction in d2983762 and WiFi ownership correction in 95913c53. The GitHub unit workflow for c88885a3 passed; this does not replace the physical checks below or the final release matrix.

Artifact Bytes SHA-256 Full application 2,097,528 1f10f44149b3f56edbe0ecb2a2bab9b51da651a6d047e2c2c1555691468d2b4c Merged image 2,163,064 3264d7a1a5713755b5dd10ad1c81bb0588e4a072eeaa126b86307a42be77bb5b Matching ELF - 0545eef46da30a86f86531199a91e887167d463c5981f193c0455cd75d633538

The package hash, linked capability, and RAM gates passed, with 20,880 bytes of margin above the required heap budget. The linked application includes HWCDC, its retained-PHY handoff and startup owner hook; no TinyUSB USBCDC class is linked. Full ESP-NOW, WiFi/LoRa OTA, and USB logging capability checks passed. These are build checks; this run did not exercise those radio/OTA features.

Both application slots are 0x640000 bytes, at 0x10000 and 0x650000. The padded partition table SHA-256 is 9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60. The merged image contains that table and the exact audited application at 0x10000. The application fits either slot with 4,456,072 bytes of margin. The merged artifact was audited, but the physical tests wrote the application only; they do not qualify a factory erase or merged-image installation.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#native-browser-boundary","title":"Native browser boundary","text":"

Chromium 154.0.8037.92 ran on the Pi with native OS Web Serial ports and streams. The actual Vue GUI and stock SerialCLI were loaded before flashing. The stock DOM Connect flow after the stock flasher returned sent the normal plain time <epoch> request. Its unchanged 5,000 ms timeout covers both native write completion and the framed response. No additional reset, cable action, post-flash settle, command retry, or keepalive was inserted to turn that connection into a pass.

Website source SHA-256 GUI index.html efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d GUI src/gui.js 919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800 GUI lib/serial-cli.js fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870 Flasher js/flash.js e9762a29f6a91f8e33736e02ff41a81cd49e7b96fe253a5d71cba7daefd46d5b Flasher lib/esp32.js 2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6

Automation selected only the single preauthorized native device. Before each write, actual ROM reads checked the partition table and valid OTA sequence/CRC, then directed the application write to the verified active slot (app1 at 0x650000). The stock loader transfer, reset and disconnect remained in place; full erase was disabled. Phase instrumentation recorded existing calls without adding hardware operations. This qualifies that stock flash backend and native GUI connection, not the complete firmware-picker selection/erase flow.

Original public identity, the ACL response, and 15 checked saved preferences were compared with the original private reference after flashing and cleanup. Only the numeric channel-busy diagnostic counter was normalized in preference comparison. No raw identities, credentials or console replies are published.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#physical-results","title":"Physical results","text":"Check Result Fresh flash, then first GUI connection with logging off PASS. Plain time clock-set acknowledgement in 1,614.4 ms; complete GUI bootstrap and exact version/reference checks passed. Power saving on, logging off, console open PASS. 600.093 seconds with zero native command writes or keepalives. Uptime advanced 18 -> 619 seconds; the first post-idle core reply took 35.9 ms. A full DOM Disconnect/Connect reread acknowledged time in 23.6 ms. Power saving on, logging off, console closed PASS. 600.100 seconds with streams closed and zero writes. Uptime advanced 627 -> 1232 seconds. The first DOM reconnect time response took 33.2 ms; it was the recognized framed clock-cannot-go-backwards rejection, not a clock-set acknowledgement. Full GUI bootstrap and version checks still passed. Separate fresh flash, power saving on, USB logging PASS. Fresh plain time acknowledgement in 696.1 ms, then 600.101 seconds open with zero writes or keepalives. Uptime advanced 18 -> 618 seconds; first post-idle core reply took 40.4 ms. Full DOM reconnect time acknowledgement took 24.8 ms. Both trial cleanups PASS. Original power saving off, logging output USB, all 15 checked preferences, public identity and ACL response were restored. The GUI port, reader and writer closed.

Uptime gates account for the surrounding query/GUI bootstrap time. None of the three quiet intervals contains a native write event. Both trials recorded zero native command errors and no GUI modem-signal calls.

Radio observations stayed in active RX with zero physical RX errors. The core error bitmask changed from 0 to 8, with zero received packets and one sent packet. This is the configured receive-silence watchdog warning, consistent with the quiet-channel recovery policy; it is not eight errors or a CPU reset. Neither trial establishes error-free RF operation or a loaded radio soak.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#retained-setup-failures-and-host-qualification","title":"Retained setup failures and host qualification","text":"

Two preceding browser attempts remain failed and unqualified. Attempt 2 timed out before metadata completion or any GUI command was recorded; its exact flasher substage was not instrumented. Attempt 4 timed out in the stock loader connection/reset loop after 674 USB JTAG reset entries. It did not reach loader main return, application writeFlash, or a GUI command. Neither failure exercised the GUI's five-second command timer, so they are not counted as firmware console timeout failures or silently converted into passing trials.

Before the passing logging-off attempt, the host TTY was normalized from VMIN=0, VTIME=0 to VMIN=1, VTIME=0, with zero explicit modem-signal calls in that preparation. No firmware or website code changed. That host setup is part of the qualified configuration; the evidence does not isolate the original ROM synchronization failure's cause. The second logging trial started without another external CLI preflight or TTY change.

The pinned stock flasher also calls connect(resetMode, romBaudrate) from detectChip, although the second connect argument is a retry count with a default of seven. The supplied value is 115,200. This explains the excessive retry budget after failed synchronization, not why synchronization first failed. It is an external flasher issue; no website patch was required for the passing GUI tests and no website deployment is claimed.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#evidence-and-limits","title":"Evidence and limits","text":"

Final read-only operator verification again matched the exact version, original 15 checked preferences, public identity and ACL response; uptime was 686 seconds and flags remained 8. The owned Chromium profile and tunnel were closed, the serial port had no owner, and original host service states were restored: ModemManager and mctomqtt active, mesh-logger inactive. No Pi network configuration changed. Restored service state does not establish MQTT delivery.

The compact safe audit contains artifact/helper/report hashes and selected outcome fields. Completed sanitized reports and phase observations are retained privately; raw reference settings remain private. Failed attempts are retained separately from the qualified results.

The USB cable remained attached throughout these trials. The native-host sleep guard keeps CPU command service available while the USB host is connected, including when its serial port is closed. Power saving on is therefore not proof of CPU light-sleep entry, measured current, or an operator button test. At this source, V4 lacks the momentary-button wake profile flag. Its subsequent software/build and console validation is recorded below. The operator had no physical button access in this setup. The user's separate screen-wake confirmation does not establish this historical image's CPU sleep state.

These checks qualify this historical c88885a3 Full application, native Pi Chromium setup, protected active-slot application flash, and measured intervals. They do not qualify every host OS, board/role, adapter, cold power cycle, factory erase, heavy logging volume, measured power consumption, or RF traffic load. GPIO/reset behavior outside the unchanged stock flasher's reset sequence was not separately qualified. The release remains unpublished; the final same-source all-board build and release packaging are separate gates.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#updated-button-profile-checkpoint-ebf2f082","title":"Updated button-profile checkpoint: ebf2f082","text":"

Source ebf2f0823ef8413987d31ccb2d044a2090e88469 enables the existing momentary-button wake flag in the shared V4 and V4-R8 profiles. Seven ESP32 USB/sleep tests and two display-power tests passed. Configuration assertions cover 71 firmware profiles, excluding four deliberately standalone partition migrators. Six host cases execute actual board sleep/USB methods, real family button constructors and implementation, role polling guards, UITask button fragments and DisplayPowerPolicy. Three controls must fail if the profile flag, gesture polling guard or display wake is removed. These are synthetic GPIO/sleep results, not a physical button press.

Updated artifact Bytes SHA-256 Full application 2,097,560 aebfd06f5e35ad636f52f329bd051deb9dfeb633361dc77396c0ee9d4456e7c1 Merged image 2,163,096 08750bd38f9f71ea37b8739a8b3c26befa91829df6d5754ad9cffbf61a796a90 Matching ELF - c2ec6d54c32c4b3cb331f9216607480e851933cf4865cc399be9409fdf587467

The normal Full target, table, two slots, capabilities and RAM gates passed, with the same 20,880-byte required-heap margin. Linked instructions now contain the GPIO0 pending-press check, active-low wake and button cleanup, plus the gesture polling guard, while retaining radio IRQ14 wake. The HWCDC PHY handoff and startup hook remain linked; no TinyUSB USBCDC class is linked. This confirms the profile flag reached the application rather than only its source recipe.

The same pinned stock native GUI/flasher and unchanged 5,000 ms deadline were used. Before this flash, host-only TTY preparation again changed VMIN/VTIME from 0/0 to 1/0 with zero explicit modem-signal calls. The app-only ROM preflight again selected the protected active slot. Exact runtime readback was v1.17.1.9-halo-keymind-cascade-dev-ebf2f082 (Build: 06-Oct-2026).

Updated-source check Result First stock DOM GUI Connect after fresh flash PASS. Plain time clock-set acknowledgement in 2,036.4 ms; full GUI bootstrap, exact version and original reference checks passed. No extra reset, replug, settle or retry. Power saving on, USB logging, console open PASS. 180.061 seconds with zero native writes or keepalives; uptime 17 -> 198 seconds. First post-idle core reply took 39.7 ms; full DOM reconnect time acknowledgement took 45.0 ms. Power saving on, USB logging, console closed PASS. 180.100 seconds with streams closed and zero writes; uptime 205 -> 391 seconds. First DOM reconnect time response took 26.7 ms and was the recognized clock-cannot-go-backwards rejection, not an acknowledgement. Full GUI bootstrap passed. Cleanup and operator readback PASS. Original power saving off, USB logging, all 15 checked preferences, public identity and ACL response were restored. Streams closed; independent operator readback again matched the exact updated version and original digests at uptime 448 seconds.

There were no native command errors or GUI modem-signal calls. Radio remained in active RX with zero physical RX errors; flags changed 0 -> 8, received packets stayed zero and one packet was sent. This remains a receive-silence watchdog warning, not a CPU crash or loaded-radio qualification. The owned browser and tunnel were closed, the serial port had no owner, original host service states were restored, and no Pi network configuration changed.

The compact safe audit records this as updated_source_checkpoint, separate from the older 600-second trials. The updated source qualifies these 180-second console intervals. The operator did not physically press the button; the user's manual screen-wake confirmation is recorded above without assigning it a firmware SHA or CPU sleep state. CPU sleep and current draw were not measured in these USB-attached tests. GitHub unit run 37426156162 for this new source is still in progress at this checkpoint. Release 1.17.1.9 remains held and unpublished.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/","title":"Hardware validation heltec v4 scan first ap 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#heltec-v4-scan-first-ap-validation-2026-10-06","title":"Heltec V4 scan-first AP validation - 2026-10-06","text":"

The expanded-partition Full repeater is the recommended image for these V4s. Minimal remains a compatibility profile for the smaller portable OTA slot; expanded nodes do not need a second minimal variant.

Source base: 8d31b3932de614ac38d238f1a01010bc5f362fe8, with the working changes described below. git pull --ff-only reported already up to date. The tested version is v1.17.1.9-halo-keymind-cascade-dev-v4fix8-8d31b393. PlatformIO commands ran sequentially in a native WSL checkout.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#failures-and-corrections","title":"Failures and corrections","text":"

The original latest-source check reproduced a WebConfig AP that reported startup but could not be discovered by a normal WiFi client. Earlier simulated tests and CLI status had not proved beacon discovery, association, or the full browser flow. Those were material coverage gaps against the reported problem.

WebConfig now removes the previous AP while preserving a shared STA/ESP-NOW driver, scans in STA-only mode, and retains the results for the first picker request. Startup verifies live SDK SSID, visibility, authentication, channel, b/g/n protocol, AP-start event, IP, and absence of a STA association before reporting success. Startup failures remain bounded by the five-second console deadline. A failed scan is canceled before AP startup; ordinary picker requests then return an empty result and permit manual SSID entry. Only explicit Rescan starts another scan on that setup AP.

The pinned Arduino 2.0.17 scanner used an uninitialized wifi_scan_config_t. Its active path left passive dwell and home-channel dwell fields indeterminate. A source/version-checked middleware now zero-initializes the structure in a private build copy. The shared PlatformIO framework remains unchanged. Before this correction, channel-zero scans were still running after 3.6 seconds and were canceled. Afterward, repeated all-channel scans completed in approximately 2.4 seconds and supplied cached results. This hardware comparison supports the uninitialized configuration as the cause of that timeout.

The standard ESP32 minimal LoRa OTA profile uses the existing compact Ed25519, SHA-512, and target-name implementations to fit its portable slot. Full and ordinary standard profiles retain their existing implementations.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#builds-and-host-regressions","title":"Builds and host regressions","text":"Check Result Full repeater app PASS, 2,098,744 bytes in each 6,553,600-byte A/B slot; 10 capability markers and internal memory gate passed. Full SHA-256 d534c9d79796886fb84690c784ae985bfef6d808d5e7879409f139e9463f126d Full running body hash 8C8A7ADBDA25501E Minimal app PASS, 1,281,576 bytes; 29,144 bytes spare in the 1,310,720-byte portable slot; 6 capability markers and memory gate passed. Minimal SHA-256 4f0d55aa3c0c85e76c50a2ba60738294515552dafa5e9496d6a33d73f1072e27 Minimal running body hash 060106D2CA3CA669 Focused host suite PASS, 65 Python tests, including four compiled AP fixture variants: Arduino 2/3 x open/password AP. Build profile tests PASS, test/test_build_profiles.sh.

New AP fixtures exercise stale independent SDK state despite successful facade returns, setter/readback failures, delayed AP events, shared ESP-NOW ownership, LAN promotion, scan timeout/cancellation, cached picker results, explicit rescans, and failed-scan manual-entry behavior. Worst modeled startup failure is 4.59 seconds. A very slow scan can consume the retry budget, so a transient first AP failure in that case returns a bounded failure rather than retrying.

Five new middleware tests execute the actual patched scanNetworks method against an SDK capture. A poisoned-local negative control fails with the old uninitialized source. Tests cover caller parameters, active/passive defaults, already-running scans, private-copy isolation, and source/version guards. Eleven existing crypto and target-name tests verify signature compatibility and preservation of all target names/IDs.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#physical-checks","title":"Physical checks","text":"

Mercerwood's Heltec V4.3 OLED repeater was identified by its exact USB serial. Its expanded partition geometry was checked before each application-only OTA upload. No partition table, bootloader, identity, or ACL was replaced.

Boundary Result WebConfig, ESP-NOW off PASS: fresh discovery, association, gzip loader, full UI, setup API, cached all-channel results, explicit rescan. Startup reply approximately 2.70 seconds. WebConfig, ESP-NOW on PASS: discovery, association, full UI/API, cached shared-channel results, explicit rescan. Startup reply approximately 0.44 seconds. 2.4 GHz off/on PASS: master off rejects individual WiFi-on; master on restores WiFi/ESP-NOW; subsequent WebConfig AP works. OTA after master off and repeated startup PASS: discovery, association, and update page. Subsequent WebConfig AP also works. Actual browser JavaScript PASS: the unmodified setup wizard opened populated networks automatically; Rescan repopulated them; manual unsaved SSID entry and picker close/reopen worked. Automatic startup after reboot PASS with WebUI temporarily enabled and no saved SSID: AP discovered/joined and setup API returned cached scan results before any manual start command. Original disabled WebUI setting restored. Minimal WiFi OTA PASS for three start/stop cycles, completed raw scans with fresh beacon age, exact-BSSID association, and the lightweight uploader page. Its raw uploader installed the supplied Full app and rebooted into its exact hash.

Full's seven-case manual run completed without an unexpected CPU reboot. Minimal cycles also completed without an unexpected reboot. The final node is back on the Full hash above. All original saved preferences, public identity, ACL, and runtime radio states were compared after restoration. The Pi's original WiFi profile and wired route are restored, temporary profiles are removed, serial ownership is released, and the original service states are unchanged.

The minimal profile omits manual WiFi/ESP-NOW group controls; their unavailable responses are recorded rather than counted as master-off recovery. Full carries that recovery qualification.

Two test-runner errors are retained in the raw evidence: an early minimal run could not find iw outside sudo's path; the corrected run then incorrectly expected automatic AP startup while the saved WebUI master was disabled. Its five minimal/AP/upload checks and Full/settings restoration passed. A separate enabled fresh-boot run correctly qualified automatic startup. An earlier update and verification overlap was also corrected by a lock covering the entire Pi test and cleanup transaction. These failures were not firmware acceptance passes.

Sanitized physical evidence is in heltec_v4_scan_first_ap_2026-10-06.safe.json. Private credentials, scanned network names, and saved secret values are excluded.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#remaining-boundaries","title":"Remaining boundaries","text":"

Future acceptance must include fresh discovery, association, usable UI/API, first cached picker results, explicit rescan, and cleanup on actual hardware; CLI success and simulated state alone are insufficient.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/","title":"Hardware validation known failures 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#known-failure-follow-up-20-21-october-2026-utc","title":"Known-failure follow-up, 20-21 October 2026 UTC","text":"

Status: V4 USB PASS; Morse false letters are an accepted limitation. Final-source CI remains in progress. This scoped follow-up preserves failed measurements and the operator's limited Morse acceptance. It does not grant broader release approval. The current V4 is a different physical board from the 18 October campaign.

Final source: 0b207a1ec3a32eb0a8ad6b8643db211de4921cd4. Safe evidence summary records source bindings, evidence digests and explicit pending fields.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#builds-and-host-verification","title":"Builds and host verification","text":"Check Source Result RAK4631 Full 0b207a1ec PASS: 712432/712704 application bytes, 272 bytes free T096 Full, FEM on 0b207a1ec Shipping build/package PASS V4.3 Full, FEM off 0b207a1ec Shipping build/package PASS: 2559496 application bytes Focused Morse host suite 0b207a1ec 111 methods PASS Targeted USB host suite 7278eacb9, included in final source 45 methods PASS, including 12 HWCDC TX backport suite methods with actual ISR scenarios Earlier unchanged RadioProfile suite fc72943f3 17 methods PASS; not rerun for final source

RAK Full retains all 13 feature capabilities plus profile.full; its build used INA260 1.5.4 and MLX90614 2.1.7. Builds and host tests do not qualify RF behavior or physical USB stability. In CI run 39523154379, all 11 non-main jobs are reported green; the main host job is PENDING. A completed workflow pass is not yet claimed.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#v4-application-and-native-usb","title":"V4 application and native USB","text":"

The earlier 4773ac4219666d0f5b4c2bff5eb72aa053014662 application passed full readback with unchanged partition table and OTA metadata. An initial private adapter's ordinary hard reset left native S3 in forced download mode despite correct readback. The pinned flasher's masked forced-download clear and watchdog reset restored the application. Its adapter guidance was clarified in meshfirmware commit 4ce4f44; the flasher already had the correct sequence.

The second logging-on native-console trial on 4773ac42 passed 600.1086 seconds with zero idle command writes. The observed idle phase started at 2026-10-21 00:13:03.667 UTC. First reply/reopen took 124/21.8 ms; uptime advanced 378 -> 979 -> 981 seconds. Identity, settings restoration, strict binary handback and cleanup passed, with zero core errors. However, watchdog recoveries rose from 0 to 2; the last reason was client inactivity (0x08). This is a transport PASS, not a clean watchdog qualification. The earlier browser startup failure and its successful restoration remain recorded separately.

Fix 7278eacb9131d613da719fee814fefcdad3adf23, included in final source, requires 100 ms of continuous presence loss before invalidating the valid client lease and recognizes the host-backed physical reader. It preserves bus-reset, stall and true-unplug recovery. The finding does not conclusively attribute both earlier events to that path.

Ordinary shipping 0b207a1ec was restored successfully at 2026-10-21 01:21:32 UTC. All 2559496 application bytes matched SHA256 04eed20ee0f964fef2a5a0f74f4aafe4b11e0a71b011ff757efc96c03a2cfb14; partition table and OTA metadata stayed unchanged. Runtime source/identity, absence of the raw diagnostic marker, strict binary handback and clean core state passed. This was an application-only update.

Final logging-on native-console case 3 on this shipping image: PASS. The idle lasted 600.0913 seconds with zero command writes; post-idle reply and reopen took 105.3/20.5 ms. Uptime advanced 172 -> 773 -> 775 seconds, with zero core errors. Watchdog recoveries stayed 2 -> 2 and boot count 0 -> 0. The watchdog's boot field counts reset authorizations, not all physical boots; the continuous uptime separately establishes no MCU restart during this case. Identity, original preferences, strict binary handback, TTY restoration and owned browser/stream cleanup passed. The wrapper finished at 2026-10-21 01:34:33 UTC without errors. This qualifies the current board and artifact for this finite logging-on idle/reopen case; it does not establish the cause of the different 18 October board's USB disappearance.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#morse-measurements-and-accepted-limitation","title":"Morse measurements and accepted limitation","text":"

These shipping controls use automatic detection without HIL threshold overrides.

No intentional TX, 30 seconds RAK4631 T096 Unchanged 08650f64 FAIL: 3 false characters FAIL: 1 false character Adaptive 4773ac42 PASS: 0 characters in this control UNQUALIFIED: 6 retained characters and command timeout during external-actor contention

The contended T096 observation is not a clean detector pass/failure attribution. The RAK control alone does not qualify complete messages or other board families.

An unchanged 08650f64 V3 transmitted to a shipping 4773ac42 V4. The quiet control came first: FAIL, 26 false characters in 300.820761 seconds. Three messages at 25/60 WPM, two repeats, then produced 4/12 exact passes and 8 failures. Both preferences and strict binary handbacks passed; the wrapper reported no errors. This one-way campaign does not replace the original two-way 24-message matrix.

The operator accepted false letters for now and stopped further detector/gate work. That accepts the false-letter limitation; it does not establish complete message accuracy or turn the failed measurements into passes. Further Morse experiments are not a mandatory gate in this follow-up. The receive setting minimum is 15 WPM, default 25; TX at 1-14 WPM remains available. Hand-timing tolerance remains, so setting validation is not an absolute physical speed cutoff.

Separate raw diagnostic captures used production RF settings and automatic squelch. Capture integrity, both preference restorations and both strict binary handbacks passed. These instrumented captures do not qualify shipping accuracy. The ordinary shipping image is restored; raw lab details remain private.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#external-nrf52-bootloader-handback","title":"External nRF52 bootloader handback","text":"

The operator reported completion of their separate bootloader testing. Its deployment record was also read: COMPLETE_WITH_KNOWN_LIMITATIONS, eight live INFO/original-application handoff proofs and gateway service handback PASS. It uses unpublished test bootloader TEST_0x02040B7E, source b07255d4cdc601f1313e4cbe020caad3b5544821, not a latest-stable release. The record reports no publication or push by that deployment.

Limits remain: scheduled concurrent USB control failed with one EPIPE and 31 successful requests; 11 unique bulk halt-clear cycles passed. RAK secondary-data preservation is uncertain after earlier manual scratch overlap. Identity and seven settings match, without a complete immediately prior contact/channel baseline. These records do not qualify final-source 0b207a1ec applications. The nRF52 hold is released; no new nRF52 campaign was started. MeshPocket remains excluded from all further testing in this session.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#remaining-scope","title":"Remaining scope","text":"

Current CI completion and final release artifact-matrix binding remain pending. The scoped V4 logging-on hardware case is complete. The 20 October fleet qualification retains its accepted USB/BLE/RF/contact coverage at its actual sources and conditions. This report neither reopens those closures nor extends them to unmeasured artifacts. Historical failures in the 18 October report remain visible. Private device identities, addresses and raw responses are omitted.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 18","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#mercerwood-11801-hardware-campaign-2026-10-18-utc","title":"Mercerwood 1.18.0.1 hardware campaign, 2026-10-18 UTC","text":"

This campaign is in progress. It is not release approval. Results below bind specific images and physical devices; successful compilation and older bench results do not qualify unexecuted hardware cases.

After the operator reset the V4, its exact USB by-id and physical-path endpoints were present at 15:09 UTC. A fresh Full ec9ebc09 binary identity snapshot passed. Authoritative original-data restoration and fresh binary/62-field comparisons subsequently passed. The owned host diagnostic bond was removed and original services were active. Full logging-on remains failed/unqualified; the staged MQTT/WSS soak has not launched while upstream changes are integrated.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#firmware-and-preservation","title":"Firmware and preservation","text":"

The initial eight-image Full matrix built successfully from clean source 652289ee823bbed22918fe584f9d49ccb2ec230f, with release-length version v1.18.0.1-halo-keymind-cascade-dev-652289ee: V4 Room Server, V4.3 Companion FEM-off, RAK4631, T096 FEM-on, MeshPocket, T1000-E, SenseCAP Indicator LoRa, and Xiao S3 WIO. Room fixes were separately built and readback-verified at d3e05abce308b52261e86c9297029b4bce3cc4a1 and d52e4e93475e0960328131e2e65cc6b992aa4dbb. The diagnostic-enabled Full V4.3 FEM-off and Indicator LoRa images built successfully at ec9ebc0914d61a02fb21f23444768ec5f847e7ec. The later MeshTower V2 SD Full build and transfer are recorded separately below.

Before ESP32 application changes, complete private flash backups were made through the pinned real flasher's identity/reset/finish path. Application-only writes checked silicon MAC, USB identity, capacity, active OTA slot, image integrity and full application readback. Partition tables and OTA metadata were verified unchanged. Original NVS/filesystem restoration is a separate operation; an application-only flash does not restore settings changed by tests. The Indicator retains its existing noncanonical 8 MiB partition table.

nRF52 updates used application-only DFU. The RAK3401 service radio and Indicator RP2040 display coprocessor were excluded. No root USB hub power operation or concurrent PlatformIO process was used.

The flashed 652289ee artifacts below were built from source 652289ee823bbed22918fe584f9d49ccb2ec230f. USB identity labels are the first 12 hexadecimal characters of SHA256 of the UTF-8 USB serial as reported; the serial-less CH340 Indicator was bound privately by its physical USB path and public device identity. These hashes bind the executed images, not unexecuted release qualification. ESP32 hashes cover application-only BINs; nRF52 hashes cover the application DFU ZIPs actually transferred.

Target Source USB serial hash Artifact SHA256 heltec_v4_room_server 652289ee8 af0a529cb0cc Application BIN 4e370dd285813ce330aae13a9a8891795145d3bbb7adef84ff4dbb19926cb677 RAK_4631_companion_radio_full 652289ee8 b11fbd38286f Application DFU ZIP 64940ae543491831a20da2176e50e44a1519917bd42c4615601ddc0c7bf82364 Heltec_t096_companion_radio_full_femon 652289ee8 ab7bc0653271 Application DFU ZIP 0a44dbc7a39fb65007713aa61c004b8bb790035c1d6c343ba150a0a77f855dfa Mesh_pocket_companion_radio_full 652289ee8 c5ba0587de70 Application DFU ZIP eb13c05521f2545719db809c5367ed31de22e9aeadc0c0992366a6ab770570c5 t1000e_companion_radio_full 652289ee8 5244f0b64696 Application DFU ZIP 7a6003afa50720e2c97cfbb625692143f66b4e7de063de63106fecd5ff4fc5fb SenseCapIndicator-LoRa_companion_radio_full 652289ee8 No USB serial Application BIN 6cb1405fb2a6c3f4889506f184751d7576ad23159c303f84c054e4ad6b124b03 Xiao_S3_WIO_companion_radio_full 652289ee8 b00e0958f2ff Application BIN 59c62ff6d985aa42740b6c26dcc30efe50c04b747d94d566e4ca4a8cd1e42e47 Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors 652289ee8 030f24857801 Application DFU ZIP 0d20f4486023a13427eb09087da70aacac5562848371a919631ab6a76f7888d1

The same V4's subsequent Room application hashes were 54962d21088f1c87c3956620181a3499102e074d2e28791003d1b0479f64c7af for d3e05abc and 7753f5f4161a6631d7f3abe44a99d70d5b892a94ad3cb69d313d54e44c608c87 for d52e4e93. Their archived exact ELF hashes are respectively 57d5b38fcc454d1ccc05763d3d8c6fa047904c42ca761be3a0393f14e799d1eb and 5db0fb151ee388ffab66ccce11b0d137907ff6450530f7812d35383c8c6aa785.

After the Room campaign, the V4.3 Full FEM-off ec9ebc09 application passed identity-bound application-only flashing and complete readback. Its 2,545,080-byte BIN SHA256 is cb1e167a222e7a8d0ca253f7e3272561a29ba6753f4173eb89c79405b2bdad2f. The separate restoration then wrote only the original NVS and SPIFFS partitions from the pinned pre-campaign full backup. Both partitions matched their original bytes on readback; the active application, partition table and OTA metadata were independently verified unchanged before runtime handoff.

A fresh Full snapshot matched all original SELF_INFO bytes, complete contact records and all 40 canonical channels. Returned DEVICE_INFO settings matched after excluding only the runtime active PIN and independently verified new build/version metadata. There were no new core error flags. This qualifies the original data readback and those returned settings, not every unexposed preference or battery-backed cold boot. At that restoration checkpoint, the Full runtime had WiFi off, an unset SSID, MQTT enabled but not running, and unavailable ESP-NOW; temporary test changes require their own restoration. Later native-console tests enabled power saving and USB logging and disabled MQTT. Those flags were unrestored at 09:22 UTC, then passed identity-bound BLE restoration and readback at 09:41:23: power saving off, USB logging off and MQTT on, with WiFi off and MQTT not running. After USB returned, both original NVS/SPIFFS partitions again matched the backup on readback; the active application, table and OTA metadata were unchanged. Fresh binary and all 62 returned CLI fields matched the original baseline. The diagnostic's owned host BLE bond was removed after restoring the original device data. This establishes those readbacks and returned settings, without extending the earlier cold-boot or unexposed-runtime qualification.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#executed-companion-checks","title":"Executed Companion checks","text":"Physical hardware Candidate USB queries/reopens Owned contact add/update/delete Add/delete survive warm reboots BLE bonded hold/reconnect RAK4631 Full 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions T096 Full FEM-on 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions MeshPocket Full 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions T1000-E Full 652289ee PASS: 100/10 PASS PASS FAIL/unqualified: four initial attempts; see 19 October follow-up below Xiao S3 WIO Full 652289ee PASS: 100/10 Not attempted: 350 occupied contacts Not tested Not tested SenseCAP Indicator LoRa Full 652289ee Identity/readback and USB WiFi/BLE mode cycle PASS Not tested Not tested Not tested

Warm tests used a confirmed-absent random contact, verified complete readback, performed the shipping guarded reboot, verified persistence, removed only the recognized owned record, then rebooted and verified original state. They do not prove cold boot. Display boards legitimately regenerate the active BLE PIN when the saved PIN selects that policy; comparisons exclude only those four runtime bytes and retain the private raw frames. Original contacts, canonical channels, public identity, name and the other returned SELF_INFO/DEVICE_INFO settings fields are checked. This comparison does not inspect every persisted preference.

The Xiao retained all 350 contact keys and all 40 channels. A known contact's advertisement and modification timestamps advanced during live mesh traffic; this was not a contact edit by the runner. No existing Xiao contact was removed to make room for a test record.

The Indicator LoRa Full mode cycle passed on 2026-10-18 UTC. USB checks verified the exact board, complete candidate version and public identity, then proved warm reboots into WiFi and BLE modes with the expected selected, boot and live service states. The original BLE selector was restored and another warm reboot verified. All 40 channels, the empty contact baseline, returned binary settings, saved radio/RXPS/radio2 configuration and queried Bluetooth preferences matched the original state, with no new core error flags. The runner opened seven USB sessions and issued 510 framed requests; it issued no RF message commands or partition/RP2040 writes. These checks do not qualify an external TCP or bonded BLE session, font download/retention, fresh NTP, visible rendering or cold boot.

The ESP-NOW Full build at ec9ebc091 failed the mandatory internal RAM gate: 233,688 bytes available versus 251,168 required, a 17,480-byte shortfall. Its native 480px WiFi canvas policy remains unchanged. Both ESP-NOW by WiFi/BLE combinations remain unqualified; the normal Reduced ESP-NOW USB image lacks those secondary transports and cannot substitute for their Full acceptance.

T1000-E BLE initially resolves services and returns identity/core data, and one attempt returned all five read-only getters. Later writes/reads fail with backend service-discovery, ATT 0x0e, or timeout errors. USB remains healthy. The probe's final disconnected event is cleanup, not proof of which side terminated a connection. These observations do not establish a root cause.

A fourth existing-bond trial captured HCI traffic. It completed encryption and service discovery at MTU 247 and validated the complete version and public key. The get name ATT Write Request (0x12) received its Write Response (0x13), then no reply notification appeared. About 2.01 seconds later the controller reported Connection Timeout (0x08), matching the negotiated two-second supervision timeout and preceding the client's eight-second reply deadline. No host HCI Disconnect command appeared in the capture. This trial therefore lost the link before client cleanup. BLE uptime was 2720 seconds and later USB uptime was 2993 seconds, consistent with the 272.70-second host interval; both reported zero core error flags. This excludes a full MCU reboot in that observed window, without identifying a firmware or RF cause. The post-trial BlueZ state was disconnected and both owned processes closed. Negotiated link parameters were a 30 ms interval, latency four and 2000 ms supervision timeout; no PHY/data-length-change event appeared in the saved capture.

A separate diagnostic build enabled the existing RAM trace and disabled debug printing, retaining the same source, version and connection parameters. Its application DFU ZIP SHA256 is 713765d9145db2aec03059a87280a065d4ae68e1a23ee4dd56a4d16f37b4f8c1; the exact ELF SHA256 is 18442324b87deef9af658d2997b668ee10912929af4be9bacf9c473bc70d2169. This trial failed during GATT setup before any Companion request was sent. The MCU initiated its documented no-app-data recovery exactly 15 seconds after security completed. All 14 recorded stack dispatches exited, with a maximum 2.93 ms dispatch and 103 ms main-loop gap. All 62 trace records were retained, with no ring wrap or recorded contention drops. HCI recorded the peripheral termination and no host Disconnect command. This is a different failure stage from the shipping post-getter supervision timeout; concurrent host Chromium startup is a possible, unproven confounder. It does not identify a production firmware fix. No old post-USB core metrics were reused for this trial.

The original shipping application was then restored through application-only DFU. Identity, complete contact records, all 40 channels and all returned device settings matched the immediately preceding baseline, including the active PIN; core error flags were zero. An explicit read confirmed trace support absent. The diagnostic image does not count as shipping BLE qualification.

GPS was enabled for a bounded 180-second local-only T1000-E telemetry window. No location appeared. The exact GPS switch was restored, sync cadence was unchanged, and packet TX counters did not advance. Outdoor acquisition, fresh-fix age, stale/no-fix behavior and sleep current remain unqualified.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#t1000-e-ble-follow-up-2026-10-19-utc","title":"T1000-E BLE follow-up, 2026-10-19 UTC","text":"

The unchanged Full 652289ee application passed a fresh five-second bonded session at 07:00 UTC and three consecutive 30-second held/reconnect sessions at 07:04\u201307:07 UTC. Each session checked the board, complete firmware version, USB-associated public identity, five read-only getters and advancing uptime. The existing bond remained, host cleanup completed, and a subsequent USB snapshot reported zero core error flags. The earlier timeout was not reproduced.

Source review separately found a use-after-free in the pinned Bluefruit dispatcher: disconnect handling deleted its BLEConnection, then role routing read getRole() through the freed pointer. Commit dc17f507dd0657cfd37dfe73e7634d6b81166f64 caches the role before deletion and refreshes it when a CONNECTED event allocates a replacement connection. The production-source ASan/UBSan regression reproduces the original memory error and passes the corrected peripheral and central paths, callback ordering, handle replacement and repeated reconnects. All 36 nRF52 BLE host tests passed. This removes the disconnect dispatcher's memory-safety defect; it does not establish the cause of the earlier on-air supervision timeout.

The shipping Full T1000-E and RAK4631 targets built successfully at that commit, including their capability and runtime-RAM gates. The T1000-E application DFU ZIP SHA256 is aa02aadad102201b30489efc80a044f98af3b9303630b4832ee21c65c3a93b40; its exact ELF SHA256 is 6076d25e4c4ecadea8552c1bb56a97865b3369c1062d4a040e90d33368f9d214. The candidate booted through identity-bound application-only DFU and passed three 30-second held/reconnect application sessions at 07:13\u201307:15 UTC, with the same identity, getter and uptime checks. Before and after the BLE trial, USB snapshots matched the pre-flash public identity, complete contact record, all 40 channels and returned SELF_INFO/DEVICE_INFO settings, excluding only the independently verified build/version metadata. Core error flags were zero. The existing bond remained and owned probe/capture processes closed.

These successful application sessions include connection retries; they are not evidence of uniformly reliable connection establishment. The baseline capture contains six 0x3e disconnects before encryption, and the candidate contains two, each about 154\u2013179 ms after a connection-complete event. All three completed sessions in each trial encrypted successfully. Baseline sessions two and three, and candidate session one, ended with 0x22 about 1.97 seconds after an explicit host HCI Disconnect request. No 0x22 occurred during the completed getter/hold traffic; the other completed sessions ended with 0x16. Both captures recorded zero drops and no remaining target handle. Neither held trial reproduced the earlier 0x08 supervision timeout. Longer soak, phone-specific testing, establishment retries and host-requested disconnect completion remain open; these captures do not assign their cause to the peripheral or host controller.

The exact original 652289ee application ZIP was restored through application-only DFU. A fresh five-second bonded session also passed after restoration. USB snapshots before and after that session matched the original complete version, all SELF_INFO/DEVICE_INFO bytes, full contact record and all 40 channels, including the active PIN; core error flags were zero. BlueZ remained paired, bonded and disconnected, and the Bluetooth service remained active. No pairing or preference setters were used; DFU transferred only the application. Raw HCI and snapshots remain private; these comparisons do not inspect every unexposed preference.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#meshtower-v2-sd","title":"MeshTower V2 SD","text":"

The MeshTower V2 SD Full image from 652289ee8 passed identity-bound, application-only USB DFU and initial SD mount/list/space checks. All 16 returned getter checks matched the original journals, including public identity, radio/power/logging settings, OTAFIX2.4.11-preview.2, and InternalFS geometry at 0xED000+28 KiB. Ten consecutive SD space/list/core triplets completed all 30 replies: uptime advanced from 119 to 125 seconds, core error flags and queue length remained zero, and the original directory metadata and reported 32.0 KiB used/959.6 MiB free stayed unchanged. The directory contained the same single 4-byte /mota/cache.off marker; no file contents were exported. Original firmware had error flags 2 before the update; the new boot is evaluated separately. These checks qualify returned values and read-only SD access only. They do not qualify card-absent fallback, cold initialization, interruption or power-loss recovery, LoRa OTA transfer/install/serving, or a bootloader update. No SD format/erase or deliberate LoRa OTA transmission was performed.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#room-authentication-and-browser","title":"Room authentication and browser","text":"

On the original 652289ee Room image, actual LAN requests from two separately bound source addresses passed per-address failure isolation, shared-budget exhaustion/refill, existing-session access, no token refund on success, and post-window recovery. The Room password checks passed duplicate-password suppression, five-unique-failure admission, the global 30-second window, nonempty admin/guest rejection during lockout, and continued public blank read-only access. Public guest wire role is 0; role 1 denotes explicit ACL read-only permission. An earlier runner used the wrong expectation; its failed sample is retained separately.

The complete two-source-address LAN suite was repeated on d52e4e93 and passed all WebConfig and Room cases in 161.67 seconds, with no temporary lockout left. These network checks use guarded timing windows, not exact millisecond boundary measurements.

The actual saved-network recovery AP passed anonymous protected-config denial, wrong-password rejection, existing-admin login/config read and logout. Status required needs_setup=false and needs_password=false, distinguishing recovery from initial password setup. The Pi used an owned temporary WLAN profile while Ethernet management/default route stayed unchanged, then restored its original WLAN profile and deleted only the temporary profile.

The real Room browser also passed throttle-error polling, Connect retry and draft preservation over this AP. The first AP browser attempt was an inconclusive harness failure: the portal's favicon redirect was outside the strict request allowlist. A narrow setup-mode redirect-chain check fixed that observer; the second run passed with native fetch unchanged and zero compose/write requests. Its unused icon follow-up failed outside the SSH-forwarded origin and is reported separately from completed API response bodies. The AP BSSID was measured, and the observed AP was open while protected settings still required the saved admin password. AP two-source throttle testing remains unexecuted.

An actual Room LoRa trial verified a temporary matching 910.525 MHz/62.5 kHz/SF7/CR5 profile before transmission. Shared guest-password login returned non-admin RW2 permission, denied admin-only ver, and returned the board index. The next mail settings request had local send admission but no decoded reply before its 35-second deadline; no delivered reply was discarded by the runner's type/prefix matching. This is an incomplete RF request/reply trial, not a demonstrated mailbox authorization fault. Source inspection confirms active sessions may read their own settings. Source contacts/returned settings, logout and the Room's original saved/temporary radio state were restored.

Post-trial counters showed 18 received packets (14 flood/four direct), six transmits, one receive error and no outbound packet, with continuous MCU uptime. This establishes reception on the temporary matching profile. It does not identify which packet was lost or qualify mailbox mutation/retry behavior.

A separate admin trial with an explicit two-second gap after each peer request passed login, exact version/board reads, complete mailbox settings, self-send, body readback, acknowledgement and both exact request replays. It required an empty own inbox and provisioned only its unchanged closed policy. The first replay returned the original message ID; replay after acknowledgement also returned that ID without re-enqueueing. Final reads verified an empty queue and the original mode, delivery and complete allow list. Source contacts/returned settings, logout and the Room radio lease were restored. This qualifies the explicitly paced live-session sequence, not unpaced reliability or durable replay across reboot/cache eviction. Owner/revision/retired-receipt metadata persisted until the separate original-filesystem restoration described above; policy preservation alone does not mean byte-identical filesystem contents. The earlier incomplete trial remains a failed sample.

Actual Chromium/native-fetch testing exposed a loop-task stack overflow while loading board.index. The saved core matches the 652 Room ELF and shows processRoomRequest -> loadRoomBoard -> recover -> filePresence -> snprintf reaching the stack-bottom watchpoint. Browser 202/429 response bodies also were not consumed under their request deadline.

Commit d3e05abc separates operation scratch frames and drains response bodies under a bounded deadline. Actual browser runs with an empty board and with an owned 2048-byte UTF-8 article both passed login, automatic throttle-error polling, production Connect retry, preserved unsent drafts and unchanged boot/browser identity. Every completed 202 response in these runs was drained.

A subsequent populated-board stress run completed five whole article reads, then triggered another panic during the sixth. Its fresh core positively matches d3e05abc, reaches the loop stack bottom, and reconstructs the populated index/filesystem/flash-cache/context-switch chain. The initial fix therefore does not qualify repeated populated-board operations. Further stack reduction and live retesting were required.

Commit d52e4e93 clears index state in place, recovers directly into the output, uses a checked heap allocation for web index metadata, and separates read/write scratch frames. Host fault and allocation-failure checks pass, including preserving an existing 2 KiB article on allocation failure. Stack-frame analysis shows a substantial reduction, but its estimated margin is not a hardware pass. The original owned article was recovered through exact title/version/content checks into a new private intent after flashing this candidate. Twenty complete live index/article reads passed with unchanged content, boot identity and continuous uptime. This qualifies the reproduced repeated-read case; original-filesystem restoration was verified separately after the Full handoff.

The populated-board Chromium check also passed on d52e4e93: exact served HTML, real admin form login, visible automatic throttle-error polling, Connect retry, and unchanged unsent draft and Room boot identity. It used unmodified native fetch, observed zero compose/write requests, and closed its owned browser.

The later CLI preflight showed sticky error bit 8 (ERR_EVENT_RADIO_WATCHDOG), versus zero shortly after the d52e4e93 boot. At observation the radio reported RX, zero received packets/errors, one direct transmit, no outbound packet, and noise floor -73 dBm. There was no MCU uptime regression. The HTTP/content pass does not establish absence of radio recovery events. Source inspection shows this Full Room build enables the five-minute successful-RX-silence watchdog even when MQTT is off. Its flag is set before the soft re-arm outcome is known. With zero received packets, that policy is consistent with the observed transition; it is not proof that HTTP caused a fault. Here, with recv=0, last_rx=0s ago is the formatter's zero-timestamp fallback, not recent-RX evidence.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#native-official-usb-console","title":"Native official USB console","text":"

The unmodified official console.js from commit 45a5257bf792b11a83f4c51c18d4f312addfb677, SHA256 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9, ran through Chromium's actual Linux Web Serial on the d52e4e93 V4 Room image. Power saving was on; WebConfig WiFi, MQTT and ESP-NOW were off.

With USB logging off, 600.35 seconds of uninterrupted idle transmitted zero commands/keepalives. The first complete CRLF reply arrived in 303.2 ms; after close/reopen, the reply arrived in 35.7 ms. MCU uptime advanced continuously and the preexisting sticky error flags remained 8. Native streams, owned Chromium and the original TTY settings were all closed/restored. This is a console lifecycle pass, not a current-consumption measurement.

With USB logging on, the same image also passed 600.38 seconds with zero commands/keepalives. The first reply took 164.5 ms and the reopened reply took 25.2 ms. Uptime remained continuous, sticky flags stayed 8, and all streams, owned Chromium and original TTY settings closed/restored. Power saving and USB logging were then restored to their original off values. Earlier startup/framing harness failures remain separate failed samples and do not qualify idle. These passes bind the Room image; final Full Companion remains a separate role-specific qualification.

The ec9ebc09 Full Companion image subsequently passed logging-off trial 03: 600.444 seconds, zero idle writes, a 275 ms first reply and an 18.1 ms reopened reply. Uptime advanced from 1284 to 1885 to 1888 seconds; error flags stayed zero. Both closes confirmed return to binary mode, and streams, owned browser and TTY settings closed/restored.

Full logging-on trial 01 started at 09:02:25 UTC with baseline uptime 2076 seconds and zero core error flags. Its nominal 600-second idle began around 09:03:30 UTC. At 09:11:13 the kernel reported the target hub port as disabled by hub (EMI?), re-enabling...; the parenthetical is the kernel's tentative wording, not a demonstrated EMI cause. Target USB path 1-1.2.1.2 disconnected at 09:11:14, followed by repeated descriptor/address errors -71. The kernel attempted a port power cycle at 09:11:15 and reported inability to enumerate at 09:11:16. Both the bound by-id and physical-path endpoints remained absent at 09:22.

The run ended at 09:13:42 with FileNotFoundError while TTY cleanup reopened the old path. That cleanup error masked the primary trial failure, which cannot be reconstructed from the old safe report. No post-idle reply, reopen check or qualified idle interval was obtained. USB recovery was still in progress at the 09:22 checkpoint; no firmware, reset or physical-fault cause was proven. The passed Room logging-on case does not qualify this Full image.

A subsequent identity-guarded, reset-only request to the exact parent hub's port 2 returned success. Port status changed from 0x101 to 0x103 after two seconds, with zero change bits: connected, powered and then enabled. The target still did not enumerate; the sibling T096 was unchanged. No hub power change or broader hub reset followed. A physical V4 RESET was requested from the operator. ModemManager was restored to active at 09:24:34 UTC; all four checked host services were active at that checkpoint.

The first read-only BLE diagnostic passed exact board, complete ec9ebc09 version and public-key checks. At approximately 09:35 UTC, core uptime advanced from 3959 to 3961 seconds with zero error flags, versus the pre-idle 2076 seconds. This establishes continued MCU uptime through the USB disappearance rather than an MCU reset in that interval; it does not establish a working USB connection.

The live USB watchdog reported Auto, stage 2, two recovery attempts and zero reboots. Its persisted last event was re-enumeration, sequence 2, uptime 2532 seconds, reason 0x08 (CLIENT_INACTIVE). Its RTC epoch is advisory and does not date this incident reliably. Offline extraction of the exact original SPIFFS active namespace found no usb_wdg, usb_wdg.bak or usb_wdg.tmp files. The journal therefore was not imported by the original-data restoration. Policy stage is volatile and starts at zero, so the evidence establishes two current-boot USB recovery attempts. These are requested actions, not proof of successful repair or proof that they caused the observed kernel errors. The later sticky TX-stall observation is separate from the event's earlier reason.

Source tracing confirms actual single-TTY ASCII stats-core renews the 900-second logging-client lease; framed USB/BLE stats and CLI getters do not. Natural expiry after the fresh pre-idle ASCII poll cannot explain a fault starting roughly 97 seconds later. A bus-reset/session-generation change, observed host/reader loss or logging-off can clear that lease earlier. No pre-failure evidence selects one of these paths. This is watchdog-progress evidence, without a firmware or physical-fault cause assignment.

BLE flag-restoration trial 01 passed at 09:41:23 UTC. Each of the three original flag values was acknowledged and read back immediately and again at the end; WiFi off and MQTT not running also matched. Uptime advanced from 4325 to 4333 seconds with zero error flags, and owned BLE cleanup passed. This restores those flags, not the whole saved-state image or the new diagnostic bond.

Guarded warm-restart trial 01 then passed exact identity and original-flag gates, durably recorded intent, sent the shipping 0x13 reboot request and observed disconnect. Pre-request uptime was 4556 seconds with zero error flags; owned BLE cleanup passed. USB was still absent at 09:45:07 and the kernel had no new entries.

The post-restart read-only trial 01 passed exact identity with uptime 34\u219236 seconds and zero core error flags, proving the deliberate warm reboot. It did not pair or create another bond. Persisted power saving off, logging off, MQTT on, WiFi off and MQTT not running all matched, and every owned cleanup check passed. The last watchdog journal was unchanged: re-enumeration, reason 0x08, uptime 2532 seconds and sequence 2 refer to the previous boot, not a new event.

By 09:48 UTC all four checked host services, including ModemManager, were active; USB was still absent. No additional software or hub recovery is planned. The operator was asked to unplug and reconnect only the V4 USB cable; physical reconnection and verified USB recovery were pending at that checkpoint. Battery-backed USB reconnection does not qualify cold boot. The new diagnostic BLE bond was retained while authoritative restoration awaited USB return.

After the operator reset the V4, its exact USB by-id and physical-path endpoints were verified present at 15:09 UTC. A fresh identity-bound binary snapshot passed the Full ec9ebc09 board/version/public-identity gates: uptime advanced 481\u2192482 seconds, core error flags and queue length were zero, contacts were empty and all 40 canonical channel records were read. That snapshot did not verify the live application flash hash. The Pi had been up approximately seven minutes at session start; neither that observation nor battery-backed USB return verifies a V4 cold boot.

The authoritative original NVS/SPIFFS restoration then passed complete readbacks, with the active ec9 application, partition table and OTA metadata unchanged. A fresh binary snapshot passed with uptime 85\u219286 seconds and zero core errors; comparison matched original identity, complete contacts and all 40 channels. All 62 returned CLI fields and the original power/logging/MQTT flags matched. Only the owned diagnostic host BLE bond was removed; all four original checked services, including ModemManager, were active afterward. USB return and restored data do not qualify the failed Full logging-on case. No cause or firmware fix is established, and WSS remains unstarted while upstream changes are integrated.

For Full, tools/hil/official_console_acceptance.py uses explicit --terminal-mode companion. It sends the shipping terminal-start control through the unmodified official module on each open and confirms terminal-stop on each logging-off close. Those four setup writes are counted separately from the zero writes during idle. Full's labeled logging-status reply is checked against the exact live mode. An earlier attempt omitted this handshake and timed out before idle; another failed during owned Chromium startup before serial opened. Both remain separate failed harness samples.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#shipping-morse-results","title":"Shipping Morse results","text":"

The unchanged Full RAK4631/MeshPocket images were tested at 902.1 MHz with requested chip TX power at most -9 dBm, three different messages, 25/60 WPM, both directions and two repeats. No HIL threshold, timing or sampling override was enabled. All 24 decoded messages mismatched. Thirty-second no-transmitter controls produced 20 characters on RAK and four on Pocket.

Wanted peaks across all 24 trials ranged from -50.5 to -46.0 dBm; the 25 WPM subset ranged from -50.5 to -47.5 dBm. Maximum observed valid sample gaps were 2-3 ms. Quiet RSSI excursions reached -110.5 dBm on RAK and -92.5 dBm on Pocket, above their roughly -126/-125 dBm floors. This supports an automatic-detection qualification failure, without identifying external RF interference versus receiver-generated excursions. The reported bad counter is consecutive read failures, not a cumulative guarantee of no SPI errors. All decoded capture lengths matched their firmware headers and reached the end marker, excluding host paging truncation in these trials. Source inspection shows automatic squelch can admit sustained quiet excursions and corrupt mark or gap boundaries; RSSI extrema alone do not establish those pulse durations.

A separate five-minute Pocket quiet capture expired automatically after 300.40 seconds, retained seven false characters, and returned the same capture on repeated firmware paging. Lifecycle/retrieval passed; silence detection failed. Original settings were read back after both campaigns. Historical successful HIL fixed-threshold trials do not qualify the shipping detector.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#host-checks-and-ci","title":"Host checks and CI","text":"

The production Room fix passed the complete workflow at d52e4e93 (run 39107275083). A later tooling run exposed a host fixture that invoked real fuser against its fake device path. The stricter ownership guard correctly refused that diagnostic error. Commit 7a5a7993 makes the no-owner fixture deterministic and retains real refusal tests for busy/error cases; its complete workflow passed (run 39109551854). The later AP browser-observer workflow also passed at 5fd33a230 (run 39110179910); both that run and the preceding 7a5a7993 run completed with all jobs green. The native-console cleanup follow-up preserves the primary JS/transport failure separately from browser/TTY cleanup failures, reports missing or replaced endpoints, and cannot qualify a cleanup failure. The native-console and Linux TTY suites passed 35 offline checks. The follow-up workflow at 76220d5d7 completed successfully with all eleven jobs green (run 39115389840). The later recovery-report workflow at 7b41d2893 also completed successfully with all eleven jobs green (run 39116894845). Those CI passes do not rerun or qualify the failed hardware case. Documentation route/link checks also passed in the CI-pinned renderer environment.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#remaining-release-gates","title":"Remaining release gates","text":"

Private raw snapshots, flash images, credentials, BLE PINs, inbox traffic and cores remain outside the repository in the dated campaign directories.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/#mercerwood-full-firmware-testing-2026-10-19","title":"Mercerwood Full firmware testing, 2026-10-19","text":"

Later Companion results are recorded in the qualification follow-up. The records below retain the earlier campaign snapshot.

Source: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6 Version: v1.18.0.1-halo-keymind-cascade-dev-4e4f27a2

Builds: 13/13 targets, 15/15 profiles PASS. The two Reduced OTA profiles are compile-only; repeater hardware status uses Full OTA installation proof. Expected radios: 13. The earlier 10-second inventory observed 12 distinct USB identities. Tag was observed separately after its later recovery; no fresh fleet-wide count is claimed. USB enumeration does not establish physical presence or absence. Build success does not mean all 13 radios passed hardware testing.

Board Build Flash USB BLE RF Bootloader Heltec V4.3 PASS PASS PASS PASS unknown n/a RAK 3401 PASS PASS runtime only PASS n/a n/a C2 baseline PASS LilyGo T-Deck PASS PASS PASS PASS PASS n/a RAK WisMesh Tag PASS PASS PASS PASS PASS C2 baseline PASS Heltec MeshPocket PASS PASS PASS PASS PASS C2 baseline PASS SenseCAP Indicator PASS PASS PASS PASS PASS n/a RAK 4631 PASS PASS PASS PASS PASS C2 baseline PASS Heltec V3 PASS HEAD runtime PASS PASS PASS PASS n/a Seeed XIAO ESP32-S3 PASS PASS PASS PASS unknown n/a Seeed XIAO nRF52 PASS transfer PASS; runtime unqualified unknown unknown unknown C2 baseline PASS Heltec T096 PASS PASS PASS PASS PASS C2 baseline PASS Seeed T1000-E PASS PASS PASS FAIL PASS C2 baseline PASS Heltec MeshTower SD PASS PASS runtime only PASS n/a n/a C2 baseline PASS

Repeater rows cover Full OTA application/runtime verification only. Their runtime-only USB entries do not claim Companion 100/10, BLE or RF qualification. C2 bootloader entries describe source-qualified installation evidence, including the later Tag recovery, separately from application results. All 8 required profiles now have installation evidence; this is not an installed-flash CRC readback.

Tag recovery is a later, separate result. Its earlier non-enumeration remains in the JSON history. Before recovery, USB descriptor error -110 persisted across cable and port changes, while T1000-E enumerated on Tag's former connection. These observations did not establish a root cause.

A physically correlated application-only Legacy BLE transfer was accepted without writing the bootloader or SoftDevice; that phase did not establish the original USB or FICR identity. A subsequent USB read matched the original serial and clean normal Full 4e runtime. Exact C2 bootloader and matching normal Full application USB DFU then both completed, with the public UF2 version TEST_0x02040BFF verified. Package manifest CRC DE20EBCB was validated before transfer; no live installed-manifest CRC measurement is claimed.

Tag then passed 100 persistent USB query cycles, 10 reopen checks, owned-contact cleanup and warm reboot, and radio2 warning/suggestion trials. Its three 30-second BLE sessions matched the USB public identity and full version, retained clean core flags and ended disconnected. One zero-hop DEV datagram passed in each direction with the RAK hub; owned channels were cleared and radio leases restored.

The V3's first 15-second discovery trial failed. Its separate 25-second scan trial is PASS; the earlier failure remains part of the record.

T1000-E's initial batch BLE trial failed. The explicit later retry is FAIL. The initial failure remains part of the record.

Seeed XIAO nRF52 completed the normal application transfer, but normal runtime was not verified within the required 75-second window. The failed recovery phase and original batch outcomes remain in the JSON trial history. Bootloader baseline evidence is reported separately.

A later XIAO recovery attempt observed runtime CDC windows of 27-151 ms. One guarded serial open was attempted, but its completion was uncertain, no explicit DTR-low operation was reached, and exact DFU was not observed during the following 45 seconds. The startup diagnostic was not installed. After the user moved the USB plug, a 20-second inventory and separate 180-second sysfs watch did not observe the original XIAO identity; the new connection repeatedly reported USB descriptor/address error -71. The other 12 USB identities were present in a separate snapshot, and the Pi reported throttled=0x0. These observations do not identify a cable, power, firmware or bootloader root cause. A separate 25-second BLE scan found no XIAO DFU advertisement. Recovery requires a stable connection to the original device before another firmware transfer.

Source hardening prevents fresh XIAO identity generation from reading an unavailable radio and defers saved-radio settings retries until hardware is available. Eight focused host tests and the XIAO Full and legacy BLE builds passed. The new Full package retains all verified capabilities and is staged on the Pi, but has not been installed. These software checks do not resolve the USB enumeration cause or change the hardware qualification counts above.

The measured V3 92-contact refresh median was 3.347882 s at 32.560 kbit/s, versus 6.191396 s at 17.606 kbit/s for the earlier firmware. These are protocol throughput measurements on one V3/Linux connection, not BLE PHY rates or phone/full-app refresh claims. Measured results.

The historical inventory observed 12 distinct USB identities during a 10-second read-only window, including the cycling XIAO nRF52. This does not mean 12 stable ports. It preceded the separate later Tag recovery and has not been replaced by a fresh fleet-wide inventory.

This is a partial campaign report: 10/11 Companion USB qualifications, 9/11 BLE qualifications and 7 two-way RF links (8 boards including the RAK hub) passed. The full campaign is incomplete.

USB qualification used 100 persistent query cycles and 10 reopen checks, owned-contact CRUD and warm reboot, plus RX-only radio2 overlap-warning and three-decimal frequency-suggestion trials. BLE passes required three 30-second sessions. RF passes required exact two-way zero-hop DEV datagrams and owned-channel/radio-lease cleanup; they do not qualify text delivery, range, cold boot, phone interoperability or every firmware feature.

RF setup attempts with no completed delivery are retained separately; they do not count as failed deliveries.

MeshPocket previously reported error flag 8. Receive silence is a hypothesis, not a confirmed radio fault. A controlled warm reboot with flushed command delivery returned clean core flags, and its later two-way RAK RF test passed. The earlier unconfirmed reboot attempt and incomplete RF setup remain part of the history.

The T1000-E retry discovered the USB-bound advertisement, but pairing failed with a D-Bus error before any completed sessions. The NUS public key was not verified. Its initial batch discovery failure remains retained; BLE qualification is unresolved.

The retained pairing reason was Page Timeout.

V4.3 history retains 4 non-ASCII RF setup aborts and one separate failed USB setup diagnosis. An intervening guarded classification completed a clean normal HEAD handshake and closed its port; it did not qualify RF or run a transition drain.

The final read-only gateway check verified the active service held the RAK 3401 primary CDC0 interface.

The unit-test workflow passed for the measured firmware source 4e4f27a2: GitHub run.

Machine-readable results.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/#mercerwood-bench-qualification-12-active-radios-20-october-2026","title":"Mercerwood bench qualification: 12 active radios, 20 October 2026","text":"

21 October follow-up: the user lifted MeshPocket's bootloader-work exclusion. Its fresh Full Companion USB, BLE, contact persistence/sync and three two-way LoRa rounds passed in the MeshPocket qualification. The exclusion and source-bound measurements below describe the earlier 20 October campaign; the other twelve boards were not retested for this update.

Status: PASS. 12/12 active boards have complete accepted bench coverage; 0 have a latest required failure and 0 remain incomplete. The original inventory has thirteen radios: eleven Full Companions and two Full OTA repeaters. The active target is twelve radios: ten Companions and two repeaters. MeshPocket was operator-excluded at this campaign's close for separate bootloader work.

All thirteen 0865 candidate builds passed. Companion updates target 08650f644b680a5dfaacb95142e3cef8529da9d2; the table records each board's actual installed application. Both repeaters retain actual c46defaefd642973cbbdb473815d56b5b261cb2e (C46). Test-only commit 190064f4b5fa97e75dcfca7876568b757ba43b31 passed all twelve CI jobs. Build manifest SHA-256: 1dbf13ae8a3419f2c8e12d0432c37fa990d5bcfbb5649ab4d1d4ed4cec21a468. Seven active nRF52 boards retain the source 88d39510140ce6894cf8e74b93cbd28d7387fa21 bootloader baseline; Companion application-only updates transferred no bootloader or SoftDevice data.

That bootloader baseline describes this campaign's measurements. A subsequent independent deployment restored the original applications after installing an unpublished test bootloader on eight nRF52 boards. Its remaining USB stress and RAK data-preservation limits are recorded in the known-failure follow-up. This does not repeat or extend the source-bound qualifications below.

Board Installed source Required USB / BLE / RF / contacts Overall Heltec V4.3, FEM off 0865 USB/BLE/contacts PASS; RF retained ABF PASS RAK 4631 0865 USB/BLE/RF/contacts PASS PASS Seeed XIAO nRF52 0865 USB/BLE/RF/contacts PASS PASS Seeed T1000-E 0865 USB/BLE/RF/contacts PASS PASS Heltec V3 0865 USB/BLE/RF retained ABF; new contacts PASS PASS RAK WisMesh Tag 0865 USB/BLE/RF retained ABF; new contacts PASS PASS Heltec T096, FEM on 0865 USB/BLE/RF retained ABF; new contacts PASS PASS Heltec MeshPocket Current state unverified Historical PASS as of 22:28 UTC only; excluded from active target Operator-excluded LilyGo T-Deck 0865 USB/RF/contacts PASS; BLE PASS with WiFi quiet PASS Seeed XIAO ESP32-S3 0865 USB/RF/contacts PASS; BLE PASS with WiFi quiet PASS SenseCAP Indicator 0865 USB/BLE/RF/contacts PASS PASS Heltec MeshTower V2 SD C46 USB10/3, warm restart and RF3 PASS; SD PASS (read-only metadata) PASS RAK 3401 MQTT gateway C46 USB10/3 retained C46; new warm/RF3 PASS; service PASS PASS

The safe evidence summary records each axis's actual tested source, artifact, measurement interval, conditions and evidence digest. V3, Tag and T096 retain valid ABF USB/BLE/RF measurements; V4 retains ABF RF. Pocket's earlier ABF and 0865 results appear only in its excluded historical record; its current installed application and bootloader are unverified outside this report for operator bootloader work. The later Pocket tests are recorded separately in the follow-up linked above. ABF is abf7ba5d5c3958580d52d6954d816c08a035a552. For active boards, changed applications, current identities and new 64/32 contact trials are separate measurements. Reviewed source assurance 577aee5162c7553f58bf13b550560da50bbf73860ab37febe3674a5ae73c01aa covers the unchanged behavior; retained results keep their original source, artifact and time. Binary equality and remeasurement are not implied.

Companion USB checks cover 100 query cycles, ten reopens, owned CRUD, persistence through warm restart and applicable radio2 checks. BLE covers three authenticated thirty-second sessions, exact USB-bound identity and device information, clean core state and strict controller lifecycle. Two-way RF verifies exact zero-hop DEV datagrams, counters and owned channel/radio-lease cleanup. Each contact trial verifies 64 owned full-stream records and a 32-record timestamp subset, then removes them and proves absence after reopen while preserving unrelated records. Repeater coverage uses ten query cycles, three reopens, verified warm restart and three nonce-bound RF rounds. Gateway retains its earlier C46 query/reopen baseline; its new held warm restart and RF rounds keep separate intervals and evidence digests. SD coverage is read-only metadata, not media integrity.

T1000-E's eleven physical USB-priority phases passed. These qualify the USB-priority policy, not simultaneous active USB/BLE. The first current cold-host-release strict failure remains recorded before one later explicit unchanged-firmware retest passed. XIAO nRF52's original unaligned QSPI failure was corrected by shared buffer-alignment fix 714cfc4546ac43c220d06abff9adb128f58d16dd; its earlier strict BLE 0x3E failure also remains recorded before the later clean same-bond result.

T-Deck and XIAO ESP32-S3 later passed strict three-session BLE with USB connected and host WiFi quiet; earlier extra-0x3E failures remain in the history. WiFi was checked at trial boundaries, not continuously. T-Deck's initial two WiFi-on failures predate its current boot, so the observations do not isolate WiFi from boot/current-PIN/state differences. Its diagnostic wrapper retained a restoration-check FAIL; later read-only records separately verified handback. XIAO's original wrapper passed handback. Separate exact 64/32 contact trials, secure controller proofs and cleanups later passed with normal WiFi restored. WiFi association at 2437 MHz was observed before and after those trials. No WiFi root cause or firmware fix is established.

V4's earlier contact setup refusal preceded a later bounded-transition trial and exact cleanup. Tower's CAD2 history and Pocket's pre-DFU core8 refusal remain recorded before their separate warm preparations; preparations are not qualification passes. Tower's cumulative receive/read-error count does not prove CRC failure. Gateway's earlier warm failure had 43 complete responsive snapshots with advancing uptime, not a proven hang. Its later CAD2 baseline refusal came after the service was stopped. A separate one-request warm preparation preceded the qualification's own warm restart and RF3; the exact service/configuration owner then returned. Latest accepted results do not relabel those failed attempts.

These finite checks do not establish RF range, phone interoperability, long-duration reliability or every firmware feature. Earlier source-bound results remain in the fleet follow-up, diagnosis follow-up, and initial USB-priority report.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/","title":"Hardware validation ota 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#ota-hardware-qualification-20-21-october-2026-utc","title":"OTA hardware qualification, 20-21 October 2026 UTC","text":"

Status: COMPLETED for the listed OTA cases. Earlier failures and unresolved observations remain recorded below; this is not a claim that every board or transport has passed all release qualification. The operator authorized erasing these test nodes without backups. MeshPocket remains excluded. No nRF52 bootloader or SoftDevice payload is part of this campaign. Temporary ESP32 bridge role changes use the board's merged image, including its ESP32 bootloader and partitions, under the authorized erase.

The initial hardware OTA results use artifacts pinned to 8b5660b00b88d8abaae094859bc1232f7ce886fd. That commit changes documentation only relative to firmware source 0b207a1ec3a32eb0a8ad6b8643db211de4921cd4. Separate OTA-check version strings produce distinct image hashes for controlled update tests. The common numeric version is 0x01120001; same-version updates are explicitly authorized for these tests. They are not claimed as numeric version upgrades. Later source, CPU-sleep and one-hop results identify their own commits below.

Device and role Transport Result Heltec V4.3 Full Companion Production browser WiFi OTA PASS: one upload installed the exact intended image; postboot identity, version, core, radio and original flags verified RAK4631 unified repeater Bluetooth application DFU; QSPI full LoRa OTA; two-frequency bridged one-hop LoRa delta Transfers and eventual postchecks PASS, including one-hop installation; initial USB version response remains under investigation Seeed XIAO nRF52 repeater Bluetooth application DFU; external QSPI full LoRa OTA Transfers and eventual postchecks PASS; initial USB version response remains under investigation RAK3401 unified repeater and gateway Bluetooth application DFU; internal LoRa delta; gateway restoration PASS: Bluetooth update and repeater service, internal LoRa delta apply, normal-radio restoration and gateway service handback MeshTower SD repeater Signed SD-backed LoRa delta and full update PASS: signed SD delta and full-image transfer, installation and LoRa postboot checks"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#v4-wifi-ota","title":"V4 WiFi OTA","text":"

The unchanged production /update page selected the ordinary shipping image and started one native browser upload. Fresh USB CLI afterward reported the full intended dev-8b5660b0 version and EndF body hash B93CB6D7775BC072. The 2559496-byte image SHA256 is 0b37ec73fa39558d13e48213fe6c4ae365800c37811584bd93ea81aba1c2a8d7. The independent postboot/restoration phase passed with zero core errors, unchanged normal radio/TX, original runtime flags, and strict Binary handback. Its private summary SHA256 is 75d0ccaad83de2ce6b928a5545a48c84c03fcaf29a1a84cd4ceb6f3a16e1444a.

The browser observer reported a CDP command error after the upload action. It did not retain an HTTP status/result-body proof. This is an observability failure recorded separately from the installed-image proof; no HTTP 200 claim is made. The unchanged firmware was not uploaded again solely to repeat that measurement. An earlier preparation attempt used an obsolete WiFi status parser and timed out; its verified restoration passed before the corrected parser was used. Neither preparation attempt wrote firmware.

A known test admin password and the actual protected lab WiFi fixture were intentionally configured and retained. Secret values and network/device addresses are omitted. The Pi's network settings were unchanged.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#rak3401-internal-lora-delta-and-gateway-handback","title":"RAK3401 internal LoRa delta and gateway handback","text":"

The physical internal-flash delta completed all 9/9 blocks in 23 seconds. The 80-second post-apply probe verified the exact intended ordinary shipping EndF body hash 4748CB6ABFB3EBF4. Normal-radio LoRa communication passed after installation, and the source's original receive-power-saving setting was restored. This application-only update transferred no bootloader or SoftDevice payload.

The first gateway restoration guard failed when the initial ver command returned Unknown; its other getters, intended image hash and zero core errors were recorded separately. A second independent guard passed the complete verification and restored the original gateway service. Exact serial-port ownership and unchanged original service configuration were verified. The first failed guard remains a failed workflow record, not a gateway pass.

The later application-only LoRa delta to commit 2bd4cb4d1 completed all 55/55 blocks in 103 seconds. An on-air install acknowledgement verified the unsigned internal-flash delta, and on-air postboot reads proved exact body 1A3C9EB603C581C3 after 80 seconds plus the full dev-2bd4cb4d version. The receiving RAK used LoRa for setup, progress, installation and postboot proof. No receiver USB command drove this update. Normal radio restoration passed. Lost postboot status/version replies required bounded getter retries; their failed attempts remain in the private log. A subsequent independent USB diagnostic snapshot verified the same latest image, original saved preferences and zero core errors before the CPU sleep measurement.

That first latest-image CPU measurement FAILED. Over 90.013 seconds, the nonhalting MEM-AP capture recorded 8013 CPU states, with one sleeping state (about 0.01% weighted sampled time). There was no halt, lockup or reset in the capture. Firmware diagnostics reported zero sleep-wait calls and zero eligible time; the surrounding diagnostic window was blocked primarily by usb.recovery. The saved settings were CPU power saving on, USB logging on, USB debug off, the system watchdog on and the USB logging watchdog in Auto. The gateway service and UART were closed during the capture.

The watchdog's Auto policy still retained a previously confirmed reader and an initial recovery stage after the serial client closed. The shared awake hint treated that waiting policy as work requiring the CPU to remain awake. A focused nRF52 fix and physical retest are recorded below. Logging, watchdog settings and the failed result have not been disabled or cleared to obtain a sleep pass. CPU debug enable was already set and stayed unchanged; the explicit firmware sleep veto is recorded independently of that limit. Subsequent authenticated LoRa reads returned the exact same latest body and boot uptime, with zero core errors and both receive/transmit counters advancing by three. Radio communication remained functional after the failed idle measurement; this is not a CPU-sleep pass.

Commit bd4524318 allows the nRF52 interrupt wait while USB watchdog policy timers run. Actual transport recovery still vetoes sleep; logging, Auto/On mode and the existing recovery/reset deadlines remain enabled. Other platforms retain their prior sleep guard. The regression failed with the old predicate and passed with the fix across the four role guards. All 31 relevant regression methods passed. Blame places the earlier shared awake predicate in 3add4695cc and 18f7edcf82; it is present in the older 8b5660b0 image as well as 2bd4cb4d. The rebuilt image passed physical confirmation below.

The fixed RAK3401 image was installed using another application-only LoRa delta: 20/20 blocks in 71 seconds. The install reply was lost, so the install command was not replayed. Exact on-air postboot body 15B81DD21FB8A906 and the full dev-bd452431 version proved the outcome after 30 seconds. Normal radio restoration passed. Independent USB diagnostics afterward verified the same image, unchanged saved preferences and zero core errors.

The fixed CPU retest PASSED. A 90.013-second nonhalting MEM-AP capture recorded 7989 states, including 6275 sleeping states: about 78.6% weighted sampled time asleep. There was no halt, lockup, step or reset during the capture. Existing CPU debug enable stayed unchanged. This measures sampled CPU state, not electrical current or exact sleep residency. The surrounding firmware diagnostic window recorded 163516 sleep-wait calls and returns, zero errors, zero pending-recovery vetoes and zero time blocked by usb.recovery. Logging, CPU power saving, the system watchdog and the Auto USB logging watchdog remained enabled. The capture occurred before the existing recovery deadline; waiting for the watchdog to expire did not produce the pass.

After the capture, authenticated LoRa reads proved the same installed body and boot, zero core errors and advancing receive/transmit counters (+4/+3). The original diagnostic-enable setting was restored, and the original gateway service resumed with its unchanged configuration and exact serial port ownership verified. The additional diagnostic leases were released.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#rak4631-and-xiao-nrf52-full-qspi-lora-updates","title":"RAK4631 and XIAO nRF52 full QSPI LoRa updates","text":"

The RAK4631 downloaded all 325/325 blocks in 375 seconds. Its install response explicitly verified a full image on QSPI, and the post-apply probe found the intended ordinary shipping EndF body hash B0886153CE824448 after 30 seconds. Normal radio settings were restored to 910.5250244 MHz, 62.5 kHz bandwidth, SF7 and CR5 with TX at 2 dBm. The repeater role was retained. Fresh USB postchecks reported zero core errors and identified the W25Q16 QSPI device as JEDEC EF4015, with 2 MiB capacity. This qualifies external QSPI apply; an internal-only RAK4631 hardware result is not claimed.

The XIAO nRF52 downloaded all 280/280 blocks in 258 seconds. Its install response explicitly verified a full image on QSPI, and the post-apply probe found the intended ordinary shipping EndF body hash 725FDE69F8C0B8EA after 30 seconds. Normal radio settings were restored to the same frequency, bandwidth, SF and CR with TX at 22 dBm, and the repeater role was retained. Fresh USB postchecks reported zero core errors. The external Puya QSPI device reported JEDEC 856015 and 2 MiB capacity.

After each application update, the first independent USB ver command returned Unknown; a second independent read passed. The gateway showed the same observation. All failed first-attempt records are retained. This unresolved startup or CLI bootstrap observation is under investigation; it is not treated as missing command support or a first-attempt pass. The successful image, role, settings and core checks above describe the later independent verification.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#two-frequency-esp-now-bridged-one-hop-lora-update","title":"Two-frequency ESP-NOW bridged one-hop LoRa update","text":"

The physical one-hop test PASSED. The V4 source ran 2bd4cb4d1 and the RAK4631 received an application-only delta to bd4524318. Its setup, discovery, download, progress checks, install command, postboot proof and settings restoration all used LoRa. The receiving RAK's USB and Bluetooth interfaces were never opened for this case.

The measured path was V4 at 909.5 MHz to a V3 bridge, ESP-NOW to a XIAO ESP32 bridge, then LoRa at 908.5 MHz to the RAK4631. Both LoRa segments used 500 kHz bandwidth, SF7 and CR5. The two bridges copied received packets before appending their own hop, with one-hop limits. Disabling the far bridge's ESP-NOW link made the target's full-key query time out; re-enabling and verifying the same configuration restored that query. This counterfactual confirms that the two-frequency bridge was required.

The ordinary file-data session populated the source catalog and its automatic discovery burst. No separate announcement command or control channel was used. The first asynchronous catalog query requested discovery; the next query found the intended manifest. Download completed 62/62 blocks. The one install command received an on-air acknowledgement verifying the unsigned QSPI delta. Subsequent normal-channel LoRa reads proved exact installed body D2073EC88FF4E459, the full dev-bd452431 version and zero core errors. Original target policies, receive power saving, output path and the source's saved contact path were restored. The package SHA256 is f0a7319570394e23f06c9a901838be3cbaf5ae520d2743607828977e5fa3a0e9.

A passive source USB capture wrote zero bytes to the radio and recorded 47 matching update requests with one hop through the measured near bridge. This is source-side packet evidence, not an independent RF capture of the whole chain. The separate frequencies, bridge counterfactual and on-air install/postboot proofs establish the complete case. The integrated private receipt SHA256 is c6d6b2e2d3aefec128303b0ef5e2d8a0be52c38f03bc17ecf3032f4118489cc6.

Preparation failures remain recorded separately: a historical channel-busy flag prompted one documented LoRa restart, and several bridge receive-power configuration attempts returned a generic busy/unsupported rejection before a fresh configuration check and acknowledged attempt succeeded. No cause for those rejections or the historical busy flag is claimed fixed.

Both temporary ESP32 bridges were returned to Full Companion images built from ebb59d482. Exact runtime bodies were E297CF6E342C8332 (V3) and 4EEB12667E5E554B (XIAO ESP32), with matching Binary/CLI identity and zero core errors. The V3's first postflash Binary handshake timed out; a later independent read passed without a reset or another flash. Its role reply required a narrow correction to the bench parser's nested prompt handling. The XIAO's positive-readiness check also rejected a valid prompt-prefixed acknowledgement before sending any Binary request. After correcting that check, its first Binary handshake and complete CLI checks passed without another flash, reset or settings change. Original failed workflow receipts remain retained. These bench corrections are not claimed as firmware fixes.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#findings-and-fixes","title":"Findings and fixes","text":"

An authenticated nRF52 application handoff does not preserve a custom peripheral Bluetooth address. The retained bond belongs to the central peer; the bootloader can return at the peripheral's factory address. The first RAK attempt looked only at the custom application address and sent no firmware. A subsequent fresh authenticated handoff found the expected bootloader at its previously USB-bound factory address and passed live model, manufacturer, firmware and DFU revision checks before starting data transfer.

The host LoRa updater forced old fixed in-place workspace limits despite the images carrying hash-bound layout records. MeshCore commit 8e96b4d3b passes an explicit workspace only when requested, allowing motatool to size and validate the recorded application/staging layout. Legacy images retain the tool's conservative fallback. The focused file passed 318 host tests, including real-tool coverage for larger internal images, external layouts, explicit bounds rejection and legacy rejection. Package preparation alone does not qualify hardware apply.

The first RAK3401 LoRa attempt stopped during preflight because deployed adaptive-storage images omitted the bootloader ABI and codec mask from ota self. It changed neither firmware nor temporary radio settings. Commit 4b2f37013 adds those actual fields before the adaptive storage diagnostics. For existing images, the host runner verifies the known adaptive reporting contract with the installed bootloader's complete read-only status, including its board/name/target identity, CRC status and storage profile. That contract guarantees application codec subset 0x5; storage flags are not treated as codecs. Bootloader packages still require directly reported capability fields.

The updater host suite passed 365 tests with zero skips, and all seven firmware diagnostic/policy host tests passed. The affected RAK3401 canonical Full/Reduced build pair passed, reporting 619832/567640 bytes of flash use. An earlier build compiled successfully but its packaging guard rejected concurrent host-source changes; the second build completed packaging from a frozen workspace. The modified adaptive branch is preprocessed out of source-only Full Companion builds. These build results do not change the 8b5660b0 hardware-artifact binding above.

Commit 4c898ecbc recognizes native DEFLATE logical-block progress in the source's passive status output. It uses that output only as a progress signal, not as proof that the receiver has completed or installed an image. The host updater suite passed 369 tests with zero skips after this change.

Commit 720b9728c adds cold-idle RAK host-preflight preparation. The preparation may initialize optional staging media, but does not itself download firmware, install an image or change saved preferences. The host updater suite passed 379 tests with zero skips. The targeted live cold-idle RAK4631 check subsequently passed: one USB restart established the unchanged, idle application with unprobed storage, then authenticated LoRa reads observed that state and the host performed exactly one explicit storage-preparation query. The result was external QSPI with ABI 3 and codec subset 0x5, with unchanged body B0886153CE824448 and no firmware download or bootloader write. The first host attempt lost a version reply before preparing storage; a fresh read-only snapshot and bounded getter retries completed the check without another restart. Both attempt records are retained.

Commit 093fd98a3 retries lost remote clock reads within the existing bounded operation budget. Validation passed 671 tests, including nine new clock-read regressions; one optional PyPI integration test was skipped. Canonical Full and Reduced RAK4631 and RAK3401 pairs, plus Full V3 and XIAO S3 WIO images, built successfully from this commit. The ESP32 images were subsequently installed as the temporary one-hop bridges. Build success alone does not qualify an update or replace the earlier hardware results.

Repeater Bluetooth provides an update service; Companion terminal/sync functionality is not claimed for repeater builds. The RAK4631 reports a 2 MiB W25Q16 QSPI device and completed its full QSPI LoRa update as recorded above. RAK3401 live diagnostics report no external NOR and 252 KiB internal staging capacity, matching the completed internal delta case.

Commit 2bd4cb4d1 automatically restarts the existing four-beacon discovery burst when a complete, changed update catalog becomes available through the source's ordinary file-data session. It requires no separate announcement command or control channel. USB or WiFi can feed files into the source radio; receiving radios still discover and download them over LoRa. All 136 affected native test methods passed, including cold catalog discovery and a negative control that removes the production readiness trigger. Canonical V4 Full and RAK3401 and RAK4631 Full/Reduced builds passed. This fixes source discovery, and does not establish a fix for the separate Tower manifest timeout.

The V4 source subsequently accepted one application-only WiFi upload of this commit. Authenticated GET and POST both returned HTTP 200, followed by exact USB version/body proof (B6E8C347E1C5FA69) and unchanged saved preferences and receive-power-saving configuration. This deploys the source firmware; it is not a receiver LoRa qualification result. The initial preparation rejected a valid start acknowledgement with a WebConfig-status suffix before any upload; the corrected continuation did not replay the start command.

During RAK4631 discovery, source receive power saving was on and discovery timed out. After disabling that setting under a restoration intent, a fresh advert was received. This sequence records an observation; it does not establish a receive-power-saving firmware defect. Source receive power saving stayed off under the owned restoration intent while the remaining LoRa cases ran. Exact final source restoration is recorded below.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#meshtower-signed-sd-updates-and-remaining-failures","title":"MeshTower signed SD updates and remaining failures","text":"

Before the MeshTower OTA cases, its unchanged earlier application had a latched core error bitmask 0x02 while still receiving. This bit records a generic channel-busy wait exceeding four seconds. It is a historical flag, not a count of two errors or evidence that the radio is currently stuck. Diagnostics were retained. One software restart cleared the bitmask to zero, observed at 39 seconds uptime. Its normal radio settings and 0 dBm TX were unchanged, and the earlier application's EndF body remained 60EE4D4C61CFCA58. A later historical channel-busy flag was recorded again after that restart. No underlying channel-busy defect is claimed fixed, and a low ADC battery reading is not identified as the cause.

The MeshTower temporary-channel rehearsal at SF5 and 500 kHz bandwidth failed three full-public-key proof attempts before any firmware transfer. Natural lease expiry and return to the normal channel were independently verified. A retry at SF7 and 500 kHz bandwidth passed full-public-key, status and ota self proofs on the temporary channel. The SF5 preflight failure remains unresolved; no firmware was transferred in that attempt.

The signed MeshTower delta update then completed all 41/41 blocks in 206 seconds using SF7 and 500 kHz bandwidth. The install acknowledgement verified a trusted signature and a delta image on SD. The post-apply probe verified EndF body hash F1909AD4C46915FD after 70 seconds and a fresh OTA-check version built from 8b5660b0. Return to the normal radio was verified.

The first signed full-image attempt stopped before TempRadio when the host observed a zero clock epoch; it transferred no firmware. The bounded clock-read retry fix above addresses the host's handling of lost reads. A second full-image attempt at SF7 and 500 kHz bandwidth passed the full-key and status proofs, but all three ota self proof attempts failed. Safe temporary-lease expiry and return to the normal radio were verified. No firmware was transferred in either of those full-image attempts; full apply and postchecks were still pending at that point.

A local USB snapshot afterward confirmed body F1909AD4C46915FD, 1133 seconds uptime, core error bitmask 0x02 and an empty queue. Radio diagnostics reported RSSI -60 dBm, SNR 11.25 dB and noise -103 dBm. These values do not establish why the remote self proofs failed. A separate USB diagnostic attempt failed on open with error 71 before any getters. The Pi's DWC USB controller then re-enumerated the same Tower while application uptime remained at 1133 seconds. This is a separate host transport failure, without evidence of an application reboot or a basis for a brownout claim.

A read-only comparison of the earlier C46 and pinned 8b5660b0 handlers did not support the proposed clock or access-control explanation for the rehearsal failure. The relevant handlers were the same; no clock or ACL cause is claimed from this review.

A later local-control diagnostic made the signed full catalog visible, but both matching pull attempts remained at manifest timeout, 0/0 blocks. No install was requested. This local-control attempt is not a wireless-only qualification pass. Its temporary radio and saved receive-power-saving settings were restored and its USB port closed. One restoration command was explicitly rejected with a misleading \"unsupported\" response; source review shows that this response also covers a busy or failed reconfiguration. A fresh identity/settings check followed by one new, acknowledged restoration attempt proved the original configuration. No particular runtime guard is identified as the cause of that rejection.

A later signed-full retry used the ordinary production file-data session and source catalog readiness, with every receiver command on LoRa. Its first attempt stopped before new data because an old failed receive session required an explicit replacement choice. The one-minute temporary-channel cleanup command lost its acknowledgement. The updater then proved identity on the temporary channel and restored the source, but did not prove that the receiver had returned to its normal channel. The next attempt could not reach the receiver on the normal channel. These are failed workflow records.

A bounded, LoRa-only recovery found the exact Tower still running body F1909AD4C46915FD on the temporary channel. One command restored its normal radio; its acknowledgement was lost and the command was not replayed. Subsequent authenticated normal-channel reads proved the full identity, unchanged body, inactive temporary mode, normal radio and 0 dBm TX. No target USB, Bluetooth, reset or flash operation was used. A historical core bitmask 0x06 was retained: channel-busy timeout and radio outside receive for over eight seconds. It is a bitmask, not a count of six errors. The recovery receipt SHA256 is bc44880091e0024966fb6b2d9fd77b21556a9899e9c9995cf772dc14ae1bd57e.

After that recovery, a fresh ordinary updater attempt passed its temporary channel rehearsal and natural return, explicitly replaced only the old failed receive session, and downloaded the signed full image 279/279 blocks in 831 seconds. The install acknowledgement verified a trusted signer and a full image on SD. Normal-channel LoRa postboot reads proved body 718365A6E08823C7, bootloader apply OK and the full dev-ebb59d48 version. The first matching body response arrived during the 60-second postboot probe. A lost optional statistics reply required a bounded getter retry. The 570682-byte package SHA256 is 4f92525f7500ad74b7f1895a62c4626a02a049b1a3978906ae8eea8617393bde. The production runner exited successfully; its private result SHA256 is de1bb4c4fa7af7fe627c00d96c94d1f7244141556c29271754379ad8f7fd7ca9. All receiver setup, download, install and postboot commands used LoRa; its USB and Bluetooth interfaces were never opened for this attempt. The source was fed through its ordinary file-data session, with no extra announcement or separate receiver control channel.

An independent, authenticated LoRa postcheck then verified the new full body and version, repeater role, normal radio/TX, inactive temporary mode, zero core error bitmask and an empty transmit queue. It removed only the owned fourth test signing key and verified that the original three key prefixes remained in their original order. The CLI exposes prefixes, so this is not an independent full-byte readback of those three keys. The postcheck repeated the identity, radio and core checks after removal and closed its controller. Lost getter/status replies used bounded read-only retries; all failed attempts remain recorded. Its private summary SHA256 is 02964ab9a2f511f96055ef6c99c59d32bb091d21dbf5c61dbe9de5ac00555659.

The passive source-only capture recorded 405398 bytes and wrote zero USB bytes. Its SHA256 is 6558971dea7e9cdc221e3e196d34f5ae00427300932c766e3fb7798d3332d194. The capture was stopped and its serial port closed before the source's original logging-off setting was restored and verified. This capture is source-side evidence; the receiver's installed-image and runtime proofs above came from LoRa.

The host cleanup defect is separate from the radio transfer: temporary-channel identity alone cannot establish that a lost relative-lease command executed. Cleanup must retain its recovery obligation until a full-key proof and inactive temporary mode are observed on the normal channel. The same requirement applies independently to controlled relay participants.

Commit c5c67f8f7 fixes that host ownership defect. It persists a private recovery record for each full-key-bound target and controlled relay before arming, records a one-shot shortening attempt before sending, and requires each participant's full identity plus inactive temporary mode on the normal channel before retiring its obligation. A shared Full Companion source must also report its actual temporary mode inactive; its saved Binary radio settings alone are insufficient. Lost relative-lease acknowledgements are not replayed. Unresolved records survive interruption without passwords, and manual read-only recovery is documented in the LoRa OTA automation guide. The regression cases cover delivered and undelivered lost acknowledgements, independent relay failures, mismatched keys, active temporary profiles, interruptions, existing recovery records, resolved-password redaction and the explicit no-install staging path. These are host changes; accepted unchanged firmware was not flashed again to validate them.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#final-source-restoration-and-handback","title":"Final source restoration and handback","text":"

After all transfers and the Tower signing-key cleanup, two fresh USB cleanup attempts received an explicit Error: radio busy; retry while restoring the source's original RXPS setting. Both attempts stopped and retained their raw failed records. This firmware rejects that command before updating or saving RXPS preferences; a missing or ambiguous reply would not permit the same continuation. Fresh read-only diagnostics showed normal receive operation, zero core errors, an empty queue and RXPS still off. They did not identify which radio reconfiguration guard rejected those particular commands.

A bounded continuation used the production updater's varying retry cadence for this exact explicit busy response, opening a fresh console after each rejection. Its second attempt received the exact positive acknowledgement and exact original configuration readback: on, level 8, preamble 16, 39657 microseconds receive and 15975 microseconds sleep. It did not retry a lost or malformed acknowledgement, reset the radio or change firmware. The continuation receipt SHA256 is 4253c91dbf2eca91e7b2716367e27e95a7dd1b952008b9ef6bb036fd673d3c62.

A separate final cleanup skipped the already restored RXPS setting, restored all five original saved flags, and verified normal radio/TX, idle detached OTA file service, original hop settings and strict USB Binary handback. It disabled the test WiFi runtime before restoring MQTT. The completed private summary SHA256 is 1c07fc8301f883eb3a33201924ca77ce4d2d02573b9a27a785f84547a06a8165. The hash-bound completed receipt retired the host's RXPS restoration intent. Owned test tunnels, target leases and the cold-idle hold were closed; the root hardware session had no remaining FLOCK descriptors before it exited. The already restored RAK3401 gateway service was left running.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#limits-and-completion","title":"Limits and completion","text":"

The listed physical OTA cases and their restoration are complete: V4 WiFi, RAK3401 internal delta, RAK4631 and XIAO nRF52 external QSPI full images, Tower signed SD delta/full, and the two-frequency ESP-NOW bridged one-hop update. Latest RAK3401 CPU sleep, post-idle LoRa and gateway handback passed. Each case records its exact firmware commit and body; these results do not qualify all 13 radios against every later commit.

Receiver setup, progress, installation and post-install verification in the new radio campaign used LoRa. Receiver USB commands did not substitute for failed radio communication. Earlier local-control diagnostics remain explicitly identified. The RAK3401's pre-fix historical channel-busy bitmask 0x02 was retained without clearing statistics or claiming a sleep result from that snapshot.

The first-command USB bootstrap observation, Tower SF5 preflight failure, earlier full-image self-proof failures, separate USB open failure and historical radio-busy flags remain unexplained. Later full-image transfer and postchecks passed under the stated conditions; this does not establish their underlying causes. Source restoration's explicit busy replies were handled with bounded retries; no radio guard defect is claimed fixed by that action. Private raw responses, keys, PINs and device identities stay outside the repo. Earlier accepted Companion tests are not repeated without a relevant change.

"},{"location":"test-results/heltec_t114_display_detection/","title":"Heltec t114 display detection","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_t114_display_detection/#heltec-t114-automatic-screen-detection","title":"Heltec T114 automatic screen detection","text":"

The canonical T114 firmware now includes the optional ST7789 screen driver and checks for the stock screen at boot. Screenless boards use the same firmware. Legacy without_display build names remain compatibility aliases; automatic release selection uses the canonical targets.

"},{"location":"test-results/heltec_t114_display_detection/#hardware-basis","title":"Hardware basis","text":"

Heltec's stock panel specification identifies the controller as ST7789V2 and describes SDA as an SPI input/output pin. The existing T114 wiring supports register reads over this shared data line; a separate MISO wire is unnecessary.

The Sitronix controller specification documents the read-ID command and serial turnaround on pages 58\u201364 and 167, and reset recovery on pages 48\u201349. Its ID bytes are programmable, so detection does not require a single guessed factory ID.

"},{"location":"test-results/heltec_t114_display_detection/#expected-behavior","title":"Expected behavior","text":""},{"location":"test-results/heltec_t114_display_detection/#validation","title":"Validation","text":"Check Result Native probe, GPIO, headless and wake regressions PASS, 4 methods with ASan/UBSan; late-turnaround, reset-delay and floating-line negative controls Resolved recipe and compatibility-alias checks PASS, 5 methods; actual PlatformIO configuration also confirms all 5 legacy aliases match their canonical targets Companion headless buttons and related navigation, reader, pairing, startup and deadline suites PASS, 55 methods, including native headless fault regression and 4 input/reader configurations Picker and release-selection regressions PASS, 78 Python methods and 3 Node suites; old assets, scoped previews, newest-role defaults and unavailable legacy choices covered Display allocation accounting and firmware RAM gates PASS, 46 methods, including production allocation tracking across 3 panel sizes and buffer configurations T114 firmware and package checks PASS, all 8 published profiles; UF2/DFU application bytes, hashes, version, hardware/OTA identity, flash limits, display capability and RAM admission checked Hosted documentation links PASS, 7 methods Physical T114 with and without a screen Not performed

The T114 1.18.0.1 preview1 packages firmware source 794f5f7b8c508b708ee8fc7db7bff066aa3d1a0a. Its seven display-capable profiles contain the verified automatic-detection marker; the eighth is the headless KISS utility. The published qualification report records each profile's flash range, retained update identity and RAM allowance. Repeater Full has the smallest RAM margin at 10,200 bytes after the display allocation and normal transient margin. KISS has no embedded EndF version or update identity; its package provenance, UF2/DFU application equality, flash bounds, hashes and RAM gate are checked.

The release-bound picker check offers those eight profiles under the single canonical T114 hardware choice, with seven automatic-display profiles and one headless KISS profile. It preserves all 111 other hardware profiles from the current main release and retains historical T114 files as exact downloads.

Native tests simulate controller replies and GPIO state; they do not establish physical detection reliability. Before calling the hardware qualified, test cold USB and battery boots with a stock screen and with no screen, then verify screen sleep/wake, Bluetooth/USB connections, and normal radio receive/transmit.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/","title":"Heltec v2 ble memory 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#heltec-v2-ble-ram-reserve-repair-22-october-2026","title":"Heltec V2 BLE RAM reserve repair, 22 October 2026","text":"

Status: build, memory and host tests PASS. Physical V2 operation was not tested in this work. The earlier RAM failure remains recorded below.

Clean source 1d4016f48 was built through the canonical builder as Heltec_v2_companion_radio_ble, using the auto feature profile, Cascade runtime defaults, USA Cascadia radio preset and release-length version v1.18.0.1-halo-keymind-cascade-dev-1d4016f4. The safe receipt binds the source, linked measurements and packaged artifact hashes.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#change-and-retained-behavior","title":"Change and retained behavior","text":"

Only V2 BLE explicitly enables the existing MESH_CONTACT_CACHE policy. Its Heltec_v2_companion_radio_ble_ps alias inherits the same setting. Other V2 profiles and the default cache policies of other boards are unchanged.

All 160 contacts, eight channels and 128 offline frames remain available. The cache keeps 16 paths and 16 shared secrets resident, rather than embedding a complete path and shared secret in every contact object. Other paths remain accessible through checked reads of existing contact records; cold ESP32 secrets are recalculated without secret-file I/O. These resident-cache limits do not reduce the contact capacity. Cold access can add filesystem reads or key-calculation work.

The Bluedroid Bluetooth host, PIN authentication, bond storage, dependencies, radio/display setup, contact protocol and 152-byte contact-file records remain unchanged. No pairing migration or repartitioning is introduced. The packaged capability verification passes with an empty reductions list.

The existing 8,192-byte static DRAM reserve remains mandatory. The runtime model additionally reserves 6,144 bytes for this explicit ESP32 cache opt-in: 4,096 bytes for retained reader buffering plus 2,048 bytes for reader objects, allocator metadata and related overhead. This is above the unchanged common filesystem and transient allowances; it is not a claim of zero heap cost. Automatic Full profiles keep their existing accounting policy.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#linked-results","title":"Linked results","text":"Check Baseline 76437c6bf Fixed 1d4016f48 Static DRAM region 124,580 bytes 124,580 bytes Occupied static span 122,964 bytes 112,140 bytes Static DRAM free 1,616 bytes 12,440 bytes Required static reserve 8,192 bytes 8,192 bytes Margin above reserve -6,576 bytes, FAIL 4,248 bytes, PASS

The fixed image saves 10,824 static bytes. Its runtime model passes with 181,400 internal bytes available versus 141,104 required, including the extra reader allowance. The modeled margin is 40,296 bytes; these are linked capacity estimates, not running free-heap or fragmentation measurements.

The application occupies 1,810,568 of 3,342,336 app-slot bytes. The partition table is byte-identical to the baseline. Both firmware and merged packages, capability verification and unchanged application-size gates pass. The failed baseline ELF/map and successful candidate/final artifacts are retained privately.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#verification-and-limits","title":"Verification and limits","text":"

All 74 host test methods passed:

The unit workflow now builds this exact classic ESP32 target at release-version length and enforces both memory gates. The inherited power-saving alias is checked by the recipe contract; an identical feature configuration was not built a second time. No firmware feature or capacity was removed to obtain the pass, and no memory gate was lowered.

No radio was flashed or contacted. Physical V2 boot, pairing/reconnect, populated contact sync, filesystem latency and sleep remain outside this build/host result. It closes the documented V2 RAM build failure, not those hardware qualification claims.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/","title":"Heltec v3 full memory 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#heltec-v3-full-memory-qualification-2026-10-19","title":"Heltec V3 Full memory qualification, 2026-10-19","text":"

Canonical V3 Full now passes its flash and runtime-RAM gates with 6,032 bytes of modeled RAM margin, retaining its existing features and capacities. Physical testing is pending: no V3 is currently exposed to the VM, and no board was flashed during this work.

Measured source: eb9259f2a97113b1d482ca9610661fb24c1c8e98. The machine-readable evidence records package hashes, allocation components, capabilities and linked ARM checks.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#what-changed","title":"What changed","text":"

V3 Full uses the existing NimBLE-Arduino 2.5.1 Companion adapter. It preserves the authenticated NUS service, PIN, address/stealth controls and frame queues. The canonical profile retains one active BLE session, 15 saved phones and 30 CCCD records for NUS TX and Service Changed subscriptions. This does not reuse the smaller default bond store from the optional trial configuration.

Bluetooth upgrade step: existing Bluedroid bonds are not migrated. Forget the previous pairing in the phone's Bluetooth settings and pair again after installing this image. The PIN setting remains available as before.

Non-PSRAM ESP32 Full also uses a bounded external-source Merkle cache. It keeps 32-leaf groups and their roots, then rereads the selected group to generate the same wire proof. It supports the same 4,096 source blocks, folder capture, raw/host-encoded/device-encoded serving, getter requests and transport protocols. A proof step issues at most one metadata transaction; jobs survive partial progress and radio backpressure.

The complete workspace is 1,004 bytes in the 32-bit Xtensa layout, with a production sizeof assertion enforcing a 1,536-byte bound. Its output block and allocation allowance remain separate. The legacy 16 KiB source-leaf allocation is absent on this path. PSRAM, nRF52 and other unqualified paths retain their existing implementation, as do explicit larger custom source limits.

The RAM check accounts for this smaller real allocation. The 16 KiB transient margin, Bluetooth/WiFi allowances, MQTT admission, stacks and minimum floors remain unchanged. There is no new OTA queue loan or reduction in offline frames.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#measured-image","title":"Measured image","text":"

Both comparisons use the canonical release-length version, Cascade profile and USA Cascade preset. The baseline is 31e693ec2; it already failed admission before the shared contact-memory changes.

Measurement Baseline Final Internal capacity after linking 177,144 189,616 Modeled allocation requirement 198,432 183,584 Modeled margin -21,288 6,032 Application bytes Not admitted 2,114,744 Application-slot capacity \u2014 3,342,336 Application-slot margin \u2014 1,227,592

The existing V3 Full recipe retains 100 contacts, 40 channels, 256 offline frames and one MQTT runtime slot. These limits were not reduced by this fix. All 24 linked/package feature checks pass, alongside the Full profile marker. USB, Bluetooth, simultaneous WiFi, direct MQTT, WebConfig, logging and OTA remain in the image. Modeled capacity is not a running free-heap or load measurement.

Final ELF SHA-256: 3a56805b2d0216026c3710e149db43dedb677684e47ad08ebfe748094c1de55b. Application SHA-256: c4f276095626560abc4c6875216c147d2fae292bf5c514e5029ef7d03454d3ce. Every file bound by the packaged memory manifest was hash-verified.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#verification","title":"Verification","text":"

PlatformIO commands ran one at a time in a clean checkout. Reproduce the V3 package with:

bash build.sh build-firmware Heltec_v3_companion_radio_full --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota\n

Hardware acceptance still requires the physical V3: preserve its flash and settings, then check boot, live heap under simultaneous transports, contacts, Bluetooth pairing/reconnect, MQTT, WiFi administration, OTA and persistence. These software results do not establish that hardware acceptance.

"},{"location":"test-results/heltec_v3_preview3_ram/","title":"Heltec v3 preview3 ram","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v3_preview3_ram/#full-companion-ram-audit-and-heltec-v3-preview-3","title":"Full Companion RAM audit and Heltec V3 preview 3","text":"

Date: 2026-10-02. Baseline: 0ccac453 plus the early startup-screen changes included in this revision. All 13 selected ordinary Full builds passed the existing linked runtime RAM guard. This is a targeted audit, not a claim that every Companion has been rebuilt or hardware-qualified.

"},{"location":"test-results/heltec_v3_preview3_ram/#builds-needing-the-most-attention","title":"Builds needing the most attention","text":"Hardware Available bytes Reserved bytes Margin bytes Generic ESP-NOW 143,872 142,336 1,536 Tracker V2 (FEM on) 180,000 177,746 2,254 Heltec V3 185,032 182,016 3,016 XIAO ESP32-C3 146,048 142,336 3,712 Heltec CT62 146,240 142,336 3,904 Wireless Tracker 184,304 178,946 5,358 Heltec T096 (FEM on) 84,152 75,298 8,854 Heltec T1 84,488 75,298 9,190 LilyGo TLora V2.1.1.6 185,304 173,840 11,464 Heltec V2 187,176 173,840 13,336 TBeam SX1276 194,160 169,744 24,416 TBeam SX1262 196,112 169,744 26,368 Heltec T114 84,336 46,880 37,456

The six smallest margins belong to non-PSRAM ESP32 Full Companions. They pass their existing release capacities but have little room for further growth. Heltec T096 and T1 are the next constrained screen-equipped nRF52 Full builds. T114 Full passes; the failed older T114 BLE-only recipe is not a Full failure.

Values come from the firmware RAM guard in the retained local out/companion-ram-*-before.log build logs. Available bytes exclude statically reserved memory and unavailable internal regions; the reservation covers startup allocations and a transient allowance. The margin is not measured live free heap or a prediction of time to failure. No safety budget was lowered.

"},{"location":"test-results/heltec_v3_preview3_ram/#v3-solution-under-test","title":"V3 solution under test","text":"

The opt-in platformio.heltec-v3-preview3.ini profile uses the existing NimBLE-Arduino adapter, pinned to version 2.5.1, instead of Bluedroid. It retains the current ordinary V3 Full capacities: 100 contacts, 40 channels, and 256 offline frames, including during mOTA. It adds no new queue-sharing or storage changes and does not enable the larger 350-contact capacity trial.

V3 image Available internal bytes Reserved bytes Margin bytes Published preview 2 185,040 182,016 3,024 Preview 3 200,152 182,016 18,136

Compared with published preview 2, preview 3 recovers 15,112 bytes of internal RAM without removing Full features. Its bootloader and partition-table bytes match preview 2. The normal release matrix is unchanged.

Bluetooth still provides the Companion server, encrypted PIN pairing, bonding, MAC policy and stealth controls. Existing Bluedroid bonds are not migrated: forget the old device in the phone's Bluetooth settings and pair again after installation. This does not require erasing contacts or identity.

The preview also contains the early Starting... screen and animated key generation display. The saved Off and pairing-only display modes remain respected. Normal UI timeout starts after startup. Fresh installs still default to Bluetooth on and infrastructure WiFi off; saved WiFi-on settings remain on. Use set wifi.enabled 1 to enable WiFi.

"},{"location":"test-results/heltec_v3_preview3_ram/#verification-and-remaining-work","title":"Verification and remaining work","text":"

The real embedded build passes the unchanged RAM guard and fits both existing WiFi OTA slots. Capability checks inspect the linked ELF, packaged application and WebConfig gzip asset. The package includes ELF-bound RAM proof and hashes.

Adapter regressions exercise actual NimBLE transport code under sanitizers, including identity/bond failures, encrypted PIN authentication, stealth, notifications, MTU gating and frame retry. Startup-screen, display policy, Companion preferences, USB defaults, identity and frame-queue regressions are also checked. Embedded compilation uses the actual pinned NimBLE library.

This is still a hardware-test preview. V3 first-boot screen, phone pairing and reconnection, contact sync, WiFi/BLE coexistence, mOTA sending, and sustained load need physical verification. A build passing RAM checks does not establish that all reported V3 boot symptoms are fixed. Do not make this stack the default on every board before those checks.

Reproduce with one PlatformIO process at a time:

pio run -c platformio.heltec-v3-preview3.ini \\\n  -e Heltec_v3_companion_radio_full_preview3 -t mergebin\npython3 -B test/test_heltec_v3_preview_profile.py\npython3 -B test/test_nimble_companion.py\npython3 -B test/test_startup_screen.py\npython3 -B test/test_firmware_ram.py\n
"},{"location":"test-results/home_text_spacing/","title":"Home text spacing","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/home_text_spacing/#home-screen-text-spacing","title":"Home-screen text spacing","text":"

On non-Indicator companions, a visible Bluetooth PIN or connection status owns a separate bottom-aligned block. It replaces the ordinary inbox instruction, clock and WiFi rows while displayed. The PIN stays below the inbox title; the label and value use measured font heights, including the MeshPocket's larger e-paper font. An unusually short viewport may omit the label, never overlap the title or clip the PIN. Indicator's dedicated pairing layout is unchanged.

The repeater home page also stacks measured font-height rows instead of fixed ten-pixel offsets. Long names and status lines are ellipsized horizontally.

Run python3 -m unittest discover -s test -p test_home_text_spacing.py -v on a host with a C++ compiler. This renders the actual companion and repeater home branches, recording every text rectangle and rejecting overlaps or clipping. It covers 128x64 OLED, 160x80 T096, 250x122 MeshPocket and larger viewports, PIN/connected/disabled/prompt states, WiFi variants, long strings and a sweep of font heights. It also checks the static reader hint. The separate native display and T096 tests exercise the driver drawing/measurement methods.

Hardware acceptance: check that the full six-digit PIN is near the bottom, with no text over it; connect/disconnect Bluetooth and verify no stale digits; then check the repeater's five rows and the V4's non-blinking reader footer.

"},{"location":"test-results/mbedtls-tls-footprint/","title":"Mbedtls tls footprint","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mbedtls-tls-footprint/#shrinking-the-per-connection-tls-footprint-on-non-psram-observers","title":"Shrinking the per-connection TLS footprint on non-PSRAM observers","text":""},{"location":"test-results/mbedtls-tls-footprint/#why","title":"Why","text":"

On a non-PSRAM Heltec V3 running two WSS/JWT broker slots, the largest allocatable block in internal DRAM walks down in ~16 KiB steps at every TLS reconnect while total free heap stays flat. Measured on hardware over 50 reconnect cycles: 62,452 -> 16,372 bytes, permanently.

The step size is not a coincidence. framework-arduinoespressif32 3.20017 (Arduino 2.0.17, IDF 4.4) builds mbedTLS with the symmetric buffer configuration:

CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN 16384      # sizes BOTH the in and out record buffers\nCONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE 1\n# CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN       -- not defined\n# CONFIG_MBEDTLS_DYNAMIC_BUFFER               -- not defined\n# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH   -- not defined\n

Read from packages/framework-arduinoespressif32/tools/sdk/esp32s3/qio_qspi/include/sdkconfig.h. Note the separate framework-arduinoespressif32-libs package in ~/.platformio belongs to the esp32c6 env (pioarduino, IDF 5.3) and is not what this env links against -- though it happens to carry identical mbedTLS values.

So each broker slot costs 2 x 16 KiB = 32 KiB of contiguous internal DRAM in record buffers alone, and two slots cost 64 KiB on a board with roughly 80 KiB free. Every reconnect frees and re-allocates those buffers, and anything that lands in the vacated hole in between prevents them from going back, which is the ratchet.

Confirmed by two independent observations: losing a whole TLS session returned exactly 16,384 bytes of contiguity on one device and exactly 32,768 on another, and per-connection teardown frees ~41.6-44.7 KB total.

"},{"location":"test-results/mbedtls-tls-footprint/#what-the-firmware-could-already-do-and-its-limit","title":"What the firmware could already do, and its limit","text":"

softDisconnect() (branch perf/mqtt-renewal-no-stop, commit 6c51e468) stops the JWT renewal bounce from destroying and recreating the esp-mqtt task, keeping its 6 KiB stack out of the hole. Measured: the staircase arrests after 2 steps at 36,852 through cycle 16, where the baseline took 4 steps and settled at 16,372 by cycle 11 -- about 20 KB better.

That is as far as the application layer reaches. MQTT 3.1.1 has no re-authentication packet, so presenting a fresh JWT requires a transport reconnect; mbedTLS's internal allocation order during the handshake is not controllable from the application. The remaining cost is the record buffers themselves.

"},{"location":"test-results/mbedtls-tls-footprint/#the-changes","title":"The changes","text":"

All three are compile-time in mbedTLS, and the Arduino framework ships precompiled .a archives (tools/sdk/esp32s3/lib/libmbedtls.a), so a project-level -D cannot change them. A custom framework build is required.

Setting From To Saving per connection CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN unset y enables the two below CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN 16384 (implied) 16384 none -- keep it CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN 16384 (implied) 4096 ~12 KiB CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE 1 n ~4 KiB

Roughly 16 KiB per connection, 32 KiB across two slots.

"},{"location":"test-results/mbedtls-tls-footprint/#why-inbound-stays-at-16-kib","title":"Why inbound stays at 16 KiB","text":"

A peer may legitimately send a 16 KiB TLS record. Lowering the inbound limit only works if both brokers negotiate the max-fragment-length extension or provably never send larger, and getting it wrong produces invalid-record and handshake failures rather than a clean error. Outbound is ours to choose: this firmware's MQTT and WebSocket frames are far below 4 KiB (MAX_TRANS_UNIT-bounded packets plus small JSON), so 4 KiB is comfortable.

"},{"location":"test-results/mbedtls-tls-footprint/#risk-on-the-peer-certificate","title":"Risk on the peer certificate","text":"

Dropping KEEP_PEER_CERTIFICATE means mbedtls_ssl_get_peer_cert() returns NULL after the handshake. Chain validation still happens -- only retention of the parsed leaf changes. This firmware verifies against a CA (GTS_ROOT_R4 / the bundle) and never inspects the peer certificate or a fingerprint, so it should be safe. It does change mbedtls_ssl_session layout, which is exactly why the whole framework must be rebuilt together rather than swapping a single archive in.

"},{"location":"test-results/mbedtls-tls-footprint/#build-procedure","title":"Build procedure","text":""},{"location":"test-results/mbedtls-tls-footprint/#do-not-use-esp32-arduino-lib-builder-for-this","title":"Do not use esp32-arduino-lib-builder for this","text":"

release/v4.4 is the branch matching Arduino 2.0.x, but its update-components.sh clones every dependency at master, so it no longer resolves. Five successive failures, each a different repo: the arduino branch name it passes to -A does not exist; jq is absent from the IDF image and its absence makes build.sh's target loop a silent no-op that still exits 0; esp_littlefs and esp32-camera master require IDF >=5.0/>=5.1; esp32-camera later needs an esp_jpeg version the 4.4 registry cannot satisfy; and tinyusb's source layout no longer matches arduino_tinyusb/CMakeLists.txt. Pinning each one in turn just surfaces the next.

"},{"location":"test-results/mbedtls-tls-footprint/#rebuild-only-the-mbedtls-archives","title":"Rebuild only the mbedTLS archives","text":"

More rigorous anyway, because it reuses the shipped sdkconfig verbatim -- so the archives differ from stock only by the intended change, with no arduino-version or DYNAMIC_BUFFER drift.

This is ABI-safe for the content-length change specifically: ssl.h declares in_buf and out_buf as unsigned char *, allocated in ssl_setup(), and no public struct embeds a CONTENT_LEN-sized array. The other precompiled archives (esp-tls, esp_http_client, esp-mqtt) therefore remain compatible. It is not safe for CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE, which changes mbedtls_ssl_session layout -- that one needs everything rebuilt together, so it is excluded here.

  1. Minimal IDF project whose only component requirement is mbedtls.
  2. sdkconfig.defaults = the shipped packages/framework-arduinoespressif32/tools/sdk/esp32s3/sdkconfig, with CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384 replaced by the three asymmetric lines. Verify the diff is exactly 5 lines before building.
  3. docker run --rm -v $PWD:/project -w /project espressif/idf:v4.4.7 idf.py -DIDF_TARGET=esp32s3 build
  4. Confirm build/config/sdkconfig.h carries OUT_CONTENT_LEN 4096.
  5. Stage the four archives under the framework's names -- note the rename:
built framework name stock size rebuilt esp-idf/mbedtls/libmbedtls.a libmbedtls.a 113,914 113,338 esp-idf/mbedtls/mbedtls/library/libmbedtls.a libmbedtls_2.a 1,252,150 1,245,718 .../libmbedcrypto.a libmbedcrypto.a 4,302,698 4,259,458 .../libmbedx509.a libmbedx509.a 676,590 673,318

All within ~1% of stock, which is a good check that only the config differs.

"},{"location":"test-results/mbedtls-tls-footprint/#wire-it-in-with-l-never-platform_packages","title":"Wire it in with -L, never platform_packages","text":"
PLATFORMIO_BUILD_FLAGS=\"-L/path/to/staged/archives\" pio run -e Heltec_v3_repeater_observer_mqtt\n

Verify with grep -oE \"[^ ]*libmbed[a-z0-9_]*\\.a\" .pio/build/<env>/firmware.map | sort -u -- every path must be the staged directory.

Do not point platform_packages at a file:// copy of the framework. PlatformIO installs it over the shared ~/.platformio/packages/framework-arduinoespressif32, silently changing mbedTLS for every other ESP32 env and project on the machine. It does this even when the copy's package.json version differs -- verified twice here, and both times the fix was rm -rf the package and pio pkg install to re-download stock. A prepended library search path keeps the change scoped to one env, because the linker takes each archive member from the first archive that satisfies an undefined symbol.

"},{"location":"test-results/mbedtls-tls-footprint/#how-the-archives-are-distributed","title":"How the archives are distributed","text":"

They are not committed: ~6 MB per architecture, and they have to be rebuilt for every platformio/espressif32 bump, so committing them would grow history permanently and go stale silently. Instead they are published as a release asset and fetched on demand:

scripts/fetch_mbedtls_4k.sh esp32s3            # download + verify against the manifest\nMESHCORE_REDUCED_TLS=1 pio run -e Heltec_v3_repeater_observer_mqtt\n

The opt-in path is deliberately loud, because both ways this can go wrong produce a firmware that looks correct and silently lacks the change:

failure what happens without a guard guard directory missing or partial linker ignores an unusable -L and resolves mbedTLS from the framework pre-build: hard error archives stale after a platform bump links the wrong build pre-build: sha256 vs manifest -L present but outranked framework archives win, flag is inert post-link: firmware.map must resolve every libmbed*.a to .mbedtls-4k/

That last one is the reason the post-link check exists rather than trusting the flag: a build flag reaching the compiler proves nothing about what got linked.

"},{"location":"test-results/mbedtls-tls-footprint/#how-to-verify-it-worked","title":"How to verify it worked","text":"
  1. strings/grep the new sdkconfig.h for the four settings.
  2. Build and check the RAM figure; static usage should be unchanged (these are heap buffers).
  3. On hardware, get mqtt.stats at boot with two slots connected: the largest free block should start roughly 24-32 KiB higher than the current 62-67 KiB.
  4. Soak across reconnect cycles and compare the floor against the two recorded runs: baseline settled 16,372 (cycle 11); softDisconnect holds 36,852 (cycle 16).
"},{"location":"test-results/mbedtls-tls-footprint/#prior-art-in-this-investigation","title":"Prior art in this investigation","text":"

.scratch/mqtt-non-psram-heap-staircase-analysis-2026-08-05.md (untracked -- .scratch/ is globally gitignored) holds the full allocation inventory. ~/mqtt-soak/STATE.md holds the soak evidence, including two retracted hypotheses worth not repeating: the perf commits were not the cause, and waev does not cap connections per IP.

"},{"location":"test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/","title":"Mesh pocket qualification 1 18 0 1 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/#meshpocket-companion-qualification-21-october-2026","title":"MeshPocket Companion qualification, 21 October 2026","text":"

Status: PASS for the automated Companion bench checks below. The user's request to test MeshPocket lifted its earlier bootloader-work exclusion. The board now runs Full Companion built from 21a0f318da26bdb1fa5257471c3d7a68a0684337, version v1.18.0.1-halo-keymind-cascade-dev-21a0f318, protocol 14. Its measured application body is 54411895235BE9C4; application ZIP SHA256 is 7841c4e7e8bbb8695ba0e796fa811efa41112367641c2790f22cc9085ba00da6. No firmware change was needed for these checks.

Check Measured result Build and application update Full capability/memory checks passed; identity-bound USB application-only DFU succeeded; exact version and EndF body verified after boot USB 100 query cycles, 10 reopens, owned contact CRUD and persistence through one verified warm restart passed; no framing errors or core error flags Bluetooth Three authenticated sessions held for 30.269 / 30.271 / 30.271 seconds; saved bond, exact USB identity/version and three clean controller lifecycles; no bond deletion Full contact refresh All 64 owned records verified in a 65-record stream, including the original contact; request to END: 2.491665 seconds Incremental refresh Exact 32 owned timestamp-selected records in a 33-record stream; request to END: 1.305048 seconds Contact cleanup All 64 synthetic contacts removed; zero remained after reopen; original contact fingerprint unchanged Two-way LoRa Three rounds, six exact direct nonce-bound datagrams, six send commands; every round passed Handback Owned channels removed; both radios' original modem settings, TX power and saved routes restored; ports closed; final Pocket core errors and outbound queue both zero

Bluetooth passed while the USB data cable remained connected. Each contact refresh used the existing bond and a separately verified clean controller lifecycle. USB persistence covers one owned contact across a warm restart; the 64-record fixture separately covers a seven-second save allowance and exact reopen readback. Neither is a cold-power persistence measurement.

The RF partner was the already restored XIAO ESP32-S3 Full Companion, retained at ebb59d48292de4a573250cf4c6a2ed7cf5f7ef69, body 4EEB12667E5E554B. Both identities and actual versions were verified before the three rounds. The temporary test tuple was 910.525 MHz / BW62.5 / SF7 / CR5, preamble 32. Exact source, destination, nonce, zero-hop route and physical packet counters were checked. This is an explicitly mixed-source pair, not a claim that every radio runs the new Pocket artifact. The RAK3401 gateway was not touched.

The existing bootloader and SoftDevice were preserved: the verified DFU ZIP contained only application BIN/DAT components. Its exact installed bootloader revision remains unverified because this Full Companion returns an unknown command for get bootloader.ver. Successful application DFU and subsequent runtime checks do not establish the independent bootloader's revision.

The first updater attempt failed before its DFU touch because the newer flasher's pinned USB helpers depend on startup globals absent from the definitions-only bench adapter. The compatible, previously reviewed updater then installed the app successfully. The adapter now validates and privately stages both bundled helpers and resolves them locally without running flasher startup. Fourteen adapter tests and five application-ZIP verifier tests passed; shell syntax and diff checks passed. This tooling-only fix did not trigger another firmware installation or repeated hardware campaign. The preinstall 0865 application's core flag 8 and the failed updater attempt remain in the private evidence; no causal firmware fix is inferred from the clean new boot.

The safe receipt binds every measured axis to its source, artifact, interval and evidence digest. Earlier Pocket results and the temporary exclusion remain historical in the 20 October fleet report. The other twelve boards retain their accepted source-bound measurements; unchanged hardware tests were not repeated.

These finite checks do not establish RF range, long-duration reliability, phone interoperability, battery current or physical cold starts. Screen/page rendering awaits operator observation. MeshPocket has no native WiFi. Bluetooth DFU is build-verified but a physical wireless update was not run in this task. This Companion can seed LoRa firmware transfers; local LoRa firmware installation is disabled and was not claimed tested.

"},{"location":"test-results/mixed_scan_validation/","title":"Mixed scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mixed_scan_validation/#mixed-bandwidth-two-channel-reception-test","title":"Mixed-bandwidth two-channel reception test","text":"

V4 transmitter on MercerWoodMesh; XIAO ESP32-S3/Wio SX1262 receiver. Channel 0: 909.5 MHz, SF7/62.5 kHz, preamble 32. Channel 1: 910.5 MHz, 500 kHz, SF and preamble below. Both use CR4/5, explicit header, CRC and 64-byte test payloads. Fast RX-preamble scanning uses four symbols of the active profile per visit, not four simultaneous channels or hardware CAD.

Each setting starts with a five-packet-per-channel continuous-RX baseline. Scanning sends 50 packets per channel in shuffled order with random host delays. Only CRC-valid, payload-validated, sequence-matched packets count. Any scan loss escalates the 500 kHz preamble by 16, capped at 128. SF9 and SF8 start at 32; SF7 starts at 48. An additional SF8/40 test was requested after SF8/48; its firmware adds acceptance of preamble 40 without changing radio timing or scanning behavior.

500 kHz SF Preamble Mode SF7/62.5 received 500 kHz received Mean scan cycle SF9 32 Fixed baseline 5/5 5/5 \u2014 SF9 32 Scan 50/50 50/50 14.676 ms SF8 32 Fixed baseline 5/5 5/5 \u2014 SF8 32 Scan 50/50 49/50 12.628 ms SF8 48 Fixed baseline 5/5 5/5 \u2014 SF8 48 Scan 50/50 50/50 12.629 ms SF8 40 Fixed baseline 5/5 5/5 \u2014 SF8 40 Scan 50/50 49/50 12.630 ms SF7 48 Fixed baseline 5/5 5/5 \u2014 SF7 48 Scan 50/50 50/50 11.603 ms

These are strong-signal, sequential-transmission lab results. Successful reception of 50 packets is not proof of zero loss over months. The receiver remains on one channel through an entire detected packet, so overlapping traffic on the other channel can be lost. Scan-cycle measurements exclude time occupied by packet reception; USB command handling can add timing outliers.

Machine-readable counts, timings and firmware/source hashes: mixed_scan_results.json.

After testing, both boards were restored to their original B soak firmware using app-only flashes. Version and normal radio settings were verified on both boards; Wi-Fi was connected and MQTT reported custom (ok). Both persistent soak loggers were restarted in phase B-post-CAD-soak, marking the intentional uptime reset.

"},{"location":"test-results/mixed_sf_bw_validation/","title":"Mixed sf bw validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mixed_sf_bw_validation/#equal-symbol-time-mixed-sfbw-four-channel-test","title":"Equal-symbol-time mixed SF/BW, four-channel test","text":"

2026-09-14. Promising, but no strict 400/400 pass. With no added settling, the mixed scanner received 61 consecutive packets on the correct channels, then timed out on packet 62. The near-ceiling +22.4 ms setting received four packets and timed out on the fifth. There were no wrong-channel payloads in either run. Both stopped on the first miss without retries.

"},{"location":"test-results/mixed_sf_bw_validation/#configuration","title":"Configuration","text":"Channel Center (MHz) SF BW (kHz) Symbol time (ms) 0 909.5 9 62.5 8.192 1 910.5 10 125 8.192 2 911.5 11 250 8.192 3 912.5 12 500 8.192

These preserve the preceding SF10/125 symbol duration exactly, rather than approximately: symbol_us = 2^SF * 1000 / BW_kHz. The supported SF/BW values and symbol-rate equation are specified in Semtech's SX1261/2 datasheet, sections 6.1.1.1\u20132. Equal symbol durations do not imply equal payload airtime or sensitivity.

The lab scanner stages four channels through the existing two production profile slots; this does not change the public two-profile protocol.

"},{"location":"test-results/mixed_sf_bw_validation/#positive-controls","title":"Positive controls","text":"

Before scanning, each profile received ten fixed-frequency probes after full normal initialization. All four passed 10/10, forty total. Zero frequency commands occurred after each setup; no stationary receiver hardware failure. These are fresh ten-packet controls, not claims of 100/100 on each new profile.

Both devices acknowledge the same exact modulation table through mixinfo. Transmitter results verify SF, fractional BW, center, sequence and packet size. RX records verify center-command word and SF/BW at packet read. Full expected payload identity and CRC remain required. Mode/cache errors are treated as fixture failures rather than ordinary RF misses.

"},{"location":"test-results/mixed_sf_bw_validation/#timing-controls-and-settling-budget","title":"Timing controls and settling budget","text":"

Six-second RX-only window at zero added delay: 141 hops, mean 637.922 us, maximum 828 us. The largest 100 us delay step with a 500 us per-hop reserve is floor((262144/4 - 41780 - 828 - 500)/100)*100 = 22400 us.

The +22,400 us RX-only verification window measured 87 hops, mean 638.241 us, maximum 833 us. Nineteen complete idle cycles measured mean 259,292.789 us, maximum 259,310 us, below the 262,144 us preamble. Every hop had two observed frequency commands and one modulation command, with no mode/cache/RX errors.

This is an empirical near-ceiling budget, not a guarantee of acquisition for every arrival phase. Longer packet holds and software jitter can defeat the idle-cycle calculation. RX is active throughout added settling.

"},{"location":"test-results/mixed_sf_bw_validation/#four-channel-results","title":"Four-channel results","text":"Added settling Strict received / attempted before first miss Failed TX profile Mean switch (us) Max switch (us) Max idle cycle (us) 22,400 us 4 / 5 Channel 2, SF11/250 660.951 892 259391 0 us 61 / 62 Channel 0, SF9/62.5 674.819 899 169798

Both failures were timeouts, with TX rc=0, RX mode=1, device errors=0. Both runs had zero mode/cache/receive errors and zero off-channel payloads. The +22.4 ms run exercised 163 hops, 326 frequency commands and 163 modulation commands. Zero-delay exercised 718 hops, 1,436 frequency commands and 718 modulation commands. Optimized RX resume counts equaled hop counts.

No-delay per-channel counts at stop:

Channel Received Attempted Good-packet mean RSSI (dBm) Good-packet mean SNR (dB) 0 15 16 -34.00 11.15 1 16 16 -33.69 8.24 2 15 15 -34.33 5.78 3 15 15 -33.27 3.47

The receiver stayed longer on some visits: maximum held visit 582,993 us with settling, 580,577 us without. Held-visit statistics include successful packet reception, so these maxima alone do not prove false off-channel preamble holds in this experiment. No IRQ trace was collected for the two failures; their precise mechanism is not established.

The longer success sequence and absence of wrong-channel payloads are encouraging relative to the earlier uniform-SF/BW tests, but do not establish a statistically measured improvement or zero-loss reliability. Each setting was censored at first failure; do not present 61/62 as an unbiased measured packet-delivery rate or either run as a completed 400-packet test. The random arrival method is seeded, not RF-phase synchronized across configurations.

"},{"location":"test-results/mixed_sf_bw_validation/#implementation-verification-and-final-state","title":"Implementation, verification and final state","text":"

HIL-only ProfileMixedChannels.h, basemixed, mixtx, scanmixed and profile_switch_mixed.py add the bounded experiment. Uniform modes preserve their defaults. Post-hop checks use the actual per-channel modulation; the uniform collector and settling evidence checks reject mixed-mode results. Receive ownership, packet guards, BUSY and command-error handling remain intact.

17 native profile tests and 48 HIL host tests passed. Tests cover fractional 62.5 kHz, equal symbol duration, table identity, normal uniform mapping, required modulation writes, TX replay, and wrong-policy rejection. Both firmware builds and normal uploads succeeded. The nRF52 ZIP is application- only, SoftDevice ID 0x0123; no bootloader or SoftDevice replacement. RAK COM29 was not opened. Production settings/source were not changed this turn.

Both boards rebooted idle after the second failed setting, with test firmware retained. No autonomous TX, background collectors or restoration. Temporary PlatformIO overlay removed. No commit or push was requested. Existing unrelated working-tree changes remain intact. git diff --check passed apart from unrelated pre-existing CRLF warnings.

"},{"location":"test-results/mixed_sf_bw_validation/#provenance","title":"Provenance","text":"

Raw capture: tools/hil/profile_switch_mixed_equal_symbols_results.json. It includes the four baselines, two timing controls and two strict scans, with complete:true, stopped:both_delay_controls_failed, and no fixture or cleanup errors. Earlier captures were not rewritten.

RX build reported 94,844 bytes RAM / 367,537 bytes flash. TX reported 18,220 / 95,908 bytes. Source is keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus existing uncommitted work and these HIL changes; this is not a clean-release comparison.

"},{"location":"test-results/morse_rx_hil_2026-10-12/","title":"Morse rx hil 2026 10 12","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#morse-receive-hardware-checkpoint-12-october-2026","title":"Morse receive hardware checkpoint - 12 October 2026","text":"

Status: incomplete hardware validation. The release default remains 25 WPM / 902.100 MHz and the public limit remains 60 WPM. Experimental 255-WPM builds are not a validated release limit.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#implementation-and-host-verification","title":"Implementation and host verification","text":""},{"location":"test-results/morse_rx_hil_2026-10-12/#hardware-identities-and-isolation","title":"Hardware identities and isolation","text":"

The Mercerwood Pi was reached at 100.78.195.38. Only identity-verified application DFU packages were written through the existing mcfirmware.sh path. No bootloader or SoftDevice was replaced, no services were stopped, and the Pi was not rebooted by this test.

Board USB chip serial RAK4631 9AB3B64C641BA927 Heltec T096 651F8E496197F882 Heltec MeshPocket 93DA83ADF7B2092B

The RAK3401 service radio and MeshTower were left alone.

Early tests put CW at 909.5 MHz and packet profiles elsewhere. This was corrected: the requested CW default is 902.100 MHz, separate from the LoRa lab control tuple 909.5 MHz / 500 kHz / SF5 / CR5. Results from the early layout are not proof of packet-spectrum isolation.

set radio only saves Companion primary settings. The test runners now use set tempradio and poll the exact active tuple; saved or pending settings do not satisfy the check. The USB-only HIL recipes explicitly include the existing temporary-radio capability. Names and saved primary settings are not changed by the corrected runners.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#observations-not-a-new-maximum","title":"Observations, not a new maximum","text":""},{"location":"test-results/morse_rx_hil_2026-10-12/#remote-acknowledgement-and-retained-capture-evidence","title":"Remote acknowledgement and retained-capture evidence","text":"

A 30-second LoRa-controlled test physically demonstrated:

  1. A six-minute request was rejected in 0.682 seconds.
  2. The valid request was acknowledged in 0.684 seconds before RX started.
  3. The timed receiver returned to LoRa after 30.889 seconds as observed by the host.
  4. All 34 retained characters read locally were returned byte-for-byte in the remote LoRa read.

The decoded RF text was not equal to the transmitted message, so this run failed overall. It used the early shared CW/control frequency and is not a clean decoder validation. The full five-minute and multiple-page remote tests remain outstanding.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#why-hardware-work-stopped","title":"Why hardware work stopped","text":"

At approximately 22:43 Pacific the RAK USB connection disappeared during the zero-gate isolated-frequency sweep. It subsequently reported v1.17.1.10-halo-keymind-cascade-preview3-9ce2a076, with get morse unknown, instead of the just-verified Morse application. Another concurrent job had flashed the shared RAK. This is not evidence of a Morse firmware crash. The affected sweep is incomplete and must not be counted as a completed speed test. Hardware writes were stopped for user coordination.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#ready-to-resume-experiment","title":"Ready-to-resume experiment","text":"

An isolated HIL-only MESH_MORSE_HIL_FAST_OFF_DEBOUNCE=1 experiment keeps ON confirmation at two ms and permits OFF confirmation at one ms for fast nominal dots. Default/production behavior, capture size and low-speed timing are unchanged. Synthetic tests pass; physical results are still pending.

The following application-only packages were compiled and staged on the Pi but not installed:

Package SHA-256 RAK_4631_companion_radio_usb-morse-fastoff.zip 0340b6135c36c7d6cb61be45ac28555ead5a2f6529feb3702466b284d2abd204 Mesh_pocket_companion_radio_usb-morse-fastoff.zip e0d94389310244655136564a1dbd55d5e4d4d18a17bbae6e1c7ae018f5bc7952

Pi run directory: /home/mikec/hwtest/runs/morse-20261012.3Lp6jA. Local raw evidence is retained in the ignored out/morse-rx/ directory; the strict test runners are in tools/hil/morse_bench.py and tools/hil/morse_remote.py.

Before resuming, coordinate exclusive use of those three radios, verify the installed images and exact USB identities, and run the isolated timing A/B, repeated mixed-message speed boundary tests, and the five-minute multipage LoRa capture/recovery test. Only then choose and verify any production cap or tuning changes. No commit or push was performed for this checkpoint.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#follow-up-13-october-2026","title":"Follow-up: 13 October 2026","text":"

The original checkpoint above is historical. Its implementation and Reader / RS232 integration were subsequently pushed as 0be4c7488 and 396304822.

RF investigation and repeated standard/uneven-timing speed qualification remain in progress. Default 25 WPM, public ceiling 60 WPM, radio bandwidth, and shipping RF filtering have not been changed. Packet control stays at 909.5 MHz / 500 kHz / SF5 / CR5; CW remains separate at 902.100 MHz by default. Raw results remain in out/morse-rx/ and the Pi run directory above.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#controlled-speed-and-human-timing-follow-up","title":"Controlled speed and human-timing follow-up","text":"

These are conditional laboratory results, not a promise of error-free reception on every board or with automatic noise detection. CW stayed at 902.100 MHz, packet control at 909.5 MHz / 500 kHz / SF5 / CR5, and requested chip TX power at -9 dBm. The Pocket / RAK pair used a volatile HIL-only -80 dBm receive floor against strong received tones. Shipping images do not contain this receive-floor setting. Earlier automatic-threshold quiet controls produced false characters on the Pocket and T096; that remains a separate qualification limitation.

The original 2.25 / 5.5-dot letter / word boundaries failed ordinary uneven timing even at 25 WPM. With seeded +/-20% phase jitter, gradual -10% duration drift, and no extra letter or word spacing, the first sweep passed only 2/6 messages at each of 25, 60 and 120 WPM. A short, fully retained RF trace decoded VW MN DOG as VW MNDOG in all six directions/rate combinations. All 20 expected marks remained present; the second word gap measured 246 / 246 ms at 25 WPM, 100 / 102 ms at 60, and 54 / 52 ms at 120. The old word thresholds were 264, 110 and 55 ms respectively.

The receiver now uses the ordinary 2 / 5-dot gap midpoints, without changing mark classification, RX allocation, RF filtering or the public speed limit. The same tight human-timing profile (seed 6789), with two repetitions of all three mixed alphabet/digit, word and punctuation messages in both directions, then passed strictly:

Requested WPM Pocket to RAK RAK to Pocket Total exact messages 25 6/6 6/6 12/12 60 6/6 6/6 12/12 120 6/6 6/6 12/12

Only leading/trailing capture whitespace is removed in the physical harness; extra internal spaces, noise characters, missing letters and ? fail the test. There were no cleanup errors. This establishes zero observed errors in those 36 messages under the stated laboratory conditions, not universal 100% reliability or a 120-WPM release qualification.

The production-equivalent RF comparison uses cold-XOSC TX, 3400-us PA ramp, RSSI window 0, two-ms sampling and ordinary OFF debounce. Compile guards and actual PlatformIO/SCons flag tests prevent faster experimental settings or a different receiver clock from being mislabeled as shipping RF behavior. The lab maximum override and diagnostics still make these HIL applications, not release images.

Requested WPM is not measured RF pace. Four strict 13-word PARIS trials per rate transmitted 643 dot units, omitting the unsent final seven-unit word gap:

Requested WPM Host-observed duration PARIS-equivalent pace 60 14.354-14.365 seconds 53.7 WPM 120 7.937-7.940 seconds 97.2 WPM

The pace includes CLI response and polling overhead; it is not an oscilloscope measurement of RF edges. It also does not substitute for varied-text testing.

Latest focused verification passed 129/129 test methods. The actual HIL scheduler / decoder matrix has 175 exact results plus one explicitly unsupported 240-WPM / two-ms-sampling / 50%-extended-spacing case. The separate human envelope matrix has 84 exact results plus 14 explicitly unsupported doubled-character-spacing cases. Unsupported transcripts remain checked and visible; they are not normalized or counted as exact-decoding passes. Former-threshold negative controls reproduce the tight-gap failure. The production receiver remains 4168 bytes, and compact STM32 still links at 228868/229376 flash bytes with its unchanged feature boundary.

Evidence filenames in the directories above:

The tested application-only gap-correction packages were:

Package SHA-256 RAK_4631_companion_radio_usb-prod-rf-gap-20261013.zip 26cc93db4f6a602e24a35828cca79efaef002b13b18192fe763eb5426515ec6b Mesh_pocket_companion_radio_usb-prod-rf-gap-20261013.zip 9d1f76c1cc42e1d30ca5daa3d30fe30232d426d72b80278cac67d2d98c21a178

The T096's earlier image and measurements do not qualify its speed under this new gap correction. No LR1110 or SX127x physical speed limit is established by the SX1262 pair. Default 25 WPM and the public 60-WPM ceiling remain unchanged.

The next faster production-RF timing buckets did not meet the zero-error criterion, even with the same HIL receive floor:

Requested WPM Standard timing Tight human timing, two repeats 130 5/6 11/12 150 2/6 6/12 170 0/6 Not repeated in this corrected sweep 200 0/6 Not repeated in this corrected sweep

At 130 and 150 some failures affected the initial short mark; 150 also had later body errors. The first RF mark currently begins before the command reply and the remaining foreground-loop work. That is a possible delay window, not a measured explanation of all these errors. No unproved transmitter or mark-classifier adjustment was made to improve the reported score.

Additional boundary evidence:

A second human-style pattern, using +/-20% jitter, gradual +10% duration drift (slowing down), no extra gaps and seed 12345, passed another 6/6 mixed messages at each of 25, 60 and 120 WPM in both directions. Across the corrected speeding-up and slowing-down profiles, that is 18/18 messages at each rate, 54/54 overall, under the same controlled receive threshold.

The subsequent 30-second automatic-threshold, no-intentional-TX control was not clean overall: the RAK captured E E, while the Pocket was empty. This is evidence against an unconditional production zero-error claim; it is not a diagnosis of the source of the quiet RSSI excursions. The current shipping speed ceiling remains unchanged.

Short, fully retained two-second initial-window traces sharpen the initial-mark finding. At 130 WPM, the failed first E measured 18 ms, exactly twice the nominal nine-ms unit; a successful first E measured 16 ms. At 150, the corresponding failed/successful widths were 16/14 ms against an eight-ms unit. Those widths were also common among later dots, so these recordings do not show an unusually long first mark. The later learned units were 11/10 ms, making comparable later dots classify correctly. Two-ms sample phase and the initial untrained threshold can therefore matter near this boundary. The trace has no transmitter deadline timestamps and does not prove that CLI work caused the errors. These partial diagnostic captures are not complete-message qualification failures or passes.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#faster-experimental-rf-comparison","title":"Faster experimental RF comparison","text":"

The corrected fast SX1262 HIL applications use warm-XOSC TX, an 800-us PA ramp, RSSI window 2, one-ms sampling and the experimental short OFF-confirmation minimum. These are not shipping RF settings. The same -80 dBm HIL receive floor and exact-message checks were used on the same Pocket / RAK pair.

Requested WPM Standard timing, two repeats Tight human timing 120 Previously all-pass; not repeated in this boundary sweep 6/6 150 Not tested in this boundary sweep 5/6 200 12/12 3/6 255 10/12 Not tested in this boundary sweep

The tight human profile is again +/-20% jitter, -10% duration drift, no added letter/word spacing and seed 6789. At 150, DOG became DKG in one direction. At 200, three messages had real body or punctuation errors. The shortest timing bucket therefore does not qualify as zero-error, and neither does the higher uneven-keying speed. Integer rounding maps requested 219-255 WPM to the same five-ms dot, so 255 is not 255 measured WPM.

The fast steady-PARIS pace check gives:

Requested WPM Host-observed duration PARIS-equivalent pace Exact PARIS messages 120 6.868 seconds 112.3 WPM 2/2 200 4.273-4.277 seconds 180.5 WPM 2/2 255 3.669 seconds 210.3 WPM 1/2

The failed 255 PARIS capture contained PANIS in place of one PARIS. These rates include the same host/CLI overhead as the production-equivalent pace measurement; pace is calculated only for the 643-unit PARIS payload, not by applying that reference count to the other test messages.

The strongest observed all-pass points are therefore 200 requested WPM for machine timing on the fast HIL pair, and 120 requested WPM for the tested uneven timing. Their steady-PARIS references are approximately 181 and 112 WPM respectively. With production-equivalent RF timing, the tested uneven-timing reference is approximately 97 WPM. None of these conditional limits resolves the automatic-threshold quiet-control failure or qualifies another board/radio family. Shipping defaults remain unchanged.

Fast HIL package SHA-256 RAK_4631_companion_radio_usb-fast-gap-20261013.zip 7722ca7a0008c660217535c19e9dd17d5923fc90510f9850b2ce188a6d753951 Mesh_pocket_companion_radio_usb-fast-gap-20261013.zip 8d153c544f19e01d45e178105b704840f855a53cb8fda2044ba102cb19b85521

Final cleanup confirmed both selected radios had Morse stopped, configured 25 WPM / 902.100 MHz, default HIL timing and automatic squelch restored. Their saved primary settings still held an earlier 919.5-MHz lab tuple; those two test devices were explicitly changed to 909.5 / 500 / SF5 / CR5, and that tuple was also confirmed live. No service radio was changed. The pair remains on the identified fast HIL applications, not shipping images.

"},{"location":"test-results/mqtt_tls_v4_regression/","title":"Mqtt tls v4 regression","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mqtt_tls_v4_regression/#esp32-mqtt-tls-connection-regression","title":"ESP32 MQTT TLS connection regression","text":""},{"location":"test-results/mqtt_tls_v4_regression/#report-and-diagnosis","title":"Report and diagnosis","text":"

The Heltec V4 report contains two separate failures:

The firmware configured a 2,500 ms network timeout to bound synchronous publishes. ESP-MQTT also uses that setting for connection establishment, including TLS and the WebSocket upgrade. It was not a write-only setting.

The shipped ESP32-S3 AES driver allocates DMA-capable internal bounce buffers for unaligned external-memory records, up to 1,600 bytes per buffer. Ample PSRAM or an aggregate free-heap reading therefore does not establish that an AES operation can succeed. The supplied log identifies an allocation failure in this driver. The same allocation error was reproduced on the lab V4 with the older firmware and five enabled WSS presets.

The shipped Arduino 2.0.17/IDF 4.4.7 TLS allocator was checked in its archives: TLS allocations use the configurable mbedtls_calloc function, so the existing PSRAM redirect takes effect. Retaining a PsychicMqttClient does not retain its TLS records: ESP-IDF frees/recreates the TLS session on reconnect.

References: ESP-IDF 4.4.7 AES implementation and ESP-TLS errors.

"},{"location":"test-results/mqtt_tls_v4_regression/#firmware-changes","title":"Firmware changes","text":"

These changes apply to the shared ESP32 MQTT bridge, including repeater, room-server and companion images that enable it.

Existing slot limits, certificate verification, authentication and backoff remain in effect. Memory pressure defers additional slots instead of consuming memory needed by existing connections and the radio.

"},{"location":"test-results/mqtt_tls_v4_regression/#validation-and-limits","title":"Validation and limits","text":"

The regression tests execute the production optimizer, adapter setters, connection admission, TLS allocator fallback, scheduler and diagnostics with controlled SDK calls and heap readings. They cover IDF 4/5 config layouts, PSRAM and internal-only modes, fragmented heap, exhausted PSRAM, delayed connection callbacks, retry timing and bounded replies. They do not execute an ESP32 AES peripheral or qualify physical WiFi behavior.

All 46 focused Python MQTT tests and 33 memory-budget tests passed after the final reconnect/buffer changes. The 1,794-test native core suite passed after the initial TLS/admission fix, before the subsequent MQTT-only refinements. The focused MQTT tests are also connected to the GitHub unit-test workflow. The new memory tests include a linked-ELF boundary that the previous WiFi-omitted policy incorrectly accepted.

Full MQTT repeater and room-server builds with USB packet logging passed on 10 October 2026 at revision 937f8610c. The V4 Full Companion build passed at d11bf3615. The RC32 build at the same revision also passed using Arduino 3.3.11 and ESP-IDF 5.5.5, exercising the newer SDK in an actual firmware build. The only intervening change corrected the host-side WiFi memory accounting. Their build logs report:

Build Application flash bytes Available internal heap bytes Required heap bytes Largest internal region bytes Heltec V4 2,182,265 205,080 198,432 172,312 Heltec V3 2,125,377 211,296 209,184 178,528 Station G2 2,017,953 201,280 200,480 168,512 Heltec V3 room server 2,104,057 201,992 196,016 169,224 Heltec RC32 without display, IDF 5 2,287,033 195,688 194,336 162,920 Heltec V4 Full Companion 2,418,405 233,880 194,336 201,112

Application flash is the PlatformIO size-check reading. Internal heap is linked capacity before dynamic allocation, not free heap measured after boot. These RAM gates passed the startup policy; they do not qualify maximum TLS/WSS/outbox load. The default MQTT startup allowance remains 24,576 bytes, with runtime admission for additional connections. See firmware memory checks for the separate maximum-slot estimate and qualification limits.

Two additional Full builds compiled and linked the shared MQTT code but were held by the unchanged RAM safety gates:

Full build held Available internal bytes Required bytes Shortfall bytes T-Beam SX1276 repeater, original ESP32 179,736 192,560 12,824 Heltec V3 Companion 182,264 198,432 16,168

Both already enabled WiFi through other build flags, so the corrected MQTT-only WiFi detection did not increase either requirement. Their capacity shortfalls require separate memory work before those Full artifacts can be qualified; neither gate was lowered and neither failed artifact was published. The shared firmware fixes still apply to their MQTT code. The original ESP32 compile check and IDF 4/5 tests cover the non-V4 source paths; physical testing in this report is limited to the V4.

Live TLS 1.2 certificate chains from 12 audited preset endpoints were verified with the exact CA PEMs extracted from MQTTPresets.h using a native host build of the pinned mbedTLS 2.28.7 submodule from ESP-IDF 4.4.7, commit 2b8e772fc1cb0732cda3bae7d1e9d6f4cfaf63d9. Hostname verification was enabled. All 11 GTS Root R4 endpoints and the ISRG Root X1 Cascadia endpoint returned verification result 0 and flags 0x00000000. Wrong-hostname and unrelated-anchor negative checks were rejected. WAEV also passed with the server root omitted from its supplied chain.

The existing cross-signed GTS Root R4 PEM is a valid trust anchor for that pinned mbedTLS verifier. Ordinary OpenSSL root-only verification rejected it, while partial-chain verification with the same PEM passed all 11 GTS endpoints. This host-probe policy difference does not establish a firmware CA failure or require a CA replacement for the reported AES allocation errors. The verifier used the native default mbedTLS configuration; this establishes pinned X.509 chain and trust-anchor behavior, not an ESP32 hardware handshake.

"},{"location":"test-results/mqtt_tls_v4_regression/#physical-v4-results","title":"Physical V4 results","text":"

MercerWoodMesh's Heltec V4 was tested over its native USB console using the existing SlowFi network. The Pi's WiFi configuration was not changed. A full 16 MiB private flash backup was made before testing. Test credentials and raw console logs are not included in the public results.

Check Result Older firmware c5fe8a0e, WAEV and Cascadia Both connected for the 150-second trial; accepted writes increased with no outbox backlog. The older firmware has no PUBACK counter, so accepted writes alone do not establish broker delivery. Older firmware, five WSS presets Reproduced esp-aes: Failed to allocate memory, 0x8018 and 0x8006. All five slots eventually disconnected. This is a failing baseline. Final firmware 937f8610c, five presets enabled WAEV, Cascadia and Analyzer US connected within 46 seconds. Analyzer EU and MeshMapper waited without allocating SDK clients because DMA memory was below the cold-start admission threshold. No AES allocation, TLS timeout/write error or CPU reset was observed during this trial. Final firmware, power saving on, 180.005 seconds without console commands All three connected brokers stayed healthy. Their pub_ack counters increased from 2/1/1 to 5/5/5. Uptime advanced from 57 to 258 seconds including surrounding queries. DMA heap stayed at 22,776 free / 22,516 largest bytes, with no outbox backlog, drops, AES allocation error, TLS timeout/write error or CPU reset. Cooperative MQTT stop The stop request completed and get mqtt.stopping returned off. The radio console remained responsive.

The three-connection measurements included about 22.7 KiB free DMA-capable heap with a 22.0 KiB largest block. Outbox activity briefly consumed additional memory. Three brokers were qualified in these conditions; this does not qualify five simultaneous WSS connections or a long-duration loaded-radio soak. Additional enabled slots wait for headroom rather than exhausting the memory needed by active TLS connections.

The final power-saving trial recorded core error bitmask 0 before and after the idle interval; bitmask 8 appeared after the subsequent MQTT stop. An earlier fixed-firmware trial also recorded bitmask 8. This is the configured radio watchdog flag on a quiet channel, also observed in earlier V4 console qualification; it is not eight MQTT errors or evidence of a CPU reset. This run does not claim zero radio watchdog events.

The attempted SSID interruption returned \"WiFi SSID saved; reboot to apply\" and did not disconnect the station. It is therefore not counted as a physical WiFi-loss recovery test. Regression tests exercise the production WiFi-loss handler and connection scheduler with retained SDK clients, including the restart memory allowance and serialized reconnect attempts.

A Pi-wide USB hub reset interrupted the older five-broker baseline. Several radios disconnected together and subsequently reappeared. That interruption is retained as a host-lab failure, not attributed to a V4 firmware crash.

After the final live check, the original NVS and filesystem backup regions were restored and their writes verified, preserving the final firmware. Saved configuration replies matched, and a separate get public.key check verified that the public-key hash was unchanged. Comparison removes console echo/prompt framing and treats the old blank get wifi.ssid response as the new (not configured) response. MQTT and power saving were both returned to their original off state. No release assets were uploaded as part of this test.

"},{"location":"test-results/mqtt_tls_v4_regression/#long-running-mqtt-recovery","title":"Long-running MQTT recovery","text":"

The 14 October 2026 follow-up report says the preview initially connects, but three reporting sites show it offline after roughly three hours while WiFi and the console still show connected. The earlier 180-second trial does not qualify this longer-running workload.

Code inspection found recovery gaps, not a verified cause of that hardware failure. The adapter clears its connection flag on an MQTT error, but the bridge previously left its separate flag set until a disconnect callback. Its five-second cache refresh only reread that same flag. A lost terminal callback could also leave a connection attempt pending indefinitely, blocking the other brokers' handshake and renewal requests. Accepted status enqueues were treated as successful publication without using the existing PUBACK counter to detect stalled delivery.

Recovery now runs on the bridge's owning task. An errored or timed-out SDK client must be proven stopped before its pending-handshake reservation is released. The client object and credentials are retained, and the existing admission checks and reconnect backoff still apply. No WiFi reset, node reboot, extra periodic heartbeat, or broker credential change is required. An idle connection with no outstanding QoS 1 publication is not timed out merely for being quiet.

Both the pending-handshake deadline and the outstanding-PUBACK deadline are 120 seconds. This leaves a generous margin over the SDK's 10-second network timeout and the configured 15-second QoS 1 retransmission interval. A failed SDK stop retains its recovery reservation instead of permitting another overlapping TLS handshake.

CLI and WebConfig slot status show recover during reserved cleanup. get mqttN.diag retains pub_ack and adds a bounded recovery count/reason (rec) and pending acknowledgment age (ack_wait) when they fit. A delayed connected callback cannot cancel cleanup after an SDK stop failed.

The delivery watch starts with the first accepted QoS 1 enqueue and observes real broker acknowledgments, including the on-connect status publication. Further enqueues cannot postpone that oldest outstanding deadline. Timeouts are serviced on the next owning-task pass; they are not a guarantee of wall-clock recovery if the worker is blocked inside an SDK call. This change does not establish or repair an indefinite SDK mutex deadlock.

All 251 native MQTT test cases and 79 MQTT Python host tests passed. The new tests execute the production callbacks, publishing, maintenance, recovery and admission methods with controllable SDK calls and clocks. They include three brokers running beyond three simulated hours, missing PUBACKs and callbacks, silent synchronous write failures, failed SDK stops, late connected events, WiFi loss, suspension/foreground wake, backoff, memory admission and timer rollover. Removing the new error-callback recovery signal makes its regression test fail, establishing that it detects the previous behavior. AP ownership fixtures also passed with address/undefined-behavior sanitizers. The six pure transport-health tests are included in the 79-test total and in CI.

The exact Full heltec_v4_repeater_observer_mqtt image compiled and linked against Arduino 2.0.17 / ESP-IDF 4.4.7. This local test image used baseline dce323e37 with the MQTT repair applied; it predates the subsequently pulled beacon changes. Its application size was 2,237,677 bytes, within the 6,553,600-byte application slot. The linked runtime RAM gate reported 204,752 internal bytes available, 198,432 required and a 171,984-byte largest region. These are build-time capacity/admission checks, not free heap measured with three live brokers or qualification of maximum simultaneous load.

No new physical three-hour V4 soak or reporter-side retest has been performed as part of this repair. These tests establish the repaired recovery behavior, not the cause of the reporter's hardware failure or website-side availability.

"},{"location":"test-results/nimble_companion_trial/","title":"Nimble companion trial","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nimble_companion_trial/#esp32-s3-nimble-full-companion-trial","title":"ESP32-S3 NimBLE Full Companion trial","text":"

Canonical Heltec V3 Full now uses the same pinned NimBLE backend, preserving its existing 15 saved phones and both notification subscriptions per phone. Its Full memory qualification is separate from the optional trials below. Switching from Bluedroid requires forgetting the old phone pairing and pairing again; stored bonds are not migrated.

These optional builds use NimBLE-Arduino 2.5.1 with the existing ESP32 Arduino 2.0.17 toolchain. They are hardware qualification builds, outside the normal release matrix and firmware picker.

See the 2026-09-08 hardware results for measured RAM, MAC-policy checks, WiFi updates, and remaining qualification.

The contact-cache guide adds six optional 350-contact trials for ESP32 boards without PSRAM, including Heltec V3. Their qualification results include linked RAM margins, V4/RAK hardware tests and the mOTA queue policy.

Hardware Trial environment Contacts Channels Offline queue Heltec V4.2/V4.3 OLED, FEM on heltec_v4_2_v4_3_companion_radio_full_femon_nimble 350 40 512 XIAO ESP32-S3 with WIO SX1262 Xiao_S3_WIO_companion_radio_full_nimble 350 40 256

The XIAO profile uses WIO radio pins CS 41, DIO1 39, BUSY 40 and RESET 42. It is not the generic XIAO profile for separately wired radio modules. Both boards retain their parent Full Companion features, USB mOTA sender, WiFi update slots, and PSRAM-backed offline queue. The V4 also retains direct MQTT.

The small-screen message layout uses 6-pixel capitals on 128x64 panels and 5-pixel capitals on very tiny panels. It includes a compact channel/sender line and full message previews. Font selection is automatic.

"},{"location":"test-results/nimble_companion_trial/#build","title":"Build","text":"

Enable the optional configuration in the ignored platformio.local.ini:

[platformio]\nextra_configs =\n  variants/*/platformio.ini\n  platformio.nimble.ini\n

If that file already contains local settings, merge this list with them. Run one PlatformIO/build.sh command at a time. Both commands below use USA Cascade:

OUTPUT_DIR=.releases/nimble-v4 bash build.sh build-firmware \\\n  heltec_v4_2_v4_3_companion_radio_full_femon_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n\nOUTPUT_DIR=.releases/nimble-xiao bash build.sh build-firmware \\\n  Xiao_S3_WIO_companion_radio_full_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n

Use the merged image at address 0 for a USB installation. The V4 uses 16 MB flash; the XIAO uses 8 MB. Both use DIO flash mode. Use the application-only .bin for an existing, matching WiFi OTA layout. Remove the optional config entry when finished to return to the ordinary release matrix.

"},{"location":"test-results/nimble_companion_trial/#bluetooth-identity-and-pairing","title":"Bluetooth identity and pairing","text":"

All existing MAC settings remain available from the ASCII USB terminal and the app's authenticated local CLI. A setting takes effect after reboot.

Command Behavior get bluetooth.mac Show the saved MAC policy. set bluetooth.mac C2:17:15:04:00:01 Use this custom random-static address. set bluetooth.mac random Generate one address and retain it across boots. set bluetooth.mac random-every-boot Generate a new address on each boot. set bluetooth.mac random-after-connect Rotate on the next boot after an authenticated connection; otherwise retain the address. set bluetooth.mac default Restore the factory Bluetooth address. set bluetooth.stealth on Pair once, then accept the saved bonded peer. set bluetooth.stealth off Restore ordinary discovery without changing the MAC policy. get bluetooth.stealth Show pairing or bonded-peer-only state.

The ble.mac and ble.stealth aliases also work. A literal custom address must be a valid BLE random-static address (first byte C0-FF). Forget the old entry in the phone's Bluetooth settings and pair again after an address change or switching Bluetooth libraries. Existing Bluedroid bonds are not migrated.

Stealth does not disable the selected rotation policy. If rotation changes the address on reboot, first pairing opens again for the new identity.

Pairing requires encryption, PIN authentication and a stored bond. NimBLE's controller byte order is converted explicitly, so a custom address appears over the air in the order entered. Failed identity setup stops advertising and retries initialization instead of advertising the wrong address.

"},{"location":"test-results/nimble_companion_trial/#v4-wifi-setup-qr","title":"V4 WiFi setup QR","text":"

The active WiFi setup page displays a compact QR code with a one-pixel white border. The normal open setup network fits a 44 x 44 pixel code, with 2 x 2 pixel modules. The SSID, portal address and HOLD STOP hint remain beside it. Short-click to leave the page; hold the button on this page to start or stop the setup AP. From USB, start webconfig ap starts the same setup session.

When the setup AP is inactive and the node has joined WiFi, this page shows WIFI CONNECTED, IP <address>, SETUP AP INACTIVE, then HOLD START AP from top to bottom. The address appears once assigned and clears when the station disconnects. While the setup AP is active, the QR page shows the setup portal's address instead.

"},{"location":"test-results/nimble_companion_trial/#regression-checks","title":"Regression checks","text":"
python3 -B test/test_nimble_companion.py\npython3 -B test/test_bluetooth_mac_contract.py\npython3 -B test/test_bluetooth_pairing_ui.py\npython3 -B test/test_heltec_v4_wifi_setup_page.py\npython3 -B test/test_firmware_ram.py\npython3 -B test/test_esp32_dram.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_companion_mqtt_stack.py\npio test -e native -f test_companion_node_prefs \\\n  -f test_ble_tx_stall_watchdog -f test_companion_frame_queue \\\n  -f test_display_driver\n

The adapter test compiles the actual transport under address/undefined-behavior sanitizers with API doubles. It injects allocation, identity and bond failures; checks PIN authentication and stealth; and exercises 176-byte frames, MTU and notification subscription gating, retry and duplicate prevention. Embedded builds compile against the pinned real library. RAM reports retain the existing wireless budget; linked headroom is not a measurement of live free heap. The MQTT save callback also has a compiler-enforced stack-frame regression check: nested preference copies previously overflowed the V4 loop task when saving MQTT settings through WebConfig, despite sufficient free heap.

Hardware qualification must additionally verify pairing and reconnection, advertised addresses for every MAC policy, contact synchronization, WiFi/BLE coexistence, and uptime under load. BlueZ results do not establish iOS or Android camera/pairing compatibility.

"},{"location":"test-results/nimble_companion_trial_results/","title":"Nimble companion trial results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nimble_companion_trial_results/#esp32-s3-nimble-trial-results-2026-09-08","title":"ESP32-S3 NimBLE trial results, 2026-09-08","text":"

These are local hardware trial results for USA Cascade Full Companions on a Heltec V4.3 OLED and a Seeed XIAO ESP32-S3 with WIO SX1262. The V4.2 shares the profile but was not physically tested. Build and command instructions are in the trial guide.

The final source revision is 9dbab431, using NimBLE-Arduino 2.5.1 and the existing Arduino-ESP32 2.0.17 toolchain. These optional profiles are outside the ordinary release matrix. This trial does not change GitHub release assets or the firmware picker.

"},{"location":"test-results/nimble_companion_trial_results/#capacity","title":"Capacity","text":"Measurement Heltec V4 XIAO S3 WIO Contacts / channels 350 / 40 350 / 40 PSRAM-backed offline queue 512 256 Application image, bytes 1,774,600 1,510,536 OTA application slot, bytes 6,553,600 3,342,336 Linked internal capacity before runtime allocation, bytes 263,944 265,576 Required runtime RAM budget, bytes 173,056 148,480 Headroom beyond that budget, bytes 90,888 117,096 Live free heap with Bluetooth and 350 test contacts, bytes 142,644 147,444 Minimum free heap during that contact test, bytes 138,024 142,788

The live contact-test readings are from revision 7f1bd8b1. Revision 9dbab431 retains the same Bluetooth adapter, contact allocation and queue sizes, and fixes the MQTT save stack use described below. Linked capacity is not live free heap; the firmware memory reports label that distinction.

A matching V4 build using Bluedroid had 248,840 bytes of linked internal capacity. NimBLE recovers 15,104 bytes at link time. The matching Bluedroid application was 2,169,848 bytes. There was no matching physical Bluedroid heap measurement, so these figures do not establish a live heap saving.

"},{"location":"test-results/nimble_companion_trial_results/#bluetooth-hardware-checks","title":"Bluetooth hardware checks","text":"

Both boards passed the following cases with BlueZ on the Mercerwood Pi:

The complete MAC suite ran on 7f1bd8b1. Final-image connection and update checks also verify the unchanged adapter after the MQTT stack fix. Synthetic contacts were removed after testing; factory MAC mode and stealth off were restored.

"},{"location":"test-results/nimble_companion_trial_results/#mqtt-save-stack-overflow","title":"MQTT save stack overflow","text":"

The V4 test reproduced an actual reset while saving MQTT settings through WebConfig. Serial output identified Stack canary watchpoint triggered (loopTask). onConfigBatchEnd() kept a second 2.8 KB preference copy while the loader and NVS code also used stack space. Free heap was not the limiting resource.

The loader already updates its destination only after validation, so the callback now passes the live preference object directly. The actual Xtensa callback frame shrank from 2,928 bytes to 32 bytes. The regression test compiles the real callback with a 512-byte frame ceiling and demonstrates that the previous nested-copy implementation fails the check.

On the corrected 9dbab431 image, the 11-setting WebConfig batch completed with all entries accepted and no reboot. The V4 connected to a local Mosquitto broker, published MQTT traffic, and kept its authenticated Bluetooth session alive through a 30-second load interval with continuous uptime and zero reported error flags. Free heap measured 120,684 then 122,140 bytes; the minimum was 118,332 bytes and the largest free block was 110,580 bytes. Disabling the test MQTT slot also completed successfully without reboot.

"},{"location":"test-results/nimble_companion_trial_results/#wifi-updates-and-qr","title":"WiFi updates and QR","text":"

Both boards completed real application-image uploads over WiFi with Bluetooth connected, followed by reboot and reconnection using the saved bond. Repeated HTTP page requests and Bluetooth status requests ran together before upload. The image hash was checked before transmission, and the running firmware version was checked after reboot.

The V4 WiFi setup page again contains a QR code. The default open setup SSID fits a 44 x 44 pixel code: 2 x 2 pixel modules and an exact one-pixel white border. The native test rendered the actual display/QRCode code and decoded the result with ZXing. SSID, portal address and the hold-to-stop hint fit beside it. Physical phone-camera scanning has not been verified.

"},{"location":"test-results/nimble_companion_trial_results/#observed-limits","title":"Observed limits","text":"

One XIAO warm reboot left its USB endpoint unresponsive while Bluetooth still answered. The user's physical unplug/replug restored USB; the later full MAC suite completed its commanded reboots successfully. The cause of that one USB failure is unconfirmed.

The XIAO initially failed to associate with the saved WiFi network. Reapplying the SSID scheduled a fresh connection, after which it joined at -53 dBm and completed WiFi OTA. No antenna change was needed for that recovery.

This was functional hardware testing, not a long-duration soak. iOS/Android app pairing, phone camera scanning, multiple TLS MQTT brokers, and an end-to-end LoRa mOTA transfer were not qualified in this trial. The XIAO parent profile does not include direct MQTT; the V4 does.

"},{"location":"test-results/nimble_companion_trial_results/#final-application-hashes","title":"Final application hashes","text":"Hardware SHA-256 V4 e6a1522ced4ed38ddd00e7cdf8393857224adbb622888a86370c0f7a3ee28d88 XIAO S3 WIO ecf7afe6222e3897556a04e022515f9dcfcb825172dae9eaaa72eb92396dd004"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/","title":"Nrf52 bluetooth dfu handoff","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#nrf52-bluetooth-dfu-handoff","title":"nRF52 Bluetooth DFU handoff","text":""},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#confirmed-application-defect","title":"Confirmed application defect","text":"

The released RAK4631 application RAK_4631_repeater-v1.17.1.7-halo-keymind-cascade-dev-2d03e098 contains an unsafe transition from the running application into the Bluetooth DFU bootloader. This transition belongs to the application's Bluefruit BLEDfu service. Replacing the bootloader does not replace that application code.

The original callback changes CONTROL to select the main stack pointer (MSP) while its compiler-generated C frame still belongs to the FreeRTOS process stack pointer (PSP). The compiled callback then restores its C frame using MSP. With the observed stack state, that restore reads outside nRF52840 SRAM before reaching the bootloader entry point. A compiler/emulator regression reproduces the fault.

This defect explains why a Bluetooth flasher can reach \"enabling bootloader\" and then lose the device before the application transfer starts. It is consistent with the reported solid blue LED and later application restart. The exact reset cause and timing in the user's report remain unrecorded. The standard application enables a 60-second watchdog; Android can also report a separate 30-second GATT connection timeout. See the physical comparison below rather than attributing every timeout to the watchdog.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#application-fix","title":"Application fix","text":"

scripts/nrf52_ble_dfu_fix.py patches a private build copy of the pinned framework's BLEDfu.cpp. It preserves the legacy B1 DFU entry marker, retained peer/bond record, and SoftDevice vector-table handoff. It leaves PlatformIO's shared framework cache unchanged.

The callback now enters the naked, nonreturning assembly helper mesh_nrf52_dfu_jump. The helper loads the bootloader vectors, sets its MSP, selects MSP, clears inherited interrupt masks, and branches directly to the bootloader. No compiler-generated C stack access follows the stack switch. The supported handler-mode path constructs an explicit exception-return frame. The callback also clears SysTick, pending FreeRTOS system exceptions, and both external interrupt banks before the handoff.

Every environment inheriting nrf52_base uses the private patch and the compiled-image check in scripts/check_nrf52_ble_dfu_handoff.py. The check requires the qualified helper instructions, rejects a CONTROL write in the C callback, and verifies that the callback branches to the helper. An unrecognized framework source or an unsafe compiled handoff fails the build check.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#verification-completed","title":"Verification completed","text":"

The CI regression requires both the ARM compiler and Cortex-M emulator; missing dependencies must not silently turn qualification into skipped tests.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#installing-the-fix-on-an-affected-application","title":"Installing the fix on an affected application","text":"

An already installed affected application may fail before it can enter Bluetooth DFU. Install an application containing this fix once through USB UF2, or force the board into hardware DFU and use the matching application package. Use the board's supported recovery entry procedure. Installing a new bootloader does not replace the unsafe callback in the application. The in-place correction to OTAFIX recovery 2.4.11 adds a narrow fault fallback for this legacy callback; the originally published 2.4.11 lacks it. When the forwarded HardFault occurs with the B1 Bluetooth DFU request marker, it sets A8 and resets directly into BLE DFU without touching the broken stack. Other faults keep their existing halt behavior. The unchanged 1.17.1.7 app was physically observed reaching DFU through this fallback. This supplies an alternative to USB application installation when the corrected bootloader has already been installed; complete application transfer is recorded below.

The recovery bootloader is a temporary bridge for bootloader migration. Complete migration to the normal physical-board bootloader according to the recovery release instructions. An R_ bootloader version is not itself evidence that Bluetooth DFU is disabled.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#separate-setup-page-and-settings-report","title":"Separate setup-page and settings report","text":"

The reporter's first application install produced setup-page errors, while the same application worked after an erase UF2 and reinstall. Application UF2 installs normally preserve the application's filesystem. The standard MeshCore erase firmware formats both InternalFS and ExtraFS, which changes stored identity and preferences.

That result is consistent with a retained-state problem, but it does not identify which setting, file, or client state failed. The original state was lost after the erase, and the exact setup-page error was not recorded. The Bluetooth handoff regression does not prove the cause of that earlier setup failure.

The old app also appends ,preamble=32 (auto) to get radio on this test device. Current firmware restores the four-field radio reply expected by the official setup app (commit ca23d38e). Its compiled CLI settings regression checks this compatibility contract. This is another concrete older-application compatibility issue, although the reporter's original setup-page error was not captured and cannot be attributed conclusively.

Current firmware already includes truncated preference checks, bounded strings, non-finite radio-value sanitization, durable identity retries, and filesystem startup recovery. The 7 passing settings tests qualify those existing protections. This Bluetooth fix adds no automatic wipe and does not require an erase as its installation step.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#hardware-testing","title":"Hardware testing","text":""},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-recovery-baseline","title":"Published recovery baseline","text":"

On 2026-10-01, RAK4631 USB serial 9AB3B64C641BA927 ran the exact reported v1.17.1.7-halo-keymind-cascade-dev-2d03e098 application and published recovery R_0x02040BFF (2.4.11). Nordic nRF Connect 4.24.3 on a second LG phone running Android 5.1.1 attempted the fixed application ZIP. It failed before payload transfer: the graph stayed at zero, the initial opcode disconnected, repeated GATT 133 reconnect errors followed, and the device later answered ver with the unchanged application. No RAK DFU advertisement was seen in a later scan. USB data remained connected for this baseline.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#historical-lab-only-nordic-fallback-with-the-unchanged-old-application-pass","title":"Historical lab-only Nordic fallback with the unchanged old application: PASS","text":"

The lab-only recovery qualification image uses packed test version 0x02040CFF, production-length text R_v0.11.0-OTAFIX2.4.12, and bootloader CRC 5B8F3E90. It is a development qualification image, not the tagged production artifact. This correction creates no 2.4.12 release. This physical result keeps its tested version and does not qualify the replacement 2.4.11 release assets. The combined serial DFU ZIP installed it, then USB UF2 restored the unchanged reported 1.17.1.7 application. Both identities were checked before retrying.

At 16:57:51 local time Nordic requested the same application ZIP. The bootloader advertised 4631_DFU at the original C3:C6:A8:FC:AD:A6 address about 11.7 seconds later. Nordic reconnected automatically, transferred the 531,708-byte application image from the 532,510-byte ZIP at about 2.2 kB/s, and activated the application at 17:02:19. Afterward USB CLI reported v1.17.1.8-ble-dfu-test (Build: 01-Oct-2026) and OTAFIX2.4.12. USB data was connected throughout this test.

No erase firmware was used. Seven queried values retained identical digests: name, TX power, latitude, longitude, frequency, airtime factor, and public identity. The radio tuple also stayed 869.6179809,62.5,8,5; its old digest exactly matches the new tuple plus the old preamble suffix. The differing raw radio reply is the intended setup-client compatibility fix, not lost settings. These are checks of the queried fields, not a dump of the entire filesystem.

The older application lacks a hash-bound EndF trailer. The installed recovery bootloader safely refused its USB bootloader updater while that app was valid, because it could not prove the staging scratch range was unused. This guard is unchanged. The combined serial/Bluetooth DFU ZIP supplies the supported installation path; permissive image identity policy does not disable flash bounds or live-application staging safety.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#corrected-2411-through-meshcore-open-with-the-unchanged-old-application-pass","title":"Corrected 2.4.11 through MeshCore Open with the unchanged old application: PASS","text":"

The second LG phone's Android legacy Open build 9.5.5/code 26 reached the Bluetooth updater, but Enable radio updater rejected a selected route's hash width before sending its command. Open commit 03b0d707 preserves each supported route's own width and rejects malformed routes before sending. Its 81 focused tests passed, including 13 new tests through the real route preparation and command path.

The corrected API 21/ARMv7 Open build 9.5.5/code 27 was installed with the phone's pairing data retained. Its SHA-256 is 5a3a2f83299f1d4e27f242680335494d0ac49db5f8fa3e2ee8b71902f1924d33. The second phone disappeared from the VM's USB inventory during USB re-enumeration before a completed Open transfer could be recorded. The automatic USB configuration watcher was stopped. Testing continued on the first LG phone, also running Android 5.1.1, using that same Open build.

The RAK4631 ran the corrected 2.4.11 qualification image with packed version 0x02040BFF and bootloader manifest CRC C2A4B64B. The exact published old 1.17.1.7 application was restored before this test. USB data was disabled at the target; USB power remained connected. Forced flood authentication through the companion and the temporary radio tuple 910.525,62.5,7,5 enabled the repeater's updater, whose reply identified the correct target Bluetooth address.

On 2026-10-02, Open's native Nordic DFU SDK 2.11.0 initially reported GATT 133 and disconnect status 8 while writing opcode 1. Its automatic retry reached bootloader revision 0800, negotiated MTU 247, and transferred all 531,708 bytes in 68,651 ms. Validation returned status 1 and Open sent Activate/Reset, logged at 03:27:16 America/Los_Angeles. Final CLI queries confirmed v1.17.1.8-ble-dfu-test (Build: 01-Oct-2026), bootloader OTAFIX 2.4.11, the restored saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. The raw radio reply differs only by the removal of the old preamble suffix. USB data was restored for verification. No erase firmware was used. This qualifies the tested application/bootloader/phone combination, not every board or a battery-only install. These measurements used a qualification image; the production archive inventory identifies release assets by their source tag, source commit, manifest CRC, and checksums.

The repair uses the existing non-latest prerelease R_0.11.0-OTAFIX2.4.11, preserving that original distribution tag and its 110 asset names. Corrected production binaries use the source-only tag R_v0.11.0-OTAFIX2.4.11. The normal latest release remains 2.4.10.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#corrected-2411-through-nordic-with-the-unchanged-old-application-pass","title":"Corrected 2.4.11 through Nordic with the unchanged old application: PASS","text":"

On 2026-10-02, the first LG Android 5.1.1 phone ran Nordic nRF Connect 4.24.3/code 114 against the same corrected 2.4.11 qualification image, packed version 0x02040BFF and manifest CRC C2A4B64B. The exact published old 1.17.1.7 application was restored before the test. Local CLI start ota returned the correct target Bluetooth address. USB data was disabled during the application transfer while USB power remained connected.

Nordic displayed upload progress at 2%, 20%, 44%, 67%, and 88%, at about 1.2 kB/s, then disconnected and the application restarted. This phone provided no Nordic INFO log; the recorded evidence is UI progress plus final CLI verification, not an exact transfer duration or a captured 100% frame.

Final CLI queries confirmed v1.17.1.8-ble-dfu-test, bootloader OTAFIX 2.4.11, all seven unchanged settings digests, and the saved radio tuple 869.6179809,62.5,8,5. The raw radio reply differs only by the removal of the old preamble suffix. No erase firmware was used. This qualifies the tested application/bootloader/phone combination, not every board or battery-only operation. The earlier lab-only 2.4.12 Nordic pass remains separate. Production release assets must pass the clean-tag build and package validation before upload; these physical tests do not bypass those gates.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#earlier-restored-node-handoff-and-exit-check","title":"Earlier restored-node handoff and exit check","text":"

The RAK4631 was returned to the published normal 2.4.10 auto bootloader and the fixed v1.17.1.8-ble-dfu-test application. The ordinary application has no EndF marker, so its bootloader UF2 replacement was safely refused; the combined normal DFU ZIP and application UF2 supplied the working path. No erase firmware was used.

A supplemental Linux Bluetooth check reached the application's DFU service revision 0100, requested handoff, and reached 4631_DFU revision 0800 at the same Bluetooth address. It then requested a return to the application. After subscribing to control notifications on the bootloader connection, the exit request returned the node to the application. Final CLI queries confirmed the normal 2.4.10 bootloader, fixed application, saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. USB data was restored. This checks the restored node's Bluetooth entry and exit; it is not an Open transfer.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-recovery-correction-verification-pass","title":"Published recovery correction verification: PASS","text":"

On 2026-10-02, bootloader recovery workflow run 36997468388 completed all 37 jobs: preparation, 35 board builds, and release packaging/publication. The corrected production source is tag R_v0.11.0-OTAFIX2.4.11, commit de88c5fcae72bb00992ac9db7fd144fec150835e.

The existing recovery 2.4.11 prerelease retains all 110 filenames and its original distribution tag object b3784854e72b1d3d68e6bef04c1890f251256f59, which still points to d6b3edf7d8b58c15187ccae54ea212cfa806d01c. The release remains non-latest; normal latest remains OTAFIX 2.4.10. All downloaded digests, archive checksums, provenance, 35 profile manifest CRCs, and HEX/DFU ZIP/UF2 reconstructions passed. Six included profiles remain explicitly pending hardware qualification, and thinknode_m8 remains excluded. The published recovery bundle SHA-256 is 803dffcfed20331205d1fbe581593865785a4e53988efd20d777e5fe19b05b3d.

The physical Open and Nordic tests used RAK4631 board qualification CRC C2A4B64B, raw SHA-256 239c23fabcee8478475b3bc3e1c2fc94daa79f386c5fd7ccf980c689220eda7a. The published RAK4631 board production image has CRC AA30573D, raw SHA-256 13188cc150682a23bdee1ed7d2340a4ff0e6701b6d18dc347b75803c9ae53ae2. Both have packed version 0x02040BFF; their complete raw binaries differ.

The production/qualification comparison found exactly eight differing data bytes out of 40,960: four in the CURRENT.UF2 FAT create/update time fields and four in the resulting whole-image manifest CRC. All remaining 40,952 bytes match, including executable instructions, vectors, CF2, version, and policy guards. The production timestamp matches the clean source commit, and the production DFU ZIP, published HEX, and workflow artifact agree. This verifies the published implementation against the tested one while keeping the physical test tied to its actual qualification artifact.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#final-test-node-restoration","title":"Final test-node restoration","text":"

After both corrected 2.4.11 phone tests and publication verification, the RAK4631 was restored to the published normal 2.4.10 bootloader and the fixed v1.17.1.8-ble-dfu-test application. Final CLI verification confirmed those versions, the saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. USB data was re-enabled. No erase firmware was used.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-production-bootloader-and-following-application-ble-test-pass","title":"Published production bootloader and following application BLE test: PASS","text":"

On 2026-10-02 an attached LG LGL51AL/Android 5.1.1 phone running nRF Connect 4.24.3 installed the published repaired RAK4631 board recovery 2.4.11 ZIP over Bluetooth. This uses the production package rather than the earlier qualification image. The package SHA-256 is 3ec67bba63dff7ed44dc1680d0f65b0b371ffd9505f5ed6540ec7de8e4999771.

The phone recorded validation success and Activate and Reset. Read-only UF2 information confirmed recovery 2.4.11. With host USB configuration blocked before the replacement bootloader appeared and VBUS retained, recovery subsequently advertised 4631_DFU at C3:C6:A8:FC:AD:A7. The phone found it in a fresh scan and read firmware revision R_0x02040BFF. The previous application connection used A6; the cached tab's reconnect to A6 logged GATT 133 after successful activation, while connecting to the new A7 DFU advertisement worked.

The same phone then installed the 531,708-byte fixed MeshCore application over Bluetooth. Validation succeeded and Activate and Reset was logged at 13:51:27 America/Los_Angeles. CLI verification confirmed v1.17.1.8-ble-dfu-test, bootloader OTAFIX 2.4.11, the saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. No erase firmware was used. The normal 2.4.10 bootloader and the original fixed application were restored afterward; final identities and settings matched the initial baseline.

The full hardware report records the host USB configuration guard, its failed trailing audit after a separate hub reset, restored hub access and policies, package identities, and evidence. This is one RAK4631/profile/phone test, not an all-board, physical charge-only-cable, battery-only, or LoRa transfer qualification.

"},{"location":"test-results/nrf52-usb-ready-fix/","title":"Nrf52 usb ready fix","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52-usb-ready-fix/#nrf52-usb-ready-hang","title":"nRF52 USB READY hang","text":"

The RAK3401 on Mercer was found stuck in the framework's TinyUSB power handler: USBD was enabled and attached, but its write-one-to-clear READY event was already consumed. The duplicate-event guard also required HFCLK to be running. If that guard missed the completed initialization, the following unbounded READY wait could stop application startup. A bootloader update alone cannot replace this USB driver in an already installed MeshCore application.

The wait and guard were inherited through the pinned Adafruit nRF52 Arduino framework, not introduced by the recent OTAFIX upstream merge. TinyUSB's wait dates back at least to 2019; the guard was added in upstream commit 7d9efd0697. OTAFIX's August 24 HFCLK backport addressed a later clock wait, leaving this earlier READY wait uncovered. The exact live interrupt ordering is not known; the stalled program counter and consumed event were read directly over SWD.

All nRF52 environments inherit pre:scripts/nrf52_usb_power_fix.py. It compiles a build-local replacement for the matching framework driver, without changing PlatformIO's shared SDK or affecting ESP32 builds. A changed/unrecognized framework implementation stops the build for review. The backport:

The poll budget is an iteration limit, not a promised elapsed-time timeout. If the clock/peripheral never becomes ready, the caller returns instead of freezing the application. A later power event can retry; permanent electrical failure can still require reconnecting USB. This does not establish or fix the separate Pi hub/controller fault.

Run python3 -B test/test_nrf52_usb_power.py -v. The tests compile the actual patched handler from the pinned-framework fixture, model W1C event semantics, and cover duplicate/nested callbacks, delayed/missing clocks and READY, removal, retry, and detached USB. Restoring the inherited READY prefix must reproduce the infinite wait under a subprocess deadline. Additional tests check SDK isolation, idempotence, fail-closed patching, the startup DETECTED/READY order, and all nRF52 environment hooks. The harness is shared with OTAFIX's TinyUSB fork at test/otafix/nrf5x_power_test.py; keep the two copies in sync when extending it.

"},{"location":"test-results/nrf52_companion_storage_recovery/","title":"Nrf52 companion storage recovery","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52_companion_storage_recovery/#nrf52-companion-automatic-extrafs-recovery","title":"nRF52 Companion automatic ExtraFS recovery","text":"

Companion builds using internal ExtraFS reserve 100 KiB at 0xD4000..0xED000. Identity and preferences remain in the separate 28 KiB primary store at 0xED000..0xF4000. This applies to internal-ExtraFS Companion builds, including Full, not repeaters or external QSPI storage.

At startup the firmware:

  1. Mounts primary storage without erasing nonblank media. Only proven-blank primary storage may be initialized automatically.
  2. Checks the exact ExtraFS address, size, block geometry, and application linker reservation, then attempts a non-formatting secondary mount.
  3. Validates primary filesystem metadata before any destructive secondary recovery. An unavailable primary store blocks recovery and identity replacement.
  4. Keeps an already mounted, metadata-valid ExtraFS unchanged. If its initial mount or validation failed, unmounts and retries once without formatting.
  5. If the retry remains unusable, automatically formats only the reserved 100 KiB region, then remounts and validates it. There is one format attempt per boot; failure keeps the contact/channel write quarantine active.
  6. Runs the existing verified migration from primary into ExtraFS. Primary contact/channel sources are retired only after every destination is copied, read back, and the migration transaction is committed. Identity and preferences stay on primary.

No terminal repair command, phone intervention, or bootloader update is needed for this recovery path when a compatible application/layout is already used. After successful recovery, the Companion storage API reports 100 KiB total rather than the 28 KiB primary fallback.

"},{"location":"test-results/nrf52_companion_storage_recovery/#data-loss-boundary","title":"Data-loss boundary","text":"

Automatic rebuilding prioritizes a usable contact/channel store over retaining an unreadable secondary filesystem. Data stored only in that damaged region can be lost. Surviving primary files are migrated, but may not contain the latest secondary-only changes. An API backup remains advisable before flashing. Primary storage is never formatted by this recovery path.

This is mount/metadata recovery, not forensic file recovery. An invalid migration journal, unreadable individual contact page, primary-source error, full filesystem, or failed migration cleanup does not on its own trigger a format of a mountable, metadata-valid secondary store. Those cases retain the existing validation, retry, and incomplete-load protections.

"},{"location":"test-results/nrf52_companion_storage_recovery/#regression-coverage","title":"Regression coverage","text":"

test/test_internal_secondary_fs_repair exercises healthy/no-format startup, successful remount, failed remount/traversal followed by one repair, and format/remount/validation failure. test/test_nrf52_extrafs_contract.py checks primary-first ordering, internal-only geometry guards, quarantine handling, and the separation between migration failures and destructive recovery.

"},{"location":"test-results/nrf52_companion_storage_recovery/#t1000-e-hardware-qualification-2026-09-03","title":"T1000-E hardware qualification (2026-09-03)","text":"

A device reporting 25/28 KiB primary fallback and contact enumeration error 5 was flashed with the recovery build. With no explicit repair command, startup activated 100 KiB ExtraFS and loaded all 125 contacts and 40 channel slots. Identity, preferences, radio settings, and channels matched the pre-update API snapshot. Add, edit, and delete of an owned temporary contact each survived a separate normal reboot; the device ended with the original 125 contacts and 27/100 KiB usage. The 161 offline/native regression tests and target build passed.

This was not lossless recovery of every historical RAM value: 21 contacts had older advertisement/local-modification timestamps, including one older advertised location. No contact was missing, and no computer-backup restoration was used. A subsequent read-only scan found no readable cached advert for any of the 125 contacts. Without a surviving newer copy, firmware cannot infer those earlier values; receiving new verified advertisements can refresh them normally.

"},{"location":"test-results/pair_4p6_300us_validation/","title":"Pair 4p6 300us validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pair_4p6_300us_validation/#sf7625-sf8500-46-slow-chirps-03-ms-reserve","title":"SF7/62.5 + SF8/500: 4.6 slow chirps, 0.3 ms reserve","text":"

User-requested follow-up to the original pair test. Completed 2026-09-15 00:21:27\u201300:23:52 UTC (September 14 local time).

"},{"location":"test-results/pair_4p6_300us_validation/#result","title":"Result","text":"Profile Requested dwell Stationary control Scanning received Slow: SF7 / 62.5 kHz 9.421 ms, approximately 4.6 chirps 100/100 97/100 Fast: SF8 / 500 kHz 21.847 ms, approximately 42.67 chirps 100/100 100/100

Full scan: 197/200 (98.5%), not lossless. All three misses were slow-profile timeouts with no device error and the chip still in RX. The collector completed every scheduled probe, with no RF retry, retune/reset on a miss, or early stop. The two controls plus the scan contain 400 unique probes; each fixed transmitter acknowledged exactly 200 packets. There were no USB result replays.

This is one finite sample. It does not establish that 4.6 is inherently worse than 4.1 or better than 5.1, nor identify the cause of the three losses.

"},{"location":"test-results/pair_4p6_300us_validation/#timing-and-unchanged-conditions","title":"Timing and unchanged conditions","text":"

Same V4 RX, RAK4631 slow TX and T1000-E/LR1110 fast TX; 909.5/910.5 MHz, CR4/5, explicit header, CRC, 32-symbol preambles, 16-byte synthetic payloads, -9 dBm chip TX power, normal RX gain 0x94, randomized interleaving and 10\u2013180 ms arrival delays. Only one transmitter sends at a time. Each TX configures once; every packet has zero frequency/modulation writes.

The single-pass production guarded switching path uses warm XOSC and buffered 8 MHz SPI. Each hop issued exactly one frequency and one modulation command, with one optimized RX restart. No double writes, detours, rollback, added settling, off-channel acceptance, cache errors, RX errors or retune failures. As before, this is a small HIL application, not full production UI/network load.

"},{"location":"test-results/pair_4p6_300us_validation/#verification-and-artifacts","title":"Verification and artifacts","text":"

Only the V4 application was updated. Exact USB identity/MAC, idle ownership and existing partition metadata were checked before writing. The nRF52 transmitter images, bootloaders, SoftDevices, gateway and unrelated radios were not flashed. All three test radios were rebooted and verified idle afterward; HIL firmware was left installed. ModemManager was restored active, gateway and host CLI services are active, and the previously paused memory soak remains inactive.

No production timing defaults changed, and no commit or push was made.

"},{"location":"test-results/pair_preamble32_validation/","title":"Pair preamble32 validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pair_preamble32_validation/#sf7625-sf8500-fast-switching-and-preamble-32","title":"SF7/62.5 + SF8/500, fast switching and preamble 32","text":"

User-requested hardware validation, 2026-09-14. This is a finite HIL experiment, not a production reliability guarantee. No production timing defaults or automatic-preamble overrides are changed by this test.

"},{"location":"test-results/pair_preamble32_validation/#fixture-and-method","title":"Fixture and method","text":" Case Loop reserve (not a delay) Slow visit Fast visit 4.1-chirp floor 0 ms 8.397 ms 23.171 ms Spare time used for 5.1 slow chirps 0 ms 10.445 ms 21.123 ms Previous reserve comparison 4 ms 8.397 ms 19.171 ms

The 5.1 case reallocates 2.048 ms from the fast visit to the slow visit; it does not increase the nominal cycle. The 4 ms reserve is omitted from the first two cases. The theoretical switch allowance is 0.6 ms per hop; actual switching is measured, never forced to equal that allowance. Changed SF/BW requires one modulation command per hop; frequency-only cache-hit timings do not apply.

The scheduler uses the same asymmetric two-profile visit budget and the actual production guarded tuneProfile() / fast RX implementation, but runs in a small RAM-only HIL application, without the full production networking/UI load. All retunes are single-pass, warm-XOSC, buffered 8 MHz SPI, zero added settling; double frequency writes, detours and rollback experiments are off. Packet/busy ownership guards are retained. Explicit 32 on both sides bypasses the historical automatic SF8/500 88-symbol rule without removing it from production.

The selected radios already had disposable HIL firmware. Updates are application-only: matching nRF52 SoftDevice IDs/address ranges and exact USB serial/topology are checked. V4 partition metadata is read back and compared before its application-only write. No bootloader, SoftDevice, settings backup, gateway firmware or unrelated radio is changed. Prior images/captures remain.

"},{"location":"test-results/pair_preamble32_validation/#results","title":"Results","text":"

Finished all 800 unique probes, 2026-09-15 00:04:54\u201300:11:19 UTC (September 14 local time). Both stationary controls passed 100/100. All three scans completed their full 100-packet sample per profile. No scan achieved 200/200.

Case SF7/62.5 received SF8/500 received Total Switch mean / max Idle cycle mean / max 4.1 chirps, no loop reserve 99/100 100/100 199/200 0.581 / 0.829 ms 32.717 / 34.733 ms 5.1 chirps, no loop reserve 100/100 97/100 197/200 0.572 / 0.834 ms 32.706 / 34.978 ms 4.1 chirps, 4 ms loop reserve 97/100 99/100 196/200 0.567 / 0.836 ms 28.701 / 31.195 ms

Switch timing includes every successful hop during each stage, including post-reception hops. The RX-only five-second windows averaged 0.554 ms per hop. The largest observed switch was 0.836 ms: 0.6 ms is a nominal allowance, not a measured worst-case bound. Direction-specific timing is in the validated capture. Idle-cycle samples exclude packet-held visits but still include USB command/loop scheduling; their occasional roughly 2 ms overshoots are real.

Every scan hop issued exactly one RF-frequency and one modulation command, with one optimized RX restart. There were zero added settling delays, double passes, frequency repeats, RF retries, USB result replays, device errors, retune failures, RX mode errors, cache errors or off-channel acceptances. All eight missed packets were sequence-matched RX timeouts with the receiver still in RX mode and no device error. Both transmitters acknowledged exactly 400 transmissions and zero frequency/modulation writes during every packet.

Successful scan packets had mean SNR approximately +12.0 to +12.1 dB on the slow profile and +12.8 dB on the fast profile. Mean RSSI was approximately -20.4 to -20.7 dBm and -18.5 to -18.9 dBm, respectively. These are strong-signal bench results; the controls passed, but weak-signal performance and possible strong-signal/front-end effects are not established by this test.

"},{"location":"test-results/pair_preamble32_validation/#what-the-extra-chirp-changes","title":"What the extra chirp changes","text":"

An extra SF7/62.5 symbol is 2.048 ms, equivalent to four SF8/500 symbols. Increasing the slow dwell from 4.1 to 5.1 preserves the full-sweep budget by shortening the fast dwell, but increases the fast profile's uninterrupted blind interval. With the nominal 0.6 ms hops, the 32-symbol fast preamble's remaining time after the slow visit plus two hops falls from 6.787 to 4.739 ms (13.26 to 9.26 fast symbols), before acquisition and scheduling overruns. This is a timing-budget calculation, not an experimentally proven acquisition threshold or a per-miss trace.

The observed 5.1 sample improved the slow result from 99 to 100, while the fast result fell from 100 to 97. This is consistent with a dwell/acquisition tradeoff; one finite sample per setting does not isolate the cause or establish a statistically reliable ranking. Simply fitting a sweep inside a preamble does not establish lossless acquisition. The 4 ms reserve is an allowance, not a sleep; retaining it also did not produce a lossless result here.

Using genuine spare time above the 4.1 floor remains a reasonable policy to investigate, but allocation must protect both profiles' acquisition/return budgets and measured switching/scheduling overruns. Do not automatically spend all of the reserve on longer slow visits, or treat the historic 88-symbol rule as disproven by this nearly-lossless sample. No production defaults or warning formula were changed by this hardware-test turn.

"},{"location":"test-results/pair_preamble32_validation/#capture-integrity-deployment-and-cleanup","title":"Capture integrity, deployment and cleanup","text":""},{"location":"test-results/pair_preamble32_validation/#follow-up","title":"Follow-up","text":"

The requested 4.6-chirp / 0.3 ms reserve follow-up completed 197/200 scanning packets: slow 97/100, fast 100/100, after fresh 100/100 controls on both profiles. Its captures are separate from this run.

"},{"location":"test-results/pr7_review_validation/","title":"Pr7 review validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pr7_review_validation/#pr-7-review-and-validation","title":"PR #7 review and validation","text":"

Validated on 2026-09-10 against integration commit fa010196b130dee9a8a6fc6325d794e2f4fb9ede, which combines base 01cc7b8a with PR head c002966c and the review fixes. This includes the stacked ESP32 memory changes already present in PR #7.

"},{"location":"test-results/pr7_review_validation/#review-fixes","title":"Review fixes","text":""},{"location":"test-results/pr7_review_validation/#automated-validation","title":"Automated validation","text":""},{"location":"test-results/pr7_review_validation/#indicator-hardware-validation","title":"Indicator hardware validation","text":"

The original SenseCAP Indicator LoRa with 8 MiB flash and 8 MiB octal PSRAM was reached through the MercerWoodMesh Pi over Wi-Fi. Its MAC, public identity, board reply, and existing partition layout were checked before updating. USB interfaces were absent, so this run does not qualify USB behavior.

The Full profile was applied to SenseCapIndicator-LoRa_comp_radio_usb_wifi. The exact application is 2,149,208 bytes:

This direct profile build uses the repository's default v1.17.1 version text and 14 Aug 2026 build-date text; the hashes above identify the tested image.

The Wi-Fi updater returned HTTP 200 and booted the application in app1 at 0x400000. The previous application remains in app0 at 0x10000; the partition table, NVS, and SPIFFS were not rewritten. Public identity, radio parameters, and USB logging preference matched their pre-update values.

The existing Companion stress harness, using a TCP socket adapter, completed 100 persistent cycles and 100 reconnects in 109.9 seconds: 501 requests and 501 validated responses, with no discarded bytes, malformed frames, or device error flags. Device info, time, storage, and core statistics were exercised. The text terminal exposed all four display-power controls and display.inbox. The OTA folder service issued a valid COUNT request, received an empty-folder reply, and returned to its disconnected state after the client closed.

After these checks, four memory snapshots showed 69,548\u201369,996 bytes of free internal heap and a 61,428-byte largest block. PSRAM availability stayed at 6,408,731 bytes. Final uptime was 262 seconds, with zero error flags and an empty outbound queue. This is a short regression check, not a long-term soak. Raw baseline, upload, stress, and final-state records are retained on the Pi under /home/mikec/hwtest/runs/pr7-review/.

No Linkflow module or RF power meter was available in this run. Linkflow power calibration is supported by the pinned upstream table and executable driver tests, not a physical RF output measurement. The Indicator's ESP32-S3 does not exercise the classic ESP32 heap-OTA default; host tests and classic ESP32 builds cover that path. Physical display readability was not inspected.

"},{"location":"test-results/preamble_detection_investigation/","title":"Preamble detection investigation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preamble_detection_investigation/#off-channel-preamble-investigation","title":"Off-channel preamble investigation","text":"

Measured 2026-09-14. The SF8 off-channel preamble indication reproduces with a stationary, fully initialized receiver, without profile hopping or fast RX reuse. Both boards reproduce it when their transmitter/receiver roles are reversed. This is evidence of transmitter-associated off-channel detection in this close-range bench setup, not evidence that the channel jump needs more settling time.

The earlier SF8 scan's long software guard explains its missed listening opportunity. The earlier SF6 scan miss remains unresolved: its trace had no preamble indication or guard hold. No production guard or acquisition policy was changed during this investigation.

"},{"location":"test-results/preamble_detection_investigation/#controlled-experiment","title":"Controlled experiment","text":"

The three completed matrices contain 128 windows: 112 transmitted probes and 16 silent controls. A separate initial 450 ms receiver-only smoke check was also performed. These are bounded diagnostic controls, not another 100-packets-per-channel capacity sweep.

"},{"location":"test-results/preamble_detection_investigation/#results","title":"Results","text":"

Entries below are windows with a preamble indication / tested windows. Each positive window reported one indication; latched IRQs do not count continuous detector activity or prove a valid packet.

Transmitter offset SF8, XIAO RX SF8, Indicator RX SF6, XIAO RX Silent 0/8 0/4 0/4 On frequency 8/8 4/4 4/4 -250 kHz 8/8 4/4 1/4 +250 kHz 8/8 4/4 0/4 -500 kHz 5/8 3/4 0/4 +500 kHz 1/8 3/4 0/4 -1,000 kHz 5/8 4/4 0/4 +1,000 kHz 5/8 3/4 0/4

All 16 on-frequency probes delivered valid payloads. None of the 96 off-frequency probes delivered a valid payload. No silent control detected a preamble. Completed windows ended in RX mode with zero device errors; there were no foreign valid payloads or trace overflows.

Most off-frequency detections stopped at PreambleDetected (0x0004). Four progressed further but failed CRC (seen_irq=0x0056: preamble, header, RxDone, CRC error): two at +250 kHz with XIAO RX, and one each at +250 and +1,000 kHz with Indicator RX. Therefore HeaderValid alone would not prove that these off-frequency indications are legitimate traffic.

In forward SF8 on-frequency controls, receiver signal strength (RSSI) sampled at the first preamble was -42 to -41 dBm; at +/-250 kHz it was -91 to -88 dBm. These are single-point receiver readings, not calibrated measurements of transmitted leakage or a sensitivity test. Board positions and attenuation were not systematically varied. Reversing roles reproduces the behavior but does not isolate its physical path.

"},{"location":"test-results/preamble_detection_investigation/#what-this-explains","title":"What this explains","text":"

The earlier SF8 four-channel scan missed probe 13 on 910.250 MHz after detecting a bare preamble while tuned to 910.000 MHz. The existing CustomSX1262::isReceiving() guard allowed 91 ms for preamble progression, consistent with the observed 92.257 ms from first preamble observation to the stale-bit clear. That produced approximately 102 ms total occupancy on the wrong channel and a 135.145 ms target-channel revisit gap. The programmed 32-symbol preamble lasted only 65.536 ms.

The delayed retune itself took 550 us. These controls reproduce the suspect off-frequency indication with no retunes at all. Together the evidence supports off-channel acquisition plus a long guard hold as the SF8 failure mechanism, rather than slow SPI or a demonstrated need for a longer post-jump delay. It does not independently prove the RF source of that one historical scan event.

The controlled-transmitter association and quiet silent controls make random ambient RF alone an inadequate explanation of the repeated bench result. Exact RF cause remains unmeasured: receiver selectivity/false acquisition, transmitter spectral leakage, and coupling between the nearby boards are not separated by these tests. No spectrum-analyzer capture or calibrated attenuation sweep was performed. Increasing channel spacing is not established as a fix: SF8 indications also occurred at +/-1 MHz.

The earlier SF6 miss had no observed nonzero IRQ or guard hold. The four successful stationary SF6 positive controls show the fixed-channel link works, but do not establish the acquisition time needed after a retune. These small, differently sized samples are not packet-error rate estimates or proof of a zero-loss channel count.

"},{"location":"test-results/preamble_detection_investigation/#follow-up-candidates-not-implemented-fixes","title":"Follow-up candidates, not implemented fixes","text":"

Semtech documents that false LoRa detections can occur and describes SetLoRaSymbNumTimeout as validating reception over multiple symbols before continuing when the modem locks. This is a candidate to investigate, not a drop-in verified fix. Its behavior in continuous RX and through each retune must be measured. See section 13.4.9, page 99 of the Semtech SX1261/2 datasheet, revision 2.2.

Useful next experiments would qualify/revalidate preambles before granting a long scan hold, and repeat the controls with lower signal levels or increased physical isolation. Any candidate must preserve legitimate weak signals and long preambles. Blindly shortening the current guard, or requiring only a header indication, is not justified by these results. No production radio change was made in this diagnostic turn.

"},{"location":"test-results/preamble_detection_investigation/#reproduction-provenance-and-checks","title":"Reproduction, provenance, and checks","text":"

See the stationary diagnostic instructions. The same diagnostic firmware binaries were used for all three matrices. Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets built and uploaded successfully. All 29 selected host/native tests passed after testing.

Raw results and SHA-256:

Tested firmware SHA-256:

"},{"location":"test-results/preamble_detection_investigation/#device-handoff","title":"Device handoff","text":"

Fresh full 8 MiB backups were taken and separately verified before flashing, in the private current-user MeshCore backup directory. No backup contents or identity/channel secrets are in the diagnostic results.

Both original images were restored after the controls, separately verified against the complete 8 MiB flash with matching digests, and hardware-reset. The RAK4631 and Indicator RP2040 were untouched; the XIAO soak logger was not restarted. The temporary HIL build overlay was removed. Investigation source, tests, and results were initially handed off locally before publication.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/","title":"Radio2 simultaneous receivers 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#simultaneous-radio2-receiver-comparison-2026-10-22","title":"Simultaneous radio2 receiver comparison, 2026-10-22","text":"

The matched-PHY five-receiver test passed: 915.000 and 916.000 MHz, both SF7 / BW 62.5 kHz / CR4/5 (RadioLib value 5) / preamble 32. Every applicable packet arrived, and all six radios passed final preservation and session checks. The preceding mixed-PHY 915/916 campaign stopped cleanly at a case boundary under user steering; its measurements remain a separate cohort. Historical 910.525 results are not current test frequencies and are not merged into either 915 cohort.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-campaign-three-scanners-and-two-fixed-witnesses","title":"Latest campaign: three scanners and two fixed witnesses","text":"

One Xiao S3 sender sent one fresh 40-packet case, with 20 primary and 20 secondary packets at preamble 32. Five receivers listen to the same physical packets concurrently. This is not separate unicast delivery to each receiver. No earlier packet is resent or replaced by this new measurement.

Role Board Source Applicable packets per case Scanning receiver Heltec V3 9b70827d 20 primary + 20 secondary Scanning receiver Heltec V4.3 OLED 9b70827d 20 primary + 20 secondary Scanning receiver Heltec T096 4773ac42 20 primary + 20 secondary Fixed primary witness Tag 08650f64 20 primary; secondary not applicable Fixed secondary witness SenseCAP Indicator 08650f64 20 secondary; primary not applicable Sender Xiao S3 WIO ebb59d48 One direct TX per planned packet

All six nodes matched their bound identity and full firmware version. The controller rejected a recorded saved baseline other than 915.000 MHz before opening hardware. V3 and V4 used normal firmware. The older T096 lacked the later shared RX fixes. Board hardware and firmware both differ, so this comparison cannot isolate either as a cause.

Primary is 915.000 MHz / BW 62.5 kHz / SF7 / CR5; secondary is 916.000 MHz / BW 62.5 kHz / SF7 / CR5, both at preamble 32. Sender power is 2 dBm. The three scanners enable radio2; each fixed witness disables scanning and stays on its assigned tuple. Receiver power saving and RXPS are off. All five USB receive collectors operate concurrently. The seeded shuffled profile order uses minimum gaps after the prior physical TX_DONE confirmation; retuning can lengthen a gap. There are no RF retries or replacement packets.

Both profiles now have the same 2.048 ms symbol duration. Scanner-derived visit policy and receive ownership still apply; matched SF/BW/preamble does not assert identical measured visits or an acquisition guarantee. This new cohort cannot be treated as another preamble-only row of the mixed-PHY test.

The migration passed on all six radios: saved primary readback was 915.000 MHz / BW 62.5 kHz / SF7 / CR5, and each physical-apply command returned ACK 00. This confirms configuration acceptance, not an RF frequency measurement. The migration explicitly saved preamble 32; earlier effective 32-symbol status did not distinguish saved auto from explicit 32. Channels, credential hashes and all other compared saved settings were retained. No test datagrams were requested by migration.

The new baseline was taken after migration. Cleanup must retain 915.000 and must not restore 910.525 on these radios, while preserving the other compared settings. This frequency change prevents treating earlier 910.525 data and the new 915.000 cases as one preamble-only comparison.

Each 92-byte DEV application payload contains MCHILGR1, the sender public key, a common group digest, a fresh 16-byte nonce and a global ordinal. The digest binds all six role-ordered public keys, witness profile assignments, the campaign nonce and exact preamble/PHY/packet plan. Keys, raw identities and payload bytes are excluded from this report. Each ordinal requires one local acceptance and exactly one physical direct TX, without a flood.

Receivers independently match exact application bytes, their owned local channel slot, DEV type and direct route. Logical profile comes from the sender ledger, not an RX-origin field in the Companion response. Queue drain, duplicates, wrong-profile witness receipts, receiver transmissions and core health are checked separately from application delivery.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-matched-phy-results","title":"Latest matched-PHY results","text":"

Test: COMPLETE, VALID, PASS. The single RF case and all six final preservation/session proofs passed, with no reported errors.

Preamble V3 P / S V4.3 P / S T096 P / S Tag primary / 20 Indicator secondary / 20 Evidence status 32, both SF7/BW62.5 20 / 20 20 / 20 20 / 20 20 20 Complete, valid; finite delivery PASS

Packet labels are one-based within each profile: P01-P20, S01-S20. All applicable missing sets are empty. Each scanner received the same 40 packets and each fixed witness received its assigned 20. The case has no reported evidence or collection errors. All 40 requests were accepted once, with 40 confirmed direct transmissions, zero floods and zero retries. All five collectors completed, with zero duplicates, wrong-profile receipts or receiver transmissions; every before/after core error flag was zero. Every applicable pair has Jaccard 1.000. The fixed witnesses have no common applicable frequency; their opposite-profile entries remain not applicable, not a 40-packet expectation.

Masks use V3, V4.3, T096, Tag, Indicator order. The opposite-frequency witness is NA, never a miss. Complete primary reception is 1|1|1|1|NA; complete secondary reception is 1|1|1|NA|1. An owned wrong-profile witness receipt is a confound. The observed masks are 1|1|1|1|NA for all 20 primary packets and 1|1|1|NA|1 for all 20 secondary packets: 40 all-applicable receptions and no fixed-witness/scanner discrepancy. A fixed-witness receipt with a scanner miss confirms delivery at that witness, not a cause of the scanner miss. No applicable receipt does not prove no RF transmission. Correlated loss alone does not assign a cause.

A valid completed measurement can still fail delivery. A campaign pass requires every applicable packet plus all evidence and preservation gates.

This finite 20-per-profile result is not a permanent-fix or zero-loss guarantee. The preceding mixed-PHY 32-symbol case at the same 915/916 MHz had scanner misses; this case changes the secondary SF/BW, airtime and derived scan timing. Run time, order and ambient traffic were not controlled between the cases. Their difference therefore does not isolate a causal setting or explain a particular earlier missing nonce. The new packets were not retries.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-completion-and-evidence","title":"Latest completion and evidence","text":"

Migration and its original six-node saved baseline: PASS. All six final readbacks retained primary 915.000 and passed preservation of the other compared settings, permissions, leases, channels, credentials and PINs. All six final session proofs passed. V4 MQTT reconnected with status (ok); this check did not independently prove a fresh publish acknowledgement. No production firmware was changed. USB collection remains active, so these rates are not a repeated measurement of earlier USB-quiet experiments.

The current controller proactively revalidates strict binary mode, public identity, full version, core health and uptime after 45 seconds of idle time, only outside RF. It performs final proofs on all six sessions before close and retains outer-finally errors. These are host measurement changes, not firmware or watchdog changes. A graceful stop marker replaces signals during an active CLI operation.

The matched-PHY controller SHA-256 is 187ab7de2464d1496ddee5235899d115270953e4ae38beaa1b0e1791823b0bb1; its comparison core SHA-256 is 6920179844efe720542368d2ea257d94c18c8c49c063efb18a60ccf6b8204b31. The completed matched-PHY raw receipt SHA-256 is 46ef6da2dabe8ba48f7acb8e8b5aec2370c8b41397787ac78020e044d50c918e; its sanitized export SHA-256 is 3bcc79cc02722c8438340c5fad49b9c9c7921f2b4e8b433669e748388a04a94f. Machine-readable evidence is the matched-PHY report. The separate saved-frequency migration source SHA-256 is a763aec8d1bd692c524f5dee773b3d1663e1eac0cc26308568e8d38153fb7cf0. The completed migration receipt SHA-256 is c523274d78c9dd3aa7511cb91ef0330866684f7f12bfc54d94967787f0d22257; the passed six-node baseline SHA-256 is bb6685fa03182395e7a697d4b7b843c023b59d7f12feebe074f2c7de76d26f78.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#previous-915916-cohort-different-sfbw-profiles","title":"Previous 915/916 cohort: different SF/BW profiles","text":"

This cohort uses primary 915.000 / BW 62.5 / SF7 and secondary 916.000 / BW 125 / SF9, with matched preambles varied across cases. All three executed cases, at 32, 64 and 128 symbols, completed as valid controlled comparisons. The user requested a graceful stop after 128; no active case was interrupted. Preamble 256 was not run. The original four-case experiment is incomplete by user steering, not a setup failure. Every executed case had 40 accepted and 40 confirmed direct transmissions, zero floods, retries, duplicates, wrong-profile receipts, receiver transmissions or evidence errors. All five collectors completed and all before/after core error flags were zero.

Preamble V3 P / S V4.3 P / S T096 P / S Tag primary / 20 Indicator secondary / 20 Evidence status 32 19 / 20 20 / 20 20 / 18 20 20 Valid; scanner delivery FAIL 64 20 / 20 20 / 20 20 / 20 20 20 Valid; all applicable delivery PASS 128 20 / 20 20 / 20 20 / 20 20 20 Valid; all applicable delivery PASS 256 NOT RUN NOT RUN NOT RUN NOT RUN NOT RUN Skipped by user steering

Packet labels are one-based within each profile: P01-P20, S01-S20. At 32, V3 missed P04 and T096 missed S02 and S15; V4.3 received all 40. Both witnesses received all 20 applicable packets. Thus each of the three scanner misses had a same-packet fixed-witness receipt. This proves delivery at that witness, not a cause of the scanner miss. All applicable receivers received the other 37 packets.

At both 64 and 128, all three scanners received 40/40 and both witnesses received their 20/20 applicable packets. There was no extraneous receiver TX or core/evidence error. This is a finite sample pass, not proof of a permanent fix or a guarantee that longer preambles prevent loss.

Masks use role order V3, V4.3, T096, Tag, Indicator, with NA for the opposite frequency witness. For example, complete primary reception is 1|1|1|1|NA; complete secondary reception is 1|1|1|NA|1. NA is never a miss. An owned wrong-profile witness receipt is retained as a confound.

Preamble Applicable reception mask Packet count 32 1\\|1\\|1\\|1\\|NA 19 primary 32 0\\|1\\|1\\|1\\|NA 1 primary 32 1\\|1\\|1\\|NA\\|1 18 secondary 32 1\\|1\\|0\\|NA\\|1 2 secondary 64 1\\|1\\|1\\|1\\|NA 20 primary 64 1\\|1\\|1\\|NA\\|1 20 secondary 128 1\\|1\\|1\\|1\\|NA 20 primary 128 1\\|1\\|1\\|NA\\|1 20 secondary

The 32-symbol witness classifications are 37 all-applicable receptions and three fixed-witness-received/scanner-missed packets: P04 at V3, S02 and S15 at T096. At both 64 and 128 they are 40 all-applicable receptions. The fixed witnesses missed no applicable packets in any executed case. The 256-symbol case was not run.

Same-packet observation Supported interpretation Fixed witness receives; scanner misses Delivered at that witness; scanner missed the same packet All applicable receivers receive Exact shared application delivery to those receivers Scanner receives; fixed witness misses Witness missed a packet received elsewhere No applicable receiver receives No local receipt; does not prove no RF transmission Opposite-frequency witness receives Confound requiring investigation; not clean applicable delivery

The raw and sanitized closed receipts agree on these counts. Correlated loss alone does not assign a receive-path cause. A valid completed measurement can still fail delivery. A campaign pass requires all applicable packets and all evidence/restoration gates.

Changing the matched preamble also changes the scanner's derived primary visit duration. At these tuples the primary symbol is 2.048 ms and the secondary symbol is 4.096 ms. The nominal idle primary visit is the larger of 9.421 ms and 2.048 * preamble - 18.842 - 2 * guarded_switch_ms; secondary visit is 18.842 ms. This is a preamble-plus-derived-dwell comparison, not an isolated preamble-only experiment. The actual guarded switch term remains part of the per-board runtime timing state (RadioProfiles::listenUs). Actual visits can be extended by receive ownership; the formula is not a timestamped account of an individual packet's reception or miss.

The mixed-PHY private controller/core SHA-256 digests are e092276f44f06d714c49befc78ae6a9738efe3cc369ede4a0b4fbe731794455b and 3c002da26512ba9324e491adb7fa8595944e921ee5bd841cec6fe8271ab5a504. All six nodes' restoration and final session proofs passed, with no reported errors. The planned-four-case aggregate completion/validity fields remain false because 256 was skipped; each of the three executed cases has its own completed/valid flags true. The raw receipt SHA-256 is 5d120a579863ffaf8ba9c29d7463f2d546e82a0ffdd012f599b0208e9908f00a; its sanitized export SHA-256 is d03d8f65bc974d56c8c49fdddcdd988496b7eca29f5bc401a7ce1196bf00036e. Machine-readable evidence is the separate mixed-PHY report.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#appendix-earlier-three-receiver-measurements","title":"Appendix: earlier three-receiver measurements","text":"

These historical runs used V3/V4 9b70827d, T096 4773ac42 and sender ebb59d48, with the old primary 910.525 MHz / BW 62.5 kHz / SF7 / CR5 and secondary 916.000 MHz / BW 125 kHz / SF9 / CR5. They used 2 dBm, matched preambles, power saving/RXPS off and three active USB collectors. Every scanner case below sent 40 distinct packets once. These are not results for the new 915.000 MHz primary.

Preamble V3 P / S V4.3 P / S T096 P / S Shared scanner masks 32 16 / 18 16 / 18 16 / 18 000=5, 001=1, 110=1, 111=33 64 20 / 16 20 / 16 20 / 16 000=4, 111=36 128 20 / 16 20 / 16 20 / 15 000=4, 110=1, 111=35 256 NOT RUN NOT RUN NOT RUN User-directed stop before RF

These three-bit masks use V3, V4.3, T096 order; omitted masks have zero count. All three measured cases were valid controlled comparisons with failed all-packets delivery. Scanner receipt sets and misses are retained:

V3/V4 receive sets matched exactly in each earlier case. At preamble 32, their Jaccard with T096 was 15/17 primary, 18/18 secondary and 33/35 overall. At 64 all pairs had Jaccard 1. At 128 their Jaccard with T096 was 20/20 primary, 15/16 secondary and 35/36 overall. Empty-union Jaccard is undefined. These correlations do not establish a loss cause or a preamble remedy.

The preamble-32 window lasted 130.650 seconds. All collectors drained, reported zero duplicates and core flags 0; no receiver transmitted. Physical RX deltas were V3 117, V4.3 112, T096 113, each including 34 owned packets; other RX deltas were 83, 78 and 79, with RX error deltas 1, 3 and 1. These background totals do not identify why a particular nonce was missed.

The fresh fixed-secondary control delivered 20/20 to all three, with zero duplicates, errors or receiver transmissions, over 65.075 seconds. The later fresh fixed-primary follow-up also delivered 20/20 to all three with no duplicates and complete drains, but an unaccounted T096 flood TX made that controlled comparison INVALID, not a clean control pass. Neither control resent a scanner packet or erased its misses.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#appendix-failed-attempts-and-restoration-history","title":"Appendix: failed attempts and restoration history","text":"

The stopped old-frequency five-receiver attempt's private controller/core SHA-256 digests were 7e3dba8ca3c3387e601111714f46741bdd933b6f041f8213aadad8eef69d3cc1 and ba0a116f90a17825d1718aef9e4138a27002fdd2ff2127be46815280245ceab2.

Private failed/measured receipt SHA-256 digests, without raw identifiers:

Receipt SHA-256 Preamble 32 and fixed-secondary c5e7cec34c376c718e936f4a86b21b197bbb2dc4b02dbdc7e079617f2d229511 Fixed-primary original failed comparison ffe5a65c17df98b444eb690f9208090637724dc7906b2fb97aedc5f2bc63ee44 First five-receiver setup, zero RF b3613de719584be3c033c31a8a973c30c88e22146d9060293aa1ec7fb574a1c9 Stopped old-frequency five-receiver partial, 25 TX 87026d5c9938ae855983f529906aca09bafa9975e01ce0176d9eef82391eed2f

Other detailed earlier attempt/recovery records are retained privately. Earlier failures and causal limits also remain in the ESP32 BLE, radio2 and sleep report.

"},{"location":"test-results/radio_chirp_math_validation/","title":"Radio chirp math validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_chirp_math_validation/#shared-radioradio2-chirp-timing-and-preamble-warnings","title":"Shared radio/radio2 chirp timing and preamble warnings","text":"

Historical validation record: the nominal 600 us policy described below has been replaced by the activation self-test and 10% guarded measured budget. The later automatic policy also removed the pair-specific 88-symbol SF8/500 floor and the fixed 300 us loop allowance; the older calculations below remain as test history. See radio profiles for current behavior.

Implemented in the keymindCascade working tree, 2026-09-14. The initial 4.1/4000-us implementation record below is retained as history; the 4.6/300-us policy update was a prior stage. No radios were flashed or RF experiments started for either function-only change.

"},{"location":"test-results/radio_chirp_math_validation/#initial-implementation-scope","title":"Initial implementation scope","text":""},{"location":"test-results/radio_chirp_math_validation/#evidence-and-limits","title":"Evidence and limits","text":"

The five-radio single-pass bench received 392/400 at 5.1 chirps. The longer-dwell repeats scored 342/348 before the user stopped 6.1, and 395/400 at 7.7. They do not establish any lossless four-channel dwell. They also do not justify treating one full sweep fitting inside the preamble as proof of acquisition.

The function retains the earlier production two-profile policy: two return opportunities on the slower channel, blind time plus sixteen acquisition symbols on the faster channel. Its 4.1 floor is the requested policy boundary, not an experimentally established detection minimum. This change requires RF qualification before describing it as a reliability improvement.

Single-pass V4 switching peaked at 804 us including post-reception cache refresh. Indicator reached 8428 us in the prior receive-only study. The pure function can model either allowance. Per the follow-up request, runtime scheduling now uses a nominal 600 us switch / 4000 us loop budget; advisory calculations raise the switch allowance if the observed maximum exceeds 600 us. The 0.6 ms nominal value does not cover all measured switches. They do not dynamically retune the production scheduler or silently lengthen already configured preambles.

See the formula and CLI examples.

"},{"location":"test-results/radio_chirp_math_validation/#verification","title":"Verification","text":""},{"location":"test-results/radio_chirp_math_validation/#automatic-46-chirp-300-us-policy-update","title":"Automatic 4.6-chirp / 300-us policy update","text":"

After the separate 4.6/0.3 ms hardware test, the user requested this policy in the shared production function. That sample was 197/200 (slow 97/100, fast 100/100), not evidence of lossless operation.

Verification covers 1,600 SF/BW pairs with exact role-dependent rounding and fast-dwell arithmetic, profile reversal, equal-symbol ties, larger explicit slow preambles, infeasible short slow preambles, single-profile off, long explicit visits, and advisory-only switch overruns. Production CLI checks assert the actual 4.60,42.67 chirp readout, loop=300us, warning values and unchanged explicit 32 on the tested SF8/500 pair. The frozen HIL plan now checks equality against the production 4.6/300-us allocation.

"},{"location":"test-results/radio_dwell_policy_validation/","title":"Radio dwell policy validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_dwell_policy_validation/#automatic-dwell-policy-46-chirps-on-both-profiles","title":"Automatic dwell policy: 4.6 chirps on both profiles","text":"

Historical validation record: the nominal 600 us switch allowance below has been replaced by the activation self-test and a 10% guarded measured budget. The current automatic policy also removed the fixed 300 us loop allowance and the pair-specific SF8/500 88-symbol floor. Values below are retained as bench history, not current automatic defaults. See radio profiles for current behavior.

The shared RadioProfiles function uses a 4.6-symbol minimum for both logical profiles of one physical transceiver, rounded upward to a whole microsecond. The slower profile is selected by symbol duration, with primary winning ties. The fast visit receives the remaining half-slow-preamble budget, subject to the same 4.6-symbol minimum. The nominal allowances are 600 us per switch and 300 us per cycle, not added delays.

The scheduler, automatic preambles, configuration acceptance and diagnostic calculator use the same minima. Preserve explicit preambles, safe-airtime validation, too-short slow-preamble rejection, recommendation rounding to multiples of eight (minimum 32), and the existing SF8/500 versus SF7/62.5 88-symbol recommendation floor. Measured switching overruns raise advisory warnings without rewriting configured values. Single-profile RX still does not perform a scan.

For SF7/62.5 plus SF8/500 with slow preamble 32, the allocation remains 9,421 us slow and 21,847 us fast, in either profile order. The fast visit already exceeds 4.6 fast symbols. Raising the fast minimum matters when less budget is available: equal SF7/500 profiles with slow preamble 30 no longer fit, because only 1,162 us would remain versus the new 1,178 us floor. Slow preamble 32 fits with a 1,418 us fast visit.

"},{"location":"test-results/radio_dwell_policy_validation/#hardware-context","title":"Hardware context","text":"

The preceding V4 RX / fixed RAK4631 and T1000-E TX bench used explicit 32-symbol preambles on both profiles, 100 stationary controls per profile and a complete 100/profile scan. Controls passed 100/100 each; scanning received slow 97/100 and fast 100/100 (197/200 total). Switching averaged 582 us and peaked at 827 us. Successful-packet mean SNR was +12.27/+13.47 dB. No RF retry or USB result replay was used. Radios were left idle.

That test used the same 9,421/21,847 us windows, so the higher fast floor does not change this particular allocation. It does not establish lossless performance or qualify other SF/BW combinations. Raw capture SHA-256: 1c2e0eae3f304fcaeb6addfeef4f2f09a3c2bec8d78c60d3f151c952d0f6d5f5. No additional RF run is required to apply the user-requested policy change.

"},{"location":"test-results/radio_dwell_policy_validation/#software-verification","title":"Software verification","text":"

The portable C++ test sweeps 1,600 SF/BW pairs and checks both 4.6-symbol minima, exact fast-window allocation, preamble/diagnostic consistency and accepted return deadlines. It also covers reversed/equal symbol times, explicit preambles, newly infeasible short settings, long requested visits, invalid parameters and advisory-only switching overruns. CLI tests exercise warnings, both timing aliases, saved/temporary configuration, persistence and bounded replies. Native integration covers routing, retries and packet ownership alongside the timing model.

Verification completed on 2026-09-14 (local date):

"},{"location":"test-results/radio_profile_switch_validation/","title":"Radio profile switch validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_profile_switch_validation/#sx1262-profile-switch-validation","title":"SX1262 profile-switch validation","text":"

Measured locally on 2026-09-13 using an XIAO ESP32-S3 with WIO SX1262. Later tests also use the SenseCAP Indicator ESP32-S3 LoRa processor. The V8 integration is the current implementation; V1-V7 sections/captures document the development history.

"},{"location":"test-results/radio_profile_switch_validation/#initial-result-v1","title":"Initial result (V1)","text":"

The production retune path averaged 1.434 ms with XOSC held, versus 3.166 ms with RC standby. Holding the oscillator saved 1.732 ms per retune (54.7%). Warm-mode maximum was 1.467 ms across 6,000 measured retunes. The 6 ms switch allowance and 4 ms loop allowance are unchanged.

Each row combines both directions and two reversed-order trials, with 2,000 measured retunes per standby policy. Times run from production tuneProfile entry until BUSY is observed low. GetStatus confirmed RX after every timed hop.

Secondary profile RC mean / maximum XOSC mean / maximum SF7 / 500 kHz 3.161 / 3.199 ms 1.434 / 1.467 ms SF8 / 500 kHz 3.166 / 3.209 ms 1.435 / 1.464 ms SF9 / 500 kHz 3.171 / 3.193 ms 1.433 / 1.437 ms "},{"location":"test-results/radio_profile_switch_validation/#scope-and-reproducibility","title":"Scope and reproducibility","text":"

The timing harness compiles the actual production wrapper, not the earlier fast/minimal RX benchmark. RC and XOSC use the same executable: the baseline changes only the standby policy after entering dual-profile mode. No TCXO supply or initial startup delay is disabled. The normal guard checks and frequency/SF/BW/CR/preamble setters remain active.

This isolates radio switching from UI, Wi-Fi, BLE, mesh dispatch, and scan-loop scheduling. BUSY polling/timer overhead is included; the RX status query is outside the timing. This is not a packet-delivery test, first-startup timing, power measurement, wide-frequency image-recalibration test, or proof of analog RF settling beyond BUSY/RX status. Indicator I/O-expander latency and nRF52 timing still need their own measurements before reducing common budgets.

Raw results: profile_switch_results.json. The collector requires all requested samples, zero failure/timeout/mode-error counts, and successful RC restoration before accepting a trial.

Build provenance:

Host regression checks also passed: profile scan/CLI (3 tests), receive contract (2 tests), and SX126x RX-status decoding (1 test). A normal heltec_v4_companion_radio_usb build and the XIAO timing build succeeded.

The XIAO's original 8 MiB flash snapshot was verified against the device while held in the bootloader before the timing firmware was uploaded. Raw flash backups stay in the current user's private backup directory, outside Git. After testing, the entire original snapshot was restored and verified again against the device before reboot. The live USB CLI then confirmed Xiao S3 WIO running v1.17.1-soak-xiao-B (11-Sep-2026). Its soak logger was left stopped; existing logs were preserved. The Indicator and RAK Tag were not flashed.

"},{"location":"test-results/radio_profile_switch_validation/#follow-up-batched-modulation","title":"Follow-up: batched modulation","text":"

On the same date, a second 12,000-retune experiment held XOSC warm in both cases and compared the old three-setter sequence with one combined LoRa modulation update. Both paths ran in the same V2 executable, with the second round reversing their order. Each mode completed 6,000 timed hops.

Secondary profile Separate setters: mean / maximum Batched: mean / maximum SF7 / 500 kHz 1.442 / 1.473 ms 1.159 / 1.198 ms SF8 / 500 kHz 1.444 / 1.480 ms 1.158 / 1.192 ms SF9 / 500 kHz 1.446 / 1.478 ms 1.156 / 1.163 ms

Overall: 1.444 ms to 1.158 ms, saving 0.286 ms (19.8%) per retune. Use this matched V2 baseline for the batching comparison; the earlier V1 warm-standby numbers came from a different executable layout.

There were zero retune failures, BUSY timeouts, RX-mode errors, or SF/BW/CR cache mismatches. All 12 trials restored RC standby after disabling profile 2. 84 packet/busy deferrals were excluded (54 separate-setter, 30 batched). First eight successful hops per trial were excluded as before.

The batching removes two repeated modem checks and two modulation writes. The helper validates a complete MeshCore bandwidth/SF/CR tuple, updates RadioLib's cached SF/BW/CR before its automatic LDRO calculation, and restores the old cache if the command fails. The wrapper's existing physical rollback and safe-transition paths are retained. Frequency changes, preamble updates, standby, IRQ/buffer setup, packet parameters, and RX restart remain unchanged. This does not qualify eliminating those other repeated operations.

Raw results: profile_switch_batch_results.json. V2 test firmware SHA-256: b4403f6f5eb0a2b0a31c2d79859122225f247296699a578ad2596ef17eaed0fd. Toolchain and RadioLib pin match the first experiment. The normal heltec_v4_companion_radio_usb build also succeeded with batching enabled.

The two new host tests execute the production method over 1,200 valid bandwidth/SF/CR/LDRO combinations, invalid inputs and wrong modem, command failures with cache rollback/retry, and the wrapper's failure short-circuit. All eight relevant host tests passed. RF packet delivery and application/UI scheduling remain outside this timing experiment; shared budgets are unchanged.

"},{"location":"test-results/radio_profile_switch_validation/#expanded-screening-redundant-rx-setup-and-spi-v5","title":"Expanded screening: redundant RX setup and SPI (V5)","text":"

Corrected V5 measurements on 2026-09-13 cover 28,800 timed retunes in 300 trials across XIAO S3/WIO SX1262 and the Indicator ESP32/SX1262. Each sweep uses SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, both directions, reversed case order in round two, and eight excluded initial hops per trial. All 300 trials completed their sample count and restored RC standby on exit. There were zero timed switch failures, BUSY timeouts, RX-mode errors or SF/BW/CR cache mismatches. The normal packet guards deferred 175 attempted hops; these were not counted as timed retunes.

Twenty-one distinct cases were screened: seven individual redundant-operation changes, a full manual RX control, combinations, 2/4/8 MHz SPI and whole-buffer ESP32 SPI transfers. Shared controls appear in both groups. All new shortcuts are HIL-only, not included in normal targets.

"},{"location":"test-results/radio_profile_switch_validation/#individual-and-combined-rx-changes","title":"Individual and combined RX changes","text":"

Means in milliseconds, with warm oscillator and batched modulation throughout. Each XIAO row has 768 timed hops; each Indicator row has 384.

RX/SPI case XIAO mean Indicator mean Production batching, 2 MHz SPI 1.182 15.190 Manual RX sequence, no omissions 1.181 15.135 Skip second standby 1.124 14.195 Skip repeated IRQ setup 1.096 14.399 Skip repeated buffer-base setup 1.146 14.437 Skip repeated packet-parameter setup 0.991 12.364 Skip RX modem query 1.145 14.427 Defer preamble to the one RX packet-parameter write 0.956 11.659 Skip wake NOP when already awake 1.146 14.576 Combined omissions, early preamble write 0.771 9.096 Combined omissions, deferred preamble 0.732 8.384 4 MHz SPI only 0.985 14.996 8 MHz SPI only 0.889 14.889 Combined/deferred, 4 MHz SPI 0.627 8.280 Combined/deferred, 8 MHz SPI 0.579 8.224

The largest individual saving is programming preamble/packet parameters once. Compare individual omissions with the manual full control when attributing their isolated costs: replacing RadioLib's staging dispatch also has overhead. Individual savings are not additive; some remove overlapping work.

"},{"location":"test-results/radio_profile_switch_validation/#whole-buffer-spi-follow-up-same-v5-binaries","title":"Whole-buffer SPI follow-up (same V5 binaries)","text":"Case XIAO mean / maximum Indicator mean / maximum Production batching, byte loop, 2 MHz 1.184 / 1.221 ms 15.185 / 15.785 ms Bulk transfer only, 2 MHz 1.070 / 1.073 ms 15.092 / 15.134 ms Bulk transfer only, 4 MHz 0.890 / 0.903 ms 14.924 / 14.983 ms Bulk transfer only, 8 MHz 0.797 / 0.806 ms 14.836 / 14.908 ms Combined/deferred, byte loop, 8 MHz 0.579 / 0.588 ms 8.222 / 8.843 ms Combined/deferred + bulk, 2 MHz 0.671 / 0.700 ms 8.369 / 9.027 ms Combined/deferred + bulk, 4 MHz 0.582 / 0.595 ms 8.252 / 8.315 ms Combined/deferred + bulk, 8 MHz 0.535 / 0.545 ms 8.203 / 8.233 ms

The fastest XIAO candidate saves 0.649 ms (54.8%) versus that sweep's already-warm, already-batched production control. At 8 MHz, bulk transfers alone add about 0.043 ms of benefit to the combined RX shortcut. The Indicator remains dominated by I2C-mediated chip-select/BUSY/DIO access; the final bulk step improves its combined mean by only 0.019 ms. Small HAL effects varied between executable layouts in preliminary screens; do not generalize them.

The Indicator exceeds the generic 6 ms switch allowance even in its fastest experimental path. These are board measurements, not a justification for reducing the shared 6 ms switch / 4 ms loop constants. Indicator scheduling and effective preambles need board-specific treatment before a production rollout. Application/display/network scheduling is still excluded here.

Raw accepted sweeps:

"},{"location":"test-results/radio_profile_switch_validation/#test-corrections-and-provenance","title":"Test corrections and provenance","text":"

V3/V4 broad screening accidentally called SX1262::startReceive() instead of CustomSX1262::startReceive() in the fallback, omitting MeshCore's extra preamble-detection IRQ flag. Their raw files are marked superseded and are not included in the counts or conclusions above. The V4 packet sweep was stopped on discovery. V5 preserves that flag in both normal and manual RX paths; a host regression test checks the Custom fallback and mapped flags. The earlier V1/V2 warm/batching experiments did not use that experimental subclass and are unaffected.

An initial V5 XIAO capture stopped on a partial serial JSON line during guarded mode exit. It is retained as incomplete, not counted above. The host now accumulates reads until newline, with regression coverage. Initial setup rejections are retried only for the explicit pre-measurement packet-guard reply, bounded to three seconds and counted; packet losses and timed failures are never retried away.

Final V5 firmware SHA-256:

Base commit/toolchain/RadioLib pin match the earlier experiments. Thirteen unique host tests passed (eight production regressions, two HIL boundary/buffer tests, three serial-reader/setup-guard tests). The HIL tests cover all 255 nonzero omission masks, normal-path fallbacks, preserved IRQ clear/SetRx, error short-circuit and full-duplex buffer lengths 1..260. They are not full driver lifecycle tests. An additional normal t1000e_companion_radio_usb nRF52 build passed with the production warm/batching changes: 591,484-byte app, runtime RAM guard passed. It was not flashed to the RAK or any nRF52 board.

"},{"location":"test-results/radio_profile_switch_validation/#qualification-boundary","title":"Qualification boundary","text":"

No BUSY wait, command status validation, stale-IRQ clear, RX command or wrapper packet-ownership guard is removed. The fastest path relies on a known RX-to-RX window and established RX setup. Correct invalidation after TX, CAD, sleep, reset, failed commands, and recovery must be designed and tested before promoting these shortcuts. The per-byte/bulk HAL experiment is ESP32-specific. Sensitivity/PER, oscillator power draw, large-frequency image recalibration, and application-driven continuous scanning remain unqualified.

"},{"location":"test-results/radio_profile_switch_validation/#actual-packet-checks-and-limitations","title":"Actual packet checks and limitations","text":"

Final forward matrix: Indicator transmitter to XIAO receiver, -9 dBm, 21 cases x SF7/8/9 x both profiles x 16/64/255-byte payloads = 378 probes. 359 sequence/length/payload/profile checks succeeded, 13 receiver-declared wait timeouts occurred, and six receiver USB replies did not arrive before the host deadline. Transmitter replies returned success. Successfully read packets had RSSI -42 to -39 dBm and SNR 10.2 to 14.2 dB. Timeout diagnostics reported RX mode and no device errors; some retained a preamble IRQ. These observations do not establish why packets were missed.

The production-batched baseline delivered 17/18 in that matrix; the fastest combined/bulk/8 MHz candidate delivered 18/18. An earlier reversed-order smoke test of the fastest candidate had one receiver timeout, so the clean 18-packet batch is not a reliability qualification. Earlier 0 dBm checks also had baseline and candidate misses. Reducing power did not eliminate them.

Reverse direction: XIAO transmitter to Indicator receiver, -9 dBm, baseline, combined/8 MHz and combined/bulk/8 MHz, with the same SF/profile/length matrix. All 54 payloads arrived intact, 18/18 per case. However, one XIAO USB TX acknowledgement was delayed past the host deadline, after which 32 later records picked up the previous command's acknowledgement. Thus the raw file has only 21 fully matched transactions despite 54 correctly received payloads. This is sequence-correlated evidence of a host acknowledgement problem, not 33 RF losses. Subsequent collector code rejects/stores stale sequence replies instead of associating them with a new command, and now asserts DTR on Windows to match the existing s3_memory_soak.py HWCDC session policy. The captures above used DTR deasserted; a causal DTR A/B test was not performed. Sequence handling has host regression coverage, but these last host changes were not rerun on HIL firmware after the boards were restored.

USB acknowledgements and actual receiver payload validity are recorded separately. Missing acknowledgements never trigger an automatic retransmit. The host drains the receiver concurrently with the transmitter's command and records timeouts without hiding them. These tests establish some real packet functionality, not a controlled packet-error-rate comparison. Fix/qualify the test transport, repeat with a stable RF reference, then test TX/CAD/reset/RXPS and continuous application scanning before promoting the shortcuts.

Earlier incomplete packet captures and superseded screens are retained for audit, not counted in the 378/54 final matrices. No identity or channel secrets were transmitted: probes used synthetic sequence-tagged bytes.

"},{"location":"test-results/radio_profile_switch_validation/#production-integration-and-usb-recovery-v8","title":"Production integration and USB recovery (V8)","text":"

Normal XIAO S3 WIO and Indicator LoRa variants now opt in to buffered 8 MHz SPI and a guarded fast RX-to-RX path. Other SX1262 variants retain default SPI/full RX setup; modulation batching and warm standby remain shared.

The fast path keeps the TCXO warm, applies one SF/BW/CR tuple and one packet tuple, clears stale IRQs and starts RX with the original BUSY handling. It reuses an established LoRa IRQ map and buffer bases only within an owned continuous-RX retune. Ordinary standby, staging/TX, CAD, sleep, reset/init, RX duty cycling and command failures invalidate that reuse. RX startup failure now fails the whole profile transaction and restores the old profile/caches with full setup; failed recovery keeps the profile marked for refresh.

"},{"location":"test-results/radio_profile_switch_validation/#sleep-recovery-discovered-during-integration","title":"Sleep recovery discovered during integration","text":"

Initial lifecycle captures found XOSC_START_ERR (0x20) after retained sleep, despite successful RX status. RC-policy controls reproduced the same flag: this was not established as a regression caused by the fast path. Simply waking in RC and entering standby before sleep did not clear it. Reapplying the already configured TCXO voltage and 1,600 us delay resolved it.

The production sleep override now enters standby first, wakes in RC, and restores TCXO configuration on wake. It refuses to clear unrelated device faults. The warm policy resumes after full RX setup. This follows the standby entry requirement in section 13.1.1 of the Semtech SX1261/2 datasheet. Section 13.3.6 describes TCXO control and the XOSC error flag; it does not by itself prove the cause of this retained-sleep observation.

Diagnostic files profile_switch_v6_recovery_xiao.json, profile_switch_v7_recovery_xiao.json, profile_switch_v7_recovery_xiao_complete.json (despite its filename, its complete field is false), and profile_switch_v7_sleep_diagnostic_xiao.json retain these failures. They are not counted as passing lifecycle tests.

"},{"location":"test-results/radio_profile_switch_validation/#usb-fix-and-scope","title":"USB fix and scope","text":"

The HIL host distinguishes Espressif native CDC (DTR asserted) from CH340 bridges (DTR/RTS deasserted). Replies are newline-assembled and sequence-matched. Firmware caches completed packet and timing reports before emitting and flushing them. A timeout or malformed report triggers a sequence-scoped result query, never a repeated TX or timing run. Randomized session sequence starts avoid a cached result from an earlier host session.

V7 had successfully fixed/retested packet result replay but a long native-USB timing sweep still lost one response. V8 adds whole-report timing buffering and replay. The incomplete V7 XIAO timing capture is retained, not silently completed. The public MeshCore Companion USB protocol is unchanged; these USB fixes are for the radio-lab transport. The previous broad Windows-DTR policy described in the V5 history above has been replaced by native-device identification.

Both boards passed 20 reopen cycles, deliberate lost timing replies, deliberate lost TX replies, stale TX reply isolation, and 18 lifecycle cases each. The cases cover TX, CAD, sleep, reset/init, RC-policy sleep and explicit TCXO-restoration controls, repeated three times. Fast counters prove one full RX fallback after invalidation and reuse only on the following retune. All command results, RX-mode checks and device-error checks passed.

V8 test firmware SHA-256 (same pinned RadioLib/toolchain as above):

Normal USB Companion builds passed for Xiao_S3_WIO_companion_radio_usb (813,901 app bytes / 63,012 static RAM), SenseCapIndicator-LoRa_comp_radio_usb (879,809 / 62,960), and t1000e_companion_radio_usb (591,980 / 179,868). Runtime RAM budgets passed on all three. These normal application images were compiled, not substituted for the user's configured soak firmware during the HIL tests.

Host/native regression coverage includes production modulation caches/LDRO, standby failure short-circuit, RX-start rollback failure, ownership guards, fast-state invalidation, TCXO wake fault preservation, buffered SPI semantics, USB result overflow/replay, partial reads and stale-sequence handling.

"},{"location":"test-results/radio_profile_switch_validation/#final-steady-state-timing","title":"Final steady-state timing","text":"

V8 ran 256 timed retunes per trial, four production configurations, SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, and two reversed-order rounds on each board: 12,288 measured retunes / 48 trials. The first eight hops per trial are excluded. There were zero retune failures, BUSY timeouts, RX-mode errors or modulation-cache errors. All 48 trials restored RC policy on exit. The normal packet guards deferred 21 XIAO and 59 Indicator hops; no setup retries or naturally missing/replayed USB reports occurred in these final sweeps.

Production configuration XIAO mean / maximum Indicator mean / maximum Warm + batching, per-byte SPI 2 MHz 1.193 / 1.227 ms 15.174 / 15.836 ms Fast RX, per-byte SPI 2 MHz 0.744 / 0.770 ms 8.426 / 8.471 ms Full RX, buffered SPI 8 MHz 0.828 / 0.840 ms 14.816 / 15.386 ms Fast RX + buffered SPI 8 MHz 0.549 / 0.556 ms 8.263 / 8.879 ms

The combined production path saves 54.0% on XIAO and 45.5% on Indicator relative to each same-image warm/batched control. Small differences from V5-V7 reflect different binaries/code layout; only within-image A/Bs are compared. Both fast configurations must increment the production success counter; full RX controls must not. Mode/cache verification happens after the timed region.

The common 6 ms switch and 4 ms loop allowances are unchanged, not delays inserted by the driver. Indicator still exceeds 6 ms. These measurements do not qualify continuous application scheduling, scan-overlap reception, sensitivity/PER, power use, temperature extremes, or image recalibration on frequency changes larger than this 1 MHz pair. Do not shorten common transmit preambles or advertise the XIAO number for other boards.

"},{"location":"test-results/radio_profile_switch_validation/#final-over-air-packet-matrix","title":"Final over-air packet matrix","text":"

288/288 payloads and command transactions passed: 144 Indicator-to-XIAO and 144 XIAO-to-Indicator. Each direction covers all four production cases, SF7/8/9 secondary profiles, both target profiles, 16/64/255-byte synthetic payloads and two reversed-order rounds. TX used buffered 8 MHz SPI at -9 dBm. Fast receiver readiness reports had to show actual production fast resumes.

There were zero RX errors, receiver timeouts, missing replies, stale replies, or natural transport-recovery queries in these matrices. Deliberately lost and stale replies were separately exercised by the recovery tests above. RSSI ranged -43 to -39 dBm and SNR 10.2 to 14.5 dB. This clean short-range matrix addresses the failures seen in the earlier V5 harness; it does not identify which single change caused each historical miss or establish a field packet-error rate.

All 18 host/native regression tests passed. The HIL builds do not include the unrelated pre-existing Companion/repeater/Wi-Fi soak edits; the normal application compile checks include the current working tree and preserve them.

A subsequent requested SF6/125 kHz multi-channel test used 32-symbol preambles and 4.8-symbol visits. It stopped on the third packet at the initial four channels (two received, one timeout); no higher count was tried. That continuous scan experiment is separate from the 288/288 stationary-after-retune matrix above.

"},{"location":"test-results/radio_profile_switch_validation/#unchanged-modulation-tuple-follow-up","title":"Unchanged modulation tuple follow-up","text":"

The initial software-only checks below were followed by same-image hardware timing and separated-radio tests. Frequency-only means improved from 548.880 to 451.897 us on XIAO and from 8,263.438 to 7,601.193 us on Indicator. Four-channel SF6 reception was not loss-free. The following paragraph records the initial implementation stage, not the current hardware-test status.

Implemented 2026-09-14 after commit 0e5955f8, without restarting hardware tests. On opted-in fast RX-to-RX hops, setLoRaModulationParams() now compares the requested SF, encoded bandwidth, encoded CR and effective LDRO against the last successfully acknowledged modulation write. An exact match within a valid owned standby window omits only SetModulationParams (0x8B). Frequency programming, the modem query, preamble/packet parameters, IRQ clears, RF-switch control and BUSY waits remain unchanged.

The acknowledgement is separate from RadioLib's mutable software fields. Auto/manual LDRO compares the resulting command byte, with the pinned RadioLib >=16 ms symbol rule. A failed write revokes the whole fast context before rollback. Ordinary SF/BW/CR/forced-LDRO setters invalidate the saved tuple even on failure. Existing full-RX staging, ordinary standby, TX/CAD staging, reset/init, sleep, duty cycling and failed RX/standby operations also invalidate it. The first hop after lost context therefore writes again; frequency-only hops that retain the context can skip subsequent writes.

All 30 selected host/native tests passed, including repeat-write counts for all supported bandwidth/SF/CR combinations and auto/forced LDRO modes, each independently changed field, LDRO policy transitions, failed-write retry, modem-query failure, ownership gates, ordinary setters and lifecycle invalidation. Both profile_switch_xiao and profile_switch_indicator compiled successfully against the pinned RadioLib. No firmware was uploaded, no radio command was issued, and no new RF result was collected. The temporary build overlay was removed. The earlier ~0.549 ms XIAO and ~8.263 ms Indicator timings do not measure this added optimization; its time saving and RF behavior await testing.

"},{"location":"test-results/radio_profile_switch_validation/#mercermesh-heltec-t096-sx1262-experiment-2026-09-21","title":"MercerMesh Heltec T096 SX1262 experiment (2026-09-21)","text":"

The Pi has one Heltec T096 (USB serial 651F8E496197F882); the other Heltec-named nRF52 is a MeshTower V2, not another T096. An application-only T096 HIL image tested two further RX-to-RX shortcuts with a buffered 8 MHz SPI bus: use SX1262 SetFs instead of warm XOSC standby, and skip SetPacketParams only when the LoRa RX packet tuple is unchanged. Both switches defaulted off in this HIL image. The image explicitly enabled the existing production fast-RX path. Normal T096 firmware does not currently enable that path or use the buffered SPI HAL, so these numbers are not stock T096 switch timings.

The same image ran 256 measured hops per configuration, after eight warm-up hops, on 909.5/909.75 MHz. freq held SF7/BW125/CR5; both alternated SF7/SF8 at BW125/CR5. Preamble was 32 symbols. The DWT cycle counter measured through RX re-entry and BUSY release. Every 8 MHz configuration had zero switch failures, mode/cache/RSSI errors, or BUSY timeouts.

8 MHz HIL configuration Frequency-only mean Frequency + SF mean Fast RX baseline 467.3 us 553.6 us SetFs only 422.5 us 517.4 us Unchanged packet tuple skip only 357.7 us 444.0 us Both 313.0 us 407.8 us

Combined savings were 154.3 us (33.0%) for frequency-only and 145.8 us (26.3%) for frequency-plus-SF relative to the same-image fast-RX baseline. Command counts confirmed one frequency command, IRQ clear and RX command per hop; the optimized cases eliminated the corresponding standby and packet commands. These are controlled short-range, same-band results, not a field packet-error rate or a qualification of large frequency jumps, sleep/wake, TX/CAD interruption, BLE scheduling, or all SX1262 boards.

A separate RAK4631 fixed transmitter sent 16-byte synthetic packets at 909.5 MHz/SF10/BW125/CR5/preamble32, -9 dBm chip power. The T096 hopped to 909.75 MHz and back before each transmission. Baseline and combined shortcuts each delivered 60/60 packets after 120 fast hops, with payload sequence and content checked. The RAK bootloader on this exact fixture enumerated as 239A:002A (the fixture's earlier 0029 expectation was corrected).

Doubling buffered SPI to 16 MHz gave only 12\u201321 us (about 3\u20134%) savings in comparable successful cases, while five of eight setup attempts failed. The combined shortcut had no clean 16 MHz result. Keep this T096 at 8 MHz pending a separately diagnosed and repeatable 16 MHz bring-up; no 16 MHz over-air qualification was attempted.

Both radios were restored via app-only DFU after the experiment: the T096 to Heltec_t096_repeater and the RAK to RAK_4631_companion_radio_full, each built at v1.17.1.7-hil-restore-e7160dfd. The T096 answered normal version and board commands; the RAK answered the Full Companion terminal's version command. The V4 observer's memory-soak service remained active. Reproducible bench code is in tools/hil/profile_switch_t096_sx1262.cpp and tools/hil/t096_sx1262_packet_probe.py.

Follow-up: owned fast RX, SetFs, and acknowledged packet-tuple reuse are now defaults for CustomSX1262 targets, including the original XIAO S3 WIO, Station G3 KISS, and Heltec V4 KISS paths. The Seeed Indicator retains its original owned-RX path but explicitly opts out of the two new shortcuts because its radio control and BUSY/IRQ pins run through an I2C expander. The owned-RX state now remembers only an acknowledged packet tuple and revokes it on ordinary packet setters, TX/CAD staging, standby outside the hop, sleep, reset, RX duty cycling, or command failure. The T096 numbers remain lab measurements, not on-board qualification of every CustomSX1262 target.

"},{"location":"test-results/radio_profiles_validation/","title":"Radio profiles validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_profiles_validation/#dual-profile-receive-validation","title":"Dual-profile receive validation","text":"

These measurements exercise the production two-profile implementation on one SX1262 transceiver, using a Heltec V4 repeater as receiver and a XIAO ESP32-S3 with a Wio LoRa module as transmitter. They follow the dedicated mixed-bandwidth scan experiment.

Counts, diagnostic replies and firmware/source hashes are in radio_profiles_results.json.

The primary profile is 909.5 MHz, SF7, 62.5 kHz, CR 4/5, preamble 32. The secondary is 910.5 MHz, 500 kHz, CR 4/5, with SF9, SF8 or SF7. Scanning uses normal RX, starts with the slower profile, and reserves two slow visits per slow preamble. Transmitters must use the corresponding preamble too.

"},{"location":"test-results/radio_profiles_validation/#method-and-limits","title":"Method and limits","text":"

The transmitter sends MeshCore adverts at varied intervals. The receiver's per-profile MeshCore RX counters are compared with the transmitter's completed TX counters before and after each run. Initial OTA announcements can add packets beyond the manually requested adverts; the table includes those transmissions. These are aggregate counters, not sequence-matched payload captures.

Runs use one transmitting profile at a time and a strong signal. They do not measure simultaneous traffic, weak-signal reception, interference or months of uptime. A single transceiver remains unavailable to the other profile while receiving or transmitting a packet. A clean finite run is evidence for a timing choice, not a guarantee that it cannot drop packets.

"},{"location":"test-results/radio_profiles_validation/#five-symbol-baseline","title":"Five-symbol baseline","text":"

Slow visits were 10.240 ms and fast visits 6.528 ms, plus actual switching time. The longest observed profile switch was 4.416 ms; no switch failures were reported.

Fast profile Fast preamble Fast RX / TX Slow RX / TX SF9 / 500 48 54 / 54 30 / 30 SF8 / 500 72 49 / 50 \u2014 SF8 / 500 80 49 / 50 \u2014 SF8 / 500 88 50 / 50 30 / 30 SF7 / 500 120 50 / 50 30 / 30

The SF8 automatic preamble retains a measured floor of 88 symbols. Automatically selected preambles always round up in steps of eight.

"},{"location":"test-results/radio_profiles_validation/#fractional-symbol-tuning","title":"Fractional-symbol tuning","text":"

The initial sweep started at 4.2 slow symbols, with an increase of 0.1 planned for any slow-channel loss. That first sweep passed all three fast settings. Each completed run used 50 requested adverts per profile. The SF8 fast run was interrupted to check SF9 preamble 32, then restarted; only its completed run is included below.

Slow symbols Fast profile Fast preamble Fast RX / TX Slow RX / TX 4.2 SF9 / 500 48 50 / 50 50 / 50 4.2 SF9 / 500 32 50 / 50 50 / 50 4.2 SF8 / 500 88 50 / 50 50 / 50 4.2 SF7 / 500 120 50 / 50 50 / 50

The four completed pairings received 200/200 packets on the slower channel and 200/200 on the faster channel. No switch failures were reported; the longest measured switch during the 4.2-symbol sweep was 4.407 ms.

The explicit SF9 preamble-32 check uses the same 8.602 ms slow and 8.166 ms fast windows as the preamble-48 check. Only the fast transmitter's preamble changes. The automatic SF9 calculation still reserves a 48-symbol preamble for the configured switching and loop-jitter budgets.

"},{"location":"test-results/radio_profiles_validation/#shorter-fast-preambles","title":"Shorter fast preambles","text":"

The follow-up starts with preamble 48 on both fast settings. Each miss advances the next candidate by eight symbols. The receive-window budgets stay at 4.2 slow symbols and the same remaining fast-channel time.

Fast profile Preamble Fast RX / TX SF8 / 500 48 49 / 50 SF8 / 500 56 50 / 50 SF7 / 500 48 42 / 50 SF7 / 500 56 41 / 50 SF7 / 500 64 45 / 50 SF7 / 500 72 50 / 50

The slower channel passed 50/50 paired with SF8/56, but then received 49/50 paired with SF7/72. Across the completed 4.2-symbol slow-channel runs, that is 299/300. This later miss invalidated the provisional 4.2-symbol choice and triggered the next headroom test at 4.3 symbols. Fast-preamble baselines carry forward as 32 for SF9, 56 for SF8 and 72 for SF7, advancing by eight on a miss.

At 4.3 symbols, the SF9/32 pairing received 100/100 slow packets and 50/50 fast packets. The next SF8/56 pairing received only 42/50 slow packets, so that candidate also failed. A control run with scanning disabled received 50/50 on the slow channel. At 4.4, SF8/56 passed 100/100 slow and 50/50 fast, but the SF7/72 pairing then received 49/50 slow packets.

At 4.5, the SF7 pairing passed 100/100 slow packets. Fast preamble 72 received 47/50, so the next baseline increased to 80 and passed 50/50. The following SF8/56 slow run received 49/50, triggering 4.6 symbols. Baselines now carry forward as SF9/32, SF8/56 and SF7/80.

At 4.6, both SF8/56 and SF7/80 passed 100/100 slow packets and 50/50 fast packets. The following SF9/32 slow run received 49/50, triggering 4.7 symbols. There were no switch failures; the longest observed switch was 4.414 ms.

At 4.7, the SF9/32 slow run passed its first 50 packets but received 99/100 over the full run. This triggered 4.8 symbols, with the same fast preamble baselines. No fast-channel run was started for the 4.7 candidate.

"},{"location":"test-results/radio_profiles_validation/#selected-bench-baseline-48-slow-symbols","title":"Selected bench baseline: 4.8 slow symbols","text":"

The 4.8-symbol sweep passed all three slow-channel checks, totaling 300/300. Slow visits are 9.831 ms and fast visits 6.937 ms, plus actual switching time. The longest measured switch was 4.491 ms, with no switch failures.

Fast profile Fast preamble Fast RX / TX Slow RX / TX SF9 / 500 32 50 / 50 100 / 100 SF8 / 500 56 49 / 50 100 / 100 SF8 / 500 64 50 / 50 \u2014 SF7 / 500 80 50 / 50 100 / 100

The SF8 miss advanced its baseline from 56 to 64, exactly eight symbols. Its slow-channel check preceded that increase; changing the fast preamble leaves the idle receive-window calculation unchanged. The final fast-channel attempts received 150/150, using explicit preambles 32 / 64 / 80 for SF9 / SF8 / SF7. These become the starting values for further tests, increasing by eight on a fast-channel miss. A slow-channel miss instead advances the listen window by 0.1 symbol.

The production automatic preambles remain 48 / 88 / 120, retaining the switching, loop-jitter and acquisition budgets. The shorter explicit values are bench results under the conditions above, not a guarantee for other boards or traffic patterns.

"},{"location":"test-results/radio_profiles_validation/#transmit-policy","title":"Transmit policy","text":"

Hardware checks send three zero-hop adverts from the V4 with the XIAO listening on profile 2. OTA startup traffic is allowed to settle before taking the counter snapshots. The completed cases are:

V4 profiles and policy TX on primary / secondary Received by XIAO Permanent / permanent, auto 3 / 3 3 Permanent / permanent, off 3 / 0 0 Permanent / temporary, auto 3 / 0 0 Permanent / temporary, on 3 / 3 3 Secondary RX only, on 3 / 0 0 Temporary / temporary, auto 3 / 3 3

A separate isolation check issued three explicit OTA announce commands with normal radio, RX-only tempradio2 and radio2.cross auto. The completed TX counters stayed unchanged on both profiles (0 / 0), confirming that locally generated OTA traffic does not fall back to the normal channel in that mode.

Turning radio2 off stopped scanning. A one-minute tempradio2 session also returned to one profile on expiry. Rebooting with a saved SF8 RX profile and an active temporary SF9 RX/TX profile cleared the temporary session and restored the saved SF8 profile, including its automatic 88-symbol preamble.

"},{"location":"test-results/radio_profiles_validation/#ota-discovery-while-retaining-the-main-channel","title":"OTA discovery while retaining the main channel","text":"

Both boards ran normal SF7 / 62.5 kHz on the primary and temporary SF8 / 500 kHz RX/TX on the secondary, using radio2.cross auto and automatic preambles 32/88. The XIAO discovered the V4's firmware catalogue over profile 2, and both boards recorded received and transmitted OTA packets there. Ten V4 zero-hop adverts then arrived on the XIAO's primary profile (10/10), with both temporary second profiles still active. The XIAO reported no switch failures and a longest switch of 4.547 ms during this check.

This hardware check covers discovery, bidirectional OTA traffic and concurrent availability of the main channel between packets. It does not perform a complete firmware transfer or install. Byte-exact transfer validation is provided by the shared OTA software tests described below.

"},{"location":"test-results/radio_profiles_validation/#software-coverage","title":"Software coverage","text":"

Native tests cover identical-packet fanout, separate retry ownership and coding rates, channel-busy backoff, generation changes, temporary-session isolation, deferred replies and stale-packet removal. Tests using the production CLI cover parsing, persistence failures, schedules, expiry and clock rollover. A physical radio mock exercises the production scan transitions, pending RX interrupts, RX power-saving restoration and failed profile switches.

The full native suite passed 1,422 cases after the RX-only OTA isolation fix; all eight KISS modem tests also passed. Subsequent timing changes through 4.8 symbols passed the 63-case retry/profile suite and production CLI and scan harnesses. Shared OTA transfer tests passed byte-exact checks with AddressSanitizer and UndefinedBehaviorSanitizer for ESP32 and nRF52 variants.

Build coverage includes the Heltec V4 repeater, room server, sensor and terminal chat, the Full XIAO Companion, and the nRF52 T1000-E repeater. The nRF52 result is a build check; these receive measurements use the ESP32/SX1262 boards above.

"},{"location":"test-results/radio_profiles_validation/#hardware-restored-after-validation","title":"Hardware restored after validation","text":"

Both boards were returned to their original B soak images after the checks: v1.17.1-soak-v4-B and v1.17.1-soak-xiao-B. The V4 retained 909.5 MHz, SF7 / 62.5 kHz, CR 4/5 and TX 0; the XIAO retained 910.525 MHz with the same modulation and TX 22. Both reconnected to Wi-Fi and their custom MQTT service. The soak logs resumed under B-post-radio2-soak. These restored images are the earlier memory-soak firmware; the new dual-profile code is validated by the finite tests above, not by a months-long uptime run. The Pi's soak service and Indicator telemetry were also healthy after restoration.

"},{"location":"test-results/radio_receive_calibration/","title":"Radio receive calibration","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_receive_calibration/#radio-receive-calibration-and-recovery","title":"Radio receive calibration and recovery","text":"

Noise calibration collects 64 idle RSSI samples at least 50 ms apart. A quiet block takes about 3.2 seconds. Both continuous RX and temporary RXPS calibration windows allow up to 30 seconds; a partial block times out without publishing. Periodic requests coalesce while a block is active. RXPS resumes after a complete block, including a held block, or at timeout. Receive and transmit activity are never interrupted just to finish calibration.

If a reception delays the return to RXPS, the pending restoration is retried after every Dispatcher receive poll, including empty polls. A false preamble that expires without another packet interrupt therefore cannot leave the radio in continuous RX until the next calibration or AGC reset. An unread packet or active reception still owns the radio; TX completion retains its normal rearm. The retry wake query reports this pending restoration as immediate work, keeping event-driven sleepers awake until the existing preamble/header timeout or RX completion releases the owner. The query does not issue SPI commands.

A calibration request waiting for successful RX does not keep the MCU awake. Repeated failed receive starts retry after 20, 40, 80, 160, 320, 640, and 1280 ms, then at most once every 2000 ms. Only the repeated idle fallback is paced: packet/TX completion and explicit retune/CAD restoration still rearm promptly. Calibration and recovery requests retain the failed-start backoff; successful RX or a new radio session clears it. A retry blocked by BUSY or an RX owner waits 2000 ms without issuing another RX command. The Dispatcher includes this retry deadline in its wake query, and deferred adaptive AGC joins a later RX retry instead of forcing 50 ms wakeups throughout a persistent fault.

In single-profile RXPS, routine refreshes wait at least 60 seconds after the last completed or timed-out block before requesting another continuous-RX window. Both periodic maintenance and passive retry checks use this cooldown; retries do not restart a partially collected block. With RXPS off, or when the selected tuple requires continuous-RX fallback, the existing two-second maintenance tick and passive-probe age check are unchanged. Startup, explicit gain/tuning changes, AGC resets, and radio recovery still request a fresh block promptly. Dual-profile scanning retains its separate refresh policy.

Frequent configured AGC resets can still dominate calibration time: Cascade repeater builds start with an eight-second AGC interval, and MQTT observers use 28 seconds. With device power saving on, their adaptive AGC schedule backs off separately, up to 60 seconds; see AGC reset settings. AGC resets still bypass the routine noise-floor cooldown. After a timed-out startup block with no valid floor, the next routine RXPS attempt also waits 60 seconds; passive retries retain their existing bounded busy escape while waiting for a valid baseline.

The longer RXPS cooldown trades a slower response to ambient noise changes for more time in duty-cycle RX. It does not reduce sample count, shorten settling, or put the radio to sleep between samples. A large upward change still needs three complete high blocks to pass the rise hold. No battery-life improvement has been measured for this scheduling change.

The block estimate is the 12.5th percentile: the average of the eighth and ninth sorted samples, retaining fractional dBm. A published update uses 25% of the previous floor and 75% of the new estimate, with a -120 dBm lower bound. Initial calibration and explicit gain/tuning changes seed a new baseline. The old published value remains available while collecting. An AGC reset drops partial samples while retaining a valid baseline for channel checks.

An estimate more than 15 dB above the established floor is held twice. The third consecutive complete high block is accepted, allowing recovery after a permanent ambient rise. A normal or quieter complete block clears the hold. Partial blocks do not advance it. There is no admission threshold based on the old floor, so an old low baseline cannot reject every sample from a higher floor.

Hardware captures with sustained different-SF/BW traffic showed the median and lower quartile following the interferer. The 12.5th percentile retained the quiet background in those captures. It needs at least nine background samples out of 64 to avoid higher-valued traffic entirely. Nearly continuous interference or enough low outliers can still mislead it. The rise hold delays large changes but cannot identify their source.

RSSI sampling waits 7 ms on SX1262, SX1268, and LLCC68 builds, or 20 ms on other families, after every receive re-arm, including CAD and TX completion, to avoid unsettled frontend readings. The ten-second trial window often failed to complete during heavy traffic with that settling guard; the 30-second cap allows more idle opportunities. Partial blocks still time out without publishing. Longer collection spends more time in continuous RX when RXPS is enabled. A quiet minimum is about 3.2 seconds; real completion time also depends on packet activity and main-loop scheduling.

On radios with a mode probe, each eligible RSSI sample first checks that the chip has not silently left RX. Confirmed standby/non-RX discards the partial block without changing the published floor. Rejected attempts are also spaced 50 ms apart. BUSY and packet ownership block sampling; families without a mode probe retain their existing RSSI path.

CAD has a bounded wait, stops RX duty cycling before scanning, and re-arms the configured receive mode on success, busy detection, or error. A completed or currently receiving packet owns the radio and blocks CAD. A packet interrupt arriving during re-arm is retained. Failed re-arm and a newly arrived packet both defer transmission.

SX126x wrappers additionally run a recovery watchdog at most once per 10 seconds while continuous RX is expected. RXPS sleep, busy SPI, pending packets, and invalid status reads do not count as non-RX evidence. Two confirmed non-RX observations request soft recovery; a third uses the existing hard recovery path where supported. An observed healthy RX state clears the sequence. Recovery preserves tuning, TX power, receive gain, and the operator's power-saving intent. The existing radio liveness and RXPS watchdogs remain active for other failures.

The radio liveness watchdog measures time without a successfully received packet. Only a positive recvRaw() result restarts its observation window; TX completion, CAD interrupts, and failed packet reads do not. Reception counts even if application parsing rejects the packet or the packet pool is full. The check runs after pending RX is drained and TX completes, before the next queued transmission, so sustained outbound traffic cannot postpone recovery.

The default silence thresholds are 30 minutes for a soft RX/AGC re-arm and 12 hours for a radio-only hard recovery where supported. MQTT observer radio.watchdog settings can override the soft interval. A successful hard recovery starts a fresh observation window to limit repeated resets on a quiet mesh; a failed hard recovery remains escalated and retries after 30 seconds. Targets with RADIO_LIVENESS_SOFT_ONLY repeat soft recovery at the soft interval because their radio cannot be reset independently. These recoveries do not reboot the MCU. The repeater's rx.watchdog is a separate whole-board reboot watchdog, enabled by default with a 24-hour sliding deadline since the last received packet passed MeshCore parsing. Its timer is updated before receive delays and routing filters; raw LoRa reads rejected by the parser do not count. MeshCore parsing checks the packet structure, not sender authentication or decryption of traffic addressed to other nodes. Temporary radio windows of at least 12 hours use a 12-hour deadline; shorter windows disable that reboot watchdog until the window ends. The temporary timing overrides are held in RAM and do not replace saved normal-mode preferences.

The status read uses the two-byte GetStatus transaction. RadioLib 7.8.1 also reads the response byte directly; the dedicated probe additionally preserves the existing SPI layout, checks transport errors, and avoids waiting on BUSY. It does not wake a duty-cycling radio to inspect its mode.

Validation lives in test_noise_floor_estimator, test_rx_power_saving, test_radio_liveness, test_radio_receive_contract.py, and test_sx126x_receive_mode.py, and test_radio_receive_retry.py. These host checks do not establish battery-life or hardware fault-recovery results.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/","title":"Rak3401 all roles cpu sleep 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#rak3401-all-firmware-cpu-sleep-checks-21-october-2026","title":"RAK3401 all firmware CPU sleep checks, 21 October 2026","text":"

Follow-up: the targeted RAK release checks record a new Full Room Server PASS at adb11e7b4: three physical LoRa round trips, 79.31% sampled CPU sleep and zero post-check errors. The original failed capture below remains unchanged; its RF cause was not established.

All 15 applicable images were installed and measured sequentially on the same RAK3401. All 13 profiles that support power saving passed the CPU sleep capture with PS on. Twelve also passed every runtime check. Full Room Server slept correctly but raised a channel-busy timeout flag, so its overall result remains failed. Terminal Chat and KISS expose no PS setting and showed no sleep. The campaign therefore is not an all-modes qualification pass.

The board is left running Full unified Repeater at efad0076, PS on. Its final core snapshot had zero error flags and an empty queue. The original mctomqtt.service and exact USB ownership were restored without changing its configuration. Updates used application-only packages; the reported bootloader version is unchanged.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#results","title":"Results","text":"

Each row has a complete 90-second CPU capture and an independently matched whole-application readback. Percentages are weighted estimates from sampled processor states, not exact sleep residency or current measurements.

Firmware/profile Source PS Sleeping/total samples Sleep estimate Result Full Companion e16db810 on 5,510/8,000 68.78% Pass USB Companion e16db810 on 6,115/7,999 76.41% Pass BLE Companion e16db810 on 5,931/8,014 74.03% Pass Full Room Server e16db810 on 5,929/8,009 74.01% CPU pass; health flag Reduced Room Server e16db810 on 5,487/7,980 68.67% Pass Full Sensor e16db810 on 5,890/7,992 73.82% Pass Reduced Sensor e16db810 on 6,099/8,028 76.00% Pass Terminal Chat e16db810 unsupported 0/8,012 0.00% PS unsupported; no sleep KISS modem e16db810 unsupported 0/8,054 0.00% PS unsupported; no sleep Full Serial1 RS232 repeater efad0076 on 6,257/8,134 76.94% Pass Reduced Serial1 RS232 repeater efad0076 on 6,428/8,094 79.53% Pass Full internal-flash OTA repeater efad0076 on 6,275/8,106 77.38% Pass Reduced internal-flash OTA repeater efad0076 on 6,138/8,073 75.94% Pass Reduced unified OTA repeater efad0076 on 6,393/8,019 79.69% Pass Full unified OTA repeater efad0076 on 5,875/8,013 73.19% Pass

The safe receipt records application/package hashes, source commits, capability and memory checks, capture flags, timings and final handback health. All 15 capture-integrity checks passed; no halt, step, interrupt masking, lockup or reset was sampled inside any capture window. Debug-control state remained unchanged.

Full Room Server's core.errors changed from 0 to 2 at uptimes 51 and 148 seconds. The value is a bitmask: 0x02 is the latched CAD/channel-busy timeout flag. The dispatcher bounds its channel-busy wait, records this flag, then permits a transmit attempt. The observation does not establish the cause of that busy condition or prove subsequent RF delivery. The CPU capture passed at 74.01%, but the strict zero-error post-check failed and remains visible. The failed result was retained, and this unchanged capture was not repeated.

Terminal Chat and KISS have no power-saving command or application idle-sleep integration. Both completed valid captures with zero sleeping observations; they are unsupported PS coverage, not PS-on passes.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#hardware-and-procedure","title":"Hardware and procedure","text":"

The tested assembly is one RAK3401 core with an external RAK13302 radio and no external NOR flash. Coverage includes Full/USB/BLE Companion; Full/Reduced Room Server and Sensor; Terminal Chat and KISS; and Full/Reduced Serial1 RS232, internal-flash LoRa OTA and unified OTA repeaters. The dedicated RAK13302 plus W25Q16 image is excluded because its required external flash is absent. The unified image exercised its no-NOR/internal-storage path.

The nine non-repeater images are bound to e16db810; the six repeaters are bound to efad0076, after the build corrections below. No claim is made that all images came from one revision. One application was active at a time, with an exact application readback before each measurement. No bootloader or SoftDevice package was installed.

Supported roles acknowledged set powersaving on, confirmed the saved value, and provided a fresh RAM-only diagnostic window. USB command ports were closed for capture. Companion intentionally prevents idle sleep with a configured USB host, so only this target's USB data authorization was temporarily disabled while retaining power, then restored. BLE sessions were disconnected for idle capture and authenticated again afterward.

The nonhalting SWD sampler used MEM-AP reads of DHCSR at jittered 7\u201313 ms intervals for 90 seconds. Sleep acceptance requires at least 5,000 samples, 100 sleeping observations, complete timing and chip-identity checks, unchanged debug controls, and no sampled reset/halt/step/masking/lockup. Read timing must remain within the 10 ms read and 25 ms gap limits. This measures observed CPU sleep states, not just a firmware eligibility counter.

Five early preparations stopped before PS setting or CPU sampling: Full/USB Companion on an arrow-prefixed reply, Full/Reduced Room Server on the first version reply, and Terminal on LF/CRLF handling. The private reader was corrected; a bounded retry of only the read-only version getter retained the first unsupported reply. Their first actual CPU captures appear above, with the earlier failed preparations retained in the JSON. Completed captures were not repeated. An interrupted read-only Sensor application readback was also preserved separately; it did not alter the installed image or repeat a CPU capture.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#build-corrections","title":"Build corrections","text":"

Commit efad0076 fixes two defects exposed while preparing the matrix. All 15 selected packages then passed their build, capability and memory gates.

The Full internal-flash and unified repeaters originally missed the required runtime heap reserve by 412 and 564 bytes. Qualified nRF52 repeaters now use the existing grouped Merkle-proof workspace instead of retaining every source leaf: a 1,536-byte allowance replaces 4,096 bytes, saving 2,560 bytes of worst-case active heap. The 1,024-block source capacity, wire proofs, sensors, tables, stack and remaining reserves are retained. No admission gate was lowered.

The Serial1 RS232 recipe already disables GPS because the bridge owns that UART, but its release capability contract incorrectly required GPS. Both RAK3401 bridge profiles now follow the existing UART ownership contract while retaining the INA monitor drivers. Compatible recipes retain their GPS checks. Validation passed 49 RAM tests, four source-proof suites and two GPS-contract tests. The proof suites include sanitized nRF52 Full/Reduced runs through the unchanged 1,024-block maximum, byte-identical proofs, allocation ownership, backpressure and simultaneous fetching/serving.

This finite sleep campaign does not qualify RF delivery, LoRa OTA installation, UART peer traffic, external storage, regulator savings or battery life. Application readback covers the package bytes, not protected storage or user data; an unchanged runtime bootloader version is separate from a full bootloader-byte preservation check.

"},{"location":"test-results/rak_release_fixes_2026-10-22/","title":"Rak release fixes 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#rak-release-fixes-and-targeted-hardware-checks-22-october-2026","title":"RAK release fixes and targeted hardware checks, 22 October 2026","text":"

Status: RAK4631 Full/Reduced Repeater builds and physical radio checks PASS; RAK3401 Full Room Server radio, CPU sleep and health checks PASS. All six selected RAK release packages passed their existing capacity, capability, Bluetooth DFU and packaging gates.

Firmware and hardware results are bound to adb11e7b4. The later 302e441e2 changes only Mini-selected CLI messages; selecting the non-Mini branches reproduces the prior RAK source byte for byte. Unchanged RAK checks were retained. The machine-readable receipt includes package hashes, RAM requirements, radio counters and CPU observations.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#ram-repair","title":"RAM repair","text":"

The fresh starting source, e34e83e6b, failed both RAK4631 Repeater profiles: Full had 81,668 internal bytes available against 85,152 required; Reduced had 84,680 against 85,152. These are separate from the earlier Full Companion flash repair.

Repeater rule targets cannot contain both a scope and a region. One tagged 32-byte name replaces their two separate names, reducing each actual rule from 332 to 300 bytes. All 63 slots remain, saving 2,016 allocated heap bytes. FPF6/7/8 file formats retain their existing field widths and validation.

Ordinary nRF52 SSD1306 builds now use the existing source-enforced 2 KiB allocation bound for this same concrete driver, correcting its previous 4 KiB estimate. This is a 2,048-byte accounting correction, distinct from the rule-table saving. Stacks, packet pools, allocator/transient reserves, sensor features and admission boundaries remain unchanged.

Target/profile Available internal bytes Required heap bytes Margin Result RAK4631 Repeater Full 81,676 81,088 588 PASS RAK4631 Repeater Reduced 84,680 81,088 3,592 PASS RAK3401 Room Server Full 153,940 58,416 95,524 PASS RAK3401 Room Server Reduced 156,928 58,416 98,512 PASS RAK3401 unified Repeater Full 78,364 74,176 4,188 PASS RAK3401 unified Repeater Reduced 81,368 74,176 7,192 PASS

These are canonical build.sh build-firmware Full/Reduced pairs with Cascade settings, --radio-preset usa-cascade-fixed and --require-ota. The canonical RAK4631 Repeater and Room Server packages retain their normalized internal-OTA target identities. PlatformIO commands ran sequentially.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#channel-busy-repair","title":"Channel-busy repair","text":"

A proven dispatcher defect retained a profile's channel-busy timestamp after its last waiting packet was cancelled or retired. A later unrelated packet could inherit the old age and immediately force a transmit with error 0x02.

New work now retires an unused wait before joining that profile. Queued work, retained driver retries and foreign/direct-OTA owners preserve their live backoff. begin() resets previous CAD state. No RAM fields or idle-loop scans were added. Genuine sustained contention still raises the original timeout.

The original failed Room Server capture lacked enough RF data to prove this was its cause. Both old installed repeaters also had latched 0x02 flags. The new software regressions and finite new-image hardware results are the evidence for this repair; historical failures remain recorded.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#physical-results","title":"Physical results","text":"

Application-only DFU installed the source-bound images on the Mercerwood Pi. USB reopen checks confirmed role, version, EndF body identity and continuous uptime. No chip/data erase, filesystem backup, bootloader or SoftDevice update occurred.

The RAK Tag was the independent LoRa witness. Each case used three fresh eight-byte nonces, exact routed trace replies and advancing target RX/TX counters. USB carried commands and observations; the request and reply crossed the physical radio link. Settings were 917 MHz, SF7, BW62.5, CR5, preamble32, with two-minute temporary radio leases. The RAK targets retained 2 dBm TX power; the Tag used its saved 22 dBm. The excluded 910.525 MHz channel was not used.

Installed image Exact-nonce round trips Core errors before/after Result RAK4631 Reduced Repeater 3/3 0 / 0 PASS RAK4631 Full Repeater 3/3 0 / 0 PASS RAK3401 Full Room Server 3/3 0 / 0 PASS

After radio cleanup, the Room Server completed one 90.009-second nonhalting SWD capture with power saving on: 6,374 sleeping observations among 8,035 samples, or 79.31% weighted sampled sleep. There were no sampled resets, halts, steps, interrupt masking or lockups; debug controls stayed unchanged. Maximum read time was 3 ms and maximum sample gap 16 ms. Uptime advanced continuously; the post-check had zero errors and an empty queue. This measures sampled CPU states, not electrical current or exact sleep residency.

The first Room preparation stopped on an unsupported ota bootloader status getter before any RF or CPU capture. Its failure and repeater restoration are retained. The corrected room-only follow-up treats that optional status page as unavailable while retaining mandatory bootloader-version and application identity checks. It did not repeat the already-passing RAK4631 tests.

Final handback leaves both RAKs on patched Full Repeater images, saved 917 MHz settings and power saving on. The RAK3401 gateway service is active again with its original configuration and exact USB owner; final core errors were zero.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#regression-checks-and-ci","title":"Regression checks and CI","text":"

The shared CAD fix initially exposed a 44-byte Mini Repeater flash overflow. Mini-only CLI text was shortened without removing commands, values or checks. Its exact release-wrapper build now passes at 245,696 of 245,760 application bytes; RAM passes at 43,920 available against 32,288 required. Four replay integration and eight Mini-profile tests passed. Other platforms select their unchanged messages.

The updated CI run was still running when this report was prepared. These targeted checks do not claim a new all-board campaign, RS232 cable traffic, physical OTA installation, maximum-load operation or a long RF soak.

"},{"location":"test-results/regulator_recovery_2026-10-18/","title":"Regulator recovery 2026 10 18","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/regulator_recovery_2026-10-18/#regulator-selection-and-recovery-qualification","title":"Regulator selection and recovery qualification","text":"

Baseline firmware source: 388134599. Later STM32 compatibility and fixture source: a2b2a8616. Post-campaign timing correction: 5083c236b. Version base: v1.18.0.1-halo-keymind-cascade-dev; package filenames and build records identify their source suffix. PlatformIO commands ran one at a time. Local software qualification is complete: eleven baseline target selections produced twelve packages at 388134599, five STM32 targets passed at a2b2a8616, and a later RAK4631 Full build passed at 5083c236b. The scoped RAK4631 recovery, shipping-image checks and final restoration also passed; hosted workflow status is recorded separately below.

The machine-readable evidence records the exact requested and packaged targets, application and container SHA-256 values, memory admission, profile reductions and source-bound linked checks. These are locally built qualification packages; this record does not publish a release or establish a successful GitHub workflow run.

"},{"location":"test-results/regulator_recovery_2026-10-18/#behavior-covered","title":"Behavior covered","text":"

The four main firmware roles save independent mcu.regulator and radio.regulator choices (auto, dcdc, ldo) where supported. Saving changes the next boot; it does not change an active radio. auto retains the board default. An explicit setter clears only that domain's fallback and crash streak, including when the same value is saved again. Unsupported hardware rejects the selection. Failed initialization reports an unknown active mode.

A domain can fall back to LDO after three consecutive known crash resets while its previous active mode was DC-DC, using a valid voltage-derived battery estimate of at least 50% and a sample at most 60 seconds old at the last heartbeat. Five healthy minutes clear the consecutive streak. The saved LDO fallback remains until an explicit setter clears it; there is no automatic return to DC-DC. Clean resets, cold starts, sleep wakes, unclassified resets, bad retained CRCs and ineligible battery samples cannot extend the streak. ESP32 brownouts are excluded.

Choices are prepared after primary filesystem/ADC startup and before display/radio/wireless initialization. Volatile fallback storage cannot acknowledge a durable setting. Crash bookkeeping uses retained RAM, and failed fallback persistence is retried after boot. Checked nRF52 APIs preserve SoftDevice ownership. KISS and other roles outside the saved-settings integration retain their original board default.

Supported CPU control covers approved nRF52 REG1 circuits. Radio controls cover supported SX1262, LR1110/LR1121 and verified Meshnology W12 LR2021 circuits. Current integrated STM32WL targets retain their existing shared MCU/radio supply configuration and expose neither selector. Their ordinary CLI reports an unsupported setting/command. The exact STM32+STM32WLx build guard removes unreachable persistence/recovery code, not a supported control. Other platforms or future STM32 external-radio drivers retain the generic implementation; regression checks require reconsidering the guard if current STM32 classes gain selection. STM32WL supply requests need a coordinated design because MCU and radio share the regulator (ST datasheet); Wio-E5 already uses SMPS for high-power TX (Seeed documentation).

CLI usage and policy details are in CLI commands and power diagnostics. ESP32 WebConfig exposes authenticated save/reboot controls under Radio \u2192 Advanced \u2192 Power regulators. Firmware-picker metadata does not advertise these new regulator controls.

"},{"location":"test-results/regulator_recovery_2026-10-18/#baseline-packages-at-388134599","title":"Baseline packages at 388134599","text":"Requested target Packaged target and profile Application bytes RAM admission margin Application SHA-256 prefix RAK_4631_companion_radio_full RAK_4631_companion_radio_full (full) 712,600 4,516 39babcaf8228 heltec_v4_3_companion_radio_full_femoff heltec_v4_3_companion_radio_full_femoff (full) 2,559,064 34,432 af0e8cc40a2a PicoW_companion_radio PicoW_companion_radio (auto) 911,148 58,432 23fc6982cec8 muzi_base_duo_companion_radio_full muzi_base_duo_companion_radio_full (full) 571,940 32,560 1d9773c16872 wio_wm1110_repeater wio_wm1110_repeater_lora_ota_no_external_sensors-full-ota (full sensors) 521,908 87,256 588f8e6c2c70 wio_wm1110_repeater wio_wm1110_repeater_lora_ota_no_external_sensors-reduced-ota (reduced sensors) 492,120 88,904 731bb26bcf44 meshnology_w12_sensor meshnology_w12_sensor (full) 1,597,944 127,288 ba348f2791d4 LilyGo_Tlora_C6_room_server_ LilyGo_Tlora_C6_room_server_lora_ota_no_external_sensors (reduced) 1,584,776 228,736 d654c84d85ca wio-e5-mini_repeater wio-e5-mini_repeater (auto) 245,652 12,240 c7297a2fadd9 RAK_4631_kiss_modem RAK_4631_kiss_modem (auto) 278,656 172,988 ae7537531dd8 Xiao_nrf52_companion_radio_full Xiao_nrf52_companion_radio_full (full) 583,772 43,944 540f935740b3 Heltec_t114_companion_radio_full Heltec_t114_companion_radio_full (full) 633,672 22,680 c6459844b69e

The C6 Room Server selector uses the build system's Auto policy. The baseline-source build selected LilyGo_Tlora_C6_room_server_lora_ota_no_external_sensors, Reduced LoRa OTA on Auto pass 2 after the expanded Full attempt exceeded flash/partition capacity. Only that exact packaged target/profile is qualified; this is not a C6 Full pass. Wio WM1110's sensor OTA policy produced both Full-sensor and Reduced-sensor packages, recorded separately. The matrix also covers all three nRF52 Full Companion CI smoke targets: RAK4631, XIAO and Heltec T114; their individual status remains visible above.

For each completed baseline package, the verifier recomputed every memory manifest's bound file hash, read the actual application BIN (directly or from the nRF52 DFU ZIP), and checked embedded version/source suffixes where present. KISS has no embedded version/source string; its source binding is the clean source-bound build record and artifact hashes. Each bound UF2 was decoded and matched against that exact BIN/DFU application, allowing only uniform final-block padding; each ESP32 merged image contained one exact copy of its application BIN. Container hashes and application hashes have separate meanings. RAM margins are modeled linked capacity and do not measure runtime allocation or physical high-water.

The baseline RAK4631 Full application is 712,600 bytes of its 712,704-byte application region, leaving 104 bytes. Its RAM admission model has 79,092 bytes available against 74,576 required, a 4,516-byte margin. The package remains the Full profile and retains its verified BLE/USB, logging, mOTA, Meshtext and Reader capabilities.

The first matrix at ef2edc52005f6bc64880911e9d08d7f782a0ddfb passed seven targets, then failed Wio-E5 Mini Repeater with a 1,908-byte flash overflow. Commit 7e3359c1a added the exact unsupported STM32WL path; its final rebuild result is recorded in the table rather than inferred from the change. No deployed filesystem boundary or working feature was removed to resolve that failure.

"},{"location":"test-results/regulator_recovery_2026-10-18/#stm32-compatibility-packages-at-a2b2a8616","title":"STM32 compatibility packages at a2b2a8616","text":"

The earlier CI run at 388134599 exposed a flash overflow in the small RAK3x72 USB Companion and a missing regulator mock in an extracted fleet-command fixture. The compatibility change keeps the exact integrated STM32WL driver on its existing shared supply, omits eight unsupported regulator vtable entries and lets the ordinary CLI report an unknown selector. The RAK3x72 conversion preserves all eight 12-bit ADC samples and the exact prior calibration/truncation using integer arithmetic. Native tests compare all 32,761 valid sample sums; the maximum product is 283,374,000 and fits uint32_t. Contacts, channels, interfaces and deployed filesystem boundaries are retained.

Target Actual BIN bytes Application region Actual flash margin RAM admission margin Application SHA-256 prefix RAK_3x72_companion_radio_usb 229,284 229,376 92 6,848 613708364b7d wio-e5-mini_companion_radio_usb 229,356 229,376 20 6,816 e1aab0f4e12f wio-e5_companion_radio_usb 229,204 229,376 172 6,856 e797ca7e77b1 Tiny_Relay_companion_radio_usb 229,200 229,376 176 6,848 5882f6965730 wio-e5-mini_repeater 245,280 245,760 480 12,240 80f462312e8d

These are the four exact STM32 USB Companion CI targets/flag sets plus the Mini Repeater rebuild. The USB Companions retain 32 KiB LittleFS; the Mini Repeater retains its existing 16 KiB LittleFS. Each BIN includes 352 bytes of alignment/layout extent beyond PlatformIO's summed flash sections; the table uses the actual packaged BIN size. In particular, Mini USB has 20 bytes of application space remaining. Independent verification matches every initialized ELF flash-load byte and Intel HEX byte to its BIN, checks all memory-bound file hashes, verifies the embedded version/source suffix and excludes private regulator fault markers. These checks establish packaged size and consistency, not hardware behavior or wireless self-update. The cable-only Mini Repeater retry omitted --require-ota after that flag was rejected during preflight.

The earlier eleven targets were not all rebuilt at a2b2a8616. Their results keep source 388134599. Read-only preprocessing of the actual MainBoard/Radio/mode headers and complete settings implementation shows identical tokens to the baseline in six ordinary configurations: generic, ESP32 with the STM32 radio name, nRF52, RP2040, future STM32 with an external SX1262, and STM32 with no specified driver. Eight-method override/runtime-dispatch controls preserve the generic virtual API; only exact STM32+CustomSTM32WLx loses those unsupported slots. The preprocessing proof uses native adapters and is a conditional-source/API comparison, not a new whole-firmware build for the other platforms. It compares 388134599 with a2b2a8616; it does not include the later intentional Morse scheduling change below.

The source a2b2a8616 GitHub workflow finished with eleven jobs passing and the main test job failing. The failure was a missing regulator lifecycle adapter in an extracted Morse role-loop fixture. Fixture-only commit a08d7f07a fixes that dependency and two downstream console/setup-GUI fixtures; 24 affected and related suites (121 tests) pass locally. The new source 5083c236b workflow is in progress; no successful hosted workflow is claimed for these corrections.

"},{"location":"test-results/regulator_recovery_2026-10-18/#software-browser-and-linked-checks","title":"Software, browser and linked checks","text":"

These are focused campaign checks. Method counts identify suite coverage; they are not a claim that the entire GitHub unit-test workflow completed in one run.

Check Result Coverage Pure recovery policy PASS, 16 scenarios Three crashes; domain independence; >=50% boundary; stale/invalid samples; CRC/corruption; clean reset; healthy interval; sticky fallback. Settings integration and actual API dispatch PASS, 21 methods Real settings implementation with ESP32/RP2040/nRF52 filesystems; deferred application; failed writes/rename/corruption; absent/volatile storage; capacity guards; exact STM32WL unsupported path and future external-radio guard controls. STM32 Companion profile/ADC checks PASS, 21 methods Existing capacities, storage, interfaces and all 32,761 ADC sums retain the prior result. Fleet and related extraction fixtures PASS, 65 tests across nine suites Real-codec fleet 44 with sanitizers; local/remote access 4; primary persistence 1; lost reply 1; save results 3; uncertain save 1; G3 dispatch 7; Mini logging 2; fleet role wiring 2. Radio regulator selection PASS, 6 methods SX1262, LR1110, LR1121 and LR2021 boot choices, supported circuits, initialization failure, repeated LoRa/GFSK initialization. nRF52 CPU/reset/watchdog integration PASS, 5 methods Checked SoftDevice API; actual versus requested mode; early reset capture; clean planned watchdog expiry and cancellation/rearm. nRF52 linker retention reservation PASS, 16 methods All eight application linker scripts; 64-byte NOLOAD reservation; deliberate overflow rejection; unchanged clock and OTA authentication ranges. nRF52 BLE DFU handoff PASS, 11 methods Clean callback only after successful final handoff checks; actual ARM/Unicorn path; aborted handoffs remain armed. WebConfig regulator controls PASS, 4 methods One native JSON/authenticated-save/forced-reboot method and three Chromium methods; supported choices, clearing fallback and failed-save behavior. Reader packing/lookup/UI PASS, 15 methods All 879 references and canonical ASCII bytes; terminal pages; screen layouts/fonts/navigation; bookmarks; backpressure and Morse guards. Adjacent CLI/preferences/button/G3 suites PASS, six suites Settings contract, CLI error/capacity bounds, CommonCLI argument bounds, Companion preferences transactions, headless buttons and G3 dispatch Complete build-profile selection suite PASS test_build_profiles: OK Existing WebConfig coverage PASS 25 real Chromium methods, three lost-reply methods, 13 source methods, seven secrets methods and five generated-gzip methods Baseline RAK Full ELF check (388134599) Result Linked RAK crypto PASS Linked RAK workspace PASS Linked RAK reader PASS

The crypto probe executes linked ARM RFC8032 keypair/signing vectors and bounds their function frames. The OTA workspace probe executes empty-queue acquisition/release and checks protected authentication ranges. The Reader probe executes the actual linked replyVerse instructions, checks canonical output/continuation commands, 157-byte reply guards and NUL termination, and measures callee frames plus a 1,536-byte caller reserve. These checks do not establish full-firmware timing, ISR/SoftDevice stack use or hardware behavior. Linked Reader negative controls also reject a valid DEFLATE stream with one altered word and a balanced enlarged function frame measuring 9,984 bytes. Only disposable ELF copies were mutated; they were never flashed or published.

"},{"location":"test-results/regulator_recovery_2026-10-18/#post-campaign-morse-scheduling-correction-at-5083c236b","title":"Post-campaign Morse scheduling correction at 5083c236b","text":"

Review of the failed Morse fixture also exposed a foreground timing issue: the periodic median-of-three battery read includes two 5 ms gaps, and a failed fallback save can block on flash. The production service now defers those two periodic operations while keyed Morse TX/RX is active, using the existing Morse timing policy. Active-domain tracking and one-second recovery heartbeats continue. Deferred sample/retry deadlines stay unchanged, stale samples cannot qualify a crash, and overdue work runs on the next eligible service tick. Unkeyed CW does not defer these operations. Boot-time sampling and deliberate recovery rearming retain their fresh-measurement requirements.

The real settings/API implementation passes 23 methods with ASan/UBSan, including TX/RX controls, no ADC/delay/flash retry during timing, changing active-domain masks, heartbeats, samples older than 60 seconds, resumed overdue work and saved LDO after a cold boot. Disabling the guard in a disposable source copy produces the expected ten failing platform subtests. The fixture-only correction at a08d7f07a separately passes 24 suites / 121 tests; it does not change firmware source.

A clean source-bound RAK4631 Full rebuild at 5083c236b passes with an actual application of 712,616 bytes, leaving 88 bytes in the existing 712,704-byte region. The modeled RAM margin remains 4,516 bytes, and the Full capability set is unchanged. Bound file hashes, ZIP/BIN/UF2 and ELF flash-load bytes agree; no private regulator fault markers are present. Linked crypto, OTA workspace and all-reference Reader checks pass. The new linked Reader check reconstructs all 879 references / 95,689 text bytes through 1,091 calls and 379,674,263 ARM instructions. Its maximum callee frame remains 6,016 bytes; with the 1,536-byte caller allowance this is 7,552 of 8,192 bytes, within the same model as the baseline.

The earlier package matrix and completed physical campaign retain their own tested revisions. Other target packages were not all rebuilt at 5083c236b, and the new periodic-task guard has not received physical Morse timing or fault qualification. The source change keeps the exact unsupported STM32WL branch unchanged.

"},{"location":"test-results/regulator_recovery_2026-10-18/#reader-flash-and-stack-preservation","title":"Reader flash and stack preservation","text":"

The Reader's pinned source, all 879 references and 95,689 ASCII text bytes (96,568 bytes including the 879 NUL terminators) are unchanged. Packing uses 4,096-byte blocks instead of 2,048-byte blocks. Compressed data plus index fell from 42,775 to 38,654 bytes, saving 4,121 flash bytes with no removed text, feature or heap cache. Canonical source JSON SHA-256: 916bf9d1feae0ad410d69f61e6c5f328ac6acba10651d751b5bb29719732c49e.

ARM GCC source-compiled frame bounds including decoder helpers were 6,264 bytes for terminal output and 6,400 bytes for the adaptive screen path. Adding the 1,536-byte outer-caller reserve gives 7,800 and 7,936 bytes, within the existing 8,192-byte loop stack. The linked replyVerse probe reconstructs all 879 references through 1,091 calls and 379,674,263 ARM instructions, checking all 95,689 text bytes. Its maximum callee stack is 6,016 bytes; adding the 1,536-byte caller allowance gives 7,552 of 8,192 bytes, leaving 640 bytes within that model. Neither compiler bounds nor emulated stack use substitutes for a physical high-water measurement.

"},{"location":"test-results/regulator_recovery_2026-10-18/#mercerwood-hardware-qualification","title":"Mercerwood hardware qualification","text":"

The physical campaign used one RAK4631 connected to Mercerwood Mesh Pi, bound to source 388134599. The private fault image was Full Companion with Reader omitted, retaining the Full USB/BLE transport configuration. Fault injection was identity-bound and explicitly armed once per fault boot. The separate shipping phase used the unmodified Full Companion with every shipping feature, with no fault instrumentation. Earlier legacy USB transport failures do not establish a recovery pass or show that the fault timer caused those failures.

Both images passed all five saved-mode combinations: (ldo, ldo), (dcdc, ldo), (ldo, dcdc), (dcdc, dcdc) and (auto, auto). The ten completed cases verified independent MCU/radio selection, unchanged active modes until reboot, the expected active modes after reboot, and zero crash counts/fallback after intentional reboots. On this board, auto selected DC-DC for both domains.

Three explicitly armed ARM UDF faults produced observed counter transitions 0 \u2192 1 \u2192 2 \u2192 3 in both domains, with three USB-instance changes and lower post-reset uptime. Both domains stayed in DC-DC without fallback at counts 0\u20132; both selected sticky LDO at count 3 while the requested choices remained auto. The voltage readings at these observations were 4,253\u20134,284 mV, estimated 100%. USB was connected; physical battery presence was not independently verified. These observations qualify the voltage-estimate gate, not battery-only operation or the exact 50% boundary on hardware.

The healthy window advanced device uptime by 314 seconds on the same boot. Neither a fourth reset nor an automatic return to DC-DC occurred. Both crash streaks cleared to zero after five healthy minutes while both sticky LDO fallbacks remained. Updating to unmodified shipping Full preserved both fallbacks.

Shipping Full also returned exact canonical ASCII for Reader references 1:1 (79 bytes, one page) and 21:17 (290 bytes, three pages), through four bounded queries with valid continuation commands. The USB instance stayed the same, uptime did not decrease and no new core errors appeared. This is a read-only USB check; it does not establish shipping Full/BLE fault or load qualification.

The first shipping manual attempt stopped at a serial ownership guard during reconnect. A subsequent read-only check proved both interfaces unowned. The unchanged guarded driver then completed all five cases from the start without another DFU. The stopped attempt remains separate evidence and is not counted among the ten completed cases; no owner was killed, service stopped or guard bypassed.

Cleanup removed only the owned trial regulator files and directly proved all three paths absent, matching the directly observed pretrial state. Original identity/preferences file fingerprints remained unchanged. The earliest pre-session absence of the new REG1 path was inferred from source/session provenance rather than captured directly: a prior diagnostic's exact 12-byte record was privately exported and verified before guarded cleanup. The current campaign directly observed absence before its mode tests and again after cleanup.

Finally, the exact original application package was restored. The original full version, complete device information/PIN, identity, contacts, all 40 meaningful channel entries and all eight checked power/USB preferences matched the saved baseline. Channel comparison preserves names and complete keys, excluding only unused bytes after the name's first NUL; it does not claim every raw reply-padding byte is identical. No bootloader update, SWD, hub power operation or test mesh message was used. Private diagnostic images were kept out of releases and moved out of the normal build-discovery directory after testing.

Physical check Result Private Full saved choices and normal reboots PASS, five combinations Private Full three qualifying real faults, then LDO PASS, both domains Healthy interval leaves sticky LDO PASS, 314-second uptime advance; streaks clear Unmodified shipping Full manual/read-only Reader phase PASS, five combinations and four Reader queries Cleanup restores original trial-file state PASS, direct absence proof Original firmware and stored state restored PASS, independently compared against baseline

The machine-readable record includes the exact private diagnostic, shipping and restored-original package hashes. Independent evidence review confirmed the completed mode matrices, fault sequence, healthy window, Reader output, cleanup and restoration. Private snapshots, keys, channel payloads and device serials are excluded.

"},{"location":"test-results/regulator_recovery_2026-10-18/#physical-acceptance-still-required","title":"Physical acceptance still required","text":"

Battery percentage is a voltage estimate, not independent proof of a physically connected battery. Sample freshness is relative to the last retained heartbeat, not elapsed wall time after a hard hang. Runtime load, current and supply measurements remain separate acceptance evidence.

"},{"location":"test-results/room_mailboxes/","title":"Room mailboxes","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/room_mailboxes/#personal-room-mailbox-validation","title":"Personal room mailbox validation","text":"

Validated on 2026-10-10 against the source changes in this commit. These checks cover the room-side implementation, not a sleeping tracker client or a live LoRa/Wi-Fi transfer. No lab radio was flashed for these mailbox checks.

"},{"location":"test-results/room_mailboxes/#behavior-exercised","title":"Behavior exercised","text":""},{"location":"test-results/room_mailboxes/#automated-checks","title":"Automated checks","text":"

The mailbox store passed 3,978 scenarios on each of five filesystem API variants (19,890 total), using AddressSanitizer, UndefinedBehaviorSanitizer and OpenSSL SHA-256. The wire/CLI protocol passed 1,393 scenarios per variant (6,965 total), using the firmware's actual Crypto SHA-256 implementation and the same sanitizers. Adafruit file mocks reject default construction, matching the actual nRF52 library.

Production room transport tests passed 18 cases. Actual JSON handlers passed seven existing base, six existing administrator and six new mailbox groups with real ArduinoJson and stores. The delivery scheduler, room login, receive admission, post permissions, HTTP ownership and stock app compatibility checks passed after updating their production-code adapters.

Chromium passed 17 existing room browser tests and nine mailbox tests, including new radio mailbox provisioning without downloading another owner's body. These checks are included in the unit-test workflow.

"},{"location":"test-results/room_mailboxes/#firmware-builds","title":"Firmware builds","text":"

The following Room Server builds passed the normal flash and runtime RAM gates:

The first nRF52 build exposed a non-default-constructible Adafruit file handle; the implementation and fixtures were corrected before final builds passed.

"},{"location":"test-results/room_mailboxes/#stack-review","title":"Stack review","text":"

The actual T1000-E ARM ELF retains the existing 8 KiB loop stack allocation. The largest mailbox store frame is 1,320 bytes. Streamed policy reads avoid loading the full receipt table in the delivery scheduler; write and readback verification run sequentially rather than stacking both operations.

A representative LoRa CLI send chain has 5,632 bytes of visible frames; the binary request path has 4,352. These estimates exclude deeper filesystem, library, virtual-call and task overhead, and are not proven worst-case bounds. They do not replace a live long-duration stack-watermark test.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/","title":"Rs232 bridge diagnostics 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#rs-232-bridge-diagnostics-22-october-2026","title":"RS-232 bridge diagnostics, 22 October 2026","text":"

Follow-up: the RAK RAM repair and hardware checks close the Full/Reduced build failure below at adb11e7b4. Physical RS232 cable and line-driver testing remain outside that follow-up's scope.

Status: host tests and ESP32 Full build PASS. The RAK4631 Reduced sensor/LoRa OTA build passes, but its Full sensor build fails the RAM gate. The same Full failure occurs on the previous source, before these diagnostics. The required nRF52 Full/Reduced pair remains incomplete and is not a qualified release. No radio was flashed or contacted in this work.

The feature implementation is 281f1a519, with additional host UART platform adapters in df08877c1. The ESP32 build used the implementation source with those test-only adapters already present locally. The nRF52 build used committed df08877c1.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#behavior","title":"Behavior","text":"

The existing get rs232.status heartbeat/automatic-standby report remains. get rs232.transport adds UART state, baud, RX/TX pins, packet source, partial/pending frame state and historical errors. Separate RX, TX, drop and test counter pages distinguish framing/checksum failures, pool pressure, blocked/short writes, filters and duplicates. Statistics occupy 120 bytes inside the bridge object, with no new persistent preferences.

rs232 test [8 hex digits] queues a reserved, one-way UART control frame. The receiver consumes it without allocating a mesh packet, updating duplicate tracking, establishing heartbeat liveness, replying or forwarding onto LoRa. Confirm receipt using the other updated board's increased probe rx count and matching rxid; local UART write completion does not prove delivery.

Fault testing found that cancelling a partially emitted control frame could confuse an older receiver's following frame. The implementation cancels an entirely unsent probe at its deadline, but retains and drains an already-started frame's original suffix. It never retransmits the prefix or interleaves another frame. A timeout records one failure; a later local completion can also count as sent. Each loop's diagnostic work remains bounded.

Both usb.logging on and usb.debug on enable bounded RS232: summaries in ordinary bridge images, at most once per second when diagnostic state changes. CLI pages and test probes require neither logging gate. See the setup and diagnostic guide for exact commands.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#embedded-build-checks","title":"Embedded build checks","text":"

Canonical builds used version v1.18.0.1-halo-keymind-cascade-dev, Cascade runtime defaults and the USA Cascade preset. These were compile/package checks, with no physical UART, LoRa, USB capture or OTA operation.

Target/profile Available internal bytes Required heap bytes Result Heltec_v3_repeater, selected Full MQTT/UART/ESP-NOW image 209,472 203,920 PASS RAK_4631_repeater, Full sensors + LoRa OTA 82,324 85,152 FAIL RAK_4631_repeater, Reduced sensors + LoRa OTA 85,336 85,152 PASS Previous e7b863494, same RAK Full recipe 82,324 85,152 FAIL

The ESP32 Full image passes its application/merged package and linked capability checks with no feature reductions. Its largest internal region is 176,704 bytes against a 21,184-byte contiguous requirement. These are linked capacity estimates, not running free-heap or maximum-load measurements.

The RAK Full shortfall is exactly 2,828 bytes on both sources. No memory guard or reserve was lowered. The new nRF52 bridge allocation is 2,656 bytes, within the unchanged 2,816-byte source assertion. Reduced is a separate supported profile; its pass does not qualify Full or replace the failed required pair.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#host-validation","title":"Host validation","text":"

Physical cable/line-driver behavior, actual peer probe receipt, UART sleep rendezvous, USB debug capture and end-to-end LoRa delivery remain untested for this change.

"},{"location":"test-results/s3_memory_soak_validation/","title":"S3 memory soak validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/s3_memory_soak_validation/#esp32-s3-ota-memory-experiment","title":"ESP32-S3 OTA memory experiment","text":"

This experiment compares the current static S3 OTA workspace (A) with OTA_HEAP_CONTEXT=1 (B), with WiFi and a real TLS MQTT connection enabled. It is isolated on experiment/s3-ota-memory-soak; published recipes retain their existing defaults. A short stress test cannot establish months of uptime.

"},{"location":"test-results/s3_memory_soak_validation/#hardware-and-scope","title":"Hardware and scope","text":"Board Role Flash PSRAM Lab identity Heltec V4.3 OLED, standard V4 Observer/MQTT repeater 16 MiB 2 MiB QSPI USB MAC 44:1B:F6:69:CF:98 Original SenseCAP Indicator LoRa Full companion plus experimental MQTT 8 MiB 8 MiB octal WiFi MAC D8:3B:DA:75:23:AC Seeed XIAO ESP32-S3 with WIO LoRa Observer/MQTT repeater 8 MiB 8 MiB octal MAC 28:84:85:B4:09:80, Windows COM31

This is not an R8 test. Both Indicator variants use the same 320x320 canvas; the native 480x480 Full-plus-MQTT configuration is not qualified by this run. One custom TLS MQTT slot per board publishes only to the isolated Pi broker. These results do not qualify five concurrent slots, JWT renewal, heavy RF traffic, every OTA receive/apply path, or months of operation.

"},{"location":"test-results/s3_memory_soak_validation/#reproducible-builds","title":"Reproducible builds","text":"

Base commit: bc0fb2c335938c97220de51098cea6162f2f0fe0, following PR #7's merge. Copy tools/hil/s3_memory_soak.ini to ignored platformio.local.ini and build the six named environments sequentially, with MESHCORE_ESP32_FULL_BUILD=1. Never run multiple PlatformIO processes in this checkout. Archive each image, ELF, map, partitions, bootloader, and memory report before the next build.

The private broker's public CA must be in ssl_certs while generating these experimental images. Remove it and platformio.local.ini afterward. Private keys stay on the Pi. WiFi credentials and the Windows logger token are in private local files; do not commit them or the generated certificate bundle. Broker certificate verification remains enabled.

The V4 and XIAO lab recipes also enable MESH_SOAK_WIFI_KEY_OVERRIDE and require the ignored local header tools/hil/S3SoakWiFiKey.h. To reproduce diagnostics using ordinary WiFi credentials, remove that define from the lab recipe; the private header is then unnecessary. Published recipes do not enable either lab flag.

Image Application bytes Link-time available internal bytes SHA-256 V4 A 1,910,392 237,360 68c2749e0ea40db91d953f6355010bc602f85424526ca6acbe1cad7fd11c8630 V4 B 1,911,256 252,760 6847ad4ad6bac2e558b8aed67de37417a497972391f13a71e0a42b5c9704dcff Indicator A 2,313,256 244,248 3d0ac27f95ddc54a32b59a3892d157713dcded285dbeb98d1bdfdb1ce0b951ab Indicator B 2,314,136 255,496 83045d7ec48befc43f3460c08c68b71da037a5476e14dd59def5706fc5a1b07e XIAO A 1,845,000 238,448 6fcc657c5ccbc5dd3d53a0cf1ceebde54479d59d329f8625e7cb7141a27d9ed8 XIAO B 1,845,912 253,864 2b233e42e0c515b152ce83ca8fe38b1f35780970bb6350dbadf2591b6a2699a1

All six firmware builds passed their RAM/flash gates. The linked internal capacity gain is 15,400 bytes on V4, 11,248 bytes on Indicator, and 15,416 bytes on XIAO. This is capacity returned while idle, not a reduction in peak OTA allocation needs. Both A/B variants keep the existing MQTT client reuse policy and internal MQTT task stack; this experiment does not move that stack into PSRAM.

"},{"location":"test-results/s3_memory_soak_validation/#instrumentation","title":"Instrumentation","text":"

MESH_SOAK_DIAGNOSTICS exposes local-only get soak, soak ota-cycle, and soak wifi-drop commands. They report free/largest/minimum internal heap, free PSRAM, OTA context size/presence, WiFi state, and uptime. No published recipe enables these controls.

The lab network has a valid 64-digit raw WiFi key. Production MQTT preferences currently accept only 63 characters. The V4 experiment uses a separate, test-only NVS key through MESH_SOAK_WIFI_KEY_OVERRIDE, without changing the production binary preference layout. This does not fix released raw-key input. The network's original nine-character passphrase was subsequently recovered from the connected Windows profile; XIAO uses it through ordinary settings. The override is compiled into both XIAO images but no override key is stored.

The Pi logger owns one persistent V4 USB handle: opening this particular USB device resets it, even with DTR/RTS disabled. Firmware installations and logger restarts therefore begin new uptime runs. Indicator telemetry uses TCP 5002. The logger distinguishes the 49.7-day millis() rollover from a reboot.

Passive samples omit get mqtt.stats: its outbox query can block behind the MQTT client lock during a TLS handshake and stall the CLI. The logger reads get mqtt.status and separately counts messages received through a verified TLS subscription. It stores topic counts, not MQTT payloads or credentials.

Two exploratory rapid-flap runs used an overly short 90-second recovery cutoff. The first also used the blocking outbox probe. Preserve those pilot results; do not count them as successful tests. Production MQTT backoff deliberately grows to five minutes and clears after two stable connected minutes, so rapid flapping is not equivalent to repeated independent connection tests.

"},{"location":"test-results/s3_memory_soak_validation/#results-and-continuing-soak","title":"Results and continuing soak","text":"

V4 and Indicator both passed the matched A/B workload: 200 explicit OTA context acquire/release attempts, four MQTT bridge restarts, one WiFi reconnection and one five-second private-broker outage per image. An initial bridge restart preconditions each run. No unexpected uptime resets occurred in these runs. A was paused after its four bridge restarts to investigate the display report, then resumed for the network faults; its elapsed duration includes that pause.

After the workload, with WiFi and one TLS MQTT slot connected:

Board A free internal bytes B free internal bytes Gain A largest block B largest block Standard V4 95,396 111,040 15,644 (15.28 KiB) 81,908 98,292 Indicator 107,168 118,520 11,352 (11.09 KiB) 94,196 106,484 XIAO (same saved settings, cold boot) 151,028 166,444 15,416 (15.05 KiB) 139,252 147,444

Free heap varies with network activity; link-time capacity is the cleaner measurement of storage moved out of static RAM. PSRAM remained about 2.01 MB on V4 and 6.33 MB on Indicator. In each B run the OTA context returned to absent after the allocation workload. Neither board's largest block shrank through those 200 allocation cycles or the reconnect workload. WiFi/MQTT recovery took about 4.3 seconds on A and 3.8 seconds on B; broker recovery took about 19.5 and 21.8 seconds respectively. These are single observed recovery times.

Workspace recreation has an observed latency cost. The 200 command cycles took approximately 189 seconds on V4 B and 379 seconds on Indicator B, versus 32 and 15 seconds on A. This includes command transport, OTA initialization, and ordinary firmware service, so it is not an allocator microbenchmark. The static comparator already owns an initialized workspace; the dynamic variant recreates it. This experiment does not claim faster OTA startup, and this cost should be checked against real session use before a default change. Code inspection shows that OtaContext::begin() calls ota_self_firmware(), which scans the running ESP32 application for its EndF identity on each call. All six experimental images contain valid EndF trailers. Caching immutable boot identity is a candidate for a separate change, not part of these results.

The real WiFi mOTA folder endpoint was tested separately on Indicator: 20 empty-folder sessions on A and 40 on B. All completed and B released its context. During B's first batch, free internal RAM went from 118,552 to 116,316 bytes, and its largest block from 106,484 to 102,388. A second batch ended at 116,308 bytes with the same 102,388-byte largest block. This retained allocation/layout change is not explained by the now-absent OTA context; record it separately and watch it during the soak. It did not keep growing in the second batch.

The original A folder script also tried V4, whose observer recipe has no TCP 5001 folder source. Its overall result therefore says passed: false, despite Indicator completing all 20 sessions. That endpoint mismatch is preserved in the evidence; it is not an OTA failure on V4. The corrected script targets only the Indicator. These empty-folder checks do not qualify a full firmware transfer.

XIAO passed 200 context cycles, four bridge restarts and one WiFi reconnect per image, with no unexpected reset. Its largest block stayed unchanged within each workload. WiFi/TLS recovered in about six seconds for both. This local test did not inject a separate broker outage or exercise a TCP folder endpoint. Verified subscriptions on the Pi confirmed actual TLS publications for both.

The first XIAO A run followed initial device configuration and finished with 126,140 internal bytes free, which would have overstated the OTA saving. It is preserved as xiao-live/A-configuration-stress.json. Reflashing A without an erase, retaining exactly the saved settings used by B, and rerunning the whole workload produced the 151,028-byte baseline above. Its resulting gain exactly matches the 15,416-byte linked-capacity change. The earlier larger gap is not credited to OTA storage; the precise retained allocation was not isolated.

The Pi stores evidence under /home/mikec/hwtest/runs/s3-memory-soak:

meshcore-memory-soak.service holds the USB connection and samples each minute. meshcore-soak-broker.service supplies the private TLS broker. Both are enabled on the Pi. The logger never reboots a board to hide a firmware failure, but reopening the V4 USB transport can itself reset it; inspect transport counts, service restarts, and uptime together when diagnosing any interruption. The final passive phase started at 2026-09-11T07:18:07Z. Installing the final logger revision (which serializes concurrent samples) deliberately restarted the service and therefore reset V4 once. That begins its long-soak uptime run; Indicator retained its uptime. Both then reported WiFi/TLS connected, absent OTA contexts, and actual broker-side publications.

The local XIAO logger owns COM31 and writes under C:\\git\\MeshCore\\out\\s3-memory-soak\\xiao-live. Its control endpoint is loopback-only with a private token. It runs while this Windows session remains alive; it is not a Pi service and will not survive a computer shutdown. A separate verified TLS subscription on the Pi confirmed XIAO publications for both images. The Pi logger's per-device counts cover V4 and Indicator only. The final XIAO B passive phase was verified at 2026-09-11T07:21:21Z, with 166,508 free internal bytes, a 155,636-byte largest block, WiFi/TLS connected, and no active OTA context. The largest block differs from the post-stress sample because this is a fresh boot, which illustrates why comparisons must use the same workload history. A Pi subscription received its publication two seconds later.

Read-only checks on the Pi:

systemctl is-active meshcore-memory-soak meshcore-soak-broker\ncat /home/mikec/hwtest/runs/s3-memory-soak/latest.json\n

On Windows, read out/s3-memory-soak/xiao-live/latest.json. Keep the computer awake for continuous local samples. Neither logger automatically reboots a radio in response to a failed sample.

The public lab CA was removed from ssl_certs, and the temporary local PIO configuration and generated test trust bundle were removed after building. Archived experimental firmware and public evidence remain in ignored out/s3-memory-soak; no test CA enters the published build configuration.

No published default has changed. Leave the experimental B images running to collect longer evidence before deciding whether to expand S3 heap allocation. More idle internal RAM gives WiFi/TLS useful headroom; it does not itself prove stability or eliminate the need to allocate the OTA workspace during use.

"},{"location":"test-results/s3_memory_soak_validation/#physical-display-checks","title":"Physical display checks","text":"

The user reported unresponsive screens during MQTT restart testing. The V4 lit after temporarily choosing on; after restoring battery and USB profiles to button, 15 seconds, the user confirmed that pressing the button wakes it. The synchronous reconnect/diagnostic work may have delayed UI service, but the exact cause of the original visual report was not established.

The T096 is a separate nRF52840 board, USB identity 651F8E496197F882. It was reflashed with current Full firmware and briefly instrumented. The trace showed the radio, sensor and UI loop advancing; the display had initialized but was marked off. Its functional CDC0 was in Binary mode after a host-session close. Sending a newline-delimited +++MESHCORE-TERM-START restored ASCII control. With display mode set to on, the user confirmed the normal screen appeared. Temporary tracing was removed by reinstalling the non-instrumented image, and battery/USB profiles were restored to button-pairing, 15 seconds. The user then confirmed that its button wakes the normal screen after timeout.

T096 recovery firmware: v1.17.1-t096-check, Full/shared OTA queue, -Os size profile, available linked internal RAM 99,892 bytes (73,728 required). ZIP SHA-256 f5cfcb001efda559ae9b4e7b105ac9766a604808f5fd708b7c533f8a11c64578. No device-wide filesystem erase was needed for this recovery. T096 does not count as an S3 A/B test.

Host validation: python -m unittest discover -s tools/hil -p test_s3_memory_soak.py. The five checks cover diagnostic filtering, missing data, resets, and counter rollover. They are not substitutes for the hardware evidence.

"},{"location":"test-results/sensor_dog_tracker/","title":"Sensor dog tracker","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sensor_dog_tracker/#sensor-dog-tracker-validation","title":"Sensor dog tracker validation","text":"

The opt-in Sensor dog tracker was checked on 2026-10-10 using executable host tests and firmware builds. This report qualifies the code paths named below; no radio was flashed for these checks, and battery current, GPS acquisition outdoors, and live RF delivery were not measured.

"},{"location":"test-results/sensor_dog_tracker/#executed-regressions","title":"Executed regressions","text":"Suite Checks test_tracker_route_policy.py 99 boundary and byte-format assertions: six-hour gates, repeater postponement, sticky four-day cutoff, invalid clocks, overflow and restart. test_sensor_tracker_store.py 6,063 storage and failure scenarios over five filesystem API modes, including torn saves, unavailable storage, durable flood reservations and a monotonic request-tag floor. test_sensor_tracker_client.py 22 tests execute actual Tracker.cpp, peer dispatch, route/store/codec and packet hash implementation: setup, Safe/Lost/Unknown cadence, fresh GPS, transmit-based response deadline, exact owner identity, nonce reuse across reboot, path updates and packet ownership. test_tracker_owner_response.py Actual Companion responder across four feature configurations: persistent contact authentication, flags and padding, time-bounded position updates, rate limits, and preserved ordinary message state. test_sensor_tracker_gps.py 5 tests: fresh parser/PVT samples, GPS ownership, 120-second bound, validated UTC recovery after a cold start, stale sync notifications and saved preference restoration. test_sensor_tracker_sleep.py 4 tests: actual sleep gates, nRF52 timer allocation/queue failures, warm radio pause and resume, Dispatcher watchdog anchors, and feature defaults. test_sensor_subscriptions.py 18 tests: existing delivery behavior plus suppression in dog mode and restoration after leaving it. test_radio_interrupt_recovery.py 9 tests: radio recovery preserves the no-flood-retry policy when a missing transmit-complete interrupt could conceal a transmission.

Host C++ tests use AddressSanitizer and UndefinedBehaviorSanitizer where available. Negative controls intentionally remove guards and verify that assertions detect the resulting regressions. Transport, UART, receiver and filesystem hardware are replaced by bounded adapters; they do not qualify physical RF range or current draw.

The ordinary PlatformIO suites native, native_kiss_modem and native_ota_channel also passed: 1,829 test cases, with no failures.

The client checks also cover:

"},{"location":"test-results/sensor_dog_tracker/#firmware-builds","title":"Firmware builds","text":"

All six firmware builds passed their flash and runtime memory checks. Values below are linked firmware bytes, rather than measured running heap usage.

Environment Flash used / limit Static RAM used Runtime internal RAM available / required t1000e_sensor 377,520 / 811,008 64,400 171,112 / 54,320 RAK_WisMesh_Tag_sensor 421,084 / 815,104 66,336 169,176 / 54,320 heltec_v4_sensor 1,490,461 / 6,553,600 97,024 277,760 / 95,296 t1000e_companion_radio_usb 427,964 / 708,608 187,272 48,240 / 38,944 RAK_3x72_companion_radio_usb 229,000 / 229,376 39,780 24,728 / 17,408 wio-e5-mini_companion_radio_usb 229,012 / 229,376 39,804 24,704 / 17,408

The compact STM32 Companion retains its existing feature exclusions and application/storage boundary. Its limited remaining flash is checked by the existing CI build. The workflow now also builds T1000-E and RAK Tag Sensors and the ESP32-S3 V4 Sensor, in addition to the executable tracker regressions. The Wio-E5 Mini check uses the complete v1.17.1.9-halo-keymind-cascade-dev version string and USA Cascade profile.

"},{"location":"test-results/sensor_dog_tracker/#limits","title":"Limits","text":"

Bluetooth Find My/OpenHaystack advertising and automatic Room mailbox polling are not implemented by this change. RP2040's interrupt-driven physical wake has not been tested on a board. The GPS/RF and current measurements still needed on T1000-E and RAK Tag are listed in the tracker guide.

"},{"location":"test-results/separated_radio_modulation_cache_validation/","title":"Separated radio modulation cache validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#separated-radios-normal-xiao-rx-gain-and-unchanged-modulation-timing","title":"Separated radios, normal XIAO RX gain, and unchanged-modulation timing","text":"

Measured 2026-09-14 on keymindCascade, base commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus local changes.

The unchanged-modulation optimization reduces XIAO frequency-only retunes from 548.880 to 451.897 us (17.7%) in a same-image control. The separated four-channel SF6 test still misses packets. Separation and disabling XIAO boosted gain were changed together; their RF effects are not isolated.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#settings-and-controls","title":"Settings and controls","text":""},{"location":"test-results/separated_radio_modulation_cache_validation/#same-image-receive-only-timing-ab","title":"Same-image, receive-only timing A/B","text":"

Each board ran four three-second windows in off/on/on/off order, rebooting between windows. SF6/125 kHz, four channels, 2,612 us nominal dwell, no extra IRQ tracing, no TX. A HIL-only hook invalidates just the acknowledged modulation tuple before each baseline hop. Optimized windows use production cache behavior. The binary and all other settings remain identical per board.

Time is entry to the production retune through BUSY-low observation. It does not include the listening dwell or full application scheduling, and is not an independent RF settling measurement. Unlike the older mixed-bandwidth tests, this frequency-only workload can skip 0x8B.

Board / policy Hops Mean Min\u2013max Actual 0x86 / 0x8B writes XIAO, always write 1,898 548.880 us 535\u2013663 us 1,898 / 1,898 XIAO, reuse unchanged tuple 1,957 451.897 us 441\u2013580 us 1,957 / 0 Indicator, always write 564 8,263.438 us 8,228\u20138,428 us 564 / 564 Indicator, reuse unchanged tuple 602 7,601.193 us 7,583\u20137,762 us 602 / 0

Pooled means are sample-weighted from reported per-window means. Savings are 96.983 us (17.7%) for XIAO and 662.246 us (8.0%) for Indicator. All 5,021 hops passed RX-mode/software-cache checks with zero retune errors or deferrals. Counters observe outgoing SPI opcodes, not merely which software branch ran. Initial setup before counter snapshots is excluded. The Indicator remains above the common 6 ms switching allowance; no common timing budget changed.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#stationary-rf-controls","title":"Stationary RF controls","text":"

Full initialization before every case, fixed RX at 910 MHz, ordinary RX with fast reuse disabled, shuffled offsets, 450 ms capture windows. Method matches the earlier investigation.

TX offset SF6 preamble windows SF8 preamble windows Silent 0/4 0/8 On frequency 4/4 8/8 -250 kHz 0/4 1/8 +250 kHz 0/4 1/8 -500 kHz 0/4 2/8 +500 kHz 0/4 0/8 -1,000 kHz 0/4 0/8 +1,000 kHz 0/4 0/8

All 12 on-frequency probes delivered valid payloads. No off-frequency header or valid payload, silent preamble, device/read error, foreign valid payload, or trace overflow occurred. SF6 off-channel detections changed from 1/24 to 0/24; SF8 from 32/48 to 4/48. These are small bounded samples, not sensitivity or packet-error-rate estimates. Signal strength (RSSI) sampled on-frequency was -41 to -40 dBm at SF6 and -42 dBm at SF8: the signal remains strong, but these are not calibrated attenuation measurements. Improvement cannot be attributed solely to separation or solely to normal gain.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#four-channel-sf6125-51-symbol-visits","title":"Four-channel SF6/125, 5.1-symbol visits","text":"

Same image, baseline then optimized, reboot both boards between runs. Each requested 100 packets/channel with varied unsynchronized arrival timing and stopped at its first miss. Extra nominal 64 us IRQ polling was enabled here, unlike the receive-only timing windows. No higher channel count was tested.

Policy Received before first miss Failed channel Timed hops Mean retune Always write 0x8B 24, then probe 25 missed 1 / 909.750 MHz 4,746 551.253 us Reuse unchanged tuple 51, then probe 52 missed 1 / 909.750 MHz 7,748 455.157 us

Optimized RF run: 7,697 hops omitted 0x8B, averaging 453.081 us; 51 hops reapplied it after received packets, averaging 768.529 us. These are different RX lifecycle paths, not a pure 315 us command-cost comparison. Both runs had one frequency write per hop and no RX-mode/cache/retune errors. Successful packets read -42 to -40 dBm with SNR 10.0\u201311.5 dB. Neither met 100/100 on every channel. More packets before the first miss does not establish a reliable improvement in packet-error rate.

The baseline miss captured a target-channel preamble at 176,194 us after arming, 1,830 us after a hop onto that channel. A guard held it there; no header or packet followed. An IRQ clear was observed at 195,025 us and the next hop completed at 195,240 us. This is not the earlier SF8 wrong-channel hold: the preamble was observed on the intended channel. Its RF source and reason for failing to progress remain unproven.

The optimized miss captured 3,256 hops over ten seconds with no observed nonzero IRQ and no guard hold, reproducing the pre-acquisition symptom of the earlier SF6 miss. Target-channel revisit intervals were 12.249\u201312.324 ms (mean 12.286 ms), shorter than the 16.384 ms programmed preamble, yet no packet was acquired during those short individual visits. This does not establish that additional post-retune settling is needed. Trace buffers did not overflow. TX returned success for both failed probes; the MCU clocks are not synchronized and there was no independent RF capture.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#provenance","title":"Provenance","text":"

RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both targets built and uploads were verified. These captures precede the subsequent SF5/250 bandwidth-selector addition.

Tested firmware SHA-256:

Raw files in tools/hil, preserved byte-for-byte:

File SHA-256 XIAO timing 45e7a640323a3c9028b6c92a5128e51307db1bd94a4846e8744d3c3ec098adfd Indicator timing f0a85826f500b3a9173bf22c6207fbfba3b6cb17f3d70b849a9fe7621ae3dfc5 Stationary SF6 1d981e5af41eabbc43f92641813bf8f7c5e7015439f5ba53ce0467fe6477d3f3 Stationary SF8 36e63da7435d0a16f61db7db7730893de392d028feca7a96960fd17ae1fa624b SF6 baseline scan cbfd200170891b334ef3958479694c6e318146993b94073eb9aee981f97fde97 SF6 optimized scan d88a621ef9aa358ce52bb3efb6aa9c4bcb77c681af20b2c66e4aebf3721e17ca

Fresh complete 8 MiB backups were saved and separately verified in the private current-user backup directory before testing. Restores had already started when the user requested leaving HIL firmware installed; those in-flight writes and verification finished safely. Both boards were subsequently loaded with the newer SF5/250 HIL build for the user's next sweep. Do not restore them again unless requested. No private backup contents were added to Git.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/","title":"Sf10 10 vs 100hz full sample validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#sf10125-full-samples-10-hz-versus-100-hz-first-pass-detour","title":"SF10/125 full samples: 10 Hz versus 100 Hz first-pass detour","text":"

2026-09-14. Both 400-packet samples completed: 100 attempts on each of four channels, with no RF retries or early stop on misses. Payload loss was 64/400 (16.0%) at 10 Hz and 81/400 (20.25%) at 100 Hz. Strict same-channel reception was almost identical: 230/400 versus 231/400. The larger offset showed no improvement in this comparison.

These are two sequential finite bench samples, not proof that the offset caused the difference in payload loss. The earlier stop-first tests could not provide this error-rate estimate and are not combined with these counts.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#settings-and-method","title":"Settings and method","text":"

The complete comparison ran from 21:09:11 to 21:42:39 UTC (14:09\u201314:42 PDT), about 33 minutes. The 10 Hz sample took about 15.4 minutes; 100 Hz about 18.0. The extra elapsed time is largely the additional ten-second timeouts.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#overall-results","title":"Overall results","text":"

\"Payload loss\" means no intact expected payload was delivered within the expectation window, regardless of reported RX channel. \"Strict error\" also counts intact payloads received while the scanner reports another channel.

Requested offset Attempts Correct-channel delivery Intact wrong-channel delivery Timeouts / payload loss Strict errors 10 Hz 400 230 (57.5%) 106 (26.5%) 64 (16.0%) 170 (42.5%) 100 Hz 400 231 (57.75%) 88 (22.0%) 81 (20.25%) 169 (42.25%)

All scored delivery failures were timeouts. The receiver also counted six packet-read errors at 10 Hz and eleven at 100 Hz. The error codes were not captured, so these cannot be identified specifically as CRC failures or mapped one-to-one to timeout probes. \"Zero invalid payloads\" in the summary does not mean there were no packet-read/CRC errors before timeout.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#per-channel-results","title":"Per-channel results","text":"

Each cell uses exactly 100 attempts. Loss counts therefore also equal percent.

Center (MHz) 10 Hz payload loss 100 Hz payload loss 10 Hz strict errors 100 Hz strict errors 909.5 10% 11% 26% 25% 910.5 23% 23% 51% 45% 911.5 19% 26% 48% 55% 912.5 12% 21% 45% 44%

The extra 17 payload losses at 100 Hz occurred on channels 0 (+1), 2 (+7) and 3 (+9). Correct-channel deliveries differed by only one packet overall. Neither offset eliminated wrong-channel acquisition/delivery or timeouts. This comparison does not isolate the underlying RF failure mechanism.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#switching-and-signal-measurements","title":"Switching and signal measurements","text":"Metric 10 Hz 100 Hz Completed hops 16,992 20,676 Mean complete two-pass switch 1263.765 us 1262.551 us Maximum complete switch 1533 us 1532 us Mean first pass 660.712 us 659.458 us Mean second pass 592.344 us 592.406 us RF commands 67,968 82,704 Modulation commands / RX starts 33,984 / 33,984 41,352 / 41,352 Maximum unheld four-channel cycle 174,139 us 173,722 us Maximum held channel visit 590,757 us 640,492 us Packet-read errors 6 11

Unheld cycles fit within the 262,144 us programmed preamble; packet-related held visits can exceed it. Aggregate timing without IRQ traces does not prove which hold caused any particular timeout. The approximately 1.2 us difference in mean switching time is negligible here and does not explain the losses.

All completed hops passed the internal first-offset/final-correction and CR command checks: 16,992/16,992 and 20,676/20,676, detour.bad=0. Second-pass blocks, retune failures, mode errors and cache errors were zero. Counts match four RF writes, two modulation writes and two RX starts per completed hop.

Telemetry limitation: the diagnostic detour.first_rf/first_mod fields are updated on a guard-deferred first attempt as well as an applied attempt, while final_rf/final_mod retain the last completed second pass. Thus the final snapshot's displayed pair can be from different attempts and appears equal in these captures. Those fields must not be interpreted as the last verified pair. Verification itself runs only after two applied passes and increments detour.n only then; its per-hop comparisons, zero failures and matching counts establish the commanded sequence. Original capture bytes are retained.

Packet group 10 Hz mean RSSI / SNR 100 Hz mean RSSI / SNR Correct-channel delivery -34.05 dBm / +8.56 dB -34.15 dBm / +8.62 dB Intact wrong-channel delivery -105.18 dBm / -13.75 dB -105.22 dBm / -13.62 dB

The markedly weaker wrong-channel readings are observations, not proof of a specific filter/image/overload mechanism.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#implementation-and-verification","title":"Implementation and verification","text":"

The HIL scanner gained scancontinue 1, default off, so its ordinary completion and timeout accounting can leave scanning active. The host gained bounded --continue-on-miss --max-channels 4 with separate delivery/strict summaries. scandetouroffset 10|100 and --first-pass-offset-hz 10|100 select the integer offset while stopped. Production radio defaults and receive guards were not changed. The controller is tools/hil/profile_switch_offset_compare.py.

23 native profile tests and 51 host tests passed. Coverage includes continuing through all 400 attempts without restart/retry, balanced channels, separated error definitions, default first-miss behavior, aborting fixture failures, both integer offsets and exact restoration. Build/upload succeeded with hash verification: RAM 94,948 bytes, reported flash 370,213 bytes.

There was one cached USB RX-result replay in each sample, with no RF retransmit. All TX commands returned success. Host/device strict receive and miss counters matched; both captures are complete under fixed_sample_complete. The master manifest is complete under both_400_packet_samples_complete, with no fixture or cleanup error.

Both boards rebooted idle afterward. RX confirmed scanner inactive, zero channels, one-pass default, detour/continue disabled and normal gain 0x94. TX confirmed no autonomous TX. HIL remains installed; RAK COM29 was not opened. The temporary PlatformIO overlay was removed. No background collector remains. This work made no commits or pushes. Existing unrelated changes were preserved.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#provenance","title":"Provenance","text":"

Source: live keymindCascade working tree at HEAD 2b5a6f209835f0f67ed73fee78d36447a4566ce6, plus existing dirty changes and these HIL additions. The RX image hash identifies the exact tested build. Earlier captures were not overwritten.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/","title":"Sf10 10hz cr detour validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#sf10125-tiny-first-pass-frequency-offset-and-coding-rate-detour","title":"SF10/125: tiny first-pass frequency offset and coding-rate detour","text":"

2026-09-14. The requested offset no longer rounds to zero. Actual outgoing commands verified a +10-step RF-word change (nominal +9.536743 Hz), then exact restoration of the original word, with CR4/6 then CR4/5. Two passes averaged 1.259 ms in RX-only timing. The first packet still timed out. This is a bounded one-probe failure, not a 400-packet loss-rate estimate.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#scope-and-precision","title":"Scope and precision","text":"

The request was to apply a slightly different center frequency and coding rate on the first full retune, then the correct settings on the second, with about 0.01 kHz difference. At these centers a float in MHz advances in roughly 61 Hz increments: adding 0.00001 MHz can produce the same float.

The HIL implementation does not make that float addition. A scoped integer offset changes the four-byte argument of the actual 0x86 frequency command by ten steps. The nominal SX1262 step is 32 MHz / 2^25; ten steps are 9.5367431640625 Hz, the nearest achievable nominal command offset to 10 Hz. See the frequency-step specifications in the Semtech SX1261/2 datasheet. Pinned RadioLib 7.7.1 also specifies this step and sets the standard sensitivity configuration bit for bandwidths other than 500 kHz. This establishes the programmed offset, not a spectrum-analyzer measurement of physical frequency.

First pass: destination RF word + 10 and CR4/6. Second pass: original RF word and CR4/5. The offset is applied to both existing duplicate frequency writes inside the first pass. Caller buffers remain unchanged; a scoped restore prevents the offset leaking into the corrected pass or an unrelated command. Rollback applies the offset only when restoring the temporary CR4/6 tuple.

Both passes retain ordinary packet/ownership/BUSY guards, command-result checks and RX startup. A blocked second pass fails the diagnostic rather than overriding a packet guard. Each completed hop verifies both outgoing frequency words and modulation words (0x0a040200 then 0x0a040100). The host requires a verification count equal to completed hops and zero mismatches.

This feature is HIL-only, off on reboot. Production defaults were not changed. The earlier +31.25 kHz draft was superseded before flashing or RF testing.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#fixture-and-method","title":"Fixture and method","text":"

Same-image RX-only ABBA controls compare two identical full passes (A) with the first-pass detour followed by correction (B), six seconds each, rebooting between windows. This is not a comparison against the original single-pass approximately 0.53 ms cached-modulation path.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#timing","title":"Timing","text":"Window First-pass detour Hops Mean total (us) Maximum (us) First pass mean (us) Second pass mean (us) A1 Off 139 1230.094 1464 631.727 594.281 B1 On 139 1259.770 1490 647.165 603.245 B2 On 139 1258.806 1481 646.547 602.957 A2 Off 139 1230.597 1467 633.209 593.252

Mean two-pass control 1230.346 us; detour/correction 1259.288 us, about 28.943 us additional total including configuration/verification bookkeeping. The detour passes themselves averaged 646.856 + 603.101 us. Both detour windows verified 139/139 completed hops with zero mismatches; all four windows had exactly 556 RF commands, 278 modulation commands and 278 RX starts, with zero guard blocks or RX/mode/cache errors.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#packet-result","title":"Packet result","text":"

Probe 1021099889, TX channel 3 at 912.5 MHz, timed out: 0 received / 1 attempted, then stopped. TX rc=0, setup 86,914 us, transmit call 529,297 us. RX was in receive mode, hardware device-errors=0. No payload-only diagnostic ran because this was a timeout, not an intact payload attributed to another channel. There were no RF retries.

During that packet window:

Across both detour timing windows and the packet window, 498/498 completed hops verified the nonzero first-pass RF offset, original final RF word and CR4/6-to-CR4/5 sequence. This proves the change was exercised; it did not resolve reception in this bounded test. The underlying RF failure remains unidentified.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#usb-setup-issue-and-verification","title":"USB setup issue and verification","text":"

The first controller attempt stopped before transmitting because the new 126-byte capability JSON plus CRLF formed an exact 128-byte native-USB reply. It remained buffered until another command was sent. This was preserved as a fixture failure, not counted as packet loss. A single LF-terminated write (127 bytes) fixed this specific reply; three independent reads then passed without a follow-up command, and the complete controller ran successfully.

22 native profile tests and 49 host tests passed. Added tests cover integer carry, unchanged caller bytes, both SPI paths, observation of shifted wire bytes, exact restoration, scope exit/nesting, non-frequency commands, overflow, detour verification failures and the USB reply length. Build/upload passed with flash hash verification: RAM 94,948 bytes, reported flash 369,593 bytes. git diff --check passed (unrelated existing CRLF warning only).

Both boards rebooted idle after completion. RX confirmed scanner inactive, zero channels, one-pass default, detour disabled, normal gain 0x94. TX reports no autonomous transmission. HIL firmware remains installed as requested. RAK COM29 was not opened. Temporary PlatformIO overlay removed; no collector left running. This experiment made no commits, pushes or production-default changes.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#provenance","title":"Provenance","text":"

Successful manifest complete, stopped bounded_tests_complete; strict capture complete under first-miss stopping. No fixture or cleanup error in that run. The build used the live keymindCascade working tree with existing dirty changes and these HIL additions, not a clean-release image. HEAD observed at completion was 2b5a6f209835f0f67ed73fee78d36447a4566ce6; the unrelated Companion USB commit advanced HEAD from the earlier experiment's base during this ongoing work. Its files were not reverted. The RX image hash above is the exact tested-image identifier. Prior captures were preserved.

"},{"location":"test-results/sf10_full_retune_twice_validation/","title":"Sf10 full retune twice validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_full_retune_twice_validation/#sf10125-apply-the-full-radio-settings-twice-per-hop","title":"SF10/125: apply the full radio settings twice per hop","text":"

2026-09-14. Two full retune passes were verified, averaging 1.276 ms per channel switch. This did not resolve reception in the bounded packet test: the first packet timed out. This is one failed probe, not a 400-packet loss measurement. No production defaults or receive guards were changed.

"},{"location":"test-results/sf10_full_retune_twice_validation/#requested-interpretation-and-fixture","title":"Requested interpretation and fixture","text":"

\"The 125 run\" was interpreted as uniform SF10/125 on four channels, not the preceding mixed-SF/BW experiment. Apply the complete retune twice to each destination: frequency, modulation, preamble/packet configuration and RX restart, retaining normal guards and command-result checks for both passes.

The earlier approximately 0.53 ms path could skip unchanged modulation. This test deliberately writes those settings again, so it measures more than 0.53 + 0.53 ms. No artificial time padding was added to meet an expected value.

"},{"location":"test-results/sf10_full_retune_twice_validation/#same-image-rx-only-timing","title":"Same-image RX-only timing","text":"

Six-second windows in 1/2/2/1-pass order, with a reboot before each. Both one- and two-pass controls force modulation writes, so the comparison isolates complete repetition rather than also changing cache policy.

Window Passes per hop Hops Mean total (us) Max total (us) Mean first pass (us) Mean second pass (us) A1 1 141 647.284 847 644.220 \u2014 B1 2 139 1275.410 1491 648.899 622.835 B2 2 139 1276.086 1488 649.000 623.288 A2 1 141 647.858 836 644.872 \u2014

Equal-weight/hop-count means: one full pass 647.571 us; two full passes 1275.748 us. The first and second passes average 648.950 + 623.062 us; the total includes approximately 3.7 us additional wrapper/BUSY bookkeeping. Both two-pass windows had 556 frequency writes, 278 modulation writes and 278 optimized RX starts. No second-pass guard block or hardware/cache error.

"},{"location":"test-results/sf10_full_retune_twice_validation/#packet-test","title":"Packet test","text":"

First probe: TX channel 3, 912.5 MHz, sequence 600920621. TX returned rc=0, setup 86,914 us, transmit call 529,297 us. RX timed out with mode=1 and device errors=0. The controller stopped without retrying or testing further packets. No separate payload-only diagnostic ran because this was a real timeout, not an intact wrong-channel delivery.

During the packet window:

This shows that completing two real settings applications does not by itself guarantee successful reception in this fixture. It does not prove every packet would fail or independently identify the cause of this particular timeout.

"},{"location":"test-results/sf10_full_retune_twice_validation/#implementation-and-verification","title":"Implementation and verification","text":"

HIL BenchWrapper::hop performs the first ordinary guarded tuneProfile, then waits boundedly for BUSY-low and requests a refresh of the same profile before the second ordinary guarded transaction. This bypasses only the same-generation no-op, not receive ownership or packet-in-progress checks. The modulation cache is invalidated for each requested full pass.

If the second pass is blocked by newly acquired reception, the diagnostic fails closed. Returning ordinary BUSY would wrongly imply that no channel change occurred even though pass one had completed; it is therefore treated as a fixture failure, not silently skipped or counted as an RF loss.

scanretunepasses 1 is the reboot default. scanretunepasses 2 and --retune-passes 2 --modulation-cache off select this bounded test. The standalone controller is tools/hil/profile_switch_retune_repeat.py. 19 native profile tests and 49 host tests passed, including real refresh, first/second failure handling, BUSY timeout, no guard override, and rejection of missing physical write/resume counts. git diff --check passed with only unrelated existing CRLF warnings.

Receiver build/upload succeeded with hash verification: 94,908 bytes RAM, 368,389 bytes reported flash. Transmitter firmware was not changed or reflashed. Unrelated RAK COM29 was not opened. Both boards rebooted idle after completion, retaining HIL firmware; no autonomous TX or background collector. Temporary PlatformIO overlay removed. No commit/push requested or performed.

"},{"location":"test-results/sf10_full_retune_twice_validation/#provenance","title":"Provenance","text":"

Manifest complete, stopped bounded_tests_complete; strict capture complete under first-miss stopping. No fixture or cleanup error. Previous captures were not rewritten. Source remains keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus existing dirty changes and these HIL-only additions, not a clean-release comparison.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/","title":"Sf10 nrf52 double write settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#sf10-125-khz-xiao-nrf52-tx-double-write-1-ms-delay-grid","title":"SF10 / 125 kHz, XIAO nRF52 TX, double write, 1 ms delay grid","text":"

2026-09-14. Fixed-channel positive control passed 100/100. The subsequent four-channel, double-write sweep failed at every 1 ms step from +0 to +23 ms.

New sender serial B35E71C1C3726CE7 first enumerated as Zephyr CDC COM33 (2FE3:0004). A 1200-baud touch exposed Seeed XIAO bootloader COM25 (2886:0044, XIAO-BOOT). The application-only DFU ZIP was accepted with its normal compatibility checks, reported programmed successfully and reappeared as COM15 (2886:8044), with the same serial and ready XIAO nRF52840/Wio SX1262 HIL identity. No bootloader/SoftDevice replacement or full erase was performed. The unrelated RAK COM29 (4D6F77FD74011872) was not opened.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#requested-test","title":"Requested test","text":"

Budget uses 539 us nominal double-write retune (ceil of the measured 538.192 us RX-only mean), 41,780 us post-settle visit and 262,144 us preamble: 262144 / 4 - 41780 - 539 = 23217 us. Last 1 ms grid point is 23,000 us. This is a nominal ceiling, not a worst-case jitter or zero-loss guarantee.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#preparation","title":"Preparation","text":"

The settling controller now accepts --step-us 1000 --frequency-repeat on; its existing defaults are unchanged. Capture policy checks reject mixed single/double evidence. The 42 host collector tests and 14 selected profile native tests passed, including grid/first-pass stop, nRF USB DTR behavior, transmitter replay, setup/TX failure, stationary payload integrity, zero-retune and host/device-count checks. git diff --check passed (unrelated CRLF warnings).

Minimal profile_switch_xiao_nrf52_tx firmware compiled successfully: 95,340 bytes reported flash usage, 18,212 bytes static RAM. It uses the production XIAO pin map and CustomSX1262 initialization, exposes only reference TX and cached USB results, and has no Mesh identity or application settings writes. Includes the existing build-local bounded nRF USB READY/HFCLK fix.

Platform: Nordic nRF52 10.8.0; Adafruit nRF52 framework 1.10701.0+sha.d5413016; ARM GCC 14.2.1; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c.

Generated DFU ZIP contains application only, requiring SoftDevice ID 0x0123. Upload used normal DFU validation without compatibility overrides. The V4's earlier no-backup erase authorization is not treated as authority to erase this new board's bootloader or unrelated storage.

Flashed nRF52 transmitter SHA-256:

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#baseline-requested-before-continuing-the-sweep","title":"Baseline requested before continuing the sweep","text":"

The initial four-channel run was interrupted at the user's request after 13 completed settings, added delay 0..12,000 us. Each missed its first packet: nine matched the test sequence but were reported on channel 2 when channel 3 was expected; four timed out. These are not 13 full 400-packet measurements and must not be presented as an unbiased loss rate. The incomplete original top manifest and all raw captures under tools/hil/profile_switch_sf10_nrf52_double_1ms_results* remain unchanged.

Added an independent HIL-only stationary receiver: one full normal radio initialization, fixed 910.25 MHz (channel 3), normal RX rearm, no profile scheduler/fast-switch/cache shortcut. All 100 packets use the same transmitter, SF10/125, CR4/5, 32 preamble symbols, 16-byte CHS1 body and -9 dBm request. No RF retries and no early stop on an RF miss; hardware/fixture faults abort.

Result: 100/100, CRC/read errors 0, foreign packets 0, hardware errors 0, observed frequency writes after setup 0. Normal gain register 0x94 and RX mode 1 verified. RSSI mean -33.610 dBm, range -34..-33; SNR mean +8.736 dB, range +7.8..+10.0. Baseline cleanly stopped and both devices rebooted idle. This confirms the fixed-channel link in this finite sample, not all-channel or interference immunity.

Capture: tools/hil/profile_stationary_nrf52_sf10_100_results.json. SHA-256: 547c30efc89c29e3f60cb975f7fc34de3e9f14bed3ed4cc9f1ba01f63402bf15. Receiver image with stationary capability compiled/flashed and hash-verified: SHA-256 0daab2e3293359280714042492639ab8c176609143cf01996ad4ba7026704768. Reported flash 364,657 bytes; static RAM 94,796 bytes. Existing double-write and four-channel switching implementation is unchanged; baseline is a separate inactive command mode during the scan.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#post-baseline-four-channel-run","title":"Post-baseline four-channel run","text":"

Fresh prefix: tools/hil/profile_switch_sf10_nrf52_postbaseline_double_1ms_results. Command: profile_switch_settling.py --receiver COM31 --sender COM15 --sf 10 --frequency-repeat on --step-us 1000 --output <fresh-prefix>.json. No old captures are reused. Stop at the first 400/400 or after +23 ms.

Completed all 24 settings. No 400/400 pass. At zero added delay, the first packet passed and the second timed out. Each +1..+23 ms setting failed its first packet. Total 25 probes: one valid, 17 timeouts and seven test-sequence packets reported on channel 2 while expecting channel 3. Each setting stops on its first miss; this is deliberately censored evidence, not a measured unbiased 4% packet-delivery rate. The lone valid packet was -33 dBm / +8.2 dB SNR. The wrong-channel records do not alone distinguish real off-channel acquisition from channel bookkeeping; no root cause is claimed.

Measured 3,086 retunes, weighted mean 526.783 us (0.527 ms), maximum 876 us (0.876 ms), excluding the separately applied settling delay. Exactly 6,172 SetRfFrequency commands confirm two per hop. One modulation command was issued across the measured scan windows (after the sole received packet); no retune failures, RX-mode errors, cache errors or CRC/read errors. All gain checks remained 0x94. This measurement is from the receiver image with the new independent baseline mode, not a controlled performance A/B against the previous image's 538.192 us RX-only mean.

At +23 ms the measured settling interval was 23,000..23,001 us and mean switch-plus-settle 23,567.2 us. That short failed window did not contain a complete unheld scan cycle, so it provides no measured final-step full-sweep latency. The ceiling remains the nominal 23,217 us calculation above.

Post-baseline top manifest SHA-256: badda7f64a7bdd445942da19dc76dea0d1aace2ae451772edf4744d4670ac28c. Interrupted pre-baseline top manifest SHA-256: 90f7b213bc44ae73714c5d9d32216ff3f1760fafd657feb5ea9299ed1e10ddf3.

Conclusion: the fixed-channel link passes its 100-packet control, but extra post-switch settling through the nominal budget does not cure this scanner failure. Investigate acquisition/hold behavior and channel attribution next; do not claim more waiting, a second frequency write, or a transmitter swap has fixed the issue. No guard/default production timing was changed here.

Collector exited successfully at the nominal timing limit, with no cleanup error; both radios rebooted idle. The previous follow-up automation remains paused. No commit or push requested; HIL firmware remains installed. Temporary build overlay removed. No background test collector remains running.

"},{"location":"test-results/sf10_rollback_failure_validation/","title":"Sf10 rollback failure validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_rollback_failure_validation/#sf10125-rollback-and-wider-spacing-failure-isolation","title":"SF10/125 rollback and wider-spacing failure isolation","text":"

2026-09-14. No strict four-channel 400/400 pass. Reverting the switching optimizations did not remove failures. Off-channel reception also occurs with a fixed, fully initialized receiver, without any channel switching. A traced timeout shows an off-channel preamble holding the scanner away from the intended channel until after its transmit preamble. Wider spacing did not eliminate either wrong-channel delivery or actual missing packets.

This isolates a concrete scanner failure mechanism, not the underlying physical RF cause. Commanded frequency words are observed, but actual emitted spectrum, antenna-port power, image responses and overload have not been independently measured. Do not claim front-end saturation or a specific spur as proven. Do not treat these first-failure-censored samples as unbiased PER.

"},{"location":"test-results/sf10_rollback_failure_validation/#fixture-and-requested-controls","title":"Fixture and requested controls","text":"

Rollback modes are runtime HIL controls on the same current source, not historical firmware checkouts. No production default or receive safety guard was changed during this investigation. Tests preserve ownership, packet-in- progress, BUSY and command-result checks. Existing unrelated working-tree changes were left intact. No commit or push was requested this turn.

"},{"location":"test-results/sf10_rollback_failure_validation/#maximum-practical-settling-control","title":"Maximum practical settling control","text":"

At SF10/125, a symbol is 8,192 us, the programmed preamble is 262,144 us, and each 5.1-symbol post-settle visit is 41,780 us. For each rollback mode:

  1. Measure six seconds of RX-only switching without added delay.
  2. Choose the largest 100 us delay step satisfying 4 * (41780 + observed_max_switch_us + added_delay_us + 500) <= 262144.
  3. Measure another six seconds; require at least eight full idle cycles and maximum observed idle cycle below the preamble before allowing TX.

The 500 us per-hop reserve covers some observed scheduling variation, not all possible jitter. RX is active during added settling. Near-ceiling cycle timing is not itself a guarantee of acquisition for every arrival phase, especially when an IRQ causes an extended packet hold. Idle-cycle statistics exclude guard/packet-held visits; those are measured separately.

"},{"location":"test-results/sf10_rollback_failure_validation/#250-khz-spacing-eleven-rollback-cases","title":"250 kHz spacing: eleven rollback cases","text":"

Each case stopped on its first probe, TX channel 3, with a real timeout. No strict 400/400 pass, TX fault, RX mode/cache failure, or hardware error. The first nine cases used the checkout's existing 1,600 us TCXO setting; the final two explicitly restored 6,000 us. Production timing was not edited.

Case Added delay (us) Zero-delay mean switch (us) Max settled idle cycle (us) Fast, cached modulation, double frequency write 22500 535.88 259297 Always write modulation 22500 625.50 259682 Individual SF/BW/CR writes 22200 840.97 259346 Full normal RX restart 22200 896.89 259577 Full RX + individual modulation 22000 1113.95 259623 Above + byte-wise SPI, warm XOSC 21800 1215.79 259220 Above + RC standby, 1.6 ms TCXO 20100 2942.50 259339 Fast, cached modulation, single frequency write 22600 450.25 259341 RC/full RX/individual/byte-wise, single write, 1.6 ms TCXO 20200 2837.50 259329 RC/full RX/individual/byte-wise, double write, 6 ms TCXO 15800 7293.25 259550 RC/full RX/individual/byte-wise, single write, 6 ms TCXO 15800 7190.31 259139

Actual frequency/modulation command counts and fast-RX counts verified each policy. Slower modes received less added delay to preserve the total sweep budget. These are eleven probes with eleven timeouts, not eleven complete 400-packet runs. Packet arrival pauses use the same seeded scheduling method but not hardware-synchronized identical RF phases across modes.

"},{"location":"test-results/sf10_rollback_failure_validation/#fixed-frequency-controls-off-channel-responses-without-switching","title":"Fixed-frequency controls: off-channel responses without switching","text":"

Each pair received three independent probes after full normal initialization. Positive controls were interleaved. The receiver remained at one frequency: zero observed frequency commands after setup, no hopping or cache shortcut. Payload success requires the exact expected sequence/channel/body, length 16 and clean CRC. It does not require RX center to equal TX center in this explicit offset diagnostic. Offset below means programmed RX minus TX.

Spacing RX-TX offset (kHz) Probes Intact expected payloads Preamble IRQ seen 250 kHz 0 6 6 6 250 kHz -250 3 2 3 250 kHz -500 3 0 3 250 kHz -750 3 0 3 250 kHz +250 3 0 2 1 MHz 0 6 6 6 1 MHz -1000 3 2 3 1 MHz -2000 3 0 0 1 MHz -3000 3 3 3 1 MHz +1000 3 3 3

At 250 kHz spacing, the two valid -250 kHz-offset packets reported RSSI -91/-90 dBm and SNR -12.8/-10.8 dB. At 1 MHz spacing, valid off-channel packets reported RSSI -102..-109 dBm and SNR -11.5..-15.8 dB. Wider on-channel controls were all -33 dBm, SNR +8.0..+8.8 dB. These are radio-reported values, not calibrated measurements of transmitted spurs or receiver rejection.

The non-monotonic offset pattern means that increasing spacing alone cannot be assumed to remove this response. These fixed-frequency tests rule out a channel-hop shortcut as a necessary condition for the observed off-channel packets; they do not establish which physical component produces them.

"},{"location":"test-results/sf10_rollback_failure_validation/#a-traced-real-timeout-at-250-khz-spacing","title":"A traced real timeout at 250 kHz spacing","text":"

Fast/double-write RX, +22,500 us settling. One channel-3 probe timed out with TX rc=0, RX mode=1 and device errors=0. The trace contains 454 events, overflow 0. Trace adds 64 us IRQ polling; use it for ordering, not an unperturbed timing A/B comparison. Times below are relative to receiver expectation arming.

RX time (ms) Evidence 169.190 Frequency command for channel 3, 910.25 MHz 234.032 Leaves channel 3 for channel 0 298.869 Frequency command for channel 1, 909.75 MHz 330.030 PREAMBLE_DETECTED (0x0004) while on channel 1 363.632 Requested next hop is held by receive-in-progress guard 726.734 Guard clears the lingering preamble IRQ 726.826 Frequency command for channel 2 791.654 Finally commands intended channel 3 again 814.586 Channel-3 hop/settling completes

TX setup was 86,914 us and its transmit call 529,297 us. Host/firmware timing places TX RF start approximately 287..289 ms after RX arming and the end of its programmed preamble around 550 ms. This estimate is not a synchronized RF measurement, but the receiver's several-hundred-millisecond absence from the intended channel is much larger than USB timestamp uncertainty.

The held channel-1 visit measured 427,395 us, versus normal idle four- channel cycle maximum 259,358 us. The guard timeout is consistent with RadioLibWrapper::calcMaxPacketMillis: (32 + 8 + 4.25) * 8192 = 362496 us, rounded to 363 ms. CustomSX1262::isReceiving() starts that timer when the guard first observes the preamble, here later than the diagnostic IRQ poll.

Thus an off-channel preamble can monopolize the scanner long enough to miss the real channel. Do not simply remove or shorten this safety guard as a production fix: it also protects legitimate weak or long-preamble reception.

"},{"location":"test-results/sf10_rollback_failure_validation/#1-mhz-spacing-strict-results-versus-payload-delivery","title":"1 MHz spacing: strict results versus payload delivery","text":"

Three strict rollback cases (fast double, fast single, fully rolled back single/6 ms) each stopped at their first probe. All three actually decoded the intact channel-3 payload while programmed for channel 2: RX 911.5 MHz, TX 912.5 MHz, RF word at read 955777024. They are channel mismatches, not lost payloads. RSSI was -102/-101/-101 dBm; SNR -11.5/-11.5/-11.2 dB.

An explicitly separate payload-delivery diagnostic then continued through such packets while retaining strict validity in raw evidence:

Mode Added delay Delivered before stop Strict same-channel Stop Mean switch Max idle cycle Fast, cached, double write 22.5 ms 3 of 4 probes 0 TX channel 0 timed out 557.721 us 259381 us RC/full RX/individual/byte-wise, single write, 6 ms TCXO 15.8 ms 2 of 3 probes 0 TX channel 1 timed out 7310.510 us 259643 us

Every delivered packet in these two runs arrived one programmed channel below its transmitter. Both runs had zero mode/cache/RX/device errors and actual switch-policy counts matched. Neither is a 400/400 pass, even under the relaxed delivery-only criterion. The true timeout in the fully rolled-back path also shows the problem is not limited to the accelerated RX restart or 0x8B cache. No traces were enabled for these two runs, so do not assign their individual timeouts the exact IRQ timeline from the earlier traced probe.

"},{"location":"test-results/sf10_rollback_failure_validation/#changes-and-verification","title":"Changes and verification","text":"

Next useful isolation is the physical off-channel response (controlled RF attenuation or reversed radio roles), followed by safe acquisition/hold-policy experiments. More settling delay alone does not address the demonstrated false-hold mechanism. No claim of an identified silicon defect or verified production fix is made here.

"},{"location":"test-results/sf10_rollback_failure_validation/#evidence-provenance-sha-256","title":"Evidence provenance (SHA-256)","text":"

Raw JSON files below are under tools/hil/; prior captures were not rewritten. Matrix manifests include per-case paths and timing controls. All listed new experiments completed under their stated stopping policies without fixture or cleanup errors. Completed first-failure runs still have incomplete 400-packet levels by design.

Capture SHA-256 profile_switch_sf10_rollback_results.json e87c504b3c89fbafe90c18fcd0ab37df34a6aeb51d95b5c69f58d60f70de185a profile_stationary_sf10_offset_results.json d3d87315513a70fc3f00cc7f17369043328f53ee3028d499087b21e767370ab7 profile_switch_sf10_rollback_fast_trace_results.json 8ac9db50f0f06eb7511d7d233957119618df62afdc7112dd83eb68d292fcd725 profile_switch_sf10_rollback_6ms_results.json 74e7bfeb23bc569a5786e2322b5e3b1753cba9f628e2969bf4f5c4296ca5817a profile_switch_sf10_wide_rollback_results.json d79096fbe3e15e0375457b487fd40be3418789e798a978efb79d8298c880eea2 profile_switch_sf10_wide_payload_results.json 1316a2c2f32a8c0773fc56587c4220ee8f6f6cdf4ab9b87e00fd361fc9395257 profile_switch_sf10_wide_legacy_payload_results.json fcf2123270634e2f27800787e20b9b8b7ae63bcfb03e17d26a4a4f6f4e23d32f profile_stationary_sf10_wide_offset_results.json 4c494a59fa793d274b0366185cfe3230b6c9fce7a57dfaa61f9f29c36465bbd3

RX firmware images used, in chronological order:

Stage firmware.bin SHA-256 Initial nine-mode rollback 5f8dbf9c4584e0de94a7aebda5a20e961b644a880ecb6a34ab5871621958696f Fixed 250 kHz offsets and traced timeout cd839caa33c869dd448cc55e9c8b959e39d105f80a2ac4a8029393698b993fd3 Explicit 6 ms TCXO rollback 0ba044035c4986749c9ad4f50c702dc8c57e8e0fecf9d46fc11515cad5678166 Wider strict rollback 5fc919c80438635b4a96021b39fae2ada9e767508d7bb73854e4e108f0d4920f Wider delivery diagnostics and fixed offsets; final installed 7b313a0f8f1eb04f6fc12197af5e9481ad43154b981ab7627e50167d2a97d8c2

The 250 kHz tests used the prior nRF52 TX ZIP hash 7466625ef5e7bcc8f7dd16db2b843db29b4312a33b0d32bc0b1e1193d6aee03a. All wider tests used the new selectable-spacing application:

Build artifacts are generated and may be replaced by a subsequent build; hashes identify the images used, not a promise that every historical binary remains in .pio/build. Source starts from keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 with the existing dirty changes and these uncommitted HIL additions; this is not a clean-release comparison.

"},{"location":"test-results/sf125_settling_limits_validation/","title":"Sf125 settling limits validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf125_settling_limits_validation/#descending-sf10sf5-125-khz-settling-limits","title":"Descending SF10..SF5 / 125 kHz settling limits","text":"

Experiment started 2026-09-14. Stopped at the user's request to replace the Indicator transmitter with a V4. No 400/400 pass was found. The saved top-level manifest remains incomplete; the SF10 manifest contains all completed settings. Original evidence is not rewritten to make an interrupted experiment look complete.

"},{"location":"test-results/sf125_settling_limits_validation/#results-at-user-requested-stop","title":"Results at user-requested stop","text":"

94 SF10/125 settings completed: every added delay from 0 through 9,300 us in 100 us increments. Each stopped on its first RF miss; none reached 400/400. The best individual setting was +1,000 us: 25 valid packets, then packet 26 failed. No lower SF was reached in this descending sweep. No result is claimed for SF10 delays above 9,300 us or for SF9..SF5 in this run.

Across these completed settings: 641 probes, 547 valid and 94 failed. Because each setting ends at its first miss, these totals are not an unbiased packet error-rate measurement and do not establish a reliable lower/upper delay limit.

For the 547 valid receptions, reported SNR averaged +8.311 dB, minimum +7.5 dB and maximum +9.5 dB. RSSI averaged -40.547 dBm, range -42 to -39 dBm. These are conditional on successful reception; no SNR is inferred for missing packets.

24,055 retunes averaged 456.620 us, weighted by hop count and excluding the intentional settling delay. Reported retune failures, RX-mode errors and cache errors were all zero. Four RX errors were reported, one each at +1,100, +5,900, +6,700 and +6,900 us. These counters do not identify the transmitter as the cause. At +9,300 us, the unheld four-channel cycle averaged 206.134 ms (maximum 206.157 ms), versus a 262.144 ms programmed preamble; this nominal time margin still did not produce an all-pass setting.

Controllers and collector were stopped, and both boards were rebooted to idle HIL. Follow-up polling is paused. Subsequent info checks returned ready on both boards, with XIAO gain register 0x94 and Indicator 0x96. Test firmware was retained; no original firmware was restored. Replacement-board testing has not started.

Saved manifest SHA-256 at stop:

"},{"location":"test-results/sf125_settling_limits_validation/#requested-method","title":"Requested method","text":"

XIAO ESP32-S3/WIO SX1262 COM31 RX, SenseCAP Indicator ESP32-S3 COM28 TX, user-separated positions, XIAO boosted gain off (actual register 0x94), Indicator gain unchanged at 0x96. Four centers 909.500/909.750/910.000/910.250 MHz; bandwidth 125 kHz, CR4/5, 5.1-symbol visits, 32 programmed preamble symbols, 16-byte synthetic packets at -9 dBm. Warm fast RX, buffered 8 MHz SPI and unchanged-modulation cache enabled. No extra IRQ polling/tracing.

Start SF10 and descend one SF at a time through SF5. For each SF, test added post-BUSY delay in 100 us increments starting at zero. Every failed setting stops on its first invalid reception/timeout; a pass requires 100/100 on each of four channels. Latest user instruction: stop the entire sweep at the first 400/400 pass, without continuing to find an upper limit or a lower SF. Without any pass, continue to the ceiling, then lower SF. Fixture errors abort the entire experiment. No failed RF probe is retransmitted.

This searches for a sampled all-pass setting, not proof of monotonic delay behavior, an upper delay limit, or zero PER. The RX modem remains active during the extra delay; the independent 5.1-symbol visit starts afterward. Thus extra settling is also extra RX occupancy, not a pre-SetRx blanking interval.

SF Visit Last 100 us delay step Programmed preamble 10 41,780 us 23,300 us 262,144 us 9 20,890 us 11,400 us 131,072 us 8 10,445 us 5,400 us 65,536 us 7 5,223 us 2,500 us 32,768 us 6 2,612 us 1,000 us 16,384 us 5 1,306 us 200 us 8,192 us

Budget: preamble_us / 4 - dwell_us - 453, using the nominal retune cost. Actual cycle means/maxima are recorded and may exceed that nominal budget. The randomized arrival pause now includes added settling when estimating two scan cycles; earlier saved captures retain their original distributions.

The stop policy changed after SF10 delays 0..5,600 us had all failed. The old supervisors were stopped while the active 5,600 us collector finished normally. Original captures are preserved. A fresh first-pass manifest reuses completed, configuration-validated raw captures with their SHA-256 hashes, then resumes at 5,700 us; no completed setting is repeated. The earlier top-level manifest is incomplete because its upper-limit policy was superseded, not due to an RF or fixture fault.

"},{"location":"test-results/sf125_settling_limits_validation/#build-and-initial-checks","title":"Build and initial checks","text":"

Both HIL targets compiled and verified uploads succeeded. The selected 53 host/native tests passed: 36 host collectors, seven profile native, four SX1262, five radio and one receive-mode test. After the stop-rule change, all 39 host collector tests passed, including new whole-sweep first-pass stopping and matching, hash-preserving capture reuse.

Actually loaded firmware SHA-256:

A one-second RX-only SF10/+23,300 us smoke check recorded 15 hops with minimum delay 23,300 us, minimum post-delay dwell 41,782 us, and zero retune/RX-mode/cache errors. Its two clean cycles averaged 262,125 us, maximum 262,127 us: only a tiny margin against the 262,144 us preamble, not a robust worst-case bound. No RF probe was transmitted during that check.

Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade, plus local changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17.

python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_limits_results.json\n

Updated run:

python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_first_pass_results.json --stop-on-first-pass --reuse-prefix tools/hil/profile_switch_sf125_limits_results.json\n

The controller reboots to idle HIL after each per-SF child completes or fails; test firmware is to remain installed, per user request. Original private flash backups are retained. RAK4631, Indicator RP2040 and unrelated S3 soak work are not part of this experiment. No commit/push requested. Temporary build overlay has been removed.

"},{"location":"test-results/sf5_250_dwell_validation/","title":"Sf5 250 dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf5_250_dwell_validation/#sf5-250-khz-four-channel-dwell-sweep","title":"SF5 / 250 kHz: four-channel dwell sweep","text":"

Measured 2026-09-14. No tested dwell from 4.1 through 8.1 chirps passed. Each setting stopped at its first invalid reception or timeout; the sweep stopped at the requested 8.1 limit. None reached 100 packets per channel.

"},{"location":"test-results/sf5_250_dwell_validation/#setup","title":"Setup","text":"
python tools/hil/profile_switch_dwell.py --receiver COM31 --sender COM28 --output tools/hil/sf5_250_dwell_normal_results.json\n
"},{"location":"test-results/sf5_250_dwell_validation/#results","title":"Results","text":"

\"Received\" means correct sequence, payload and reported receive channel. Each row is a first-failure sample, not a 100-packet/channel PER measurement.

Chirps Requested dwell Received / attempted First failure Mean retune 4.1 525 us 1 / 2 channel mismatch 458.122 us 4.6 589 us 0 / 1 timeout 450.787 us 5.1 653 us 0 / 1 timeout 450.667 us 5.6 717 us 0 / 1 timeout 452.270 us 6.1 781 us 2 / 3 timeout 452.991 us 6.6 845 us 0 / 1 timeout 451.082 us 7.1 909 us 0 / 1 timeout 453.796 us 7.6 973 us 0 / 1 timeout 453.568 us 8.1 1,037 us 4 / 5 timeout 457.886 us

All 16 requested transmissions returned success. Seven met the full receive checks. Of nine failures, one returned a matching synthetic sequence on a different reported receive channel and eight timed out. Every timeout run also recorded one rx_errors increment. These are receive/read/decode errors, not retune hardware faults; without an IRQ trace their exact cause and association with the expected probe cannot be reconstructed.

All 64,507 timed hops passed RX-mode/software-cache checks with zero retune failures. Weighted mean switch time was 452.739 us, excluding listening dwell and full application scheduling. Each hop programmed a frequency. Modulation rewrites occurred only when the prior context needed re-establishing, rather than on every frequency-only hop.

The 4.1 second probe was transmitted for channel 2 (910.000 MHz), but its sequence was returned while the receiver reported channel 3 (910.250 MHz), 16 bytes, RSSI -66 dBm, SNR 0.5 dB. It therefore failed the channel check; the saved valid=false also does not independently certify all payload bytes. No synchronized RF/frequency-register capture was taken, so this is evidence of wrong-channel reporting/reception, not proof of its physical mechanism. Successful correct-channel probes read -42 to -41 dBm, SNR 7.8\u20138.8 dB.

"},{"location":"test-results/sf5_250_dwell_validation/#interpretation","title":"Interpretation","text":"

SF5/250 did not establish a solution to the close-range problem, and increasing dwell alone did not produce a clean four-channel result. The wrong-channel sequence and receive errors prevent interpreting this as a pure minimum-dwell/acquisition test. It is not evidence that 8.1 is a reliable setting merely because four probes passed before its miss.

There is also a timing tradeoff: a symbol lasts 128 us, so 32 programmed preamble symbols occupy only 4.096 ms. A ~453 us retune costs about 3.54 symbols. At 4.1, four nominal dwells plus four such retunes consume about 3.91 ms before other loop costs; at 8.1 they consume about 5.96 ms. Longer visits give acquisition more uninterrupted time but increase the time spent away from each channel. This arithmetic motivates testing a longer preamble or fewer channels separately; it does not prove the cause of any particular failure. No such follow-up was transmitted after the 8.1 limit.

Unlike the earlier separated SF6 run, this changes SF, bandwidth, absolute preamble duration and diagnostic polling. The RF settings also leave channel spacing unchanged at 250 kHz. It is not a matched comparison isolating receiver sensitivity or the modulation cache.

"},{"location":"test-results/sf5_250_dwell_validation/#provenance-and-handoff","title":"Provenance and handoff","text":"

Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade, plus uncommitted production cache and HIL changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c; Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL builds and verified uploads succeeded. All 39 selected host/native tests passed, including bandwidth/dwell calculation and bounded sweep stopping. No commit or push was requested for this work.

Tested firmware SHA-256:

Raw manifest references all nine per-dwell captures. SHA-256 values (all files under tools/hil):

File suffix after sf5_250_dwell_normal_results SHA-256 .json aaf00ef8531cb2c5c7e4dfb78d3b8b03ce61d9afe8323fb33e70be09656adf54 _4p1.json a9c6baf0bece79c9b9c96bfa97e3cd9932e0c04fc76b43f61ed350512306341a _4p6.json 63863b55108374011afbe555bfeb97034b010e194cdad83a212d5d286d1992fe _5p1.json ff1d2927365e3634984bc0b843a09f2934c5dcac49b60b862ff4db030e0f7e74 _5p6.json 33c91687d0b61dd83e2a08d8358b43f21fa27f9e80f9503feedeb657453a7efd _6p1.json 5baa8d1bfcf0ccc947c92d563dc555802ac599ce1e8d260a623964936a99da4e _6p6.json 90f6b4d7785add771e30da7dfa78672fd75ad13d8a5965c21ec1a37e15d8dabb _7p1.json eb99afd932a5579c765f8a6c0a18db4bacc317829b0654d11fbf2f569c3a8618 _7p6.json bdb88da82437fa6e01ad90ab927d364b4d67aaadbac7597c3aacb9799b053211 _8p1.json 404ee179e4c10333bd6fddae9f0faa9da81f6193dcb5fff6d9bd37a466c319a3

Both boards were rebooted after the sweep and answered ready with correct HIL capabilities; XIAO gain remained 0x94. Per user request, HIL firmware is left installed, idle with no autonomous test transmissions. Private original flash backups remain available. The RAK4631, Indicator RP2040 and unrelated S3 soak work were not modified. The temporary build overlay was removed.

"},{"location":"test-results/sf6_125_settling_validation/","title":"Sf6 125 settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_125_settling_validation/#sf6125-four-channel-post-switch-settling-sweep","title":"SF6/125 four-channel post-switch settling sweep","text":"

Measured 2026-09-14. No setting from +0.0 through +1.0 ms passed 400/400. The requested delay was applied after BUSY went low without consuming the subsequent 5.1-symbol listening visit. Each run stopped on its first miss; the sweep would have stopped at the first 400/400 pass, but reached its nominal timing limit instead. No RF probes were sent beyond that limit.

"},{"location":"test-results/sf6_125_settling_validation/#method-and-timing-budget","title":"Method and timing budget","text":"

At SF6/125, a symbol is 512 us. The requested listening visit rounds up to ceil(5.1 * 512) = 2,612 us; 32 programmed preamble symbols last 16,384 us. Using the user's nominal 453 us retune, the extra per-hop budget is:

16,384 / 4 - 2,612 - 453 = 1,031 us

Thus the 100 us grid runs from 0 through 1,000 us; 1,100 us would exceed the nominal full-sweep calculation. Sync/header time is not spent as extra scan budget. This is not a worst-case scheduling or acquisition guarantee.

The delay is HIL-only: after a successful guarded production retune and BUSY-low observation, the CPU waits without SPI/IRQ operations, then starts a separate 2,612 us listening clock. The SX1262 is already receiving throughout that delay and may acquire a packet. This tests extra RX occupancy/settling, not a pre-SetRx delay or a period with RX disabled. Ordinary RX restarts after processing packets reset the visit clock without another channel delay. Zero delay preserves the previous production RX-start clock.

An initial receive-only, 300 ms smoke check at +1,000 us measured 74 hops, minimum applied delay 1,000 us and minimum subsequent dwell 2,612 us. Its 17 clean full cycles averaged 16,235.941 us, maximum 16,260 us. No RF probe was transmitted in this check. The longer packet experiments below capture more scheduling variability, including host command handling.

"},{"location":"test-results/sf6_125_settling_validation/#results","title":"Results","text":"

All times in the last two columns are measured means, not target values. \"Received\" is successes before the next probe timed out; every row ended in one timeout. These first-failure samples do not estimate comparable PERs.

Added delay Received before miss Switch + delay Clean full cycle 0.0 ms 44 0.452381 ms 12.256886 ms 0.1 ms 4 0.548387 ms 12.669976 ms 0.2 ms 34 0.650424 ms 13.074105 ms 0.3 ms 53 0.753410 ms 13.485859 ms 0.4 ms 47 0.850830 ms 13.868454 ms 0.5 ms 66 0.952289 ms 14.273736 ms 0.6 ms 14 1.050333 ms 14.671156 ms 0.7 ms 21 1.151351 ms 15.076094 ms 0.8 ms 63 1.255113 ms 15.482272 ms 0.9 ms 19 1.352843 ms 15.880361 ms 1.0 ms 25 1.452750 ms 16.283488 ms

All 401 transmissions returned success: 390 valid receptions and 11 timeouts, spread across the eleven independently stopped settings. None completed 100/100 on each channel. All 52,609 timed hops passed RX-mode and software-cache checks, with one observed frequency write per hop and zero retune failures. Each positive-delay hop waited at least the requested time; every run's minimum subsequent idle dwell was 2,612 us. No receive-error counter increment or device error was reported. Successful RSSI was -42 to -40 dBm and SNR 9.5\u201311.8 dB.

switch measures production retune entry through BUSY-low observation; settling measures the following wait, including timer overhead; switch_and_settle combines them. Even zero requested delay records roughly 1\u20132 us of timer/bookkeeping overhead in the combined boundary. Listening dwell and full application scheduling are not included in retune time.

idle_cycle measures consecutive channel-0 return times, excluding initial cycles and cycles containing packet/guard-held visits. It still includes loop and USB work encountered during otherwise unheld visits. At +1.0 ms its mean is 100.512 us inside the preamble budget, but its maximum was 17.475 ms, exceeding 16.384 ms. At +0.9 ms the observed maximum was 16.153 ms. Therefore the last grid point fits only the nominal/mean calculation, not every observed full cycle. Legitimate packet holds can extend scan timing further.

"},{"location":"test-results/sf6_125_settling_validation/#conclusion-and-limits","title":"Conclusion and limits","text":"

Extra post-switch settling through the available timing budget did not establish loss-free four-channel reception. The 66 successes at +0.5 ms are not evidence that it is an optimum; the results are small, first-failure samples with different uncontrolled RF arrival phases. The added delay also extends total RX occupancy, so any improvement would not isolate analog settling from longer acquisition time.

Every failure here was a timeout without a reported receive/decode error. Without an IRQ trace or synchronized independent RF capture, that does not prove absence of a preamble/header indication or identify the precise cause. It does not justify changing production packet guards or adding a permanent settling penalty. No production default was changed by this experiment.

"},{"location":"test-results/sf6_125_settling_validation/#reproduction-provenance-and-handoff","title":"Reproduction, provenance and handoff","text":"
python tools/hil/profile_switch_settling.py --receiver COM31 --sender COM28 --output tools/hil/sf6_125_settling_normal_results.json\n

See harness instructions. Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade plus local changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets compiled; only XIAO needed an upload for the new receiver-side delay. All 45 selected host/native tests passed, including delay/visit separation, packet restart, timer wraparound, clean-cycle accounting, bounded grid and first-pass stop.

Actually loaded firmware SHA-256:

Raw manifest and per-delay captures are preserved byte-for-byte. SHA-256, file suffixes after tools/hil/sf6_125_settling_normal_results:

Suffix SHA-256 .json 41a20c3d5b809cd225772980fbe0d305c14391659f10d685bf78f8bdf326e61c _0us.json 47a73a35ed72ebc61b88bfe315e86bf7e15ba1d2d93e68ce92ce681e1ef73cef _100us.json b585ea01f605bc876da86ba2a35fbe6e7cc6543eb5e827ba190c51dca10dcb85 _200us.json 6ce378cd079eefae8450c7f5672281095b9b99e28e0c7920f024a4396606bb40 _300us.json d93e7a258dddb6731c030e395e09c3f06b4d5988638cc85dc5cd0745256c6202 _400us.json 56bfe4cb7cbab2a660a232a2bf85b9bb87221ffade7c9bbce6d1116e6c30fcbf _500us.json fffb6c33a506f537913084fc4f5cc9f8009a4f0f61e4f290dd81a7409f53d1bd _600us.json 7bc6dd41f0079f37bdbf77fc48c4113e26589bcce704850b106c33d057e34659 _700us.json 54170f5b63bbe8ffa231852bf892ea55d8579c33631775f29fb7c127197afb2a _800us.json f7c53375b29764b4369b84e8ac3244aa37e385c8738a9dfd7f3257d85a64a705 _900us.json 56bc3691f2046cafcf2115b2f6b7bd164e9c9039733ea51028db292deea10230 _1000us.json f708af37caee8f689f2436567382458d63aba1269e8306ac6e4d0079677b9115

Both boards rebooted to idle HIL after testing and answered ready. XIAO gain remained 0x94 and settling policy was returned to zero. Test firmware stays installed, per user request; no restore, commit or push was performed this turn. RAK4631, Indicator RP2040 and unrelated S3 soak work were untouched. The temporary build overlay was removed; no autonomous test TX remains.

"},{"location":"test-results/sf6_5p1_trace_validation/","title":"Sf6 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_5p1_trace_validation/#sf6-125-khz-four-channels-at-51-chirps","title":"SF6 / 125 kHz: four channels at 5.1 chirps","text":"

Measured 2026-09-14. Seven packets received, then packet 8 missed. The run stopped at that first miss. No RF retry, other dwell, or higher/lower channel count was tested. The target of 100 packets per channel was not reached.

Follow-up: SF8/125 at the same 5.1-chirp dwell missed on packet 13 and captured an off-channel preamble hold. That differs from the absence of observed preamble in this SF6 failure.

"},{"location":"test-results/sf6_5p1_trace_validation/#setup","title":"Setup","text":"
python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output tools/hil/sf6_125_5p1_trace_results.json --samples 100 --preamble 32 --max-channels 4 --dwell-symbols 5.1 --trace\n
"},{"location":"test-results/sf6_5p1_trace_validation/#results-and-what-the-trace-establishes","title":"Results and what the trace establishes","text":"

Probes 1\u20137 succeeded on channels 3, 2, 1, 0, 2, 3, 1. Probe 8, sequence 944962150, missed on channel 0 (909.500 MHz). Thus channel 0 had already received a valid probe; this was not a channel that never worked at all. Successful packets had RSSI -42 to -40 dBm and SNR 9.5 to 10.8 dB.

The failed probe has a complete, non-overflowed 3,160-event trace:

The scanner was not stuck on the wrong channel by a false preamble. It also did not acquire a header and then fail a CRC check. The evidence locates the failure before observed preamble acquisition, not at payload decoding. It does not prove why acquisition failed: a short-visit/arrival-phase issue remains a candidate, as does an unobserved RF/reference issue. There was no independent RF capture, and the two MCU clocks were not synchronized.

"},{"location":"test-results/sf6_5p1_trace_validation/#actual-timing-during-the-failed-expectation","title":"Actual timing during the failed expectation","text":"Measurement Minimum Mean Maximum Receive dwell, 3,158 hops 2,613 us 2,628.706 us 2,646 us Retune, trace hop rows 526 us 538.700 us 630 us Channel 0 revisit interval, 789 cycles 12,643 us 12,666.829 us 12,705 us

The repeatable scan timing rules out a large software scheduling stall in the failed capture. The 32 programmed symbols occupy 16.384 ms, longer than that idle scan cycle; nevertheless, revisiting within the preamble did not ensure successful acquisition during these short individual visits.

"},{"location":"test-results/sf6_5p1_trace_validation/#successful-acquisition-traces","title":"Successful acquisition traces","text":"

Times below are relative to the logged hop completion onto the eventual receive channel, not a precisely captured RF edge or hardware RX-start edge. The trace observed the following progression for every successful probe: PreambleDetected (0x0004), HeaderValid (0x0014), RxDone (0x0016), consumption.

Probe Channel First observed preamble after hop RxDone after hop 1 3 1,838 us 32,982 us 2 2 1,836 us 35,470 us 3 1 1,844 us 35,151 us 4 0 2,332 us 37,329 us 5 2 1,847 us 35,532 us 6 3 1,831 us 30,731 us 7 1 1,329 us 32,567 us

After detecting preamble, the production guard kept those visits on-channel until reception completed. The failed probe never reached that observed state. This is not evidence that 5.1 symbols always suffice, nor does 7/8 estimate a stable packet-error rate or establish a channel-capacity limit.

"},{"location":"test-results/sf6_5p1_trace_validation/#instrumentation-and-reporting-caveats","title":"Instrumentation and reporting caveats","text":"

The failed capture made 115,783 diagnostic IRQ polls, costing a total 2,821,617 us (about 24.4 us each; maximum 61 us). These reads are not free: they alter loop load, although actual dwell and revisit timing were measured. This is not a matched A/B against the earlier untraced 4.8-symbol run, so seven successes versus two cannot be attributed solely to the extra dwell.

The run used channel_trace: 1. During analysis, a reporting bug was found in the newly added held_dwell aggregate: production startRecv() resets the visit timestamp after reading a packet. That aggregate therefore omitted the preceding held reception time. Do not use its 2.709 ms mean as total held occupancy. The timestamped IRQ/hop trace above is the evidence for the actual 30.7\u201337.3 ms hop-to-RxDone intervals. The failed capture had no RX restart, so its dwell rows are unaffected.

The source now keeps a separate channel-occupancy timestamp (channel_trace: 2, trace_format: 2). Both HIL targets compiled with this reporting-only correction; it was not flashed or RF-tested, and the saved raw result was not rewritten. Scheduler, packet guard and radio commands were not changed by that correction. Regression coverage includes passive SPI observation, bounded/frozen capture, occupancy accounting, page identity, requested dwell, and stopping at the first miss.

"},{"location":"test-results/sf6_5p1_trace_validation/#provenance-and-device-handoff","title":"Provenance and device handoff","text":"

Raw result SHA-256: 7caa16483b7b0000b388a1906395fe3d381bd0e40de73f48cd5c876d96deab3b.

Actually tested firmware SHA-256 (trace version 1, before reporting fix):

Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. No normal production firmware change this turn.

Fresh full 8 MiB snapshots were saved and digest-verified before flashing:

Both snapshots were restored afterward, separately verified against the full flash, and hardware-reset. Snapshots remain in the private current-user backup directory. The RAK4631 and Indicator RP2040 were not modified; the XIAO soak logger was not restarted. Changes were initially handed off locally before publication.

"},{"location":"test-results/sf6_channel_scan_validation/","title":"Sf6 channel scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_channel_scan_validation/#sf6-125-khz-channel-count-test","title":"SF6 / 125 kHz channel-count test","text":"

Measured 2026-09-13. The run stopped on the third packet at the initial four channels: two arrived intact, then the receiver timed out. No five-channel test, lower-channel test, retry or retransmission was performed. Consequently this run does not establish a maximum channel count with 100% reception.

Follow-up: four channels at 5.1 symbols with an IRQ trace stopped at packet 8 and observed no preamble acquisition during the miss.

"},{"location":"test-results/sf6_channel_scan_validation/#requested-setup","title":"Requested setup","text":"

The 100-packet-per-channel target was not reached: the explicit first-miss stop rule ended the run during its first shuffled pass.

"},{"location":"test-results/sf6_channel_scan_validation/#observations","title":"Observations","text":"Probe Channel center Outcome 1 910.250 MHz Valid 16 bytes, RSSI -40 dBm, SNR 10.8 dB 2 910.000 MHz Valid 16 bytes, RSSI -40 dBm, SNR 11.0 dB 3 909.750 MHz Receiver timeout; transmitter returned success

All three transmit acknowledgements and receiver reports matched their sequence IDs, with no missing/replayed/stale USB replies. On the miss, the receiver reported RX mode and zero device errors. The scanner recorded zero retune failures, RX-mode errors, modulation-cache errors or CRC/read errors. No probe was sent on 909.500 MHz before the stop condition.

Timing Minimum Mean Maximum Production retune to BUSY low, 3,467 hops 504 us 514.652 us 726 us Idle receive dwell, 3,463 visits 2,458 us 2,462.480 us 2,468 us

These samples include scanning during the bounded 10-second wait that confirmed the missing packet. There were 2,451 deferred attempts while production preamble/header/packet ownership held a visit; this is not a count of packets or failed retunes. Held visits are excluded from idle-dwell timing. All 3,467 successful retunes used the production fast-RX path.

The means imply about 11.91 ms per four-channel idle cycle, excluding packet-held visits and small loop bookkeeping. Fast retuning alone did not produce 100% reception in this setup. This short stop-on-first-miss experiment does not identify the exact acquisition/phase cause, estimate a stable PER, prove that three channels would pass, or establish a universal board limit.

"},{"location":"test-results/sf6_channel_scan_validation/#implementation-and-reproduction","title":"Implementation and reproduction","text":"

This is a lab-only N-channel scheduler over the real tuneProfile() path. It reuses the two production profile slots as staging slots for the next frequency. Normal MeshCore radio2 remains two-profile. The explicit test schedule intentionally does not use the normal two-profile automatic-preamble fit calculation or generic 6 ms switch / 4 ms loop allowances. It keeps the existing packet-protection guards and measures real dwell/retune overhead.

python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output results.json --samples 100 --preamble 32\n

The controller has regression tests for balanced samples, starting at four, incrementing only after a perfect level, stopping without another TX, and distinguishing fixture failure from an RF miss. A separate RX-only 0.5-second setup check verified the scan timing before any capacity-test packets were sent. The capacity run itself was not repeated.

Build provenance: base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus the local fast-switch and HIL changes; Arduino ESP32 2.0.17, Espressif32 6.11.0, RadioLib 7.7.1 pinned at 187ef24791c3d844939b2be13a68bd890bd04e4c. This is the extended V8 harness with channel_sweep: 1; its binaries differ from the earlier two-profile V8 measurements, so timings should not be treated as a same-image A/B.

Firmware SHA-256:

"},{"location":"test-results/sf6_channel_scan_validation/#device-handoff","title":"Device handoff","text":"

After the first-miss stop, both original 8 MiB flash snapshots were restored. Separate full-flash verification matched each backup's digest, followed by hardware reset. The private backups remain in the current-user backup directory. The Indicator RP2040 and RAK4631 were not modified. The interrupted XIAO soak logger remains stopped, and the temporary PlatformIO HIL overlay was removed. Implementation, tests and reports were initially handed off locally before publication.

"},{"location":"test-results/sf8_5p1_trace_validation/","title":"Sf8 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf8_5p1_trace_validation/#sf8-125-khz-four-channel-comparison-at-51-chirps","title":"SF8 / 125 kHz: four-channel comparison at 5.1 chirps","text":"

Measured 2026-09-14. 12 packets received, then packet 13 missed. The first three balanced rounds passed (3/3 on every channel); the next probe on channel 3 failed. The run stopped at that miss. The target of 100 packets per channel was not reached. No RF retry or other channel count was tested.

"},{"location":"test-results/sf8_5p1_trace_validation/#setup-and-comparison","title":"Setup and comparison","text":"

Same XIAO ESP32-S3 + WIO SX1262 scanner (COM31) and SenseCAP Indicator ESP32-S3 reference transmitter (COM28). Four centers: 909.500, 909.750, 910.000 and 910.250 MHz. CR4/5, 32-symbol programmed preamble, 16-byte synthetic packets, -9 dBm, warm production fast RX and buffered 8 MHz SPI. Same production packet guards and read-only 64 us nominal IRQ polling; events buffered until each result. Randomized balanced channel order, seed 606125, varied arrival timing.

Setting Earlier SF6/125 This SF8/125 One symbol 512 us 2,048 us Requested 5.1-symbol dwell, rounded up 2,612 us 10,445 us 32 programmed preamble symbols 16.384 ms 65.536 ms Whole-run average measured retune 539.427 us 524.124 us Outcome before first miss 7 received, packet 8 missed 12 received, packet 13 missed Miss trace No observed preamble or hold Off-channel preamble and prolonged hold

Changing SF increases real listening and preamble durations together. This is not a matched same-image A/B or an isolated post-retune settling experiment. The small retune-time difference must not be treated as a new optimization; no extra settling delay was inserted. The earlier reporting-only held-visit timestamp correction is included in this trace-version-2 run.

"},{"location":"test-results/sf8_5p1_trace_validation/#failed-probe-an-off-channel-hold","title":"Failed probe: an off-channel hold","text":"

Failed sequence 388715032 targeted channel 3 / 910.250 MHz. The scanner instead observed PreambleDetected on channel 2 / 910.000 MHz, then its normal packet guard deferred the intended jump to channel 3. It never observed a valid header, header error, CRC event, or RxDone for that preamble.

Times are software observations relative to expectation arming:

Time Event 207.649 ms Last hop onto target channel 3 before the long gap 240.542 ms Hop onto channel 2 250.030 ms PreambleDetected (0x0004) observed on channel 2 251.013 ms Guard defers channel 2 \u2192 3 at the 5.1-symbol deadline 342.287 ms Guard issues a clear of the still-latched preamble bit 342.794 ms Hop onto channel 3 finally completes 342.851 ms IRQ read confirms zero bits 9,999.270 ms Expectation timeout, RX mode, zero device errors

Channel-2 occupancy was about 102 ms. The target channel's revisit gap grew to 135.145 ms, compared with its ordinary ~43.875 ms cycle and the 65.536 ms programmed preamble. The delayed retune itself took only 550 us.

The preamble-to-header budget from calcMaxPacketMillis() is ceil((32 + 8 + 4.25) * 2.048) = 91 ms. The strict millisecond timeout in CustomSX1262::isReceiving() clears a preamble that does not advance to a header. The observed 92.257 ms from the first IRQ observation to that clear is consistent with this existing protection mechanism.

This identifies a prolonged off-channel hold that removed the next listening opportunity. It does not show that the ~0.52 ms jump needs more settling time. The RF source of that preamble is not proven: adjacent-channel pickup/ false detection or unrelated RF are possibilities. There was no independent RF capture or synchronized MCU clock. The earlier SF6 miss had no observed preamble or hold; this result does not establish the same cause for that miss.

Simply shortening the guard risks cutting off valid long-preamble packets. Successful traces show that reception legitimately keeps a channel occupied well past 10.445 ms. No production guard change was made in this diagnostic.

Follow-up: the stationary preamble investigation reproduced transmitter-associated off-frequency indications with full RX initialization and no hopping, on both boards. It strengthens the off-channel detection explanation without independently identifying the RF source of this historical missed probe. The SF6 miss remains a separate unresolved case.

"},{"location":"test-results/sf8_5p1_trace_validation/#measurements-and-checks","title":"Measurements and checks","text":"

This first-miss sample neither estimates a stable packet-error rate nor establishes a channel count with 100% reception.

"},{"location":"test-results/sf8_5p1_trace_validation/#reproduction-and-provenance","title":"Reproduction and provenance","text":"
python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output tools/hil/sf8_125_5p1_trace_results.json --samples 100 --preamble 32 --max-channels 4 --dwell-symbols 5.1 --trace --sf 8\n

Raw result SHA-256: 0d7e97173937f42708febf8276cdadc36578740735058d5da39b1b576e9ca49e.

Tested firmware SHA-256:

Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets built successfully. All 27 selected host/native tests passed, including SF scaling, propagation to both RX staging slots and TX, cache checks, trace handling and first-miss stop.

"},{"location":"test-results/sf8_5p1_trace_validation/#device-handoff","title":"Device handoff","text":"

Fresh full 8 MiB backups were saved and digest-verified before flashing:

Both were restored after the test, separately full-flash digest-verified, and hardware-reset. Backups remain in the private current-user backup directory. No identity or channel secrets were used in the synthetic RF probes. The RAK4631 and Indicator RP2040 were not modified; the XIAO soak logger was not restarted. The temporary HIL build overlay was removed. Source, tests and results were initially handed off locally before publication.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/","title":"Solarxiao 33s head bootloader lora ota 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#solarxiao-33s-head-bootloader-lora-ota-and-repeater-acceptance-2026-10-21","title":"SolarXiao 33S HEAD bootloader, LoRa OTA and repeater acceptance, 2026-10-21","text":"

The SolarXaio MPPT 12V LiFePO4 carrier with a Seeed XIAO nRF52840 and Ebyte E22-900M33S passed a full application update over LoRa and subsequent repeater acceptance on the Mercerwood bench. TX was configured to -6 dBm and repeating enabled. Both settings survived the application update and a further warm restart. Final core error flags were zero.

Machine-readable results bind this result to the following revisions. These were the checked-out HEAD revisions used for qualification; subsequent commits require separate testing.

Component Qualified revision or value MeshCore source efad0076816f7ba2293c68f56f8db18ac17344b9 Application environment solarxiao_33S_repeater, Full OTA package Application version v1.18.0.1-halo-keymind-cascade-dev-efad0076 OTAFIX bootloader source b07255d4cdc601f1313e4cbe020caad3b5544821 Bootloader profile xiao_nrf52840_ble, board 28860044, name XIAO_DFU Bootloader packed test version 0x02040B7E, displayed as OTAFIX2.4.11-preview.126 Verified bootloader manifest CRC 76EBABBF; ABI 3, capabilities 0E SoftDevice S140 7.3.0 Application body hash 6280E8156DB1042F Application image size 578,752 bytes Application image SHA-256 1a182b5338f7655b4fe057436768b84069c00aa151971c96d6c2b6f7c98e8524

The bootloader is the named HEAD build. Its preview version string retains the 2.4.11 base label; this campaign did not install the 2.4.11 release artifact. The bootloader and SoftDevice were updated through serial DFU, followed by restoration of the preceding application. Identity and saved settings remained intact, and the running application confirmed QSPI bootloader-apply support.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#full-lora-application-update","title":"Full LoRa application update","text":"

A Heltec V4.3 Full Companion running source revision 2bd4cb4d seeded the exact Full .mota package. The destination started on application revision e16db810 with body hash C9137AAAB9A5FCDB. USB CLI controlled the target; application image bytes traveled over LoRa. The target received zero firmware bytes over USB during this application OTA test.

Both radios used a temporary 909.5 MHz, 500 kHz, SF7, CR 4/5 profile with a 32-symbol preamble. Target RX power saving was temporarily disabled for the transfer; its configured TX remained -6 dBm. The source retained its saved power-saving policy and TX setting.

Manifest 55E532FA reached 283/283 blocks, ready to install, with destination session age 439 seconds. Host seeder logs showed reads of all 283 payload blocks. After the install request, the target booted the expected version and reported body hash 6280E8156DB1042F. Its bootloader identity and CRC still matched the qualified HEAD build.

A preliminary attempt to activate scheduled temporary channels through remote administration failed before payload transfer. The completed test used local USB CLI to activate the maintenance channel. Fully remote scheduled activation and OTA with target RX power saving enabled remain unqualified by this run.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#restart-and-repeater-acceptance","title":"Restart and repeater acceptance","text":"

Ten USB query cycles, three close/reopen cycles and a warm restart passed. The version, body hash, board, role, public identity, TX setting and repeating state remained consistent. An independent XIAO S3 WIO Companion then sent three nonce-bound trace requests using the target's exact four-byte prefix route on the normal channel.

Round Exact nonce and one-hop route Target direct RX/TX delta Trace elapsed Receive error delta 1 PASS 1 / 1 0.498 s 0 2 PASS 1 / 1 0.555 s 0 3 PASS 1 / 1 0.536 s 0

These traces establish reception and reply forwarding by the updated target. The final core error flags were zero; the final packet counters reported zero receive errors. The source and trace partner retained their saved settings.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#final-configuration-and-cleanup","title":"Final configuration and cleanup","text":"

The first source cleanup check sampled the radio before its deferred restore finished and reported an active window. A subsequent check confirmed temporary radio inactive and exact equality with all saved source settings. The JSON preserves that initial cleanup result and the completed restoration check.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/","title":"Solarxiao advanced 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#solarxiao-advanced-carrier-profile-2026-10-21","title":"SolarXiao Advanced carrier profile, 2026-10-21","text":"

The older SolarXiao 2W Advanced already supplies battery voltage and processor temperature through XIAO self telemetry on channel 1. This change adds no new physical measurements. It adds an explicit saved carrier selection, capability reporting, live self-reading CLI aliases and guidance to existing history.

The published Advanced Standard schematic uses a BQ24650 analog charger. It has no INA monitor or digital charger interface; status LEDs, charger NTC and analog setpoints are not connected to XIAO measurement/control inputs. Firmware cannot recover panel V/I, battery current/power, battery temperature or charger flags from that design.

Both existing solarxiao_33S_repeater Full and Reduced packages built and passed package, capability and runtime memory checks. Native host fault tests passed under ASan/UBSan. These are build/host results, not hardware acceptance. No firmware was installed on SEA Sherwood, which the user confirmed is unreachable. Its exact installed target, bootloader and physical revision were not verified. The earlier Mercerwood MPPT bench report does not qualify this new image on physical hardware.

See the controls guide and machine-readable validation record.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"Item Result Application source de1bb0f2e6a37b02f1a9f0995ea539c818c3abe9 Version v1.18.0.1-halo-keymind-cascade-dev-de1bb0f2 Target / hardware ID solarxiao_33S_repeater / solarxiao_33S, unchanged EndF OTA target 0x3caf966e, unchanged Full application 597,816 bytes; body hash C5339ADF076AEC89 Reduced application 567,756 bytes; body hash 68D86AA255060031 Full memory check PASS: 150,584 internal bytes available; 76,960 required Reduced memory check PASS: 152,232 internal bytes available; 76,960 required DFU ZIP, UF2, linked capabilities PASS for both profiles Linked carrier commands and history PASS for both profiles Source changes to RF/PA, bootloader, flash layout or battery protection None

Both packaged applications retain the native MPPT marker and contain the Advanced selector, BQ24650 capability text, live XIAO-reading response, history guidance and unavailable-measurement explanation. Application body hashes and EndF identity fields were checked independently. The build's artifact manifest hashes matched every published local package, UF2 and capability file.

These packages contain compiled LoRa/Bluetooth update support. No LoRa OTA, boot, radio forwarding, charging or electrical test was performed for this image. Future deployment requires checking the destination's installed target and matching board/storage-aware bootloader; no remote update was attempted.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#host-coverage","title":"Host coverage","text":"

The following checks passed:

Reproduction:

python3 -m unittest discover -s test -p 'test_solarxiao*.py' -v\npython3 -m unittest discover -s test -p test_xiao_battery_safety.py -v\nbash build.sh build-firmware solarxiao_33S_repeater \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

PlatformIO ran once at a time; the build entry point produced Full and Reduced sequentially. Local packages and logs are retained under /home/mesh/hwtest/solarxiao-advanced-20261021/.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#using-the-profile-after-a-compatible-update","title":"Using the profile after a compatible update","text":"
set solar.board advanced\nget solar.board\nget solar capabilities\nget solar readings\nget solar history\n

The operator selects the board. A missing/unpowered I2C device never identifies Advanced automatically. The absent-record default remains MPPT for existing installations; a failed presence query must yield a complete valid record or block native access. The separately stored selection does not alter existing charger overrides. Selecting MPPT again re-enables and reloads those overrides.

Channel-1 voltage and MCU temperature are unchanged. Voltage/temperature history remains the existing 30-minute, seven-day RAM history and resets on reboot. The MCU temperature is not battery temperature, and a voltage trend does not measure charge current, solar power or charger state.

"},{"location":"test-results/solarxiao_auto_2026-10-21/","title":"Solarxiao auto 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#solarxiao-automatic-carrier-detection-and-base-support-2026-10-21","title":"SolarXiao automatic carrier detection and Base support, 2026-10-21","text":"

SolarXiao repeater firmware now defaults to automatic carrier detection when there is no saved selection. A verified INA260 identity enables MPPT support; until then, the firmware reports self-only telemetry without guessing Base versus Advanced. Existing saved manual selections retain their meanings. Manual auto, base, advanced and mppt settings remain available.

Base needs no additional telemetry driver. The published Base Standard schematic uses a CN3791 analog charger with no digital interface or INA monitor. Its battery connects to the XIAO BAT input; charge status drives an LED. Like the older BQ24650 Advanced design, it provides existing XIAO battery voltage and MCU temperature only. There are no new physical measurements for either board. Base Lite publishes a README but no schematic; its exact wiring is not independently qualified here.

Radio target selection stays separate. The published Base schematic specifies E22-900M30S; the pictured 2W Advanced has M33S. The existing 30S/33S target must match the fitted module. Charger detection does not change RF settings, PA limits, hardware/OTA identity, bootloader, flash layout or battery protection.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"Item Result Application source 5c44324330a4b3a2e79059dbfa5e086d4cfc1e08 Version v1.18.0.1-halo-keymind-cascade-dev-5c443243 33S OTA target / hardware 0x3caf966e / solarxiao_33S, unchanged 30S OTA target / hardware 0xb38ebf22 / solarxiao_30S, unchanged Native host checks 22 PASS: 19 carrier methods and 3 existing power methods; native harnesses use ASan/UBSan XIAO startup/battery safety checks 6 PASS Full/Reduced DFU, UF2, linked capabilities, memory PASS for both targets Target Profile Application bytes EndF body hash Internal bytes available solarxiao_33S_repeater Full 599,132 7AB98467B6E3A468 150,576 solarxiao_33S_repeater Reduced 569,136 217075127849EE7C 152,224 solarxiao_30S_repeater Full 599,132 0C386CB1FC49608C 150,576 solarxiao_30S_repeater Reduced 569,136 ACBB1554247BB0B5 152,224

Every memory check requires 76,960 heap/transient bytes. Both profiles in both targets retain compiled LoRa/Bluetooth update support and telemetry history. Independent package checks verified the application version, EndF body length, body hash and target/hardware IDs. Package, UF2 and manifest file hashes matched. All four applications contain the native power marker, verified/pending Auto responses, Base/CN3791 and Advanced/BQ24650 descriptions, live self-reading response and history guidance.

The first 30S dependency installation failed before compilation because of a local disk quota. Reusing the identical inherited 33S dependency cache resolved the build setup; the final 30S Full/Reduced pair passed. PlatformIO processes ran sequentially throughout. Logs and packages are retained under /home/mesh/hwtest/solarxiao-auto-20261021/.

See the machine-readable validation record for exact package/application/UF2 hashes and detection parameters, and the controls guide for commands and hardware limits.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#detection-and-host-acceptance","title":"Detection and host acceptance","text":"

Auto reads only INA260 identity registers at carrier address 0x40: manufacturer 0xfe = 0x5449 and device 0xff & 0xfff0 = 0x2270. Address ACK, wrong identity, missing response and an incomplete second read do not enable MPPT. No INA or charger configuration is written by detection.

The first probe occurs after startup initialization. Unverified detection retries every five seconds, including across the millisecond timer wrap. A positive identity is retained until reboot or a successful mode change; temporary communication loss does not demote it. Before verification, the adapter does not read the LTC4162, replay saved overrides or emit native carrier telemetry/history. Self battery/MCU readings remain available.

The actual adapter harness verifies delayed discovery and reloads retained charger overrides only on the transition that enables native access. A dedicated long-uptime test first services manual MPPT at a low timestamp, disables it, then detects Auto at UINT32_MAX - 1000: replay occurs immediately once, the next check waits exactly five seconds across wrap, and a simulated charger reset reapplies the override without redundant writes afterward.

Manual Base/Advanced and unconfirmed/corrupt selections make zero native bus calls. Saved Auto restarts as unverified; original eight-byte manual MPPT and Advanced records keep their exact meanings without being rewritten. Fault tests cover metadata/read errors, wrong-size/corrupt records, partial writes, failed rename, corrupt publication and ambiguous public readback. Explicit successful selection recovers. Existing parser bounds, reply canaries, missing readings, history feature propagation, core charger checks and telemetry permissions/channel-order tests also pass.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#deployment-limits-and-commands","title":"Deployment limits and commands","text":"

No firmware was flashed to any device in this run. No physical Auto detection, Base/Advanced electrical measurement, boot/soak, radio forwarding or LoRa OTA transfer was tested for this image. SEA Sherwood remains unreachable and unchanged; its installed target and bootloader have not been verified. Earlier Mercerwood hardware results belong to their named firmware revisions.

With no saved selection, Auto requires no setup. To replace a saved manual selection after installing compatible firmware:

set solar.board auto\nget solar.board\nget solar capabilities\n

Manual overrides use set solar.board base|advanced|mppt. When native access becomes enabled, retained /solar_power overrides are reloaded. Fresh LTC4162 chemistry/cell-count checks still independently guard all charger writes.

"},{"location":"test-results/solarxiao_basic_2026-10-21/","title":"Solarxiao basic 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#solarxiao-three-power-profiles-2026-10-21","title":"SolarXiao three power profiles, 2026-10-21","text":"

Base and Advanced share the same onboard power telemetry: XIAO battery voltage and MCU temperature. Their analog charger designs differ, but the published layouts expose no additional digital charger measurements or controls. The firmware now combines them into one Basic power profile without claiming which charger is fitted. Additional power measurements require added hardware/wiring.

There are three canonical choices:

Auto remains the default without a saved selection. Old base and advanced command values remain compatibility aliases for basic, not separate modes. Readbacks and help show only the three canonical choices. Existing Advanced record value 1 and Base value 3 both load as Basic without a startup write. New Basic/alias saves use value 1. MPPT and Auto retain values 0 and 2.

Radio target choice remains independent: select the existing 30S/33S target for the fitted E22 module. No RF settings, PA limits, OTA identities, bootloader, flash layout, battery protection or automatic detection behavior changed.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"

Application source: aa91fffae41465ba932f069f2a45e2a3ae1c7ad4. Version: v1.18.0.1-halo-keymind-cascade-dev-aa91fffa.

Target Profile Application bytes EndF body hash solarxiao_33S_repeater Full 598,824 37EE3060D73CAB2E solarxiao_33S_repeater Reduced 568,828 2174A0D9CB1EA7B9 solarxiao_30S_repeater Full 598,824 3525D6629B234891 solarxiao_30S_repeater Reduced 568,828 8037A8127A7D324A

All four DFU/UF2 builds passed linked capability, packaging and runtime memory checks. Full has 150,576 internal bytes available; Reduced has 152,224, against 76,960 required. Independent package checks verified version, EndF body length, body hash, unchanged target/hardware IDs, artifact hashes and linked Basic capabilities. Every application contains the three-option help and omits the old four-option help. PlatformIO ran one process at a time.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#host-acceptance-and-scope","title":"Host acceptance and scope","text":"

All 28 checks passed: 19 carrier methods, 3 existing power methods and 6 XIAO startup/battery safety checks. Native harnesses use ASan/UBSan. Compatibility tests cover every old record value 0..3, both aliases, canonical Basic saves, identical actual-adapter gating and no startup rewrite. Every out-of-range record value 4..255 fails closed with no native bus access.

The actual adapter retains live self readings in Basic mode while native commands, periodic access, telemetry and voltage-history calls make no native bus calls. Existing detection/retry, timer-wrap/long-uptime replay, persistence fault, identity, permission, parser-boundary, canary and history checks pass.

These are host/build results only. No device was flashed and no physical Auto, radio, charger, boot/soak or LoRa OTA test was performed for this image. SEA Sherwood remains unreachable and unchanged. Earlier hardware reports apply only to their named firmware revisions.

See the validation record for package hashes and the controls guide for hardware sources and commands. Packages and logs are retained under /home/mesh/hwtest/solarxiao-basic-20261021/.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/","title":"Solarxiao ina260 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#solarxiao-ina260-battery-telemetry-2026-10-21","title":"SolarXiao INA260 battery telemetry, 2026-10-21","text":"

The SolarXaio MPPT 12V LiFePO4 carrier on the Mercerwood bench now reports its INA260 measurements in the normal MeshCore Full repeater build. With the user reporting a 13.2 V bench supply applied to BAT, all three LoRa telemetry replies reported 13.26 V, with signed current readings of -11, -8 and -8 mA. Three subsequent nonce-bound repeater trace tests passed at -6 dBm.

Machine-readable results contain the measurements, exact firmware identity, trace evidence, cleanup and limitations.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#discovery-and-correction","title":"Discovery and correction","text":"

The original Full build contained the INA260 driver, but its carrier I2C pins were reversed and its generic INA260 address was 0x41. Read-only diagnostic transactions identified this board's INA260 at 0x40, manufacturer ID 5449, device ID 2270, with SDA=P0.09 (variant pin 30) and SCL=P0.10 (variant pin 31). Three USB-only readings were about 0.11125 V before the user connected the BAT supply.

The SolarXiao target now supplies these carrier pins through PIN_BOARD_SDA and PIN_BOARD_SCL, and overrides TELEM_INA260_ADDRESS to 0x40. Generic XIAO wiring and the generic INA260 address remain unchanged. Discovery tries the INA260's device-ID check before the INA219 driver can claim the same address and write an incompatible configuration.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#qualified-build-and-live-acceptance","title":"Qualified build and live acceptance","text":"Item Result Source revision a947f936ecd279b7b3c97b4d832c4efc9a8a696b Environment and installed profile solarxiao_33S_repeater, Full Application version v1.18.0.1-halo-keymind-cascade-dev-a947f936 Application body hash E6E4E0E7CA5DB232 Application image 579,792 bytes Application SHA-256 0ba24397e11c0c30b0e3213ea597c536c4bc075c25fae44fc39fa43ce0e5626b Full and Reduced builds PASS; memory, DFU and nine capability markers per package Board startup tests Six PASS, including carrier pin override and GPIO safety negative controls; ASan/UBSan Warm restart PASS; sensor discovery, identity and saved settings preserved LoRa telemetry Three replies; channel 2 has voltage, current and power Repeater traces Three PASS; exact nonce, four-byte route and one target hop Trace direct RX/TX 1 / 1 per round; zero added receive errors Final core error flags Zero

The requested setup retained USB power, applied the supply to BAT and left solar disconnected. This checks sensor discovery and functional readout; it does not calibrate the INA260 against an independent voltage reference. Channel 1 is the XIAO internal ADC reading. Channel 2 is the carrier INA260. Reported power was 0 W in these near-zero signed-current samples; it is not an independent power or charger-performance measurement.

The normal radio remained 910.5250244,62.5,7,5, repeating stayed on, and TX stayed -6 dBm. RX power saving remained on,level=8,preamble=16,rx=39657,sleep=15975. The telemetry controller's temporary contact and forwarding connection were removed, the trace partner's settings were preserved, and USB endpoints were closed.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#scope-and-retained-evidence","title":"Scope and retained evidence","text":"

The installed application was updated through USB serial DFU. The qualified HEAD bootloader remained at revision b07255d4cdc601f1313e4cbe020caad3b5544821, manifest CRC 76EBABBF. The earlier full LoRa OTA transfer on application efad0076 is documented in the bootloader and LoRa OTA report. This run verifies LoRa telemetry and repeater forwarding on a947f936; it does not repeat a complete firmware transfer on that application.

An additional UART-pin diagnostic probe timed out after the successful INA260 reads. The original application and settings were restored before installing the final fix. The first trace run passed all three RF traces but failed a cleanup assertion that compared the entire changing radio2.status response. The corrected check compares saved radio settings exactly and verifies radio2 remains off; the subsequent complete run passed. Its final cumulative receive error counter was 7, with zero added during each owned trace round.

No LTC4162 charger configuration registers were written. The charger SKU is LTC4162-FSTM as reported by the user. Charging behavior and other carrier revisions are outside this acceptance result. The Reduced package was build-checked only; live INA260 acceptance used the Full package.

"},{"location":"test-results/solarxiao_power_2026-10-21/","title":"Solarxiao power 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_power_2026-10-21/#solarxiao-mppt-and-battery-controls-2026-10-21","title":"SolarXiao MPPT and battery controls, 2026-10-21","text":"

The Mercerwood SolarXiao 33S repeater now has a native LTC4162-FST charger interface and INA260 battery monitor in both Full and Reduced firmware. All 75 named register fields were read on the board. Supported setting changes passed live readback and persisted across a warm restart. The original charger register values were restored and all test overrides/calibration flags cleared. Three nonce-bound repeater forwarding tests passed at -6 dBm.

Machine-readable acceptance record contains exact build identities, readouts, command replies, fault-test coverage, RF samples, cleanup and limits. See the SolarXiao controls guide for commands, units, permissions, persistence, calibration and telemetry channels.

"},{"location":"test-results/solarxiao_power_2026-10-21/#qualified-firmware-and-hardware","title":"Qualified firmware and hardware","text":"Item Result Source ea8176ddc6b2caa7e52f8dcba46178f69f6cbbea Target / installed profile solarxiao_33S_repeater / Full Version v1.18.0.1-halo-keymind-cascade-dev-ea8176dd Application body hash 5A46FDAABF34C62D Application size 595,040 bytes Application SHA-256 d6068baf837c0348ea841a04a2412bd4724a93022f2d8b750af15ca1fb4fbe76 Full / Reduced qualification PASS: build, runtime memory, DFU, UF2 and linked capabilities Native power marker in both images PASS Native host test methods 3 PASS, including eight core scenarios and real Wire/atomic-store boundaries; ASan/UBSan XIAO startup/shutdown checks 6 PASS, including GPIO safety negative controls Powered USB readings 75 named register fields; FST chemistry code 6, four cells Settings / persistence 37 commands including 10 rejected writes; readback, restart and exact register restoration PASS LoRa carrier telemetry PASS: three replies with all carrier channels 2..13 Repeater forwarding 3 PASS; exact nonce, four-byte route, one target hop Bootloader HEAD b07255d4cdc601f1313e4cbe020caad3b5544821, CRC 76EBABBF, preserved

Full has 150,608 internal bytes available against 76,960 required by the runtime memory check; Reduced has 152,256 available. The installed public key, primary radio, RX power saving and bridge settings remained unchanged. Repeating stayed on and TX stayed -6 dBm. Each trace added one direct RX and TX with zero receive errors. The cumulative receive-error counter was 2 after the trace run and 17 after all telemetry work; core error flags remained zero. The trace partner's settings were preserved and endpoints closed.

"},{"location":"test-results/solarxiao_power_2026-10-21/#powered-bench-acceptance","title":"Powered bench acceptance","text":"

The user supplied 13.2 V at SOLAR/VIN with BAT disconnected and USB connected. Initial native readouts were approximately 13.2926 V input, 13.2785 V output, 27.76 C die and 27.73 C modeled NTC temperature. BAT was open: its changing floating/capacitor voltage is not a battery measurement or a charger load test. The charger reported battery_short while BAT was disconnected.

The test exercised force/fast telemetry, MPPT, input-current limiting, manual input-voltage floor, suspend/resume, JEITA selection, manual charge current and cell voltage, C/x threshold, CV/total timers and saved calibration metadata. For example, 13000 mV input floor read back as 13078.1 mV, matching the hardware step. Manual targets of 1000 mA and 3500 mV/cell read back correctly with JEITA off. These are register-interface tests without a connected battery.

Saved policy reloaded after restart with nine register overrides and the explicit calibration-source flag. Every modified register was restored to its captured value, then mppt reset cleared saved overrides and calibration flags. Final MPPT and JEITA were on; force/fast telemetry were off. Host fault tests also simulated a charger reset, read/write faults, ignored writes, corrupt or wrong-size storage, failed atomic publication, identity mismatch, masked-bit preservation, thermal ordering and one-shot/W0C behavior.

"},{"location":"test-results/solarxiao_power_2026-10-21/#supply-state-radio-checks-and-scope","title":"Supply state, radio checks and scope","text":"

The first install check found the charger unavailable. It restored the verified previous firmware with identity/settings intact; the user then confirmed the bench supply had been off and switched it on. The final firmware installed and passed powered reading/control tests.

During a later interval when the charger was unavailable again, authenticated LoRa CLI returned an explicit unavailable status while INA260 readings remained available. Three telemetry replies carried channel-2 voltage/current/power; charger channels were omitted. Temporary contacts and forwarding connections were removed. One remote version read required a retry.

After the supply was switched on again, authenticated LoRa commands identified FST/four cells and read charger/battery/status values. Three subsequent telemetry replies contained every carrier channel 2..13: input 13.29 V, output 13.27..13.28 V, die 25.4..25.5 C and modeled NTC 24.9 C. Two telemetry attempts missed replies before a successful retry. An earlier powered CLI session also needed retries and missed its first telemetry response; all temporary contacts/forwards were cleaned up. Final USB checks confirmed VIN 13.2926 V, MPPT/JEITA on, zero saved overrides, zero calibration flags and the same firmware/bootloader identities. These results establish successful readout, not a loss-free radio link.

No actual battery charging, loaded-current calibration, resistance measurement or ship-mode entry was tested. The default 10 milliohm sense resistors and 10 kohm / 3490 K NTC model come from reference documentation; the supplied Standard schematic uses a single-cell L42M, not this exact 12 V FSTM revision. Actual carrier component values and the temperature model remain configurable.

The new application was installed by USB DFU. This run does not repeat a full LoRa firmware transfer; the earlier successful HEAD bootloader/OTA transfer is in the separate OTA report. Reduced was build-checked, while live tests used Full. An exploratory Room Server build exceeded its flash/UF2 limits; native power controls are therefore scoped to the requested SolarXiao repeater targets. No Room Server or Companion package is qualified by this report.

"},{"location":"test-results/solarxiao_room_2026-10-21/","title":"Solarxiao room 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_room_2026-10-21/#solarxiao-room-server-power-profiles-and-history-2026-10-21","title":"SolarXiao Room Server power profiles and history, 2026-10-21","text":"

The canonical SolarXiao QSPI OTA Room Server builds now initialize and service the native power interface. Both 30S and 33S support the same auto, basic and mppt profiles as Repeater, including saved overrides, native telemetry and authenticated administrator commands. Basic still supplies only existing XIAO battery voltage and MCU temperature; no Base/Advanced measurements were invented.

The earlier size failure used a legacy plain Room recipe. This run qualifies the existing solarxiao_30S_room_server_lora_ota and solarxiao_33S_room_server_lora_ota recipes with Full and Reduced sensors. Both fit the unchanged 757,760-byte OTAFIX application region. No features, client capacities or flash reservations were reduced to make them fit. Plain Room compatibility/recovery targets remain separate and unchanged.

"},{"location":"test-results/solarxiao_room_2026-10-21/#qualified-source-and-artifacts","title":"Qualified source and artifacts","text":"

Source: 1d2fb6a1b471f2984a5a4133854cad4b52150337. Version: v1.18.0.1-halo-keymind-cascade-dev-1d2fb6a1.

Target Profile Application bytes EndF body hash solarxiao_33S_room_server_lora_ota Full 524,424 761289FD9CEFE79B solarxiao_33S_room_server_lora_ota Reduced 494,888 9CA04F37C960797A solarxiao_30S_room_server_lora_ota Full 524,424 AB4F7D7F85872201 solarxiao_30S_room_server_lora_ota Reduced 494,888 FBD9FAA560F5D0CE solarxiao_33S_repeater Full regression 598,824 DEB7475A4B4B0BBC solarxiao_33S_repeater Reduced regression 568,828 8CDBFB5FA2615124

All six builds passed DFU/UF2, linked capability and runtime-memory qualification. Room Full/Reduced have 156,352/157,984 internal bytes available against 65,280 required; Repeater has 150,576/152,224 against 82,752 required. The model now explicitly reserves 5,792 bytes for capped I2C voltage history and allocator overhead, retaining the previous general sensor allowance.

Independent package checks verified application version, body length/hash, artifact hashes, three power modes, native power marker and self/history responses. Room packages also contain persistent Room history and board commands. The linked Room code retains posting, access control, mailbox, board articles and persistent post history. Existing Room target IDs remain 30S 0x5bb567cd and 33S 0xa8cb8e8b; their hardware IDs remain solarxiao_30S/solarxiao_33S. Repeater keeps 0x3caf966e.

"},{"location":"test-results/solarxiao_room_2026-10-21/#history-correction-and-host-checks","title":"History correction and host checks","text":"

History previously configured channels once before Auto's first probe. An MPPT discovered later could therefore provide live telemetry but never gain voltage-history storage. The shared driver now reserves history IDs 2..5 at boot without any bus access. Only active, valid readings produce samples; empty channels remain absent from populated history results. Late INA260 detection, later charger input power and Basic-to-MPPT changes can supply samples without rebooting or clearing other sensor history. This fixes both Room and Repeater.

Four native channels allocate 1,440 bytes; the shared 16-channel cap limits native plus optional history to 5,760 bytes. The new budget test compiles the real history class and binds that allocation to the RAM policy. It also checks disabled-history/native cases and unrelated platforms.

Passing checks include:

PlatformIO ran one process at a time. Final packages and logs are retained at /home/mesh/hwtest/solarxiao-room-20261021/final-firmware/ and its parent. The validation record contains exact hashes.

"},{"location":"test-results/solarxiao_room_2026-10-21/#deployment-scope","title":"Deployment scope","text":"

This is host/build qualification, not physical Room acceptance. No device was flashed, no Room boot/soak or RF test was run, and no LoRa firmware transfer was performed for this image. SEA Sherwood remains unreachable and unchanged. The matching board/storage-aware OTAFIX bootloader is required for these Room targets; the existing bootloader/scratch layout and 30S/33S PA settings were preserved. See the controls guide for commands and target selection.

"},{"location":"test-results/spiffs_regular_file_reads/","title":"Spiffs regular file reads","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/spiffs_regular_file_reads/#spiffs-regular-file-reads-and-login-replay-state","title":"SPIFFS regular-file reads and login replay state","text":"

Arduino-ESP32 SPIFFS can return a truthy directory handle from a read-open of a nonexistent filename. File::operator bool() alone does not prove that a regular file exists. A directory's size() and read() are zero.

The first privileged login on an upgraded G2 encountered this in ClientACL::writeClientLoginReplayCeiling(): opening the not-yet-created /s_login_replay appeared successful, then subtracting the eight-byte trailer from size zero underflowed. Copying the supposed records failed and login was rejected. Waiting or changing the repeater clock cannot fix that file-open bug.

"},{"location":"test-results/spiffs_regular_file_reads/#fix","title":"Fix","text":"

Use mesh::openFileRead() for regular-file reads. It checks existence and rejects directory handles, while preserving real empty files. Replay record counts also validate the minimum trailer size, record alignment, and maximum count before subtraction. An existing replay file that becomes unreadable is not treated as a new store.

No replay records are cleared and authentication is not weakened. Invalid or unwritable replay state still fails closed. The first successful privileged login creates a 44-byte file: one 36-byte identity/ceiling record plus its eight-byte integrity trailer.

Setting the repeater clock must not clear this file. Admission compares the sender's timestamp with that sender's saved boundary, not with the repeater's current time. Clearing the boundary could make captured requests reusable. A sender clock rollback is a separate condition and remains subject to the saved boundary after this fix.

Companion directory enumeration uses the separate openDirectory() API, which deliberately permits SPIFFS virtual directories. Regular-file reads are also enforced for companion data, repeater/room logs, flood-rule verification, and HTTP packet-log downloads (missing logs return 404; real empty logs 200).

"},{"location":"test-results/spiffs_regular_file_reads/#audit-boundary","title":"Audit boundary","text":"

The audit covered file opens and size arithmetic in src and examples. Identity, region, clock, and common preference loaders already gate reads with filesystem existence checks; SPIFFS's exists() explicitly excludes directory handles. Relevant MQTT length subtraction follows validated headers and exact reads. ESP32 OTA staging uses partition APIs, not these SPIFFS file handles. Intentional directory enumeration must not be changed to a regular-file read.

"},{"location":"test-results/spiffs_regular_file_reads/#regression-checks","title":"Regression checks","text":"

The pre-fix actual ACL code was reproduced as first_login=rejected, one missing read-open, and no replay file. The fixed code accepted that identical scenario without a missing read-open and created the valid 44-byte record. Host simulations do not replace a post-flash LoRa login test on the G2.

"},{"location":"test-results/station_g3_g2_compatibility/","title":"Station g3 g2 compatibility","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/station_g3_g2_compatibility/#station-g3-and-g2-compatibility-audit","title":"Station G3 and G2 compatibility audit","text":"

Source audit on 2026-10-18, starting from 1147c28b279f01443c9ec44c3b71cc767f11482e. A user reported that G2 firmware works on a physical G3 while G3 firmware has problems. The comparison below identifies differences in configuration and startup; it does not establish the cause of that device's symptoms or claim a successful hardware retest.

"},{"location":"test-results/station_g3_g2_compatibility/#radio-and-peripheral-pins","title":"Radio and peripheral pins","text":"

The G2 radio map matches G3 physical slot 1 exactly. G3's _r2 suffix refers to physical socket 2, not motherboard revision 2. Current G3 firmware detects the populated socket at boot and retains both legacy target names.

Signal or build macro G2 G3 slot 1 G3 slot 2 SX1262 DIO1 / interrupt 48 48 2 SPI NSS / chip select 11 11 43 SX1262 reset 21 21 44 SX1262 BUSY 47 47 1 SPI SCK 12 12 39 SPI MISO 14 14 41 SPI MOSI 13 13 40 External LNA control Not controlled by G2 firmware 10, active low 42, active low PA PL1 control Not controlled by G2 firmware 9, shared; high selects higher level 9, shared I2C SDA / SCL 5 / 6 5 / 6 5 / 6 User button 38 38 38 PIN_GPS_RX / PIN_GPS_TX 15 / 7 15 / 7 15 / 7

The radio and FEM maps agree with the manufacturer's ESP32-S3 pinout viewer and its pin data, and the tracked G2 recipe, G3 recipes, and runtime socket table. The GPS macros name the peripheral's signals: the ESP32 startup calls Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX), giving MCU RX GPIO7 and TX GPIO15. No incorrect radio map was established. Both base recipes select a 1.8 V TCXO and disable the SX1262's internal boosted RX gain by default. The base TX power is 7 dBm in both; their configured maximum chip-drive limits differ (G2: 19 dBm; G3: 22 dBm). This audit does not change those power limits.

"},{"location":"test-results/station_g3_g2_compatibility/#ordinary-profile-flash-compatibility","title":"Ordinary-profile flash compatibility","text":"

Before this change, both board manifests declared QIO flash, but the G2 recipe overrode it to DIO. Ordinary G3 profiles inherited QIO. The G3 board manifest now selects DIO as well, following the known-working G2 startup policy. All G3 role recipes, including the legacy _r2 targets, inherit that setting.

ESP32-S3 Full builds already force DIO through the Full partition policy. This change therefore corrects an ordinary-profile difference; it does not explain a failure in an already verified DIO Full image. The PSRAM memory type remains qio_opi. Flash capacity, partition layouts, USB configuration, and radio pin assignments are unchanged.

Ordinary G2 firmware uses the legacy default.csv partition table, while G3 uses default_16MB.csv. Electrical compatibility does not make those layouts interchangeable. When moving a node that currently runs a G2 image to a G3 image, check its installed layout and use the documented partition migration if needed to retain its identity and settings. This fix does not change either layout.

The two board manifests otherwise have matching build/upload settings; their board names and manufacturer-page URLs distinguish them. DIO is a compatibility choice here, not a claim that QIO caused the user's reported problem.

"},{"location":"test-results/station_g3_g2_compatibility/#bounded-radio-startup-probe","title":"Bounded radio startup probe","text":"

The G3-only socket probe added in f765604cb8 imposed a 100 ms BUSY wait and only three RC-standby attempts before rejecting a socket. G2 goes directly through RadioLib's normal initialization, which permits a longer reset and RC-standby settling interval. This is a separate, reproducible startup compatibility gap, independent of the flash-mode difference.

The corrected probe shares one rollover-safe 1,000 ms deadline across reset, BUSY waits, and standby/version retries. It services cooperative USB while waiting. It retains identification-only operation, safe chip-select/reset cleanup, explicit socket overrides, and the existing fallback rules.

The native production-code fixture models both sockets and timer rollover. Elapsed times below are simulated fixture clocks, not physical measurements:

Modeled condition Previous probe Corrected probe BUSY remains high for 200 ms after reset Rejected at 111 ms, no version-register reads Detected at 202 ms, 16 bytes read First five standby commands ignored Rejected at 44 ms, 48 bytes read Detected at 67 ms, 96 bytes read BUSY never clears Rejected early Rejected at exactly 1,000 ms, no register reads Socket absent Rejected after three attempts Rejected at exactly 1,000 ms 800 ms BUSY followed by persistent command refusal Rejected early Rejected at exactly 1,000 ms total

An immediately responsive chip still completes its probe in under 20 ms in the fixture. Negative controls restoring the old short BUSY budget or the three-attempt limit fail the delayed-startup regressions. These results prove the software tolerance gap; they do not prove it caused the user's hardware failure. No transmit or TCXO-setting override was introduced.

"},{"location":"test-results/station_g3_g2_compatibility/#physical-socket-commands-in-the-normal-infrastructure-cli","title":"Physical socket commands in the normal infrastructure CLI","text":"

The follow-up audit also found that Repeater, Room, and Sensor infrastructure commands consumed all get and set requests in the shared CLI. Their final board-command hook was reached only for other verbs. Consequently, the G3's normal get radio.slot and set radio.slot ... commands never reached the board implementation. The separate pre-radio USB recovery console and KISS binary commands called the board directly and were unaffected.

Unknown shared getters and setters now reach the board-command hook before returning an unknown-setting reply. Shared commands, including the guarded FEM setters, retain priority. Socket changes still save only the next-boot selection; they never switch the running radio or reboot behind the operator's back.

"},{"location":"test-results/station_g3_g2_compatibility/#receive-gain-changes-during-transmission","title":"Receive-gain changes during transmission","text":"

The follow-up comparison from eb00b5370 found another G3-specific runtime defect. G2 firmware leaves the external amplifier controls alone. G3's setLNAEnable() reapplied receive mode unconditionally, so even repeating set radio.fem.rxgain on during an active transmission enabled the LNA before the transmit-completion hook. This was reachable through both the normal infrastructure CLI and Companion gain setters.

The G3 FEM now tracks startup/sleep, receive, and transmit mode. A gain setter updates the requested receive preference while preserving the transmit bypass; the requested state is applied when receive mode resumes. Startup and terminal sleep also keep the LNA disabled until an explicit receive transition. PA-level changes retain their existing policy of applying at the start of the next transmission. The inactive socket's LNA is left disabled.

The unconditional receive-mode call came from the runtime FEM preference implementation in 7509e51e3d, Kevin Le, 2026-08-12. This finding concerns mode handling; the PA/LNA pin assignments and polarities match the manufacturer documentation.

"},{"location":"test-results/station_g3_g2_compatibility/#complete-saved-radio-restoration-in-room-and-sensor-roles","title":"Complete saved-radio restoration in Room and Sensor roles","text":"

The same comparison found a shared role issue that can affect G3, G2, and other boards. Room and Sensor could clear their saved-radio retry flag after accepting the frequency/modulation tuple even when the subsequent saved TX-power command was rejected. Room also ignored rejection of its saved internal RX-gain command. An incoming packet or BUSY response during runtime restoration could therefore leave only part of the saved configuration applied.

Their saved-radio helper now reports success only after every applicable setting has been accepted. Rejection keeps the existing retry/backoff active, including restoration after a temporary radio setting expires. No preference layout, default setting, or Sensor RX-gain feature is added by this fix. This shared issue alone does not explain why G2 firmware works better on the user's G3.

"},{"location":"test-results/station_g3_g2_compatibility/#saved-fem-and-socket-settings","title":"Saved FEM and socket settings","text":"

G3 firmware actively drives its LNA and PA-level GPIOs. G2 firmware does not, so a saved FEM setting can have a visible effect only after switching to G3 firmware. Once the normal CLI has started, read these values before changing settings:

get radio.slot\nget radio.fem.rxgain\nget radio.fem.txgain\n

If startup reports Station G3 radio unavailable, the normal CLI has not started. Press Enter and use the limited USB recovery commands instead:

!g3 help\n!g3 get radio.slot\n

That recovery console accepts only socket selection, help, and reboot; the normal FEM getters are unavailable there.

radio.slot reports the saved selector, active socket, and detected sockets. An explicit socket override does not silently fall back to another socket. radio.fem.rxgain controls the external LNA and is distinct from radio.rxgain, which controls the SX1262's internal boosted gain.

Preserve the saved values and jumper positions while diagnosing the device. The manufacturer documents GPIO control as effective with the corresponding LNA P/LNA S or PA PL1 jumper removed; PA PL2 selects the hardware power-level pair. Changing gain or jumpers is an operator choice, not an automatic part of this compatibility fix.

"},{"location":"test-results/station_g3_g2_compatibility/#git-history-and-attribution","title":"Git history and attribution","text":"Change Introducing commit and author Initial G2 support and board manifest acac20dc89, Rastislav Vysoky, 2025-03-07 G2 radio pin flags moved into the variant recipe 7cb2e0863a, recrof, 2025-10-12 Original G3 slot-1 map, PA9 and LNA10 75fb07fc2c, Chris / Cisien, 2026-05-09; merged in upstream PR #2515 G2 DIO recipe override 1a43123d2c, mikecarper, 2026-08-24 G3 second-slot recipes and GPIO map d8d72b7833, Adam Gessaman / agessaman, 2026-09-05 Runtime socket discovery and shared pin binding f765604cb8, mikecarper, 2026-10-15

git blame and the introducing diffs were checked, rather than attributing copied pin values to the latest refactor. Original G3 support is present in upstream dev; the second-slot recipes and runtime discovery were added in this fork. The original G3 pull request reports hardware testing by its author. That historical report does not qualify the current firmware or the user's device.

"},{"location":"test-results/station_g3_g2_compatibility/#validation-boundary","title":"Validation boundary","text":"

The host recipe regression resolves every tracked G3 environment, checks that ordinary roles cannot override DIO, and verifies that qio_opi PSRAM remains unchanged. It also executes the existing Full policy for canonical Repeater, legacy _r2 Repeater, and Full Companion configurations. A deliberate nested QIO override must fail that check.

Check Status G3 recipe suite, including all 16 current role environments and the deliberate QIO override PASS, 10 test methods Existing ESP32 Full partition policy suite PASS, 11 test methods Native socket-detection suite, including 144 selection cases and 20 delayed/deadline cases PASS, 6 test methods, ASan/UBSan Combined socket detection/settings, boot recovery, radio pins, KISS slot and GPIO-hold suites PASS, 36 test methods, including the 6 detection methods above Ordinary Repeater, legacy _r2 Repeater and KISS firmware builds PASS; all bootloader and application image headers verified as DIO Canonical Full Companion package PASS; 24 linked capability checks, internal RAM and application-slot checks passed; bootloader/application and merged-image headers verified as DIO Follow-up G3 gain-mode tests through production CLI/Companion setters and GPIO helpers PASS, 2 test methods; both sockets, failed saves and old-setter negative control Follow-up Room/Sensor saved-radio startup and runtime restoration PASS, 4 test methods; actual wrapper rejection, retry/backoff and temporary-radio expiry paths Follow-up normal infrastructure socket-command routing PASS, 7 test methods; real board/NVS dispatch, shared guards and old-dispatch negative controls Combined follow-up native regression batch PASS, 30 test methods with sanitizer coverage Follow-up Repeater, Room, Sensor and ESP32-C6 Room firmware builds PASS, all 4 environments Follow-up canonical Full Companion package PASS; 24 linked capability checks, internal RAM and application-slot checks passed Follow-up hosted documentation link checks PASS, 7 test methods Physical G3 boot and RF receive/transmit Not performed for this change

No successful physical G3 boot, RF receive/transmit trial, or reproduction of the user's original problem is claimed by these host checks.

"},{"location":"test-results/t1000_usb_bluetooth_priority_2026-10-20/","title":"T1000 usb bluetooth priority 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/t1000_usb_bluetooth_priority_2026-10-20/#t1000-e-usb-priority-20-october-2026","title":"T1000-E USB priority, 20 October 2026","text":"

Source: ac0daf3cef31a13163d469c1fd538c06708fe05a.

T1000-E Companions now stop Bluetooth advertising and disconnect the Bluetooth client whenever a USB data host is enumerated. This implements the requested USB-priority policy after the simultaneous USB/BLE investigation did not establish a reliable coexistence fix.

USB priority does not depend on an open serial application or DTR. A charge-only supply does not acquire priority. Bluetooth is requested again after the data host has been absent for 500 ms, preserving manual-off and wireless controls. The policy does not change saved preferences, pairing credentials or identity. It applies to the T1000-E Bluetooth and Full Companion recipes.

Check Result Full Companion build, capability markers, runtime RAM and application-only image bounds PASS Bluetooth Companion build, capability markers and application-only image bounds PASS Priority, startup/wake, manual controls, reply routes, bus-reset hold and timer wrap Six host tests PASS Existing Bluetooth preference/CLI and wireless control tests Eleven host tests PASS nRF52 BLE regressions, including late connect after disable 42 tests OK; eight emulator-dependent tests skipped Contact-stream concurrency Two host tests PASS Hosted documentation links Seven Python tests and JavaScript checks PASS Install Full on the exact Pi-connected T1000-E PASS; application only, identity unchanged USB runtime and saved on/off commands while paused PASS; status reports bluetooth off (USB data connected) Saved on during USB priority PASS; command acknowledges saved on while remaining paused Twenty-second BLE scan with USB enumerated and serial handles closed Zero target events among 100 scan callbacks Post-test USB readback PASS; same identity/version, zero core flags, zero recorded crashes Physical unplug, BLE recovery and reconnect qualification on this build Pending user unplug USB insertion during an established BLE session Host regression covered; hardware check pending Charge-only and cold-boot behavior on this build Host policy covered; hardware checks pending

The installed version is v1.18.0.1-halo-keymind-cascade-dev-ac0daf3c (Build: 20-Oct-2026). Its application ZIP SHA-256 is 64e09ff2eb8b5e3228f656c7ea99b000c053736b8a020b7eab1429b4d00ab43f. The bootloader and SoftDevice were not changed. All opened USB ports were closed, with no framing rejects in the recorded runtime checks. The original saved-on Bluetooth choice was restored after the temporary on/off test.

This verifies USB-priority behavior, not simultaneous active USB/BLE, complete T1000-E feature qualification or qualification of the other campaign boards. Historical battery and wall-power passes belong to their original builds. Safe evidence summary records the current source and artifact bindings.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/tdeck_radio2_display/","title":"Tdeck radio2 display","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/tdeck_radio2_display/#t-deck-radio2-scheduling-during-display-redraw","title":"T-Deck radio2 scheduling during display redraw","text":"

Measured on 18 October 2026 (America/Los_Angeles), using one ESP32-S3 T-Deck with 16 MiB flash and 8 MiB PSRAM. The receive-only harness uses the production CustomSX1262Wrapper, profile scheduler and ST7789LCDDisplay. It never sends radio packets or starts WiFi/Bluetooth. Source base: 22cdb7014, with the changes and application readback hash identified in the machine-readable results.

"},{"location":"test-results/tdeck_radio2_display/#change","title":"Change","text":"

T-Deck renders into a 153,600-byte RGB565 canvas allocated explicitly in PSRAM. It transfers at most 960 pixels per TFT transaction (384 microseconds of wire time at 40 MHz). Each transfer ends and releases chip select before servicing the radio. CPU rasterization also offers bounded cooperative checkpoints. The callback runs only the radio driver, without recursively entering the mesh dispatcher or UI. Pending-packet, active-reception and transmit guards remain.

The T-Deck radio also uses the existing buffered 8 MHz SPI HAL. The TFT and radio retain their shared HSPI peripheral and separate chip selects. Display wake restores the backlight without repeating controller initialization. Other boards keep their existing display path. If PSRAM allocation fails, the driver warns and falls back to direct rendering.

"},{"location":"test-results/tdeck_radio2_display/#same-image-measurements","title":"Same-image measurements","text":"

The two profiles were 909.5 MHz / SF7 / BW62.5 and 910.5 MHz / SF8 / BW500, both CR5 with explicit 32-symbol preambles. These are measurement controls, not a packet-delivery or preamble recommendation.

Each stress case ran three times for five seconds, requesting 20 redraws per second. Rendering could take longer than the requested frame interval; actual frame counts and elapsed times are retained in the results. Values below use all samples across the three trials, with worst observed maxima.

Display path Radio SPI Mean retune Maximum retune Maximum gap between radio service calls Static screen Bytewise, 2 MHz 497 us 525 us 584 us Original direct redraw Bytewise, 2 MHz 610 us 629 us 76,089 us PSRAM canvas and bounded strips Bytewise, 2 MHz 581 us 668 us 1,343 us PSRAM canvas and bounded strips Buffered, 8 MHz 468 us 581 us 1,237 us

At a normal requested refresh rate of one frame per second (two four-second trials per case), the original 2 MHz path's maximum service gap was 73,383 us; the new 8 MHz path's was 1,251 us. Reducing refresh frequency alone does not remove the individual blocking redraw.

Retune samples bracket the production retune hooks. They are separate from display frame duration and the gap between radio service calls. Histogram percentiles in the JSON are bucket upper bounds, not exact percentiles.

All 30 trials completed 7,025 profile switches with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. Every final RX-mode and modulation-cache check passed. The minimum remaining loop stack was 3,340 bytes in this harness. No packets were decoded, so these measurements do not establish packet delivery under redraw, loaded-channel behavior, Bluetooth/ WiFi interference, or long-term reliability.

"},{"location":"test-results/tdeck_radio2_display/#reproduction-and-checks","title":"Reproduction and checks","text":"

Include tools/hil/tdeck_display_switch.ini in platformio.local.ini, build tdeck_display_switch, and flash the exact T-Deck. The board needs a ROM-compatible DIO boot header; the first QIO-header attempt failed before the application started. The supplied harness recipe selects DIO.

Run the collector with --fps 20 --seconds 5 --repeats 3, then with --fps 1 --seconds 4 --repeats 2. Only one tool may own the selected serial port, and PlatformIO commands must run sequentially.

Sixteen focused native display/R8/radio tests passed. They cover pixel parity, clipping, partial updates, strip bounds, released SPI before callbacks, callback recursion protection, PSRAM failure, mode toggling and sleep/wake. The new display regression is included in CI.

"},{"location":"test-results/tdeck_radio2_display/#full-companion-smoke-check","title":"Full Companion smoke check","text":"

The optimized canonical Full Companion image v1.18.0.1-tdeck-radio2-fast-22cdb701 compiled and passed its unchanged RAM gate (230,680 internal bytes available; 164,896 required). It was installed on the same board. USB ASCII commands and Binary Companion device queries worked; Bluetooth reported on. Device queries retained 350 contacts and 40 channels. A Bluetooth client connection was not tested.

With the screen held on, a temporary receive-only secondary accumulated 759 switches with zero switch errors during a short runtime check. Its maximum successful active-pair retune was 1,013 us and the guarded budget was 1,115 us. One additional primary packet was received during that interval. This is a smoke check rather than a controlled packet-delivery trial. Returning to single-profile RX power saving raised the lifetime max diagnostic to 4,510 us; that teardown is outside the active pair's measured switching budget.

The board was left running Full Companion with Bluetooth on, radio2 off and its original button-pairing USB display mode. Seven hosted-document tests also passed after adding this report.

"},{"location":"test-results/tdeck_radio2_retune_experiments/","title":"Tdeck radio2 retune experiments","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#t-deck-retune-command-priority-and-spi-experiments","title":"T-Deck retune command, priority and SPI experiments","text":"

Measured on 18 October 2026 (America/Los_Angeles) on one ESP32-S3 T-Deck with 16 MiB flash and 8 MiB PSRAM. This follows the display redraw scheduling measurements. The receive-only harness uses the production radio wrapper, profile scheduler and display driver. It does not start Bluetooth or WiFi or send radio packets. The sanitized measurements retain each result once, firmware image hashes, counters, sample counts and timing summaries; serial-port identifiers and raw console transcripts are omitted.

For this tested board, the selected setting is 8,888,888 Hz with the confirmed modem and IQ caches enabled, normal owner priority, and the original SPI clock phase programming. This was the highest bulk-transfer clock that passed these checks. It does not establish a board or transceiver physical maximum. The canonical Full Companion build and runtime smoke checks are recorded below.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#measurement-method","title":"Measurement method","text":"

The primary profile was 909.5 MHz / SF7 / BW62.5 / CR5 with a 32-symbol preamble. The secondary was 910.5 MHz / SF8 / BW500 / CR5, using a 32-symbol preamble for the main and load comparisons and 40 symbols for the IQ comparison. These controlled settings are not a packet-delivery or preamble recommendation.

All 42 controlled trials used the same 394,304-byte application, SHA-256 9afce4f59227382dfe383b6b53ac52001644b05f8636c8f4c4f3ed6e12937668, built from base 22cdb7014 with the experimental changes applied. Each case ran three times for five seconds with either a static screen or requested 20 Hz buffered redraws. Actual frame counts are retained because rendering can take longer than the requested interval.

Retune timings bracket the radio wrapper's production retune hooks. Radio service gaps, display frame durations and receive-window deadline lateness are separate measurements. Table means combine both retune directions and all three trials, weighted by sample count; maxima are the worst observed. The source means were recorded to three decimal places. JSON percentile fields are histogram bucket upper bounds, not exact percentiles.

Command counters count the instrumented RadioLib HAL transfers, transferred bytes, packet-type queries and acknowledged cache skips. They describe actual execution of the selected policy rather than an estimated command saving.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#confirmed-modem-query-cache","title":"Confirmed modem query cache","text":"

The cache omits the repeated packet-type query only while a previously confirmed LoRa modem remains valid. Lifecycle invalidation and the existing receive and packet-ownership guards remain part of the production path.

Policy at 8 MHz Static mean retune Static maximum Buffered mean retune Buffered maximum Query on each hop 428.810 us 450 us 486.632 us 595 us Reuse confirmed modem 400.021 us 423 us 459.761 us 575 us Query on each hop, scoped priority 3 439.244 us 460 us 495.471 us 605 us

Across the cache-enabled main cases, 1,844 hops recorded 1,844 skipped modem queries and zero actual modem queries. The corresponding uncached baseline recorded 1,844 modem queries. Scoped priority alone did not improve these quiet harness timings.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#scoped-priority-under-a-competing-task","title":"Scoped priority under a competing task","text":"

The single existing radio/UI owner normally ran at priority 1 on core 1. The experiment temporarily raised only the retune call to priority 3, then restored priority 1. It did not create a second task that could manipulate radio state. The competing task ran on the same core at priority 1, performing a bounded 250-microsecond CPU burst every two RTOS ticks.

Both policies enabled the confirmed-modem cache. The load cases recorded 25,269 competing-task bursts. Every result confirmed the selected actual retune priority and restoration of the original owner priority.

Priority during retune Static mean / maximum Buffered mean / maximum Static maximum service gap Buffered maximum service gap 1 429.704 / 665 us 594.379 / 825 us 730 us 1,494 us 3, then restore 1 405.178 / 492 us 527.755 / 832 us 738 us 1,519 us

Scoped priority reduced mean retune time with this particular competitor, but did not improve the worst complete radio-service gap. This synthetic task does not establish behavior with a connected Bluetooth client, network traffic or the other services in Full Companion.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#iq-workaround-reuse-when-packet-parameters-change","title":"IQ workaround reuse when packet parameters change","text":"

Using a 40-symbol secondary preamble makes the packet tuple change between the two profiles. The comparison tests reuse of the already applied normal-IQ workaround while retaining the packet-parameter writes required for that change. Both cases used the confirmed-modem cache and normal owner priority.

IQ workaround cache Static mean / maximum Buffered mean / maximum Off 511.358 / 533 us 584.149 / 708 us On 430.664 / 457 us 491.475 / 600 us

All 12 IQ trials passed their runtime checks. The enabled cases recorded 1,843 IQ workaround skips across 1,843 hops. This is timing and state evidence from a quiet receiver, rather than a packet-delivery comparison.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#spi-sweep-and-transfer-path-comparisons","title":"SPI sweep and transfer-path comparisons","text":"

The fresh-boot sweep and bytewise diagnostic used a separate 395,408-byte application, SHA-256 d0124aac39a949f8a1cc292cb039f64ac61351d3fd3ca248137f14b4fb06ac90. Every independent clock attempt began with a device reboot. Each inspection performed 100 paired reads of packet type and the LoRa sync word, with standby and recovery at 8 MHz. Static and buffered timing trials followed only after the preceding checks passed. Clock order alternated between repeats.

The frequency column is calculated from the SPI peripheral's configured clock divider, captured during the radio HAL transfer. It is not an oscilloscope measurement of the clock signal.

Requested MHz Divider-derived MHz Complete passing attempts Incorrect modem reads per 100, across three inspections 8 8.000000 3/3 0, 0, 0 9 8.888888 3/3 0, 0, 0 10 10.000000 0/3 6, 7, 7 11 10.000000 0/3 5, 5, 6 12 11.428571 0/3 20, 20, 28 13 11.428571 0/3 22, 20, 20 14 13.333333 0/3 59, 49, 67 15 13.333333 0/3 58, 54, 55 16 16.000000 0/3 64, 68, 64

These inspections recorded no sync-word mismatch or explicit SPI read error; the failing check was an incorrect modem value. The sweep's highest passing divider-derived frequency was 8.888888 MHz for this bulk-transfer implementation. This observation must not be treated as the radio's physical SPI limit.

A separate bytewise diagnostic read all 100 modem values correctly at both 10 MHz and 16 MHz, with no sync-word mismatch or reported read error. Its 16 MHz bulk comparison returned 29 correct values and 71 incorrect values; returning to bulk 8 MHz read all 100 correctly. The different transfer paths therefore prevent interpreting the bulk failures as a transceiver clock limit.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#clock-register-experiment-and-longer-read-checks","title":"Clock-register experiment and longer read checks","text":"

The final diagnostic and comparison image was 396,560 bytes, SHA-256 6e830f93dd3bb632e73e47878e8bbe61795a0389fa1ba75aab3c2a1ff9e14a85. With the original phase programming, 10,000 paired modem/sync-word reads passed at both 8 MHz and 8.888888 MHz, with zero incorrect modem values, sync-word mismatches or reported read errors. A 10 MHz check returned 58 incorrect modem values in 1,000 pairs.

Inspection found differences between some N/pre-divider/phase fields and an experimental reconstruction of the clock register. Changing those phase fields did not improve the link: at 8.888888 MHz it introduced 587 incorrect modem values in 10,000 pairs; at 10 MHz it returned 64 incorrect values in 1,000 pairs, compared with 58 using the original programming. The experiment also failed at higher tested clocks. It was rejected for production and remains confined to the HIL helpers under tools/hil.

These observations neither explain every transfer-path difference nor establish a causal driver defect. The reported frequencies remain divider-derived values; clock waveforms and phase lengths were not measured with an oscilloscope.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#final-comparison-with-exact-clock-settings","title":"Final comparison with exact clock settings","text":"

All 24 final trials used that same final image, with exact clock settings enabled and the rejected phase-field correction disabled. Each policy ran twice for five seconds in both display modes. A nominal 9 MHz request in the earlier sweep produced an 8,888,888 Hz divider; the final setting requests that exact frequency, avoiding the rounded-frequency divider search. The timing comparison below is within this final image rather than across different instrumentation builds.

Secondary preamble Clock setting Caches Static mean / maximum retune Buffered mean / maximum retune 32 8 MHz Neither 426.751 / 457 us 518.300 / 626 us 32 8 MHz Modem 404.635 / 438 us 493.754 / 610 us 32 8.888888 MHz Modem 402.134 / 436 us 491.574 / 608 us 40 8 MHz Neither 562.482 / 605 us 652.882 / 777 us 40 8 MHz Modem and IQ 437.898 / 472 us 528.139 / 648 us 40 8.888888 MHz Modem and IQ 435.327 / 471 us 526.319 / 653 us

The additional clock increase saved about 2\u20133 microseconds in mean retune time for these cached paths. Avoiding redundant commands supplied the larger saving. The cache-enabled final trials recorded 4,912 modem-query skips; the IQ-enabled cases recorded 2,457 IQ workaround skips.

Across the cached 8.888888 MHz final cases with secondary preambles 32 and 40, the best observed individual retune was 378 us and the worst was 653 us. The separate Full Companion smoke check below reached 1,224 us under its application workload; those measurements are not interchangeable.

The selected T-Deck configuration uses MC_SX1262_SPI_HZ=8888888, MC_SX1262_FAST_MODEM_CACHE=1 and MC_SX1262_FAST_IQ_CACHE=1. Scoped priority stays off because it did not improve the maximum complete radio-service gap. The clock-register correction stays off because its hardware comparison was worse. This selection is limited to the tested board and workload.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#full-screen-redraw-duration","title":"Full-screen redraw duration","text":"Renderer and radio policy Frames Mean redraw Minimum Maximum Original direct redraw, bytewise 2 MHz 204 73.603 ms 72.298 ms 75.131 ms Final buffered redraw, 8.888888 MHz, preamble 32 176 57.035 ms 56.123 ms 57.839 ms Final buffered redraw, 8.888888 MHz, preamble 40 176 57.161 ms 56.085 ms 57.964 ms

A typical redraw therefore fell from about 73.6 to 57.1 ms: roughly 16.5 ms saved, or 22% less time. The worst observed redraw fell from 75.1 to 58.0 ms. These durations include the cooperative radio service performed during rendering.

The original result combines three five-second trials from the first display HIL image; each final row combines two five-second trials from the final comparison image. They are observations across different images and combined policies. Most of the saving comes from the PSRAM canvas path; this comparison does not isolate the benefit of increasing the radio clock above 8 MHz.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#checks-limits-and-remaining-follow-up","title":"Checks, limits and remaining follow-up","text":"

The 42 controlled trials completed 12,920 profile switches and 1,801 display frames, with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. All final RX-mode, modulation-cache and priority-restoration checks passed. The minimum remaining owner-task stack was 3,288 bytes.

The 24 final exact-clock trials also passed every runtime check, completing 7,367 switches and 1,054 frames with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. All final RX-mode, cache and priority checks passed. Their minimum remaining owner stack was 3,224 bytes.

No packets were decoded in those trials. They do not establish reception under redraw, channel-load behavior, Bluetooth/WiFi coexistence or long-term reliability. The SPI sweep failures are retained separately from the passing 42-case control matrix, and were not followed by dependent timing trials on the failed link.

All seven hosted-document link tests passed after adding this report. Twenty-three focused native regression tests also passed. The final HIL source, including the isolated clock experiment helpers, compiled.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#reproduction","title":"Reproduction","text":"

Build and install the receive-only HIL using the display report's build instructions. The experiment controls change RAM only and the harness has no radio transmit path. On its USB console, reproduce the final buffered, cached comparison with:

exactclock 1\nclockfix 0\ninspect 9 1 10000\ntrial 2 9 1 1 0 0 40 5 20 1\n

The trial selects buffered display, nominal 9 MHz, modem and IQ caches on, normal owner priority, no synthetic load, secondary preamble 40, five seconds and requested 20 Hz redraws; the last number is the result sequence. Check actual_spi_hz for 8,888,888, exact_clock for true and clock_timing_corrected for false, alongside the error and cache counters. actual_spi_hz is derived from the configured divider during the transfer; it is not an oscilloscope measurement.

For repeated 8 MHz control cases, the collector accepts a port placeholder:

python tools/hil/tdeck_retune_experiments.py <port> --suite main --spi 8 --seconds 5 --repeats 3 --fps 20 --output retune-main-8mhz.json\n

tools/hil/tdeck_spi_sweep.py explicitly reboots before every independent clock attempt. Reboot resets the RAM-only exact-clock flag to false. Its original clock matrix and the final manual exact-clock comparison therefore use different clock-setting policies and must be interpreted separately.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#full-companion-follow-up","title":"Full Companion follow-up","text":"

The canonical Full Companion v1.18.0.1-tdeck-radio2-tuned-22cdb701 compiled with the selected 8,888,888 Hz radio SPI setting, both caches enabled, and normal owner priority. Its unchanged RAM gate reported 230,680 internal bytes available, 164,896 required and a 197,912-byte largest region. These are linked capacity checks rather than free heap after boot. The application body size check was 2,261,101 bytes; the packaged application was 2,261,784 bytes, SHA-256 9251694050ad0acdf4699cbedfabc11fe0597d28e57156f1e30d2b80df5ac9a3. All 31 capability verification checks passed.

The application was flashed into app0 while retaining saved device state. Five intentional reboots returned the expected firmware, Bluetooth on and radio2 off. USB Binary Companion device queries reported protocol 14, 350 contacts, 40 channels and manufacturer LilyGo T-Deck.

In the first 45-second runtime check, the screen was held on and a temporary RX-only secondary used a 40-symbol preamble. The node completed 2,450 switches with zero switch errors. At least six primary packets arrived while scanning (RX=6 advanced to RX=12). The longest observed active-pair retune was 1,224 us, with a provisional guarded budget of 1,347 us. The timing recommendation window had not finished, so this remains a runtime smoke check.

A second runtime check also retained zero switch errors. Its lifetime max field already contained 4,518 us before that active test, following the earlier return to single-profile operation. That value is not attributed to an active-pair retune. The first run's results remain in its log; the second run's JSON replaced the earlier runtime JSON, so the sanitized report identifies these sources separately.

Windows completed a fresh PIN pairing, and a subsequent connection confirmed ENCRYPTION_AND_AUTHENTICATION. A requested 30-second, 10 Hz DEVICE_INFO exercise returned 69 successful queries and then timed out; it recorded no notification-queue overflow. It did not complete the intended 300-query workload and is not a passing sustained BLE test. That earlier run overlapped administrative settings writes.

A controlled retry configured the screen and secondary before starting BLE, kept USB open without configuration changes during traffic, and performed cleanup after traffic finished. It completed 30.060 seconds with 208 successful DEVICE_INFO queries, achieving 6.886 queries/second against the requested 10 Hz. Reply latency was 118.109 ms minimum, 145.063 ms mean, 239.963 ms p95 and 300.103 ms maximum. There were 213 notifications, including five unrelated notifications, and no queue overflow or traffic disconnect. The authenticated connection was still alive immediately before normal cleanup. APP_START was not required. This passes the sustained-duration check while documenting the lower achieved query rate; it does not establish the cause of the earlier timeout.

During that retry, radio switches advanced from 6,494 to 8,402 with zero errors, and four primary packets arrived (RX=125 to RX=129). The provisional guarded switch budget was 1,281 us. The lifetime 4,527 us max was already present before the retry and is not attributed to an active-pair retune.

The documented cleanup returned radio2 to off and the USB display mode to button-pairing, and Bluetooth was on.

"},{"location":"test-results/udp_bridge_2026-10-22/","title":"Udp bridge 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/udp_bridge_2026-10-22/#eight-peer-wifi-udp-bridge-22-october-2026","title":"Eight-peer WiFi UDP bridge, 22 October 2026","text":"

Status: host tests and all three Full repeater builds PASS. No radio was flashed or contacted. Physical WiFi/UDP delivery, router policy, LoRa forwarding and OTA operation remain outside this validation.

Clean firmware source f4a9a05f7 was built with the canonical builder, version v1.18.0.1-halo-keymind-cascade-dev, Cascade runtime defaults and the USA Cascade preset. The safe receipt binds source, build recipes, linked memory measurements and package hashes.

"},{"location":"test-results/udp_bridge_2026-10-22/#resulting-behavior","title":"Resulting behavior","text":"

Supported ESP32 Full repeaters gain a default-off UDP bridge with eight explicit IPv4/port peer slots. One bounded, nonblocking socket runs only on a connected station with an IP address and exact station-only SDK mode. It pauses on disconnection, AP/AP+STA, browser/deferred WiFi OTA and manual WiFi shutdown, then restores eligible saved intent. It can join using existing WiFi credentials without an MQTT broker or a running MQTT worker.

Packets use versioned HMAC-SHA256 authentication with the existing bridge.secret; no new password is added. Fresh challenge/proof exchanges establish boot sessions, with receiver-boot binding and monotonic receive sequences rejecting replayed mesh packets and probes. Changing the shared secret closes old sessions and discards old queued work before further socket service. Authentication does not encrypt the UDP payload.

Local LoRa traffic can fan out to the eight direct destinations. Incoming UDP enters the local LoRa mesh, with the usual filters and admission rules; it is not forwarded to other UDP peers. A volatile packet-origin marker prevents later LoRa TX callbacks, including OTA retries, from exporting that same UDP ingress packet again. The marker survives retry copies, is never serialized, and fits existing packet padding. Unrelated builds do not gain it.

The setup and diagnostic guide covers saved settings, status, counters, one-way probes and runtime USB summaries. ready counts authenticated boot sessions, not current peer liveness. Match a fresh probe's ID and increased receive count on the destination to establish reachability. Probes do not allocate mesh packets, echo, retry or reach LoRa. Automatic handshake controls have their own bounded retry policy.

"},{"location":"test-results/udp_bridge_2026-10-22/#embedded-build-results","title":"Embedded build results","text":"Requested target Selected Full environment Available internal bytes Required heap bytes Model margin Result Heltec_v3_repeater Heltec_v3_repeater_observer_mqtt 209,392 209,040 352 PASS heltec_v4_repeater heltec_v4_repeater_observer_mqtt 202,496 197,776 4,720 PASS heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt 200,264 195,728 4,536 PASS

Each image passes application and merged packaging, linked capability checks including bridge.udp, and unchanged memory/size gates. All capability receipts have empty feature-reduction lists. No feature or reserve was removed.

The enabled-case model adds 5,120 bytes even though UDP defaults off: a source- asserted 3,072-byte object allowance and 2,048 bytes for the bounded socket and receive buffering. The socket requires a successful 768-byte receive-limit configuration. WiFi stack memory is counted once alongside MQTT, with UART and other existing allocations still reserved. The V3 model margin is small; these linked capacity estimates are not running free-heap, fragmentation or maximum-load measurements. Physical startup/load/soak checks remain necessary.

An initial compile attempt at 5e25faa3d exposed aggregate initialization unsupported by the pinned ESP32 compiler. f4a9a05f7 uses explicit field assignment. The host suite now compiles the production UDP implementation with strict C++11 before linking its test driver. The earlier failure is retained privately and excluded from qualifying packages.

"},{"location":"test-results/udp_bridge_2026-10-22/#host-checks","title":"Host checks","text":"

Router client isolation cannot be diagnosed conclusively from a successful WiFi association alone. The physical network must allow the configured peers to exchange UDP. This work did not change router settings or attempt to bypass network isolation.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/","title":"Udp bridge hardware 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#five-board-wifi-udp-bridge-hardware-qualification-22-october-2026","title":"Five-board WiFi UDP bridge hardware qualification, 22 October 2026","text":"

PASS for the tested Full Repeater configuration. Five physical ESP32 boards passed the directed UDP matrix, rejection checks, reconnects, software-reboot persistence and physical LoRa forwarding through eight configured UDP peers. All final images use source 36d9735e5. Independent raw-packet and cleanup audits passed before consolidation of the machine-readable results.

Physical coverage combines two disjoint scopes: twelve accepted passing cases from failed run05 and eighteen from passing run07. Run05 remains failed; the first twelve cases were not repeated. Failed run06 sent no data and remains preserved separately.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#coverage-and-results","title":"Coverage and results","text":"Check Completed result Canonical Full builds and installation Five boards; capability and memory gates passed; no feature reductions Directed UDP probes 300/300 across 60 directions, five probes each Security rejections 20/20: replay, bad authentication, unknown endpoint and malformed frame on each board Recovery WiFi reconnect and saved UDP-off choice passed on all five Qualified software reboots Five restarts, retained configuration and 20/20 fresh probes Physical LoRa delivery 25/25 exact application receipts, five per ingress board UDP-off negative controls 5/5; none crossed the frequency gap Independent copy audit 100 physical-peer copies and 100 Pi-endpoint copies; zero UDP transit Final cleanup Five boards quiet, original RXPS restored and owned fixture stopped

The matrix covered 20 board-to-board, 20 board-to-Pi and 20 Pi-to-board directions. Initial station readiness was observed on the first read-only poll for every final image. Each board had 23 continuity samples spanning at least 1,043 seconds, with advancing uptime and no new latched error bits. The reboot qualification used deliberate software restarts, not cold power cycles. Identity, exact firmware/model, shared secret, UDP port and all eight peers persisted; eight authenticated sessions returned.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#boards-and-builds","title":"Boards and builds","text":"Board Selected Full environment Modeled internal-memory margin Heltec V3 Heltec_v3_repeater_observer_mqtt 352 bytes Heltec V4.3 OLED heltec_v4_repeater_observer_mqtt 4,720 bytes LilyGo T-Deck LilyGo_TDeck_repeater 53,632 bytes Seeed XIAO ESP32-S3 WIO Xiao_S3_WIO_repeater_observer_mqtt 9,960 bytes SenseCAP Indicator LoRa N8R8 SenseCapIndicator-LoRa_repeater 1,904 bytes

The shared V4 target detects the V4.3 FEM at runtime. The Indicator tested here has 8 MiB flash and OPI PSRAM. Its separate 16 MiB/QSPI N16R2 recipe is not interchangeable and was not physically qualified. Existing display support was retained in the repeater recipes. Build receipts bind source, recipe and application/merged-image hashes, verify both OTA slot capacities, and report empty feature-reduction lists. Slot checks do not qualify an OTA installation.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#physical-method-and-operating-conditions","title":"Physical method and operating conditions","text":"

Each ESP32 had four other physical ESP32 peers plus four controlled Pi UDP endpoints: eight configured destinations, not eight physical peer radios. All joined an existing WiFi network as stations. MQTT, ESP-NOW and RS232 bridges were disabled. UDP used the existing bridge shared secret without another password; authentication does not encrypt the traffic.

Physical trials used 917 MHz ingress and 918 MHz egress, SF7, 62.5 kHz bandwidth, CR5, preamble 32 and TX 2 dBm. Radio2 and LoRa RX powersaving were off during packet trials. Each ingress received one UDP-off control and five fresh positive frames. T096 and Tag served as the physical source/sink witnesses. Their different frequencies prevented a direct LoRa delivery from substituting for bridge forwarding. The Pi did not inject mesh data, and each accepted trial had exactly one physical send with no application retry.

The independent audit checked source counters, exact application bytes and sequence, actual Pi event membership, four matching Pi copies per positive frame, all four physical-peer copy observations, and exactly one designated LoRa egress. Physical-peer observations include explicitly accounted duplicate receipts. No receiving UDP peer re-exported the frame to UDP. Before each role, the harness waited at least 65 seconds after radio activation and proved at least two seconds of stable RF/UDP counters.

Historical watchdog flags and cumulative receive errors were retained in the raw baseline. V3 had four receive errors recorded during setup; no measured packet added one. A previously latched bit 8 was admitted only with explicit live RX history, matching diagnostic/physical error counters, soft-only recovery and an empty queue. This does not attribute every historical flag to idle maintenance. Every measured packet required unchanged latched flags and soft/hard recovery counters, zero new physical receive errors, and zero new witness receive errors. There was no blanket exemption for bit 8.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#runtime-memory-and-final-state","title":"Runtime memory and final state","text":"

These measured internal-memory values are bytes. The boot minimum is the firmware low-water counter read at cleanup, not a minimum across every earlier boot. Modeled build margins above are separate capacity estimates.

Board Before cleanup free Reported boot minimum After cleanup free V3 74,400 42,652 114,800 V4.3 81,164 76,028 106,528 T-Deck 151,584 145,812 176,996 XIAO ESP32-S3 WIO 136,464 131,304 161,844 Indicator N8R8 106,644 101,672 132,036

All final core and memory queues were zero. V3 had no PSRAM; the other boards reported it. Cleanup restored all five original RXPS-on profiles, disabled UDP/repeat/logging, cleared all eight test peers, selected bridge source TX and left WebConfig inactive. Identity, saved radio, WiFi/secret and CPU power policy were preserved. No CPU-clock setter was issued; actual CPU-frequency readback is not claimed. Cleanup performed no flash, erase, reboot, stats clear or intentional data send. The final owned fixture stopped once, following the separately receipted stop of its predecessor.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#fix-and-preserved-failures","title":"Fix and preserved failures","text":"

The original V4.3 recovery failure exposed UDP/WebConfig station ownership: an unused automatic recovery AP prevented the saved station connection from returning. 36d9735e fixes that handoff while preserving explicit setup/OTA ownership and saved UDP-off intent. Twelve native ASan/UBSan tests and independent review preceded the five final builds; the final physical matrix then passed the formerly failing recovery sequence. The UDP protocol did not change.

Earlier failures were retained rather than replaced by later passes. The history hash index records actual receipt outcomes and separate preserved archives.

Preserved attempt Outcome and disposition Baseline matrix01 V4.3 recovery failed; XIAO recovery was not run. Final 36d9735e matrix02 passed all five. Initial Indicator join One WiFi off/on joined with the same saved credentials; separate root cause remains unproven. RF02 / RF03 Zero data trials: busy witness setter, then Pi USB/Ethernet host fault. Recovery03-01 readback adapter failed; corrected recovery03-02 passed all seven devices. This was not measured RF packet loss. RF04 One accepted negative request; automatic OTA bootstrap traffic changed the strict negative-control counters. A bounded bootstrap/quiet wait was added. RXPS restore01/03; setup04 V4.3 restoration rejected; setup04 separately failed on a startup queue. Restore02 and final cleanup restored all five original profiles. Ordinary V4.3 RXPS is supported. RF05 Twelve strict passes, then preflight stopped on a latched watchdog flag before V3 data. Overall failed status retained. RF06 Zero data; historical-baseline gate incorrectly required current last-RX age and absolute zero cumulative errors. Run07 retained baselines and enforced zero new errors per packet. Fixture and offline adapters Original fixture ended during the host fault; two explicit RF fixture replacements preserved identical configuration. Exact getter/model adapters were corrected without firmware changes or repeating accepted packets.

Full CI is separate evidence. Test-only fixture repairs were pushed in 8da70b84d and 52a36c89d; focused tests passed. At report preparation, CI run 39869220511 had twelve successful non-main jobs while its main test was still running; full CI success is not asserted here.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#evidence-and-limits","title":"Evidence and limits","text":"

The consolidated result binds all 32 opaque private-file hashes, both independent audits and the disjoint campaign receipts. Its SHA-256 is f6a384cc496f8c1a457ddd10df581925ca0a444ebad51fec65155fed3e68a4f3. The final private archive SHA-256 is 22adb7f49b6c1650248f63acf9f6f9a70971e5c7b3c3e17400c4b28803eb4206. That tar covers final evidence; earlier failures remain in separate preserved archives identified by the history index. Private contents are not published.

The later noise-floor telemetry change in f4ade738d was not installed on these radios and is outside this hardware result.

This qualifies Full Repeater UDP forwarding under the stated radio/power conditions. It does not qualify other firmware types, every power/radio profile, eight physical peers, AP-mode UDP, arbitrary router/VLAN policies or client-isolation bypass. Simultaneous bridge backends, BLE, physical firmware OTA application, cold starts, maximum load and long-duration soak remain outside this campaign.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/","title":"Unit test workflow fix 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#unit-test-workflow-follow-up-2026-10-22","title":"Unit-test workflow follow-up - 2026-10-22","text":"

Baseline: ab6412e73c1c36a53e2ecf08c79e3c223200cb3d on keymindCascade. The starting failure was workflow run 39886822765.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#failure-and-build-scope","title":"Failure and build scope","text":"

The run exposed three independent failures:

The earlier calibration report's direct PlatformIO builds did not use the release wrapper. In particular, the wrapper links the Mini's runtime USB packet logger and embeds release metadata and Cascade defaults. A direct recipe passing its size check does not qualify that release artifact.

The relevant release builds must use these exact workflow commands, one PlatformIO process at a time:

bash build.sh build-firmware RAK_4631_companion_radio_full \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascade-fixed --profile cascade --require-ota\n\nDISABLE_DEBUG=0 bash build.sh build-firmware wio-e5-mini_repeater \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascade-fixed --profile cascade --allow-no-ota\n

The exact Mini release-wrapper build passed with 245,376 of 245,760 application bytes used, leaving 384 bytes. Its runtime-memory gate also passed: 43,920 bytes available against 32,288 bytes required. The application boundary and existing 16 KiB filesystem are unchanged. The complete board matrix is tracked in the unit-test workflow.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#rak4631-full-companion-changes","title":"RAK4631 Full Companion changes","text":"

The exact Full Companion release-wrapper build passed with 707,504 application bytes before its EndF trailer. The stamped UF2 application occupies 707,584 of 712,704 bytes, leaving 5,120 bytes within the existing boundary. The runtime memory gate passed with 81,228 bytes available against 74,576 bytes required; the DFU handoff and 13 capability checks also passed. Reader, contacts, Meshtext, sensors, Ethernet, SD support and LoRa OTA remain enabled. No flash boundary or memory gate was relaxed.

The RAK4631 recipe explicitly selects its existing voltage-policy calibration store. RAK4631Board::begin() loads that policy before application settings. This lets the compiler omit an unreachable second /battery_cal backend while retaining the existing 20-byte NVP2 record and its CRC. The store fails closed if called before policy initialization. A missing or corrupt policy initializes safe defaults; unsuccessful writes still return an error. Other board recipes retain their generic calibration backend and fallback behavior. A typed setter also avoids formatting a CLI command only to parse it again.

The linked ARM crypto checks passed both RFC8032 vectors. Key generation used 1,200 bytes of function-frame stack and signing used 1,904 bytes, within the 2,048-byte gate. These figures exclude caller, ISR and task stacks. Linked OTA workspace checks passed four acquisition/release lifetimes without touching the protected SD authentication range. Neither check is hardware qualification. The linked Reader check passed all 879 references (885 calls), comparing 82,624 canonical bytes. Its maximum function-frame stack was 6,016 bytes; the check also reserves 1,536 caller bytes within the 8,192-byte loop stack. This excludes ISR/task stack and physical high-water measurements.

Beacon frequency and bandwidth parsing now shares the radio CLI's plain decimal parser. This removes the general-purpose strtof/strtod conversion code from the Full image and recovered 5,732 bytes in the final comparison. Normal decimal settings, optional signs and decimal fractions remain available; radio limits still reject invalid frequencies and bandwidths. Scientific and hexadecimal numeric syntax is rejected, matching the ordinary radio CLI grammar. NaN, infinity, whitespace and trailing junk are rejected as well.

Constant CLI error replies share their existing bounded formatter and prefix. The existing command replies retain their text, capacities and termination; new calibration replies use shorter wording while retaining the reading, effective multiplier and persistence result.

The first rerun exposed an RS-232 packaging check that requires one complete error-message literal as evidence that the merged command handler survived linking. That handler retains its original literal. The capability check is unchanged. Both Full and Reduced T114 Repeater release-wrapper builds then passed all eight capability checks and their flash, RAM and DFU gates.

The next run passed all twelve other jobs and reached a stale release-inventory assertion in the main test job. The two SenseCAP Indicator LoRa repeater recipes added by 3236164d2 legitimately preserve their N8/N16 partition layouts. The fixture now expects 151 targets and explicitly checks both additions; all partition-migration exclusions remain. The corrected selection suite, nRF52 OTA profile suite and XIAO combined-layout suite pass locally. No firmware source changed for this inventory correction.

Calibration coverage includes three command/scaling tests, three store tests (including four platform backends), 19 battery-forwarding tests and four power policy compatibility tests. The actual nRF52 policy/store integration checks pre-initialization rejection with present or absent storage, failed mounts, corrupt policy CRC, saved calibration, unchanged NVP2 bytes, reset, bounds and write/rename failures. A sentinel /battery_cal remains untouched throughout the RAK policy test. These are host tests, not physical ADC qualification.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#repeater-diagnostic-changes","title":"Repeater diagnostic changes","text":"

Compact RX and TX packet records share one formatter. It emits the same uppercase hexadecimal bytes, R or T prefix, and LF terminator. It retains the existing bounded serial writer and DROP recovery marker.

When neither debug output nor packet-file logging is compiled, retry callbacks avoid formatting discarded diagnostic strings. Flood-retry reachability still updates for eligible successful echoes. Eligibility preserves the pending bridge state, source mask, progress marker, target mask and route-hash checks. The existing logged callback path remains intact.

The Repeater supplies its timestamp formatter whenever debug output, packet-file logging or noncompact USB packet records need it. Builds with no timestamp consumer inherit the base formatter without linking the unused conversion.

These changes retain the Mini's release USB packet logger, calibration, routing, host-service bridge, 50 neighbours and existing 16 KiB filesystem. They do not add feature cuts or change flash and RAM qualification gates.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#host-regression-results","title":"Host regression results","text":"

Passed checks:

The complete native PlatformIO suite also passed all 1,897 test cases. The remaining 502 host scripts passed after fixture repairs, along with the required firmware-memory/ARM checks, both required Room browser suites, voice and G726 sanitizer variants, notification JavaScript checks and paired nRF52 logging checks. These results cover the steps beyond the stale inventory assertion without repeating unchanged firmware builds locally.

Check Coverage test/test_serial_packet_log/test_serial_packet_log.cpp - 8 tests Exact RX/TX bytes and LF; small FIFO; initially absent host; stalled host; 20 ms mid-record stall budget; truncation; drop counting and recovery markers. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. test/test_mini_repeater_logging.py - 3 tests Actual Repeater RX, Dispatcher TX, packet codec and diagnostic callbacks across all eight carrier/management/file-logging combinations; maximum packet sizes; runtime logging gate; stalled writes; file-logging independence; host CLI and authorization contracts. Flood-retry eligibility within that harness Actual eligibility, bridge-heard mask and textual formatter agree for 576 source/target/progress/hash-size combinations in each feature variant. Missing state, empty/invalid paths, source-only echoes and progress-marker-only echoes do not refresh reachability. Timestamp consumers within that harness Production declaration and definition compile and return the expected formatter across all 16 debug/file/packet/compact combinations. test/test_cli_command_utils/test_cli_command_utils.cpp - 62 tests Includes three shared error-formatter regressions: exact output with literal percent directives; every capacity from zero through a complete reply; null destination at zero capacity; termination and untouched guard bytes; truncation at the 160-byte CLI boundary. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. CommonCLI dependency audit - 34 scripts, 166 tests The 32 CommonCLI fixture suites and the Morse CLI and Beacon Manager suites pass after source-extraction fixtures include the real shared formatter. Includes RAK SD context handoff and its negative control, USB/ESP-NOW/RS232/UDP diagnostics, persistence, GPS, OTA, and the Morse FEM guard. test/test_beacon_manager.py - 20 tests, included in the audit above Actual Beacon Manager and extracted decimal parser accept ordinary signed/unsigned decimal tuples; reject exponent/hex notation, NaN/infinity, whitespace, trailing junk and out-of-range radio tuples. Existing scheduling, transactional storage and negative controls also pass.

The Mini harness uses AddressSanitizer and UndefinedBehaviorSanitizer on Linux. Radio and serial hardware are stand-ins. These results do not qualify physical USB, RF, ADC readings, charging, bootloaders or installed devices.

"},{"location":"test-results/usb_serial_backpressure/","title":"Usb serial backpressure","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/usb_serial_backpressure/#native-usb-backpressure-and-radio-liveness","title":"Native USB backpressure and radio liveness","text":"

The native USB defaults for eight ESP32-S3 board families now use hardware USB Serial/JTAG (HWCDC). The TinyUSB behavior below remains available to explicitly selected TinyUSB builds. UART and non-ESP32 transports retain their existing backend.

ESP32-S2/S3 builds using native TinyUSB CDC (ARDUINO_USB_MODE=0 and CDC-on-boot) must not wait for a computer to read USB output. In the bundled Arduino-ESP32 2.0.17 core, USBCDC::write() can wait indefinitely for transmit space; its configured timeout bounds a mutex, not that wait. This can stop the same loop that services LoRa, even while the USB connection still appears open.

"},{"location":"test-results/usb_serial_backpressure/#firmware-behavior","title":"Firmware behavior","text":"

Host software must also keep reading independently of command writes. A serial relay should start its reader before the first command, use finite write and response deadlines, avoid discarding received packet logs, and cancel I/O during shutdown. A response timeout must not allow a late reply to satisfy another command: the CLI has no transaction identifiers. Updating firmware alone does not correct an indefinite host-side serial write.

"},{"location":"test-results/usb_serial_backpressure/#regression-checks","title":"Regression checks","text":"

Run only one PlatformIO process in the checkout at a time.

python test/test_esp32_tinyusb_nonblocking.py\npython test/test_esp32_usb_session_fix.py\npython test/test_esp32_tinyusb_role_hygiene.py\npython test/test_esp32_tinyusb_cooperative_output.py\npython test/test_esp32_usb_serial_hygiene.py\npython test/test_nrf52_usb_logging_contract.py\npython test/test_nrf52_usb_lifecycle.py\npython test/test_common_cli_logging_transaction.py\npython test/test_companion_usb_logging_reply.py\npython test/test_usb_diagnostic_logging.py\npio test -e native -f test_nrf52_debug_output -f test_serial_packet_log -f test_serial_mode_switch -f test_mesh_tables\n

The first test compiles the real USB facade against a simulated 64-byte FIFO, including stopped readers, reconnects, protocol transitions, and other-platform fallbacks. Host simulations cannot establish that every real USB driver or endpoint failure has recovered.

A Full Station G2 validation build with USA Cascadia radio settings and the Cascade profile can be made using the normal build entry point. The portable standard recipe preserves the deployed partition layout but omits LoRa OTA; it still has the browser firmware uploader. The auto/full recipe instead enables the expanded feature set and partition layout: its merged image is not an app-only update for a device with the legacy layout.

MESHDEBUG_OVERRIDE=on PACKET_LOGGING_OVERRIDE=on \\\n  bash build.sh build-firmware Station_G2_repeater --build-profile full \\\n  --radio-preset usa-cascadia --profile cascade\n

Before installing on hardware, preserve the device identity, preferences, and existing partition layout. Then verify USB command responses and repeated LoRa logins with the relay running, paused, and stopped, including a host that leaves USB open without reading. Check LoRa recovery separately from USB OUT recovery; fixing transmit backpressure does not prove an unrelated OUT endpoint fault is resolved. Do not erase or repartition the radio as part of this test.

"},{"location":"test-results/usb_serial_backpressure/#usb-logging-ownership-and-session-recovery","title":"USB logging ownership and session recovery","text":"

On a single-port Companion, enabling USB logging parks framed USB traffic before opening the diagnostic gate. A TCP/browser terminal retains its CLI ownership; USB is log-only during that session. Disabling logging restores USB traffic without accepting bytes typed during the log-only interval. A dedicated nRF52 logging port does not park the separate Companion port.

A framed USB logging command retains its original requester and queues its acknowledgement before changing ownership. The handoff advances without waiting for the host: live logging stays unchanged while the complete USB reply is backpressured. Disconnect/reset cancels that session's pending handoff; an already-saved preference can still take effect at the next boot. BLE/WiFi replies are never used as a fallback for a refused USB acknowledgement.

On native ESP32 TinyUSB, build-local framework hooks capture DTR and USB session boundaries synchronously in the USB owner task, before Arduino's delayed events. They purge old input at the boundary and reject late input while DTR is low, without deleting a new host's first query during main-loop cleanup. An armed old IN packet (or a racing writer) triggers immediate soft-disconnect, a minimum 20 ms detached interval, and quarantine until fresh enumeration. Shared SDK files are not modified; a changed native-CDC descriptor fails the build closed until the hook is reviewed. This path is specific to the pinned Arduino-ESP32 2.0.17 native CDC implementation; hardware Serial/JTAG and UART are unchanged.

Disabling diagnostics is not itself a protocol change and does not discard functional replies from the shared ESP32 text queue. Actual Binary/mOTA changes use the explicit terminal-output barrier. Touch and invalid-Ethernet-hostname diagnostics also honor the runtime logging gate and bounded USB stream.

Ethernet and enabled MQTT diagnostics honor the runtime logging gate and use the bounded USB facade. Infrastructure logging setters commit preferences before changing live output; failed saves leave previous settings unchanged. A successful save with a failed MQTT runtime change reports those two outcomes separately.

ESP32 native-USB repeater and room ACL listings retain short-write suffixes and advance one row per service pass, using their existing output buffer. Reconnect cancels the old listing. The listing is a bounded live view, not an immutable snapshot of clients changed during output.

Stored dumps make progress even when HWCDC falls back to 256/512-byte TX rings. Recent-repeater headers/rows and EOF retain every unwritten suffix; a short write never causes their accepted prefix to be retried.

nRF52 FIFO cleanup also checks for an already-armed IN packet. If one exists at a session boundary (including a pending zero-length packet), both USB ports briefly detach and re-enumerate: a FIFO clear alone cannot recall endpoint RAM. Reattachment waits for the local unplug event and a 20 ms minimum interval, without blocking the mesh loop. Dedicated CDC descriptors are added while detached even if the host has not yet completed enumeration. These recovery paths have native regression coverage; real host/board timing still requires hardware qualification.

The nRF52 callback only drops the pull-up and closes producer gates. The application loop sends the stack's unplug event afterward, so a full owner-task event queue cannot make its consumer wait on itself. The detach interval starts when that application-side stack detach actually occurs. A regression models a full finite event queue as well as the separate armed endpoint packet.

"},{"location":"test-results/usb_serial_backpressure/#local-qualification-2026-10-03","title":"Local qualification (2026-10-03)","text":"

These are the upstream USB-session qualification results before the subsequent USB watchdog, GPS preference, and repeater trace merge. The byte counts below describe those earlier binaries, not the current merged source.

The two custom qualification builds used the lab profile 909.5 MHz / 500 kHz / SF5 / CR5. No board was flashed, and no physical disconnect/reconnect or hardware endpoint recovery was claimed by these checks.

"},{"location":"test-results/usb_serial_backpressure/#esp32-hwcdc-migration-and-upstream-findings-2026-10-04","title":"ESP32 HWCDC migration and upstream findings (2026-10-04)","text":"

The board defaults change 78 native-TinyUSB recipes across Heltec E213, E290, T190, Wireless Tracker V1.1 and V2, Station G2 and G3, and T-Beam 1W. Two G2 recipes already used HWCDC. All 80 recipes in these families select ARDUINO_USB_MODE=1 and ARDUINO_USB_CDC_ON_BOOT=1; upload touch and waiting for a replacement upload port are explicitly disabled. Their framework stays on PlatformIO Espressif32 6.11.0 / Arduino-ESP32 2.0.17. Existing ESP32 UART recipes and all non-ESP32 recipes are unchanged.

HWCDC output uses the existing bounded facade, early RX/TX allocation, shared writer exclusion, and fresh capacity check inside that exclusion. The pinned SDK receives a build-local TX suffix/interrupt backport from upstream Arduino-ESP32 #12606. KISS now prepares buffers before starting the USB driver and uses this facade. Its debug/logging gates stay closed so diagnostics cannot split a binary frame. A USB bus reset drops partial host commands and old queued frame suffixes while preserving radio transmission state and settings. Radio work continues while USB cleanup retries.

On S3, the pinned HWCDC initialization also restores RTC PHY ownership to the hardware Serial/JTAG peripheral. A software restart after running TinyUSB can otherwise retain ownership by the OTG controller, including an OTA update to an HWCDC image. When that retained state is detected, initialization first briefly detaches the old OTG pads so the host can enumerate the changed device. Ordinary HWCDC startup does not incur that extra interval.

HWCDC does not expose the same DTR session boundary as TinyUSB. Closing a host port while leaving USB connected is not proof of a new firmware session; a partial command may survive that close. Bus reset/unplug cleanup and protocol client leases must be tested separately. Hosts should rediscover the port after the backend change: the USB product and /dev/serial/by-id path can change. Espressif confirms that hardware CDC descriptors are fixed in silicon, so firmware cannot preserve a custom TinyUSB product name. Arduino-ESP32 maintainer comment, ESP-IDF maintainer comment.

Upstream reports checked for this transition:

Report Documented behavior Relevance and mitigation ESP-IDF #9826 S3 retains TinyUSB PHY ownership across software/watchdog restart, preventing USB Serial/JTAG operation. Pinned HWCDC initialization restores the official S3 hardware PHY route and detaches the former OTG connection first. Initialization fixtures seed retained RTC state; the controlled physical G2 OTA transition below also passed. Arduino-ESP32 #12782 Open report of HWCDC failing to reconnect after about two minutes idle on C3/C6/C61/S3, latest master / IDF 5.5.4. Reported SDK differs from our pinned Arduino 2.0.17 / IDF 4.4. Synthetic tests cover idle lease expiry and reply admission, not physical enumeration on that SDK. ESP-IDF #18996 Open report of S3 USB Serial/JTAG failing after repeated software resets from macOS, on IDF 6.0.2; includes XIAO S3. Not qualified by Linux port reopen tests and not reproduced on our pinned SDK. CPU reset and host-port reopen are distinct cases. ESP-IDF #13287 S3 may remain in ROM after upload when download mode was entered with physical BOOT/RESET. Our older esptool does not offer the newer watchdog-reset option. Press RESET after a manual BOOT-mode upload if the application does not start.

Espressif's USB Serial/JTAG guide describes the fixed CDC/JTAG peripheral and its light/deep sleep limitations. This migration uses only its serial role; it does not provide TinyUSB composite interfaces such as MSC, HID, or a second CDC.

Additional regression commands (only one PlatformIO process at a time):

python3 -B test/test_esp32_hwcdc_recipes.py -v\npython3 -B test/test_hwcdc_tx_backport.py -v\npython3 -B test/test_hwcdc_write_capacity.py -v\npython3 -B test/test_hwcdc_role_transport.py -v\npio test -e native_kiss_modem\n

The recipe check resolves all 821 tracked environments using PlatformIO's real inheritance and SCons flag/unflag processing. It checks all 500 ESP32 selectors, including all 78 migrated recipes, and the 321 non-ESP32 recipes. The transport test compiles production Companion framing, KISS output methods, setup/loop, and the bounded facade with the patched SDK against simulated USB registers and RTOS queues under ASan/UBSan. It covers withheld readers, stale capacity, partial frames, queue limits, session reset, idle leases, and cleanup retries. The role fixture injects a confirmed session boundary; separate SDK tests execute the bus-reset callback and the S3 retained-PHY initialization. The setup/loop fixture uses stub transport functions to check ordering and continued radio service rather than real peripheral recovery. Native KISS tests also exercise the full modem's radio lifecycle during reset. These tests do not establish physical USB timing or host compatibility for boards that were not connected to the lab.

"},{"location":"test-results/usb_serial_backpressure/#physical-g2-http-ota-and-usb-transition-2026-10-05","title":"Physical G2 HTTP OTA and USB transition (2026-10-05)","text":"

The latest OTA qualification used commit 95913c5301318fb738fb8f50e314a57a2dd9eb07, including the uploader fix from d2983762 and the station ownership fix. A current-source build explicitly selected TinyUSB; the target selected the normal HWCDC backend. Both ran on the physical Station G2 with the same existing partition table and two 0x640000 application slots. The independent audit checked packaged application hashes against metadata and matching preserved ELFs, the USB SDK transforms, and linked RAM requirements. Both ELFs contain the production 30-second admitted-upload timeout, queued-response guards and specific write-failure text; neither links the removed raw Update.printError calls. This is a controlled same-source transition, not released stock 1.17.1 migration evidence.

Artifact App bytes SHA-256 TinyUSB observer, 95913c53 1,978,200 cf72b3ce872fb0887273022255fe5826070b5b40f5aa8057f50e90e7c92d4b72 Normal HWCDC observer, 95913c53 1,950,824 aeeadb1f165164c5b01d5855918e20bb0dc37bedcb9f0a8b88f52faa4c435486

The AP upload used the actual multipart /update handler and an app-only image. Its HTTP identity endpoint was matched to the node read over USB before the POST. USB topology was sampled every 20 ms without serial reads or writes across the upload/restart boundary. After 65,536 wire body bytes, Linux's SIOCOUTQ on that same HTTP socket fell from 59,987 outstanding bytes to zero in 0.554 seconds. Only then did the host pause body sends for 4.510 seconds; there were no serial operations during that pause. TCP acknowledgement was observed; the firmware's application receive timestamp was not directly instrumented.

Measured gate Result App-only multipart upload with the intentional body pause HTTP 200, 22.559 seconds Firmware-owned software restart and USB backend change TinyUSB device 120 disappeared; a transient TinyUSB device 121 preceded hardware CDC device 122 on the same physical topology First plain website time <epoch> after HWCDC discovery Clock-set acknowledgement in 2,997.319 ms; strict deadline 5,000 ms Extra post-restart settling, serial commands during upload, external recovery None Identity and saved runtime settings Preserved against the original preflight Admin access Exactly the original and recovery Admin ACL entries remained Core diagnostics after the upgrade Error flags 0, uptime 4 seconds Pi WiFi rollback Original connection restored and temporary AP profile deleted; runner completed in 35.598 seconds

The final 95913c53 hardware CDC application passed two quiet 180-second trials with power saving enabled. There were no serial reads, writes or keepalives during either interval. With the handle open, the first core reply took 18.551 ms and uptime advanced from 43 to 223 seconds. With the handle closed, the first plain website clock request after reopening was acknowledged in 14.728 ms and uptime advanced from 224 to 404 seconds. No CPU restart occurred. The preexisting radio CAD warning 0x02 remained unchanged throughout these trials; this was not a zero-error radio test. Original power saving off, other saved settings and both Admin ACL entries were restored and verified.

After those trials, a single plain start ota command used the connected station's advertised LAN address. The actual /update page returned HTTP 200 in 1.365 seconds and /update/identity returned HTTP 200 in 0.084 seconds with the exact G2 identity. The command reported WiFi on and WebConfig stopped. Stopping OTA and restoring the original WebConfig were verified; no firmware was uploaded in this endpoint probe. Uptime continued from 533 to 535 seconds, the station remained connected at -80 dBm, and identity, original settings, both Admins and power saving off were retained. These HTTP measurements use a separate diagnostic bound; the first USB website request still uses 5 seconds.

Final restoration also verified the Pi's original WiFi connection, no temporary OTA profiles or return timers, and active mctomqtt, ModemManager and mesh-logger services. Each service reported exit status 0 and no restarts. This verifies service health; MQTT packet delivery was not measured in this run.

Baseline preparation also used app-only HTTP OTA: hardware CDC d2983762 installed TinyUSB 95913c53 in 19.083 seconds, and its first plain clock acknowledgement took 3,163.722 ms. Original identity, settings and both Admins were verified before and after this reverse transition; no rescue action was used. Its Pi WiFi rollback completed in 31.958 seconds.

The first query used the stock website's plain clock command immediately after opening the discovered hardware CDC port. Later queries were tagged to keep unrelated diagnostics from satisfying a response. Neither a longer command deadline nor an arbitrary startup delay was added.

The uploader fix addresses a separate bounded-gap problem: the pinned HTTP listener gives ordinary clients a 3-second receive timeout, which remained in effect while an admitted multipart upload was receiving its body. OTA now extends only that admitted request to 30 seconds after MD5 validation. Normal, unauthorized, malformed-MD5 and concurrent requests retain ordinary admission behavior. send() queues a response before it reaches the wire, so both the upload and completion callbacks preserve an already queued error. Begin/end errors and short writes return HTTP errors without calling raw USB prints from the shared AsyncTCP task.

Three ASan/UBSan uploader tests passed, together with eight OTA startup tests and fifteen WiFi lifecycle tests. The uploader fixture executes production callbacks and the pinned AsyncTCP inactivity condition. A 4-second body pause survives, an abandoned owner remains open at 29.999 seconds and aborts at 30 seconds, ownership is released without reboot, and a later upload completes. Negative controls fail when the timeout extension or queued-response guards are removed, or when raw USB error prints are restored.

Historical results remain distinct:

The earlier d2983762 hardware CDC application passed two quiet 180-second trials with power saving enabled. There were no serial reads, writes or keepalives during either interval. With the handle open, the first core reply took 17.353 ms and uptime advanced 180 seconds. With the handle closed, the first plain website clock request after reopening was acknowledged in 8.905 ms and uptime advanced 181 seconds. Original power saving off, other saved settings and both Admin ACL entries were restored and verified.

A normal start ota probe while the station was disconnected correctly reported its fallback AP address; a Pi still on its original LAN could not reach that separate AP. This does not qualify the LAN endpoint. Source review then identified a missing WebConfig retry owner when MQTT is compiled in but has no running worker. The infrastructure fix refreshes ownership from actual MQTT worker state on the mesh loop. Constructor credential loading, browser credential writes and the 64-hex password capability use a separate immutable storage choice, so taking over reconnects cannot truncate or reroute saved credentials. An active external worker retains its connection; a stopped worker releases it for a fresh portal attempt. Six ASan/UBSan tests exercise production role startup, connection fallback, retry, worker handoff and credential paths, including the existing manual Companion startup behavior. The 95913c53 OTA transition, quiet power-saving trials and normal LAN endpoint checks passed as reported above. The current GitHub unit workflow has not yet completed. Release 1.17.1.9 remains held and unpublished. These results qualify this G2, Linux host and pinned framework; they do not establish physical compatibility for every affected board or host operating system.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/","title":"V4 mqtt bellevue g2 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#v4-mqtt-setup-and-bellevue-g2-full-repeater-21-october-2026","title":"V4 MQTT setup and Bellevue G2 Full Repeater, 21 October 2026","text":"

Status: PASS for the requested setup and deployment. The Mercerwood V4 is left connected to its saved US analyzer MQTT preset. Bellevue's Station G2 now runs the current Full Repeater application, with repeating enabled and its USB gateway service restored. No firmware source change was needed.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#v4-mqtt","title":"V4 MQTT","text":"

The V4 retains its measured 2bd4cb4d Full Companion application. Its saved MQTT switch was already on, while WiFi was off and the MQTT region was empty. The setup saved region SEA, enabled the existing WiFi configuration and kept the analyzer-us preset. This preset uses WSS and device-signed JWT authentication. WiFi and admin passwords were not changed.

The first connection obtained two broker PUBACKs. A subsequent 137.367-second held observation recorded PUBACK growth from 3 to 5, uptime from 29,881 to 30,017 seconds, zero core errors and empty core queues. Every sample had an empty MQTT outbox and zero queue/drop counters. Free DMA memory stayed at 50,056 bytes or more, with a largest block of 49,140 bytes.

The one-minute test status interval was restored to its original five minutes. That setting restarts the MQTT worker; a separate final check confirmed WiFi, MQTT and a new broker acknowledgment after the restart. WiFi and MQTT remain enabled. Other saved broker slots and radio settings were not changed.

PUBACK verifies that the broker accepted a publication. An independent subscriber payload receipt was not measured because no subscriber account was available. This finite observation does not replace a long-duration WSS soak.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#bellevue-station-g2","title":"Bellevue Station G2","text":"Item Result Canonical target Station_G2_repeater_observer_mqtt, built through the Full release profile Installed version v1.18.0.1-halo-keymind-cascade-dev-observer-a926341f Application body / target 4233FACEB6C44EAE / DBB6A855 Application SHA256 459f2e1de20460b54ac00cd545dfe11490b13081072c3aed47b1398dd276dcf5 Build gates Full capability and memory gates passed; 200,432 available internal bytes versus 192,656 required heap bytes Installation Actual ESP32-S3 identity and 16 MiB layout verified; CRC-valid OTA metadata selected the active slot at 0x650000, size 0x640000 Readback All 2,118,264 application bytes verified; partition table, OTA metadata and NVS hashes unchanged during the write Persistence Original public identity, name, radio, TX power, WiFi SSID, MQTT settings and power-saving choice preserved Repeater mode Role-specific getter accepted; repeating was already on and remains on Final health Core errors 0, outbound queue 0; local packet counters showed 84 received and 18 sent Service handback Original mctomqtt.service is active and again owns the exact G2 USB endpoint

The combined Full recipe retains observer in its version string and includes the repeater implementation, MQTT, ESP-NOW, USB logging, WiFi OTA and LoRa OTA. The build's Full overlay enables the LoRa update path. Saved operating settings remain 910.525 MHz / BW62.5 / SF7 / CR5 and TX 19 dBm.

The application-only update preserved the existing expanded layout. It made no whole-chip erase or full-flash backup. Small protected NVS reads were hashed and removed; after boot, saved logical settings and the full public identity were independently compared with the original observations.

The first postboot check had zero core errors and one in-flight radio packet at uptime 13 seconds. The initial harness required an immediately empty queue and stopped handback at that check. A bounded follow-up on the same installed firmware verified the exact version, body, identity and repeater role, then recorded an empty queue with zero errors at uptime 149 seconds and restored the original USB gateway. That early failed gate is retained in the evidence; no second firmware installation was performed.

The safe receipt records artifact bindings, timings and evidence hashes. The local packet counters demonstrate observed radio activity; independent peer delivery, RF range, physical OTA installation and long-duration behavior were not tested in this deployment.

"},{"location":"test-results/v4_pixel5_font_trial/","title":"V4 pixel5 font trial","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/v4_pixel5_font_trial/#small-screen-message-fonts","title":"Small-screen message fonts","text":"

Small-screen Companion builds automatically select a compact font for received message text and the channel/sender line, including Ch 0 Public. SSD1306 and SH1106 OLEDs and the U8g2 T-Echo Card display use this on panels smaller than 160 pixels on both axes. ST7735 TFTs now use their native 160x80 dimensions and normal font. The shared renderer selects by panel dimensions, including rotation:

Panel size Font Capital height Line spacing At least 128 x 64, or 64 x 128 rotated; both axes below 160 Squeezed Regular 6 6 pixels 8 pixels Smaller panels, including 72 x 40, 128 x 32 and 64 x 48 Picopixel 5 pixels 7 pixels Either axis at least 160 Normal display font Depends on panel Depends on panel

Both fonts allow one pixel for descenders and one blank pixel row between lines. Character widths vary: most letters advance by 4 pixels, with narrow letters taking less space and M and W taking 6 pixels. Squeezed Regular 6 is a public-domain font by Oliver Kraus. Its bitmap data is stored as constants without heap allocation.

On a 128 x 64 OLED with the single-button message reader, the compact channel/sender line starts at y=8 and message text at y=16. The header uses the same small font. Five complete 6px-font rows fit above the navigation hint at y=56. The software comparison fits three representative 160-character messages in those five rows. Capacity depends on the characters; messages with many wide letters can still overflow. Text wraps at character boundaries, with ... on the last line if necessary. Unsupported characters appear as ?. Long channel/sender names are ellipsized to stay on their own line. An enabled channel footer reserves its own space. Readers without the button hint retain the y=14 origin and y=22 message text.

On V4 and other single-button builds using this reader, the home screen says hold button: inbox. Hold the user button for about 1.2 seconds to open it. The bottom continuously shows 4 <<- 2 <- tap -> 1 ->> 3 hold: Exit without alternating or blinking. Double tap goes to the previous message, one tap advances, three taps select the next channel, and four taps select the previous channel. Channel selection cycles through All, configured channels, and direct messages, starting at the newest message in each filter. The header shows All, Ch N, or DM, including when a channel is empty. Hold to return home; advancing past the last message also returns home. The complete hint fits in the V4's existing 8-pixel footer at y=56, leaving five message rows. The footer keeps the same 6px font, using 108 pixels with 10-pixel margins on the V4. Narrower screens first use the compact hold:X hint without the extra spaces or arrow dashes, then split the same controls across fixed lines. The hint appears even when the inbox is empty. These are button taps. Touchscreen and joystick builds retain instructions appropriate to their controls. During the first eight seconds after startup, holding the button on an ordinary home page enters CLI rescue instead; wait for that startup window to finish before opening the inbox. Exiting a message preview and the WiFi setup page's hold action remain available immediately.

The Reader uses the same button hints. Three taps jump to the first verse of the next chapter; four taps jump to the first verse of the previous chapter. Chapter navigation stops at the beginning and end of the book. It reserves the hint before pagination and resumes saved bookmarks at the page containing the same text, even after the available page size changes.

Larger display classes keep their existing font. Menus, Bluetooth PINs and WiFi setup QR codes keep their normal layout. TFT drivers use native panel coordinates; the SenseCAP Indicator retains its existing scaling. The normal OLED font uses 7-pixel letters, 8-pixel line spacing and 6-pixel character advances.

The preview buffer holds a complete 160-byte MeshCore message plus its terminator. Previously the main message UI allocated 78 bytes, leaving room for only 77 bytes of text. It still retains 32 previews; larger records add about 2.8 KB of RAM. The firmware RAM guard includes that increase. This history is separate from the offline queue and its mOTA policy.

The tiny 72 x 40 T-Echo Card interface now previews the latest received message below its status bar, with three small-font message rows. A button press dismisses it. It stores one full message, and shows ... when the screen fills. Incoming text does not replace an active Bluetooth pairing PIN.

"},{"location":"test-results/v4_pixel5_font_trial/#v4-hardware-trial","title":"V4 hardware trial","text":"

Enable platformio.nimble.ini in the ignored platformio.local.ini as shown in the NimBLE trial guide, then run:

OUTPUT_DIR=.releases/v4-smallfont bash build.sh build-firmware \\\n  heltec_v4_2_v4_3_companion_radio_full_femon_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-squeezed6-trial \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n

This build keeps NimBLE, 350 contacts, 40 channels, the V4's 512-frame PSRAM queue, USB mOTA sending and WiFi OTA support. The smaller font is also the default in ordinary small-screen Companion builds from this source. The older _nimble_pixel5 environment name remains available, but also uses the automatic 5px/6px selection.

Use the application .bin for WiFi OTA. A clean USB install uses the merged image at address 0. When manually writing the application at 0x10000, an existing OTA selector may still boot app1. Check the running ver afterward. On the matching V4 16 MB layout only, clearing the 8 KB otadata partition at 0xe000 selects the newly written app0 without clearing NVS/settings. Confirm the partition table and verify the app write before changing that selector.

A custom build can set -D UI_SMALL_MESSAGE_FONT=0 to restore the old font and spacing. Remove any explicit UI_MSG_PREVIEW_SIZE flag too if the old preview capacity is desired. These are compile-time options, not CLI commands. No settings erase is necessary when changing between matching V4 layouts.

"},{"location":"test-results/v4_pixel5_font_trial/#verification","title":"Verification","text":"
python3 -B test/test_ssd1306_picopixel.py\npython3 -B test/test_firmware_ram.py\npio test -e native -f test_display_driver -f test_companion_message_history\n

The native tests exercise both capital heights, automatic font selection, 160-character messages, complete rows at display edges, long sender lines, navigation hints, reserved footers, overflow markers and tiny/rotated screen geometry. The additional rendering comparison checks all 95 printable ASCII glyphs in each font, under address/undefined-behavior sanitizers. Picopixel is compared pixel for pixel with Adafruit GFX; Squeezed Regular 6 is compared with the upstream BDF fixture independently of the converted C++ tables. This needs a cached PlatformIO Adafruit GFX library; set MESHCORE_GFX_LIBRARY to its directory if needed. It reports a skip when the library is absent. The native tests do not require that dependency. Run only one PlatformIO command at a time.

"},{"location":"test-results/v4_pixel5_font_trial/#automatic-5px6px-results-2026-09-08","title":"Automatic 5px/6px results, 2026-09-08","text":"

Source revision 469b47d1 passed five representative firmware builds. All five retained the same static RAM usage and startup heap margin as their earlier 5px builds. The four Full profiles also passed required OTA packaging.

Hardware/profile Display Selected capital height RAM beyond the required startup budget, bytes V4.2/V4.3 Full NimBLE SSD1306 6 pixels 88,072 T096 Full, FEM on ST7735 6 pixels 24,778 Station G3 ESP32 Full SH1106 6 pixels 82,080 RAK3401 Full SSD1306 6 pixels 46,108 T-Echo Card BLE Companion U8g2, 72 x 40 5 pixels 30,992

The native display/history suites passed 36 tests. The Python font, RAM, pairing, display-profile, queue and QR checks passed 38 tests, including pixel comparisons for all 95 ASCII glyphs in each font. The actual SSD1306 software rendering matches the approved 6px comparison image pixel for pixel.

Both NimBLE-V4-VM and NimBLE-V4-Trial were flashed with v1.17.1.5-halo-keymind-cascade-squeezed6-trial-469b47d1. Their partition tables and application hashes were verified, their settings retained, and running versions confirmed. Each passed 201 USB protocol requests without error flags. The Mercerwood V4 reconnected over bonded Bluetooth at MTU 179 and retained the factory-address policy.

The XIAO sent a private 160-byte LoRa message to the Mercerwood V4. All bytes arrived, and the V4 stayed responsive through 20 seconds of display refreshes with no uptime reset or error flags. Both temporary channel settings were restored. After that interval the V4 had 147,196 bytes of free internal heap, a minimum of 146,328 bytes, and a largest free block of 139,252 bytes.

These are short functional checks. Physical readability remains a user judgment. A message with many wide characters can still need truncation.

V4 application size: 1,778,936 bytes (1,152 bytes larger than the earlier 5px trial). SHA-256: 8e6797b90741bf013b3757668e5a23378ac8c0b0ed9c75830939f13305c56a68.

"},{"location":"test-results/v4_pixel5_font_trial/#earlier-5px-hardware-and-build-results-2026-09-08","title":"Earlier 5px hardware and build results, 2026-09-08","text":"

Source revision eeea15ef passed seven representative firmware builds:

Hardware/profile Display RAM beyond the required startup budget, bytes V4.2/V4.3 Full NimBLE Picopixel SSD1306 88,072 T096 Full, FEM on ST7735 24,778 Station G3 ESP32 Full SH1106 82,080 RAK3401 Full SSD1306 46,108 Wireless Tracker Full NimBLE capacity trial ST7735 28,022 Wio Tracker L1 Full SH1106 45,744 T-Echo Card BLE Companion U8g2 30,992

These are linked-capacity checks before runtime allocation, not live free heap measurements. The six Full profiles also passed their required OTA packaging checks. The T-Echo Card row is a BLE Companion build.

Both physical V4.3 nodes, NimBLE-V4-VM and NimBLE-V4-Trial, were flashed, their image hashes verified, and their running versions confirmed. Each passed 201 USB protocol requests with zero reported error flags. The Mercerwood V4 also reconnected over authenticated, bonded Bluetooth with MTU 179 and the existing factory-address policy.

A private XIAO-to-V4 LoRa test delivered all 160 message bytes. The V4 continued responding during 20 seconds of message redraws, with zero error flags. Both temporary channel configurations were restored. Free internal heap after that interval was 147,200 bytes, minimum 146,340 bytes, and the largest free block was 139,252 bytes. These are short functional checks; physical readability remains a user judgment.

A matching pre/post-boot Mercerwood measurement showed the longer preview using 2,816 additional PSRAM bytes, with unchanged internal free heap. Boards without PSRAM use their ordinary RAM for the larger preview records.

The native display/history suites passed 31 tests. The Python font, RAM, pairing, display-profile, queue and QR checks passed 38 tests. The Adafruit comparison covers all 95 printable ASCII glyphs.

V4 application SHA-256: d914d80124b499d7b719f8427f3794ab1b6d15fd0112e3ef89ba2fac26b5a974.

"},{"location":"test-results/preview-1.17.1.8/","title":"Index","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preview-1.17.1.8/#11718-preview-1-test-results","title":"1.17.1.8 Preview 1 test results","text":"

This preview contains USA Cascade builds for RAK3401 Repeater and Heltec V4 OLED Full Companion, Full Repeater, Full Room Server, and Full Sensor. The GitHub prerelease has the matching firmware assets.

The RAK3401 source test run and Heltec V4 source test run passed. The RAK3401 UF2 and DFU application payloads match, with verified DFU CRCs. Both RAK3401 packages passed capability and memory checks. The four Heltec V4 OLED variants passed capability and memory qualification. These are build/package checks, not physical on-device tests of every variant.

"},{"location":"test-results/preview-1.17.1.8/#preview4-local-regression-validation","title":"Preview4 local regression validation","text":""},{"location":"test-results/preview-1.17.1.8/#rak3401-package-results","title":"RAK3401 package results","text":""},{"location":"test-results/preview-1.17.1.8/#heltec-v4-oled-reports","title":"Heltec V4 OLED reports","text":""},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/","title":"Heltec v4 preview4 bluetooth oled","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#heltec-v4-preview4-bluetootholed-regression-validation","title":"Heltec V4 preview4 Bluetooth/OLED regression validation","text":"

Date: 2026-10-02. These are observations on one physical Heltec V4.3 OLED, not qualification of every board or phone. The preview4 images are local, unreleased builds with uncommitted changes on keymindCascade at 7324bf06efef236009dac2d507775fa4e19bc856.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#target-and-method","title":"Target and method","text":"

A complete, hash-checked 16 MB original flash backup was taken first. Only otadata and the application were written for the temporary diagnostic image; the bootloader, partition table, NVS and SPIFFS were retained. No other Pi radio was flashed. The helper did not request LoRa transmissions; ordinary background radio activity was not disabled.

COMPANION_PAIRING_UI_HIL is an opt-in diagnostic macro, absent from production builds. It records actual startup phases, display power/pairing/USB state, public identity, and the real 1024-byte SSD1306 framebuffer. It does not access the private key. Frame capture occurred during an actual outstanding passkey request, not a manually invoked firmware callback.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#observed-hardware-results","title":"Observed hardware results","text":"Check Result Fresh pairing with a non-default PIN Passed twice: exactly one actual passkey request per fresh pairing; BlueZ reported Paired and Bonded. PIN display while USB remains connected Passed twice: display on, pairing active, USB present, and captured frame containing PAIR PIN 246810. This verifies the display driver's real framebuffer and wake state, not an optical photograph. Protected BLE transport Passed: MTU 179, 82-byte device-info response and 176-byte ATT command payload, live CLI/status responses, zero core error flags. Custom MAC with stealth Passed: fresh discovery/pairing, live bonded-peer-only advertising status, bonded reconnect with no new PIN, then USB-commanded reboot and another bonded reconnect with no new PIN. Saved random MAC Passed: USB-commanded reboot retained its address with stealth enabled. random-after-connect, unused boot Passed: reboot over USB, without BLE authentication, retained the address. random-after-connect, authenticated boot Passed: actual authenticated BLE connection armed the next boot; address stayed unchanged during that session and changed on the following USB reboot. The new address was discoverable with stealth retained. random-every-boot Passed: two successive USB-commanded boots used different addresses, with stealth enabled. Saved identity startup Passed: Starting/Loading phases only, no Generating/Saving phase. The same public identity was retained throughout the run.

The suite finished with suite_passed, six observed application addresses, two captured OLED frames, and identity_preserved: true. An earlier harness attempt stopped because it did not recognize a terminal prompt immediately followed by a diagnostic line; the parser was corrected and the complete suite rerun. This was not a BLE pairing failure.

Stealth on ESP32 uses minimal, unnamed, allowlisted advertising after pairing; it is not radio invisibility. Bonded reconnection and policy/rotation behavior were tested here. Actual advertising-payload capture and rejection of an unbonded second central were not part of this run.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#original-target-restored","title":"Original target restored","text":"

After the suite, the complete original 16 MB image was written back. Esptool verified the flash data hash. Live serial readback then confirmed:

The full backup remains private on the Pi. Raw diagnostic logs and captured OLED pixels remain in the ignored local preview4 output folder; neither the backup nor unreviewed radio logs are published. Only test-created BlueZ device records were removed by the helper.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#automated-regression-coverage-and-builds","title":"Automated regression coverage and builds","text":"

The NimBLE regression fixture reproduces the pinned library's dispatch rule: a non-default static passkey bypasses onPassKeyDisplay(). Firmware now leaves NimBLE's sentinel at 123456 and returns the actual configured PIN through that callback, preserving authentication while notifying the UI. Tests cover random and saved PINs, USB coexistence, button wake/navigation, disabled display/BLE, saved identity, generation, persistence failures, and removal of the generation label once generation finishes.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#remaining-hardware-gates","title":"Remaining hardware gates","text":""}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Home","text":"

View this page on MeshCore Docs.

"},{"location":"#meshcore-documentation","title":"MeshCore documentation","text":"

Choose firmware with the firmware picker. For help with a device already in service, start with the FAQ.

"},{"location":"#current-release","title":"Current release","text":""},{"location":"#documentation","title":"Documentation","text":"

The navigation groups the remaining guides under Documentation, including board-specific updates, networking, power, and protocol references.

"},{"location":"#tools","title":"Tools","text":""},{"location":"#engineering-records","title":"Engineering records","text":"

Found a mistake or missing information? Edit the documentation.

"},{"location":"WiFi/","title":"WiFi","text":"

View this page on MeshCore Docs.

"},{"location":"WiFi/#wifi-and-mqtt-by-firmware-type","title":"WiFi and MQTT by Firmware Type","text":"

MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:

The firmware role and the build profile are separate choices. USB logging does not itself provide MQTT. MQTT-capable Full Companions include that feature without mqtt in their filename; check the artifact capability manifest. See feature switches by role for the different Companion and infrastructure controls and the USB web console.

"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does *_repeater Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active *_repeater_observer_mqtt Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default *_room_server Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active *_room_server_observer_mqtt Yes Yes Runs the room server and uplinks radio traffic *_companion_radio_wifi Yes No Exposes the MeshCore companion protocol on TCP port 5000 *_companion_radio_wifi_mqtt Yes Yes Runs both the TCP companion interface and the MQTT uplink *_companion_radio_full* ESP32, subject to board/transport selection Where compiled on ESP32 Includes supported USB/wireless transports; configure MQTT through WebConfig Dedicated USB, BLE, or serial companion (not Full) No No Uses the transport named by the target instead *_repeater_bridge_espnow Build-dependent on ESP32 No Uses ESP-NOW for its bridge; bridge.format wrapped|raw selects legacy bridge framing or primary-ESP-NOW compatibility; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface *_sensor Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface *_lora_ota_no_external_sensors On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact start ota browser uploader

Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.

"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"

The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.

For a received packet, the flow is:

LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n                                  \\----> normal MeshCore filtering/handling\n

Packets transmitted by the node can also be queued according to mqtt.tx. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.

On infrastructure observer builds, MQTT publication and LoRa repetition are separate:

Fresh MQTT observer settings are:

Up to six broker configurations can be saved. The number that can be active at once depends on the target and available memory. A non-PSRAM ESP32 should normally use no more than two TLS/WSS brokers; some classic ESP32 targets are reliable with only one. Excess configured slots remain saved but show as inactive in get mqtt.status.

The bridge has a bounded packet queue for broker or WiFi outages and reconnects automatically. A non-PSRAM build holds 6 packets and a PSRAM build holds 50. When the queue is full, the oldest item is replaced; after five minutes with no broker connected, stale queued packets are flushed. MQTT packet delivery is best effort rather than durable storage. Each enabled slot publishes independently, so one failed broker does not intentionally stop the other slots.

"},{"location":"WiFi/#automatic-network-outage-power-saving","title":"Automatic network outage power saving","text":"

ESP32 MQTT firmware uses one shared outage timer for MQTT and NTP. If no broker demonstrates a working connection for five minutes, the MQTT worker stops its clients and NTP request before suspending background WiFi. Saved WiFi and broker settings remain enabled. Failed requests, a WiFi association, and NTP replies alone do not restart the outage timer or keep a failed MQTT uplink awake.

After 30 minutes, the firmware opens a recovery window lasting up to two minutes. A broker acknowledgment or a connection that stays up for 30 seconds returns the node to normal operation. Otherwise WiFi is suspended for another 30 minutes. An explicit management request can ask for an earlier probe. Automatic NTP requests have a 15-second timeout; failed requests wait 30 minutes instead of repeatedly running a blocking server search. Successful NTP retains the daily refresh schedule.

Active browser administration, setup/OTA access points, connected Companion clients, and an attached WiFi OTA seeder protect their network access. An idle WebConfig listener does not prevent the cooldown. An enabled SNMP agent keeps a working LAN connection available. ESP-NOW keeps its radio and channel; only the failed infrastructure connection is suspended. LoRa continues operating, and when CPU power saving is enabled, its normal sleep eligibility returns after background WiFi shuts down. USB console and logging sleep rules still apply separately. This policy does not automatically turn off working services or expire an active setup access point.

See MQTT_IMPLEMENTATION.md for the complete preset list, custom broker configuration, topic formats, authentication, diagnostics, and memory limits.

"},{"location":"WiFi/#mqtt-observer-setup","title":"MQTT observer setup","text":"

WebConfig serves gzip only, including its bootstrap and captive-portal success pages. Static WiFi setup and browser OTA pages use the same format. Builds use Google Zopfli 0.4.3 with 1,000 iterations; validated results are cached under .pio/gzip-html. There is no Brotli dependency or second WebConfig page copy. Browsers decompress the pages automatically over ordinary HTTP. WebConfig returns HTTP 406 when a client explicitly excludes gzip. Its large page closes the TCP connection gracefully after the final acknowledged bytes so browsers do not report a completed download as aborted. The bootstrap uses ES5 syntax and Promise-based loading, including on Android 5.1.1 with Chrome 43; newer JavaScript syntax must not prevent the page loading.

Most full-size ESP32 MQTT observer builds have the shared WebConfig portal. On a fresh device with no saved SSID:

  1. Join MeshCore-Setup-XXXX with the node's admin password (factory default password). A display's WiFi QR code uses the hotspot's current credential.
  2. If the captive page does not open, browse to http://192.168.4.1/.
  3. Enter WiFi, radio, identity, and MQTT settings.
  4. Select Save & Reboot.
  5. After reboot, check get wifi.status and get mqtt.status.

ESP32 management uses one saved admin password for WebConfig login, browser firmware uploads, and setup WiFi. Choose 8\u201315 printable ASCII characters so the hotspot can use it for WPA2. Other valid admin passwords (1\u201315 UTF-8 bytes, without CR or LF) leave the hotspot open and require an HTTP admin login. There is no separate setup WiFi password. A protected first-time setup AP with no saved SSID retains its provisioning flow; recovery with a saved SSID requires the existing admin login. Deliberately passwordless images retain their local no-password policy. The portal uses plain HTTP, so use a trusted network.

Changing the password ends existing browser sessions after the save succeeds. The submitting page keeps the save result and tries the new password once, then asks you to sign in if needed. A partial save shows which settings failed and does not reboot. If the setup hotspot restarts, reconnect with the new password when it is eligible for WPA2. An unconfirmed password save keeps the previous runtime password active, but the saved state after reboot may differ; read the reported error before retrying.

Credentials for other systems or different permissions remain separate:

Credential Why it does not become the node admin password Router WiFi and MQTT broker login These are issued by the network or broker, which may serve many devices. Bluetooth pairing PIN BLE pairing requires its numeric PIN format. Guest and room passwords Members and guests should not receive node administration access. Channel, bridge, fleet and alert keys These are shared with peers or recipients and have their own protocol requirements. SNMP community and management-report credentials Monitoring systems receive these credentials; they should not also receive the node admin password.

Admin login limits apply on both LAN and recovery AP: five failures lock the failing address for 30 seconds after its fifth failure. All addresses also share a budget of six failures, refilling one allowance every 6 seconds; while empty, even correct credentials receive HTTP 429. A success clears only its own address's count. The room browser uses a separate password budget; a WebConfig session does not bypass it.

On an expanded FULL profile with no saved SSID, this automatic setup AP has an absolute 30-minute window. Browser activity or a phone left associated with the AP does not extend it. If no SSID has been saved when the window expires, WebConfig closes and the ESP32 WiFi radio remains off automatically for the remainder of that boot. A reboot or power cycle starts a new 30-minute setup window; the timeout is deliberately not written to preferences. An administrator can still override the automatic cutoff with an explicit start webconfig command.

Once an SSID is saved, the provisioning cutoff no longer applies. When WiFi is selected by logging.output wifi|both, the station stays enabled and keeps trying the saved network indefinitely: ESP automatic reconnect remains on and the explicit fallback advances through 15, 30, 60, 120, then 300-second retry intervals, remaining at five minutes until it reconnects. MQTT broker retries use their own backoff; a repeatedly failing broker eventually receives one probe every 30 minutes. Selecting logging.output off|usb keeps the MQTT bridge off, so saved credentials alone do not force WiFi on unless WebConfig is also enabled explicitly.

The equivalent MQTT observer CLI setup is:

set wifi.ssid Your WiFi Name\nset wifi.pwd Your WiFi Password\nset mqtt.iata SEA\nset name MyObserver\nreboot\n

The SSID and password values are the rest of the command line. Spaces are allowed and quotes must not be added. SSIDs may contain at most 31 characters. ESP32 infrastructure MQTT builds with WebConfig share one canonical WiFi configuration between this CLI and the browser portal. Passwords accept 0-63 characters or exactly 64 hexadecimal characters for a raw WPA PSK. Leave the password value empty for an open network. Reboot after changing credentials to apply them; an already-running MQTT worker keeps its current connection settings until it stops and restarts.

Older observer-only credentials remain usable until canonical WiFi settings are saved. The first CLI edit preserves the other legacy credential while saving the complete pair. Once canonical settings exist, empty, incomplete or unreadable canonical settings do not revive old observer credentials. Historical SSID-only canonical settings continue to represent an open network. Builds without this WebConfig integration retain their legacy 63-character password limit; this change does not add MQTT support to currently disabled Pico targets.

On these ESP32 MQTT/WebConfig builds, an empty CLI SSID clears the configured network while retaining saved broker settings:

set wifi.ssid\nreboot\n

Use this to return the node to unconfigured provisioning. The WebConfig setup form still requires a nonempty SSID. After saving a new network, verify get wifi.status, get mqtt.config.valid and get mqtt.running; credentials alone do not prove a broker connection. A local get wifi.pwd can inspect the password, while an authenticated remote command returns only set/unset.

Useful checks are:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget mqtt.enabled\nget mqtt.rx\nget mqtt.tx\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n

To change a configured observer through WebConfig without leaving the portal enabled after every reboot:

start webconfig\n

This uses the LAN when WiFi is connected. To force the setup AP, the MQTT bridge must release WiFi first:

set mqtt.enabled off\nstart webconfig ap\n

After changing WiFi credentials, use Save & Reboot or reboot. For broker-only changes that keep the existing network, stop the temporary portal and restart the bridge:

stop webconfig\nset mqtt.enabled on\n

Current MQTT observer artifacts use the expanded FULL partition profile so WebConfig and the complete role CLI are retained. Install the matching merged image over USB once when moving a device from the legacy partition layout.

"},{"location":"WiFi/#wifi-companion-setup","title":"WiFi companion setup","text":"

Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off. Bluetooth starts where the build supports it, and saved WiFi enablement is preserved across upgrades. Before using the setup AP or station, enable WiFi through the USB or Bluetooth CLI:

get wifi.enabled\nset wifi.enabled 1\n

ESP32 simultaneous-transport builds apply the change without a reboot. SenseCAP Indicator exclusive-transport builds and Pico W require a reboot. WiFi-only compatibility builds without any other CLI transport retain WiFi on so they are not left unreachable. Saving an SSID/password alone does not enable WiFi on an opt-in build. Use set wifi.enabled 0 to save it off again. This controls infrastructure WiFi, not a primary ESP-NOW mesh radio.

A *_companion_radio_wifi build replaces the BLE or USB companion link with the MeshCore companion protocol over TCP port 5000. The phone or computer must be able to reach the device on the same LAN.

Most ESP32 *_companion_radio_full targets keep all three Companion links at once: USB, BLE, and TCP port 5000. The two SenseCAP Indicator Full targets are the exception: USB remains available, while a saved next-boot selector starts exactly one secondary wireless Companion transport, either BLE or infrastructure WiFi/TCP. On the ESP-NOW Indicator, this selector never stops the primary ESP-NOW mesh radio. Full ESP32 images also provide a source-only LoRa mOTA service on ports 5001 and 5002 whenever their WiFi transport is active. See the full Companion guide for its build, terminal mode, and complete update-source workflow.

The LilyGo T-Beam 1W Full Companion maps one press of the physical BOOT button (GPIO0) to a persistent WiFi on/off toggle. Turning WiFi off closes WebConfig, Companion TCP, mOTA, and MQTT network services while USB, BLE, the display, GPS, and LoRa remain available. Another BOOT press turns WiFi back on even after reboot. WiFi/WebConfig starts first; BLE starts two seconds later so the ESP32-S3 can reserve the setup server's internal heap before both radios run together.

The companion loads runtime credentials saved in NVS. A non-placeholder compile-time WIFI_SSID/WIFI_PWD can be used as a first-boot fallback, but saved credentials take priority. With no credentials, its WebConfig portal starts in setup-AP mode. With credentials, the WebUI is enabled by default on the station IP.

Full Companion can also be provisioned from its USB terminal or TCP port 5002:

get wifi.ssid\nget wifi.status\nset wifi.ssid SlowFi\nset wifi.pwd your-password\n

Credential writes are persisted immediately. After the reply has drained, the Companion restarts its WiFi station with the saved pair. A port-5002 client is expected to disconnect and can reconnect at the new LAN IP. USB masks the password as it is entered; get wifi.pwd is intentionally unavailable. The standalone credential store accepts an empty password, ordinary passphrases up to 63 characters, or an exact 64-character hexadecimal WPA/WPA2 PSK. A non-hexadecimal 64-character value and every longer value are rejected.

After the saved station first connects following boot, every ESP32 WiFi Companion requests a fresh UTC time from NTP. A verified reply updates both the MeshCore clock and any detected hardware RTC; the next explicit refresh is due 24 hours after that success. A failed request times out without blocking the radio loop and retries after five minutes. While WiFi is disconnected or only the setup AP is available, the request waits for a station connection. Builds with a configured MQTT bridge use its boot-and-daily NTP owner instead, so two firmware services never reconfigure the ESP32 SNTP client at the same time. A later WiFi reconnect does not add another successful-sync request; it preserves the existing 24-hour deadline. Until a fresh internet reply succeeds, LoRa clock consensus remains available as the fallback.

On Full Companion targets whose primary mesh radio is ESP-NOW, ESP-NOW, the setup AP, and the infrastructure-WiFi station must share one 2.4 GHz channel. The persisted channel defaults to 1 and can be inspected or changed from the text terminal:

get espnow.channel\nset espnow.channel 6\nreboot\n

Valid values are 1 through 13; use only a channel permitted in your region and supported by the router. set espnow.channel saves the new value but does not move the running radio; reboot the node to apply it. Every primary ESP-NOW node that should communicate with it must use the same channel, and the configured router's 2.4 GHz radio must be fixed to that channel as well. A router left on automatic channel selection can move the infrastructure connection away from the ESP-NOW mesh.

espnow.channel configures the node's primary ESP-NOW mesh radio. It is distinct from bridge.channel, which applies only to the separate ESP-NOW bridge feature. WiFi power saving does not let ESP-NOW and infrastructure WiFi operate on different channels. On an ESP32 Full build whose primary radio is ESP-NOW, wifi.powersave max is unavailable because a station using maximum modem sleep can miss ESP-NOW broadcasts; use min for coexistence. The primary mesh transport holds the driver's RF wake reference continuously, so min does not suspend its ESP-NOW receiver.

Espressif long-range WiFi (LR) trades speed for reach: its raw rates are 250 or 500 kbit/s, with up to about 1 km in suitable line-of-sight conditions. LR traffic needs compatible Espressif hardware at both ends. An LR-enabled access point sends LR beacons that ordinary phones, routers and Raspberry Pis cannot receive. A mixed b/g/n+LR station can still join an ordinary router, using normal WiFi rates for that connection. MeshCore's setup and OTA access points therefore use ordinary b/g/n. See Espressif's LR compatibility guide.

ESP-NOW compatibility also depends on the bytes and PHY used above the common Espressif transport. The historical *_repeater_bridge_espnow firmware wraps each MeshCore packet with a magic value, checksum, and secret-based XOR, while Generic_ESPNOW and SenseCapIndicator-ESPNow send the serialized MeshCore packet directly using the ESP-NOW LR PHY. Updated bridge firmware can select either behavior without changing the board target:

get bridge.format\nset bridge.channel 1\nset bridge.format raw\n

On a Heltec V4, the ordinary companion_radio_full image remains a LoRa-primary Companion and does not instantiate this bridge. Select and flash the existing heltec_v4_repeater_bridge_espnow role before using these commands. Treat that Companion-to-repeater change as a role/partition migration: back up the node and use the exact V4 merged install artifact.

wrapped remains the default and interoperates with existing bridge nodes that use the same channel and bridge.secret. raw interoperates with primary-ESP-NOW nodes and other raw bridges; it requires channel 1-13, enables LR reception, forces LR transmission, and ignores bridge.secret. Receive parsing is strict to the selected format, so migrate peers deliberately rather than expecting one bridge to accept both. Match bridge.channel on the LoRa gateway to the primary nodes' espnow.channel; a primary-node channel change takes effect after its reboot.

Raw mode retains MeshCore's normal packet parsing, authentication/encryption, and the bridge's duplicate suppression, but it does not provide the wrapper's secret-based network isolation. That XOR wrapper is itself only lightweight isolation, not cryptographic security. Multiple LoRa/ESP-NOW gateways covering the same nodes can temporarily amplify duplicates, even though seen-packet tracking prevents an individual bridge from immediately echoing a packet back.

If the configured network remains unavailable for two minutes, the companion opens its setup AP so the credentials can be repaired. It continues retrying the saved network. WiFi modem sleep has its own persisted wifi.powersave setting and is independent of the Companion device power-saving setting. The WiFi card in Companion WebConfig exposes none, min, and max. Fresh Cascade-profile builds select min; target-default builds select none. A saved setting takes precedence after an upgrade.

WiFi-only Companions can use all three modes. A Full Companion that runs WiFi and BLE simultaneously requires at least minimum modem sleep. It reports min when an old or target-default none value is found and rejects a new none selection. When ESP-NOW is also the primary mesh radio, max is unavailable: the access point does not buffer ESP-NOW broadcasts for a sleeping station, so maximum modem sleep can lose them. Firmware caps a previously saved max value to and reports it as min, and rejects a new max selection.

The SenseCAP Indicator Full targets select BLE or infrastructure WiFi instead of running both. On a WiFi-selected boot, the LoRa layout accepts none|min|max, while the ESP-NOW layout accepts none|min and still rejects max. On a BLE-selected boot infrastructure WiFi is off; the LoRa layout accepts min|max for the saved WiFi setting, while ESP-NOW + BLE requires min. Device power saving remains independent of this modem policy.

ESP32 WiFi Companions also have a saved admin password for LAN WebConfig and browser OTA, initially password. Their get password command reports status without revealing it. The credential is stored in NVS alongside the WiFi settings and survives a filesystem erase. Use password <new_password> through an available administrative connection to change it.

The WebConfig Advanced card exposes device power saving on WiFi Companion, repeater, and room-server builds. It also exposes RX power saving on radio chips that support receive duty cycling. RXPS can select continuous receive, levels 1-10, automatic or explicit 16/32-symbol timing assumptions, or manual receive/sleep windows. The timing assumption does not change the radio's actual wire preamble. The WebConfig WiFi card exposes WiFi modem power saving. All three settings are persisted across reboots.

The three settings are independent. A WiFi Companion keeps its transports available while device power saving reduces CPU and GPS idle power. An infrastructure node can sleep when device power saving is enabled, so its WiFi services may be temporarily unavailable. RXPS only duty-cycles the LoRa receiver. Fresh Cascade-profile builds default to device power saving on, RXPS on at level 8 with a 16-symbol preamble, and WiFi modem power saving at min on every build that includes ESP32 WiFi. The RXPS preamble value is a saved timing assumption, not the wire length: starting with v1.17.1.5, SF5-SF8 packets normally use a 32-symbol physical preamble. Firmware selects 64 for the whole SF/BW tuple only when 32 cannot enable RXPS at any level and 64 can (SF5/BW250 and SF6/BW500), then 128 only when neither shorter length works (SF5/BW500).

When ENABLE_OTA is included, a WiFi companion also listens on:

These ports do not replace the companion protocol on TCP 5000. On a companion_radio_full build, port 5002 is the same role-specific text terminal available over USB, including chat, remote administration, radio and power settings, WiFi/WebConfig management, tempradio, and the source-only ota commands. Other OTA-enabled Companion builds expose the same local maintenance commands as binary command 0x42, plus ota ... commands. LoRa staging and installation on the Full Companion itself remain disabled.

Port 5002 is plaintext and has no independent login gate. Use it only on a trusted LAN or temporary setup network, especially when entering a remote-node admin password with the terminal's login command.

FULL ESP32 builds share the port 5001 folder seeder. It starts whenever that role has a usable WiFi station or setup access point and stops when WiFi stops. For example, a FULL repeater can run start webconfig to join its saved network, then accept:

motatool serve --dir ./motas --tcp <repeater-ip>:5001 -v\n

The TCP connection supplies .mota files for the node to advertise and relay over LoRa; it is not a raw .bin uploader. start ota continues to provide the direct browser uploader on HTTP port 80. Sign in with HTTP Basic username admin and the node's current admin password. A FULL role without WebConfig but with browser OTA support can use the MeshCore-OTA access point raised by start ota; its seeder address is 192.168.4.1:5001. The runtime OTA hotspot uses the same WPA2 eligibility rule as setup WiFi. Temporary partition migration and recovery applications retain their own documented access policy.

For browser uploads from an existing 1.17.1.8 ESP32 node, see the 1.17.1.9 WiFi upgrade guide. It covers the node AP, old-page completion behavior, and choosing an application-only image or the exact partition-migration package.

Only one external folder link can be active. A TCP client is rejected while ota folder on is using USB serial, and disconnecting motatool automatically removes the TCP folder. Port 5001 has no login layer, so expose it only on a trusted LAN or temporary setup network. Firmware target and hash checks still apply at the receiving node, along with its configured signature/trust policy.

The HTTP admin login does not protect trusted TCP consoles or delegated remote CLI access. Those administrative paths can change the node's admin password; limit them to trusted users and networks.

When the shared seeder is running, get wifi.status appends its live state:

OTA TCP 5001: listening\nOTA TCP 5001: client connected\n

The first state means WiFi is usable and the node is waiting for motatool serve --tcp. The second means a motatool folder is currently attached and available for LoRa OTA service.

"},{"location":"WiFi/#wifi-companion-with-mqtt","title":"WiFi companion with MQTT","text":"

A *_companion_radio_wifi_mqtt build combines both systems:

The companion owns WiFi connection and recovery in this build. The MQTT bridge waits for that connection rather than creating a second one. Stopping MQTT does not disable the TCP companion service.

MQTT-capable Full Companions also combine these services. The WebConfig page contains MQTT settings, and the USB/TCP and browser terminals accept the same mqtt.* settings as infrastructure for shared features. Use set mqtt.enabled on|off to control MQTT without discarding broker settings. Use set logging.output off|usb|wifi|both to select both outputs together.

"},{"location":"WiFi/#webconfig-without-mqtt","title":"WebConfig without MQTT","text":"

Full-size ESP32 repeater and room-server builds can include WebConfig even when MQTT is absent. The portal then shows node, radio, and WiFi-related controls but removes the MQTT wizard step and MQTT tab.

In this case WebConfig owns WiFi only while it is needed. Stopping the portal disconnects WiFi and turns the WiFi radio off. There is no persistent MQTT connection keeping WiFi active.

Unified FULL USB + WiFi and FULL logging-fallback repeater/room-server builds both provide these CLI controls and status checks. The unified profile includes the MQTT bridge; the fallback is used only where no matching MQTT environment exists:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget webui\nset wifi.ssid SlowFi\nset wifi.pwd your-password\nset wifi.powersave none\nset wifi.cli on\n

get wifi.status distinguishes an unconfigured node, an inactive WiFi radio, a station connection attempt, the setup AP, a connection failure, and a working LAN connection. For a LAN connection it reports the SSID, IP address, and RSSI. If the shared OTA seeder is active, the same reply also reports whether TCP port 5001 is listening or has a motatool client attached. get wifi.powersave reports the saved standalone WebConfig setting as none, min, or max. An inactive status is normal when webui is off: run start webconfig to connect temporarily. Standalone credentials can be changed through WebConfig or with the listed CLI commands. Changing the SSID or password stops an active WebConfig session; start it again to connect with the new values. Use set wifi.pwd with no value for an open network. get wifi.pwd returns the saved password on a local connection; LoRa callers cannot read it. Standalone WiFi accepts ordinary passphrases up to 63 characters and exact 64-character hexadecimal WPA/WPA2 PSKs; other 64-character values and all longer values are rejected. MQTT observer WiFi passwords remain limited to 63 characters by their fixed persisted layout.

get webui starts with the saved boot setting, then reports the current session. For example, > off, http://192.168.1.130/ means automatic WebConfig startup is saved as off, but a temporary session started by start webconfig is currently active at that URL.

"},{"location":"WiFi/#webconfig-cli-terminal","title":"WebConfig CLI terminal","text":"

The terminal defaults to on. Enable or disable it from an existing admin CLI:

set wifi.cli on\nset wifi.cli off\nget wifi.cli\n

The saved on setting becomes active only when the WiFi station client is connected and WebConfig is running in LAN mode. It is never exposed on the open setup access point. When active, the WebConfig page has a CLI tab. Repeater and room-server commands use their local CLI after admin authentication. The browser can run get password, get prv.key, erase, set freq, stats-core, stats-radio, stats-radio-diag, stats-packets, and explicit WiFi/MQTT credential getters. It also streams get acl and raw log dumps back to the browser. Log reads pause for slow clients and resume as output is read, using a fixed 160-byte file chunk. A dump snapshots the file length, so live logging cannot extend it indefinitely. The browser keeps the complete requested dump, including files larger than 64 KiB.

Companion uses the same complete terminal as USB and Full Companion TCP port 5002, including card, import meshcore://..., list, to, messages, remote login, and delayed replies. It uses the Companion's trusted LAN. See Companion browser terminal for contact import, terminal ownership, and disconnect behavior.

Type a command at the prompt, or paste multiple lines and confirm the sequence. Blank lines and comments are ignored. The device reports the supported batch limit. The browser sends each line after the previous command finishes and continues polling for incoming text after the sequence. help lists the device's actual terminal commands; Tab completes command names and settings.

The up/down arrow keys recall commands entered during the current browser session. Commands available in the terminal still depend on the firmware role and build profile. Commands such as stop webconfig, set wifi.cli off, WiFi credential changes, and reboot operations stop the remaining block and can disconnect the page before it receives their final reply.

"},{"location":"WiFi/#build-profiles","title":"Build profiles","text":"

build.sh produces several profiles. A profile changes the features compiled into a selected target; it does not change that target into another firmware role.

This fork defaults every build.sh target to the USA Cascadia radio preset and the Cascade firmware profile. The script resolves the live USA/Canada entry by name, so its changing number in the downloaded menu does not affect builds. If the preset service is unavailable, it falls back to 910.525 MHz / BW62.5 / SF7 / CR5. Use --radio-preset target --profile default only when a build intentionally needs the target's original radio and profile defaults. --radio-preset usa-cascadia is the stable explicit name; legacy numbered choices remain accepted but their meaning can change when the service reorders or adds presets.

Build profile WiFi/MQTT behavior Standard Uses the selected target's role. Where USB is a safe plaintext console, the same artifact embeds debug/packet logging behind persistent get/set usb.logging. Ordinary legacy-slot ESP32 repeater/room-server artifacts omit WebConfig when needed to fit. ESP32 MQTT observer and ESP-NOW bridge targets are automatically promoted to FULL; WiFi-companion targets keep their companion partition profile. Legacy logging No separate artifact is emitted; USB logging is part of the ordinary image. Logging output itself is not a direct MQTT uplink. MQTT Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. Non-companion ESP32 MQTT observers always use FULL expanded partitions. FULL ESP32 USB + WiFi Uses the board's MQTT target with USB packet logging and direct WiFi MQTT together, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. get/set logging.output off\\|usb\\|wifi\\|both persists the active paths. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. FULL ESP32 logging fallback Uses the board's non-MQTT target only when no matching WiFi MQTT environment exists. It keeps debug and packet logging, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. Persistent usb.logging off also provides normal output-off operation, so ESP-NOW FULL roles need no second non-logging image. LoRa-OTA no-external-sensors A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and up to 254 neighbors, subject to target RAM limits.

Repeater profiles use up to 254 neighbors. Selected Generic E22, Heltec V2, Meshadventurer, T-Beam OTA/MQTT, and TLora MQTT profiles use 50 to preserve internal-DRAM reserves. Consult the artifact capability manifest and ESP32 memory budget for the exact target.

The interactive Option 1 FULL everything choice and the standalone FULL command select the unified USB + WiFi image when a matching MQTT target exists; otherwise they select the logging fallback. The build matrix no longer emits any separate standard logging image, or a non-MQTT FULL twin for a role covered by the unified image. Expanded Full infrastructure profiles include LoRa OTA, WebConfig where supported, up to 254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM limits still apply.

FULL images change the ESP32 partition layout. Flash the matching *-merged.bin once when installing that layout. A partition-layout change can invalidate NVS, including saved WiFi, MQTT, name, and admin settings. Later updates using the same layout normally preserve them.

"},{"location":"WiFi/#wifi-power-behavior","title":"WiFi power behavior","text":"

MQTT observers and FULL standalone ESP32 repeater/room-server builds support:

set wifi.powersave none\nset wifi.powersave min\nset wifi.powersave max\n

Fresh Cascade-profile builds default to min; target-default builds use none, which gives the most predictable MQTT and radio performance at the highest power use. min and max reduce power but may add latency or reduce reliability on busy nodes. A saved operator setting takes precedence on an upgrade. ESP32 WiFi Companions expose the same values in their WebConfig WiFi card and USB text terminal. Full Companion also accepts the text commands from TCP port 5002. The normal binary Companion protocol can read or write the setting over USB, BLE, or TCP port 5000 without entering terminal mode. A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects none because coexistence requires WiFi modem sleep. An ESP32 Full Companion whose primary mesh radio is ESP-NOW also rejects max; maximum modem sleep can cause the station to miss ESP-NOW broadcasts because the access point does not buffer them. A previously saved max value is capped to and reported as min. The SenseCAP Indicator exception uses the active-mode rules above rather than assuming BLE and infrastructure WiFi are active together. ESP-NOW and infrastructure WiFi still must use the same fixed channel.

MQTT observer targets normally limit ESP32 WiFi transmit power to 11 dBm unless the board configuration overrides MQTT_WIFI_TX_POWER. This setting affects WiFi only, not LoRa transmit power.

"},{"location":"WiFi/#companion-wifi-compatibility-settings","title":"Companion WiFi compatibility settings","text":"

ESP32 WiFi Companions, including Full Companion, provide two independent, saved compatibility controls. Defaults remain bgn and normal; neither setting changes the running connection until a reboot. They are available through the USB terminal, framed Companion CLI over USB/BLE/TCP, and the browser command terminal where included.

get wifi.protocol\nset wifi.protocol bg\nreboot\n

wifi.protocol bgn permits ordinary 802.11b/g/n. bg excludes 802.11n for access-point compatibility testing; it does not change LoRa or radio2. B/G-only is rejected when it conflicts with a primary ESP-NOW radio or an active long-range ESP-NOW owner. The temporary setup AP retains its existing B/G/N compatibility policy.

The separate WiFi-only boot skips Bluetooth controller/host initialization, not merely advertising. USB and LoRa remain available; existing Bluetooth names, PINs and bonds are not erased. Enable and configure WiFi first, and retain USB access when testing an unreliable WLAN:

set wifi.enabled 1\nset wifi.mode wifi-only\nset wifi.powersave none\nreboot\nget wifi.status\n

Saving none while a WiFi-only reboot is pending queues it for that boot; it never disables modem sleep while the current Bluetooth stack is active. get wifi.status reports the connection, SSID, IP and RSSI (dBm), followed by saved/boot/live radio settings in the same bounded reply:

> connected ssid=MyWiFi ip=192.168.11.102 rssi=-74\nprotocol=bgn/bgn/0x07 mode=normal ps=min coex=yes ota=5001:listen\n

protocol lists saved / boot-selected / live SDK bitmap (0x03 = B/G, 0x07 = B/G/N). For example, protocol=bg/bgn/0x07 shows a B/G-only change waiting for reboot. mode shows one value when saved and boot choices match, or saved / boot-selected when they differ, such as wifi-only/normal. ps is the live modem-sleep mode; coex indicates whether Bluetooth coexistence is required. ota is off, <port>:listen, or <port>:client. n/a means the SDK could not read that value; it does not claim the radio is off. IP and RSSI are omitted when not applicable. Individual getters remain available, but no separate detailed-status command is needed.

Test B/G-only and WiFi-only separately to identify which change helps. A successful local test does not establish compatibility with an unavailable AP. Turning WiFi off/on, or using set bluetooth off, is not equivalent to a true WiFi-only reboot. During that boot, Bluetooth cannot be re-enabled until wifi.mode normal is saved and the device is rebooted.

To restore the usual Cascade combination:

set wifi.mode normal\nset wifi.protocol bgn\nset wifi.powersave min\nreboot\n

The SenseCAP Indicator's existing exclusive companion.transport wifi|ble selector remains authoritative; it does not expose wifi.mode.

"},{"location":"WiFi/#recognizing-the-wrong-firmware","title":"Recognizing the wrong firmware","text":"

get wifi.status, get wifi.ssid, get wifi.powersave, and get wifi.cli are available on MQTT observers and on FULL non-MQTT repeater/room-server builds with WebConfig. ESP32 WiFi Companions also expose all four commands, credential setters, and start webconfig [ap] through their USB text terminal. Their browser CLI defaults to on in station/LAN mode. Full Companion also exposes its complete role-specific text terminal on TCP port 5002, including chat and streaming commands. MQTT commands such as get mqtt.status and set mqtt1.preset ... require MQTT code in the image, on Companion or infrastructure. Unknown settings return Error: unknown setting: <name>. Older firmware that used the discontinued compact CLI can instead report Unsupported in this firmware when a command was cut for space.

Check the complete firmware filename and role. In particular:

Rolling firmware back does not restore settings erased by a full flash or a partition-table change. If the correct MQTT target still has no configuration, provision WiFi and MQTT again.

"},{"location":"WiFi/#troubleshooting","title":"Troubleshooting","text":"

For an MQTT observer:

get wifi.status\nget mqtt.enabled\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n

For a connection that works initially but later stops reporting, inspect each configured slot's pub_ack count in get mqttN.diag. Accepted publishes are not proof of delivery: a QoS 1 publish is confirmed only when the broker acknowledges it. A quiet radio channel alone is not a failed MQTT connection.

The shared MQTT bridge recovers failed transports on its owning task, including errors without a subsequent disconnect callback and connection attempts that never finish. It also watches accepted QoS 1 publishes for missing broker acknowledgments. Recovery retains the existing broker configuration, memory admission and reconnect backoff; it does not reboot the node or cycle WiFi. See the MQTT recovery regression for the deadlines, test coverage and hardware limitations. Older preview images do not gain these changes until their firmware is updated.

Common causes are:

For a WiFi companion, find its station IP in the router, connect the client to TCP port 5000, and use the MeshCore-Setup-XXXX AP at http://192.168.4.1/ if it cannot join the saved network. The hotspot uses the admin password when eligible for WPA2, as described above. Current firmware uses standard b/g/n for the setup AP, including ESP-NOW-capable targets. A running LR mesh conflicts with a conventional access point; current firmware refuses that AP startup and defers LR startup while an AP is active. A primary ESP-NOW target keeps its infrastructure station and mesh on the persisted espnow.channel (channel 1 by default). Its configured 2.4 GHz router and every other primary ESP-NOW node must use that same channel. MQTT-capable Companions also accept get mqtt.enabled, get mqtt.running, and get mqtt.status in their USB/TCP and browser terminals. Extended slot and NTP diagnostics remain infrastructure features.

"},{"location":"bootloader_version_stock_audit/","title":"Bootloader version stock audit","text":"

View this page on MeshCore Docs.

"},{"location":"bootloader_version_stock_audit/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"cad_scan_validation/","title":"Cad scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"cad_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"carrier_wave/","title":"Carrier wave","text":"

View this page on MeshCore Docs.

"},{"location":"carrier_wave/#carrier-wave-rf-checks","title":"Carrier-wave RF checks","text":"

The constrained wio-e5-mini_repeater omits Morse and CW tools to fit its existing application region. Its packet radio and radio2 are unchanged. Other boards keep their existing carrier-tool support.

"},{"location":"carrier_wave/#tx-only-morse-messages","title":"TX-only Morse messages","text":"

Morse TX mode keys an unmodulated carrier into dots and dashes. It does not send LoRa packets or automatically retransmit channel chat or private messages. The default is 902.100 MHz at 25 words per minute (WPM). Nothing transmits until an explicit morse send command is accepted.

Command Action morse send TEST DE N0CALL Send the text once using the saved Morse frequency and speed. morse stop Stop Morse immediately and restore packet reception. get morse Show idle/active state, frequency, WPM and remaining time. get morse.freq Show the Morse TX frequency in MHz. set morse.freq 902.100 Save the separate Morse TX frequency, rounded to 1 kHz. get morse.wpm Show the saved sending speed. set morse.wpm 12 Slow transmission for easier hand decoding; supported range is 1\u201360 WPM.

The default 25 WPM can be changed; for an easy-to-copy test, set 10\u201312 WPM. Standard timing uses one unit for a dot, three for a dash, one between elements, three between letters and seven between words, with a dot of 1200 / WPM milliseconds. Letters are sent case-insensitively. Messages are bounded to 80 ASCII characters, must contain supported Morse characters, and must fit within 180 seconds at the chosen speed. Supported punctuation is . , ? ' ! / ( ) & : ; = + - _ \" $ @; tabs, newlines and non-ASCII text are rejected. Invalid messages are rejected before the transmitter is keyed. There is no automatic callsign insertion.

The Morse frequency is independent of radio, tempradio, radio2 and tempradio2. Before sending, the firmware rejects a frequency that overlaps either packet-radio profile, including a guard outside its occupied bandwidth. It does not automatically find another channel or change your mesh settings. Choose another morse.freq if 902.100 MHz overlaps either profile. A frequency accepted as a saved value must still be supported by the actual radio at send time. Morse frequency/speed changes are rejected while Morse or CW is active.

There is one physical transceiver: packet reception and transmission pause throughout the message, including the spaces between dots and dashes. Normal receive/profile scanning is restored afterward. Use local USB/BLE for immediate status and stopping; a reply to a remote LoRa CLI command waits until the message finishes. Changing either packet profile during Morse can abort it to preserve the frequency-separation safety checks.

An unmodulated carrier has no selectable LoRa bandwidth. This uses native CW on SX1262/SX1268/LLCC68/STM32WLx and LR1110/LR1121/LR2021 radios, and zero-deviation FSK CW on SX1276-family radios with reset/recovery support, rather than pretending the narrowest receive filter sets the transmitted bandwidth. Morse sidebands depend on keying speed and transmitter edges. This is not a guarantee of a particular occupied RF bandwidth or freedom from interference. Unsupported radios reject the command without beginning transmission. The chips' configured TX ramps use 3400 microseconds on the SX126x-family and 304 microseconds on the LR-family to reduce keying splatter. These are driver settings, not measured occupied-bandwidth results. Direct FEM gain changes are blocked while Morse owns the radio.

Frequency and speed are saved in a separate checked /morse_prefs file; identity, contacts and existing radio preferences are unchanged. An active message is never restored after reboot. TX uses the board's existing set tx power setting and hardware power limits. Unreadable or corrupt saved Morse settings without a usable backup block sending rather than silently transmitting on the default frequency. After storage recovers, verify and save morse.freq (and the desired WPM); a verified save makes sending available again.

Use an appropriate antenna or dummy load, lawful frequency/power and any required operator identification. The default frequency is a convenience, not permission to transmit or a claim that the channel is clear. Check the local band plan and listen first.

"},{"location":"carrier_wave/#timed-morse-reception-and-retained-captures","title":"Timed Morse reception and retained captures","text":"

Supported radios can temporarily listen for keyed carriers and decode the power envelope into text. This is a separate manual session, not another MeshCore packet channel or an authenticated sender. Nothing decoded is automatically transmitted, relayed or inserted into a channel/DM inbox.

Reception requires a saved timing hint of 15-60 WPM. The default remains 25 WPM. Slower speeds of 1-14 WPM remain available for transmission; listening rejects them with a receive-specific error instead of silently changing the saved setting. Set morse.wpm to at least 15 before listening.

Command Action morse listen 1 Listen for one minute at the saved morse.freq, using morse.wpm as the timing hint. morse listen 0.5 Listen for 30 seconds. Fractions are allowed. morse listen 5 Listen for the maximum five minutes, then restore packet reception. morse stop End TX or listening early. A received capture is retained. get morse Show TX/RX state, actual RX filter bandwidth, remaining time and capture length/truncation. morse stats Show retained receiver sample counts, noise/min/max/last power, timing unit, largest sample gap and read failures. morse read Read the first retained capture page. morse read 128 Read beginning at character offset 128; use the offset returned by your previous page. morse clear Clear the retained capture when no Morse session is active.

Listening is bounded to 1\u2013300 seconds for every role. Keep the first 0.5 seconds quiet while the receiver learns its noise floor; start the sending radio at least one second after listening begins. Start with matching WPM on the sender and receiver. Noise, fading, interference and timing mismatch can produce errors; decoded text has no sender identity or integrity check.

Automatic detection retains sustained noise peaks learned during the quiet interval, after initial receiver settling. It keeps that quiet envelope across marks and gaps rather than dropping the threshold back toward the average noise level. There is no fixed per-board RSSI cutoff. A stable quiet floor retains the six-dB acquisition margin; a noisier environment requires a signal above its learned excursions. The envelope relaxes gradually when conditions become quieter. Stop and start listening to run the initial learning interval again in a new environment.

The receiver uses a continuous native FSK receive filter and live received signal strength (RSSI) to follow the carrier's on/off envelope. It does not transmit or require a packet preamble, sync word, CRC or direct-data pin:

Radio family Current Morse RX filter SX1276-family 2.6 kHz SX1262/SX1268/STM32WLx, LLCC68 4.8 kHz LR1110/LR1121, LR2021 4.8 kHz

Small-flash STM32 production builds disable Morse reception by default (MESH_ENABLE_MORSE_RX=0) because the receiver does not fit alongside their existing features. Morse TX, frequency/WPM settings and stopping remain available. Their listen/read/stats/clear commands return an unsupported-RX error. The STM32 filter above describes an explicitly enabled custom build, not RX support in those default production images.

get morse reports the actual configured bandwidth; this is not a measured selectivity or decoder-speed guarantee. Unsupported hardware rejects listening without changing the radio. A board also needs a proven complete LoRa-modem restoration path before native FSK RX is enabled. Narrow filters require sufficiently aligned oscillator frequencies. Passive listening may use either packet profile's frequency because it does not transmit; TX overlap protections still apply. FEM and saved Morse settings cannot be changed while a session owns the radio.

Morse sessions defer periodic sensor/GPS processing, screen redraws and background storage work so they do not interrupt envelope timing. Pending work and its deadlines are retained and resume when the session ends. Buttons, command connections, watchdogs and battery shutdown checks remain active. morse stats reports gap in milliseconds: the largest observed gap between receiver samples, including unavoidable safety work. Large gaps can limit reliable speed even when the RF signal is strong.

Repeater, Room, Sensor and Companion roles retain a bounded 4096-character RAM capture after their normal packet receiver resumes. A successful new listening session replaces it; a failed start does not. morse clear or a reboot removes it. If the capture fills, listening stops early and truncated=1/capture-quota status is shown instead of continuing to discard incoming text. This does not change node keys, ACLs, stored radio settings or other files.

Capture replies fit one CLI response and begin with a page header such as > 0000/0250 next=0125 truncated=0 followed by text. Send morse read 125 to continue at the returned offset; next=END means the last page. Exact page length depends on the requesting transport's reply capacity, so follow next rather than assuming a fixed page size. Captures remain readable until you start another listening session or clear them.

A Full Companion's owned buffered USB ASCII terminal or browser terminal shows decoded progress as [Morse RX] ..., paused while USB input is being edited or an earlier reply is draining. Complete records use that terminal's existing ordered output queue. Raw UART/HWCDC/TCP terminals use get morse/morse read instead of unsolicited progress. Binary Companion USB/BLE frames never receive raw text. Companion apps and other clients can request the same capture through their CLI using morse read; it is not presented as an authenticated chat message. There is no automatic LCD/inbox injection.

One transceiver cannot listen for Morse and MeshCore LoRa simultaneously. During listening, normal packet reception/transmission is paused. An authorized remote morse listen first queues its reply on the existing packet-radio settings and starts only after the exact reply copies finish transmitting, with at least one successful transmission. It does not infer that the remote client received the reply. Failed queue admission/all-copy failure, permission revocation, changed Morse settings or a 30-second handoff deadline cancel the request without starting RX. A briefly busy radio can retry admission for up to 10 seconds after reply transmission, within that same handoff deadline. For remote captures, send morse listen 1, start the Morse sender after the queued reply and initial quiet learning interval, wait beyond the selected minute (plus any admission delay), then request get morse and morse read over LoRa. Use a local console to stop early. Hardware recovery failures hold exclusive ownership until the receiver can be safely restored; get morse reports recovery pending.

Lab images compiled with MESH_MORSE_HIL=1 additionally provide morse trace [offset]. A page begins > offset/length t=total n=next x=wrapped and contains up to eight elapsed_ms,rssi_dbm,flags; records. n=END ends the retained window; x=1 means earlier entries were overwritten. Offsets address the oldest of the latest 512 retained records; retrieve after stopping or check that t stays unchanged between pages. Flags are raw key 1, committed key 2, debounce candidate 4 and noise learning 8. This diagnostic command and its trace storage are absent from ordinary production builds.

"},{"location":"carrier_wave/#continuous-carrier-checks","title":"Continuous carrier checks","text":"

cw and cw2 transmit a continuous, unmodulated carrier at the selected profile's frequency and the configured TX power. They are available in the shared ASCII CLI on SX1276-family, SX1262-family, and LR1110 builds, including repeaters, Companions, room servers, and sensors that use that CLI.

Command Action cw on Use the active primary frequency for 10 seconds. cw on 2.5 Use the active primary frequency for 2.5 seconds. cw2 on 0.75 Use the active second profile for 0.75 seconds. cw off or cw2 off Stop the carrier immediately. Either name stops it. get cw or get cw2 Show which carrier is active and its remaining seconds.

set cw ... and set cw2 ... are also accepted. Durations range from 0.001 to 60 seconds, rounded to milliseconds. The deadline runs in the main loop, so it is not a precision pulse generator or a hardware cutoff.

cw2 requires an active radio2 or tempradio2 profile in rxtx mode. An rx or disabled second profile cannot transmit a carrier. cw uses tempradio when that is the current primary profile. There is one physical transmitter: a carrier uses one profile at a time and ignores the cross-TX setting. Stop it before selecting the other carrier. Repeating on on the same profile restarts the duration.

Packet transmission, reception, profile scanning, CAD, and receive recovery checks pause during the carrier. The firmware keeps the MCU awake to service the deadline, controls the board's external amplifier, and resumes normal reception and profile scanning afterward. It rejects entry during a packet or a busy radio. Profile retuning waits until the carrier stops; replacement or expiry of the active second profile stops its carrier. Nothing about CW is saved across reboot.

The normal set tx <dBm> command also works during CW for meter sweeps. It keeps the board's power limits and PA calibration, resumes the carrier after changing power, and does not extend the CW timeout. TX power retains its usual saved behavior. SX1276 temporarily uses FSK with zero frequency deviation, then rebuilds the LoRa configuration before resuming reception.

Use the local USB or BLE console for immediate replies and manual stopping. An on-air CLI reply must wait until the carrier ends because LoRa packets cannot transmit during the test.

A second radio on the same frequency can detect the carrier by measuring live signal strength (RSSI, short for received signal strength indicator): how much radio energy the receiver sees right now, even when it cannot decode a packet. A less-negative reading in dBm means a stronger signal. Compare it before, during, and after transmission. The normal stats-radio command shows the last received packet's RSSI, not this live reading, so it cannot by itself confirm a continuous carrier. CAD and packet counters are not carrier detectors either. A rising live reading confirms RF activity, not calibrated output power, frequency accuracy, antenna match, or SWR. Afterward, exchange ordinary LoRa packets to verify both RX and TX recovery.

The reproducible bench receiver that provides this live reading is tools/hil/cw_observer.cpp, built with tools/hil/cw_observer.ini on a separate RAK4631. It never transmits on its own. See the hardware validation for measured results, reproduction steps, and the tested preamble settings.

"},{"location":"carrier_wave_validation/","title":"Carrier wave validation","text":"

View this page on MeshCore Docs.

"},{"location":"carrier_wave_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"charge_voltage/","title":"Charge voltage","text":"

View this page on MeshCore Docs.

"},{"location":"charge_voltage/#battery-charge-voltage","title":"Battery charge voltage","text":"

The charge.voltage commands change the battery charger's constant-voltage target on supported boards. They are available through the shared text CLI in the companion, repeater, room-server, sensor, and secure-chat roles. They do not change the battery voltage display, low-voltage shutdown, battery chemistry, or charge current.

get charge.voltage\nget charge.voltage.options\nset charge.voltage 4.1\n

Values are in volts. Query the options first: supported targets depend on the PMU fitted to the board. The firmware rejects values that the detected PMU cannot represent; it does not round them to a supported target.

On T-Beam boards, a successful change is saved in NVS and reapplied after a restart. The firmware reads the PMU setting back before reporting success. Boards without a saved setting retain their existing startup behavior: the full PMU driver sets 4.2 V, while the compact classic T-Beam observer driver leaves the charger setting untouched. A failed hardware write, readback, or preference save is reported as an error.

If the saved target cannot be read, is invalid, or cannot be restored, startup does not overwrite it with a default target. get charge.voltage reports the live charger voltage with (boot restore failed). A successful saved change clears that warning. This cannot guarantee restoration while the PMU or storage is failing, but it avoids silently resetting a saved lower target to 4.2 V.

"},{"location":"charge_voltage/#supported-boards","title":"Supported boards","text":"Board family Detected PMU Charge targets (V) LilyGo T-Beam SX1262 or SX1276 AXP192 4.10, 4.15, 4.20, 4.36 LilyGo T-Beam SX1262 or SX1276 AXP2101 4.00, 4.10, 4.20, 4.35, 4.40 LilyGo T-Beam S3 Supreme SX1262 AXP2101 4.00, 4.10, 4.20, 4.35, 4.40

Targets above 4.2 V require batteries rated for the selected charge voltage.

The classic T-Beam driver detects the fitted AXP192 or AXP2101. Radio type does not determine which PMU is present. T-Beam 1W uses a different power design and does not support this setting.

Neither AXP192 nor AXP2101 supports a 3.65 V target. The lowest supported target is 4.10 V for AXP192 and 4.00 V for AXP2101. These PMUs cannot be configured as LiFePO4 chargers by changing this setting.

"},{"location":"charge_voltage/#heltec-mesh-solar","title":"Heltec Mesh Solar","text":"

Mesh Solar reports charge.voltage as unsupported because its CN3795 charger's constant-voltage target is set by hardware. Heltec supplies separate Li-ion (I) and LiFePO4 (F) hardware versions, with 4.2 V/cell and 3.6 V/cell charging respectively. Cell-count soldering and switches must also match the battery configuration. A software battery-type setting does not convert one hardware version into the other. See Heltec's usage guide and MeshSolar datasheet, sections 4.1 and 4.4.

The BQ4050 on this board is a battery gauge and protection controller. Its ChargingVoltage() is a requested voltage for a compatible smart charger. The pinned Mesh Solar library calls that parameter eoc, but writing it does not change the CN3795's voltage-feedback circuit. Valid charge termination also requires the charging current to taper, so lowering eoc alone does not create a reliable lower-voltage charge cutoff. Charge-overvoltage protection is a separate safeguard, not the charger's normal constant-voltage target. See TI's BQ4050 technical reference manual, sections 4.5, 4.6, and 4.11.

These commands do not write Mesh Solar's BQ4050 data flash or change its battery type, protection limits, cell count, or charge-FET configuration.

"},{"location":"charge_voltage/#battery-profiles","title":"Battery profiles","text":"

The nRF52 battery.profile, battery.full, and battery.empty settings control voltage-based reporting and protection. They do not change any charger target. See Battery profile commands.

"},{"location":"cli_build_matrix/","title":"Cli build matrix","text":"

View this page on MeshCore Docs.

"},{"location":"cli_build_matrix/#cli-availability-by-firmware-build","title":"CLI Availability by Firmware Build","text":"

MeshCore command availability is determined in three layers:

  1. Role - repeater, room server, sensor, companion, bridge, or KISS modem.
  2. Build profile - standard, logging, OTA, or FULL.
  3. Compiled hardware features - WiFi, MQTT, GPS, external sensors, PSRAM, Ethernet, and similar optional support.

The complete command descriptions are in CLI Commands. This page describes the commands intentionally omitted or limited by build profile. The CLI Command Availability Matrix expands this summary into separate nRF52 and ESP32 command tables.

"},{"location":"cli_build_matrix/#selecting-a-build-profile","title":"Selecting a build profile","text":"

The runtime settings profile and the feature profile are separate switches:

bash build.sh build-firmware RAK_3401_repeater \\\n  --profile cascade \\\n  --build-profile auto\n

--profile default|cascade selects saved-setting defaults. It does not select which code is linked. --build-profile auto|standard|full controls features and partition policy:

Selection Behavior auto nRF52 repeater, room-server, and sensor builds publish the required pair: Full supported sensors + LoRa OTA and Reduced sensors + LoRa OTA. Both must pass flash, RAM, packaged-application OTA, and layout checks; a reduced image cannot substitute for a failing full image. The same policy applies to canonical bulk/release builds and external-QSPI/SD recipes. ESP32 boards with a qualified expanded profile use it; other standalone ESP32 repeaters and room servers attempt the complete recipe in their current application region, with a measured flash/partition-overflow fallback to their reduced LoRa OTA recipe. Other roles keep every capability declared by their resolved PlatformIO recipe and fail instead of silently removing one that does not fit. Compiler errors and missing-capability checks never trigger or conceal a reduced build. standard Preserves the deployed/portable partition contract and documented reductions. nRF52 infrastructure still publishes both OTA sensor profiles in that same exact layout; this is not an override to remove OTA or skip the full profile. full Requires a qualified ESP32 expanded-partition target (or an explicitly named Full Companion). Install a matching merged image when this changes the partition table.

Successful builds also emit <firmware>.capabilities.json. The sidecar records the effective profile, logical OTA target, actual PlatformIO base, artifact name target, promised capabilities, and every reduction selected by the script. The build fails if a promised linked marker is absent. Current invariants include retry.preset for repeater/room-server roles, WebConfig for the Indicator and ESP32 Full Companion, TempRadio/OTA controls for install-capable Companions, host-mOTA controls for Full Companion, and the advanced flood-rule engine for Full room servers. Resume mode will not accept an old or failed artifact without a verified sidecar.

For a single ESP32 repeater or room-server target, auto selects its matching combined Full MQTT/USB/WiFi image when that image covers the plain Full recipe. The direct observer and Full-matrix paths use the same full-usb-wifi-ota artifact and runtime output controls. The ordinary bulk release publishes one Full OTA identity per exact ESP32 board and role. Audited plain targets keep their identity while compiling the combined observer/ESP-NOW sources; G2 keeps its deployed observer identity. T-Beam SX1262/SX1276 choose the observer in the ordinary release. TLora V2 retains two Full images because UART, MQTT and ESP-NOW together exceed its internal RAM budget: the normal repeater provides UART plus ESP-NOW, and the observer provides MQTT plus ESP-NOW. T-Beam plain Full recipes remain available as explicit alternatives with different routing capacities. Build one with bash build.sh build-firmware <target> --full-exact, then deliberately install it over LoRa with ota pull <MID8> flash and ota install on compatible hardware and partition layouts.

The ordinary ESP32 release publishes Full images. Standard and reduced OTA recipes, including those that fit a 1.25 MiB portable application slot, remain available through explicit builds for recovery and migration. Select --build-profile standard to retain an exact target's original partition contract. They are omitted from the ordinary release and interactive board menu. That menu also omits transport/default aliases covered by Full and development utilities; it retains distinct hardware, roles, storage, and measured capacity exceptions. Exact legacy names remain accepted by build-firmware and build-matching-firmwares.

For the normal-role partition migrations, the release publishes a canonical Full image under the normal target identity. Use its qualified wireless expansion package when one exists, or install its matching merged image over USB. mOTA rejects an incompatible partition signature until that migration is complete, then later Full updates use the normal target ID. Moving from a plain target to a differently named sibling can also change the LoRa OTA identity; partition expansion does not by itself authorize that transition.

Legacy supported ESP32 Wi-Fi repeaters can use the Wi-Fi partition migration bridge to preserve their identity and install an expanded Full image without a USB flash. The default cascade settings profile enables device and LoRa RX power saving; saved preferences take precedence, and active USB/network services can prevent device sleep. --profile default instead uses the upstream power-saving defaults.

"},{"location":"cli_build_matrix/#role-comes-first","title":"Role comes first","text":"Role Text administration CLI Repeater Full repeater administration surface, subject to the profile differences below Room server Room-server administration surface, subject to the profile differences below Sensor Sensor command surface; it does not acquire the repeater administration tree Full, serial, Ethernet, USB, BLE, or WiFi companion Uses the companion protocol; Full combines every qualified transport for that exact board KISS modem Uses the KISS/TNC frame interface, not the repeater text CLI Bridge Uses its base role plus commands for the bridge transport compiled into that target

A command belonging to a different role is not considered a profile cut. For example, adding FULL features to a sensor does not turn it into a repeater administrator.

Repeater profiles use up to 254 neighbor entries. Measured internal-DRAM limits reduce selected Generic E22, Heltec V2, Meshadventurer, T-Beam OTA/MQTT, and TLora MQTT profiles to 50. Check the artifact capability manifest and ESP32 memory budget for the exact target.

See local maintenance commands for USB, BLE/binary Companion, TCP/Ethernet, and browser CLI privileges. A direct connection can access local diagnostics and explicit credential getters; relaying a command over LoRa keeps its remote restrictions.

"},{"location":"cli_build_matrix/#profile-matrix","title":"Profile matrix","text":"Build/profile Command availability Standard non-MQTT repeater or room server Keeps the normal role CLI and, where USB is a safe plaintext console, embeds packet logging behind persistent get/set usb.logging and separately saved get/set usb.debug verbosity. USB debug defaults off and requires compiled diagnostic support. The explicitly selected portable policy can omit WebConfig and browser WiFi OTA, so those commands are unavailable and the omission is recorded in the capability manifest. Legacy standard logging No longer emitted separately. Its behavior is compiled into the ordinary artifact. Size-constrained STM32 targets embed packet logging without verbose MESH_DEBUG. LoRa-OTA (-ota-) LoRa OTA adds the ota ... commands; it does not otherwise reduce the role CLI. ESP32 no_external_sensors artifacts retain the compact browser WiFi uploader, the complete CLI, and up to 254 neighbors, subject to recorded internal-DRAM reductions. nRF52 repeater/room-server/sensor release pair full-ota retains the complete supported sensor recipe; reduced-ota omits declared optional sensor drivers. Both include verified LoRa OTA, use the same logical target identity and exact storage/layout contract within the pair, and retain the role's normal commands. Repeaters/room servers also verify retry.preset. RAK3401 and RAK4631 reduced builds retain GPS where compatible and INA219/INA226/INA260/INA3221 I2C voltage/current monitors. External QSPI/SD targets also publish both choices. Companion/Terminal Chat and KISS roles do not enter this policy. ESP32 MQTT observer or ESP-NOW bridge Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. FULL ESP32 USB + WiFi Uses the matching MQTT target with packet logging, USB debug defaulting off, and the complete command surface supported by that role and hardware. Debug verbosity can be enabled only for diagnostics compiled into that image. get/set logging.output off\\|usb\\|wifi\\|both selects and persists the active output paths; get/set usb.debug separately saves USB verbosity. FULL ESP32 logging fallback Uses the matching non-MQTT target when no compatible MQTT sibling exists, preserving its hardware and role CLI. Full LoRa repeaters also include runtime ESP-NOW with set espnow.enabled on\\|off and get espnow.running; newly combined modes default off. Dedicated images remain only for measured capacity exceptions. USB debug defaults off; get/set usb.debug selects verbose output independently of packet capture. nRF52 dual-CDC Full Companion Fresh installs expose only interface 00; it starts as an ASCII terminal and automatically hands a complete < frame to framed Companion. The same interface also carries exclusive serial mOTA traffic. set usb.logging on reboot adds output-only interface 02 for ASCII RAW/RX/TX records and optional debug. Alternatively, set usb.logging stream reboot keeps CLI replies and packet records on interface 00 for unmodified one-port serial bridges; Binary Companion and serial mOTA require logging off on that port. BLE and source-only LoRa OTA remain available. get/set usb.logging persistently controls whether the logging interface is present; get/set usb.debug changes verbosity without a reboot. Send CLI/control queries to interface 00, not the log endpoint. ESP32 single-TTY Full Companion Every ESP32 Full image starts with the ASCII terminal on its one USB TTY and automatically hands a complete < frame to framed Companion. On 1.17.1.5, run set powersaving off first. set usb.logging on switches that TTY to an input-capable plaintext RAW/RX/TX logging terminal and makes framed Companion unavailable on USB; set usb.logging off stops logging but leaves the TTY in normal ASCII mode. get/set usb.debug separately controls saved verbose diagnostics, defaulting off. The terminal stop token or a valid incoming framed probe then performs the ordinary switch to Binary Companion. A saved logging-on setting starts directly in that logging terminal and disables automatic frame detection. BLE, WiFi, and source-only LoRa OTA remain available. ESP32 Full uses Arduino-ESP32 2.x where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but still expose only one TTY. no_external_sensors Trims selected optional environmental/ranging drivers and their settings; it does not remove generic I2C, core repeater discovery, routing, or runtime RS-232 commands. RAK3401 and RAK4631 profiles retain the four common INA I2C voltage/current monitors. GPS-preserving RAK nRF52 OTA profiles retain their GPS commands and provider; RAK4631 defaults the bridge to UART 2 because RAK12501/L76K GPS uses UART 1. Legacy target suffixes remain stable for OTA identity compatibility.

The four retained INA drivers are entries in the optional environmental-sensor table, not the complete set of RAK I2C consumers. Compatible reduced profiles also retain the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 I2C GPS as separate board peripherals. RAK12501/L76K GPS uses UART Serial1. The explicit RAK4631 Serial1 bridge omits the combined GPS provider, including the otherwise non-UART RAK12500 path.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42. To use both devices on one bus, leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and build with -DTELEM_INA3221_ADDRESS=0x43.

logging, OTA, and FULL describe independent build features in historical filenames. Current standard artifacts use no -logging- infix because their USB logging is runtime controlled. USB packet capture and debug verbosity share the canonical artifact, not a new build/profile name. usb.logging remains the master; verbose diagnostics need both that switch and saved usb.debug on. Fresh and legacy preferences default debug to off. Setting debug on cannot restore code compiled out of an image.

"},{"location":"cli_build_matrix/#canonical-bulk-build-policy","title":"Canonical bulk-build policy","text":"

Bulk and release-matrix commands omit legacy names whose behavior is already available from a canonical image:

The old aliases still work with build-firmware and build-matching-firmwares. Dedicated LoRa OTA repeater images are not collapsed outside the MeshTower consolidation above; they retain their exact storage, bootloader, role, and target identity contracts. Companion boards keep transport-specific canonical images only when no exact Full recipe has passed the combined flash/RAM qualification. ESP32 deliberately uses one TTY: Binary Companion and plaintext USB logging are mutually exclusive there. nRF52 retains its optional second CDC port.

The OTAFIX 2.4.11 release is the current bootloader reference, not an all-board qualification claim. Consult its exact-board manifest and unavailable-profile reasons. Internal application storage accepts exact-base in-place deltas that fit the workspace; full application packages need the matching external NOR or microSD profile. The retired internal-only MeshTower V2 bootloader requires one local installation of the combined SD/internal profile. Its changed identity/layout must not be crossed by an ordinary LoRa application or bootloader update.

"},{"location":"cli_build_matrix/#complete-cli-policy","title":"Complete CLI policy","text":"

The compact ESP32 CLI has been removed. retry.preset is a checked invariant for every repeater and room-server artifact, not a FULL-only command. MQTT observers and ESP-NOW bridges are automatically promoted to FULL builds with expanded partitions rather than dropping administration commands. This keeps tempradio, LoRa OTA, power saving, RXPS, logging, statistics, sensor, ACL, routing, and advanced radio commands whenever the role and compiled hardware support them.

Some commands still have hardware or feature limits. MQTT neighbor-table publishing and discover.scopes require the compiled WITH_MQTT_NEIGHBORS feature. PSRAM boards enable it automatically, and selected non-PSRAM variants opt in with MQTT_NEIGHBORS_WITHOUT_PSRAM. discover.neighbors does not require MQTT or PSRAM.

"},{"location":"cli_build_matrix/#discovery-invariant","title":"Discovery invariant","text":"

discover.neighbors sends the zero-hop node-discovery request used to refresh the repeater neighbor table. It is available in every repeater build profile, including standard, logging, OTA, unified FULL, and FULL logging-fallback builds.

The exact command is:

discover.neighbors\n

It accepts no options and returns:

OK - Discover sent\n

Some MQTT room-server targets also expose the command as part of their compiled neighbor-table feature. It is not a cross-role guarantee: companion, KISS, sensor, and ordinary room-server firmware use different interfaces or do not maintain the repeater administrator neighbor table.

"},{"location":"cli_build_matrix/#feature-dependent-commands","title":"Feature-dependent commands","text":"

Even in a FULL build, a command can be unavailable when its underlying feature does not exist on that target:

When diagnosing an unavailable command, check the role first, then the filename profile, then the target's compiled hardware features and its adjacent .capabilities.json file. A current artifact that promises the command but does not contain its linked marker is rejected during the build.

"},{"location":"cli_command_availability/","title":"Cli command availability","text":"

View this page on MeshCore Docs.

"},{"location":"cli_command_availability/#cli-command-availability-matrix","title":"CLI Command Availability Matrix","text":"

This page is the command-by-command companion to CLI Availability by Firmware Build. Each command name links to its detailed documentation.

The tables cover the text administration CLI used by repeater, room-server, sensor, and bridge firmware. Full Companion also has its own ASCII USB/TCP terminal; see feature switches by role. Companion apps use the companion protocol, KISS firmware uses framed KISS/TNC commands, and terminal-chat firmware has its own interface, so those build roles are not represented in the main matrices. The Companion section below lists its framed maintenance commands. Infrastructure get password is available locally in every profile; LoRa requests cannot read it. get acl and stored packet log are infrastructure features, not Companion contact or live USB-logging commands.

Room users also have an owner-only command set described in Personal mailboxes: mail settings, mail mode, mail allow, mail deny, mail delivery, mail check, mail inbox/mail list, mail read, mail ack, and mail delete. Authenticated room writers can use mail send; readers cannot send. These work as encrypted CLI requests or ordinary direct !mail ... messages to the room. Regular users can also read get room.board and get room.board.read; board publishing and deletion remain administrative.

Build columns mean:

USB packet output and verbose diagnostics are separate saved controls in current source. Where usb.logging is available, get/set usb.debug on|off selects debug intent without changing packet capture or adding a build variant. Debug defaults off for fresh and older preference images; effective output requires usb.logging and usb.debug on plus compiled diagnostic support. See live USB logging for transport restrictions and Full Companion RAW output.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42; they cannot coexist on one bus at those addresses. Leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

Cell values mean:

Runtime state can still make an included command fail. Examples include no GPS fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without .mota apply support.

"},{"location":"cli_command_availability/#companion-framed-cli","title":"Companion framed CLI","text":"

GPS-equipped Sensor builds also provide the local-only dog tracker settings: get tracker, get/set tracker.owner, get/set tracker.mode, get/set tracker.interval, get/set tracker.lost.interval, get/set tracker.path, and tracker check. They are excluded from compact STM32 defaults. LoRa CLI cannot change them.

Companion builds are not represented by the role/profile matrices below. Their local terminal and command 0x42 (CMD_RUN_CLI_COMMAND) expose these settings over the normal binary USB, BLE, or TCP connection:

Command Availability stats-core, stats-radio, stats-radio-diag, stats-packets Local terminal and binary command 0x42 erase Local terminal and binary command 0x42; reboot after erasing stored settings set freq <MHz> Local terminal, binary command 0x42, and authorized LoRa CLI; reboot to apply get public.key Companion text terminal and binary command 0x42; read-only, no private-key export feature required get prv.key Local terminal and binary command 0x42; requires private key export enabled get password ESP32 WiFi Companion reports management-password status without revealing it; other Companions report no admin password; infrastructure returns its password locally password <new-password> ESP32 WiFi Companion and infrastructure: change the saved admin password; ESP32 setup WiFi, WebConfig and browser OTA reuse it get/set lost.reply Local Companion CLI and framed command 0x42 when MESH_ENABLE_LOST_REPLY is enabled; saved off, no or yes private-message auto response get wifi.pwd, get mqttN.password, get mqttN.token Local terminal and binary command 0x42; corresponding WiFi/MQTT feature required board Every Companion text terminal and command 0x42 version Every Companion text terminal and protocol-v14 command 0x42; returns the untruncated build identity get storage.layout Every Companion text terminal and command 0x42; reports the running platform's storage layout memory ESP32 Full Companion text terminal and command 0x42 get/set radio.rxgain Companion with a supported radio get/set radio.fem.rxgain Companion on a board with controllable LoRa FEM LNA get/set wifi.powersave ESP32 WiFi Companion; active transport constraints still apply get/set bluetooth.name Companion firmware set pin <0-999999> Companion text terminal, command 0x42, and authorized LoRa CLI; saved, reboot to apply; affects Bluetooth-capable builds get/set bluetooth.mac Every Companion build with Bluetooth get/set display.touch Touchscreen Companions using the shared UI; text terminal and framed CLI; off after reboot get/set display.inbox Shared Companion UI; history (default), pending, or unread; saved across reboot uf2reset Every nRF52 Companion text terminal and local command 0x42; local only

Deprecated binary aliases remain receive-only for older clients; new clients should use command 0x42. See Companion radio binary protocol.

"},{"location":"cli_command_availability/#nrf52","title":"nRF52","text":"Area Command Scope Standard Logging LoRa OTA Operational reboot All text CLI roles Yes Yes Yes Operational poweroff; shutdown Board power-off support Yes Yes Yes Operational uf2reset Local connection; supported UF2 boards Local Local Local Operational clkreboot All text CLI roles Yes Yes Yes Operational clock sync All text CLI roles Yes Yes Yes Operational clock All text CLI roles Yes Yes Yes Operational time <epoch_seconds> Clock only moves forward Yes Yes Yes Operational advert Advert-capable role Yes Yes Yes Operational advert.zerohop Advert-capable role Yes Yes Yes Operational erase Local connection Local Local Local Information ver All text CLI roles Yes Yes Yes Information board All text CLI roles Yes Yes Yes Diagnostics get storage.layout Internal layout and configured QSPI or SD storage Yes Yes Yes Diagnostics memory ESP32 only No No No Diagnostics sensor Hardware wiring summary Yes Yes Yes Diagnostics powerlog Reset, shutdown, and boot-voltage summary Yes Yes Yes Neighbors neighbors Role with a neighbor table Yes Yes Yes Neighbors neighbor.remove [pubkey_prefix] Role with a mutable neighbor table; omit prefix to clear all Yes Yes Yes Neighbors discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Neighbors discover.scopes MQTT observer with compiled neighbor support No No No Statistics clear stats All full-parser text CLI roles Yes Yes Yes Statistics stats-core Local connection Local Local Local Statistics stats-radio Local connection Local Local Local Statistics stats-radio-diag Local connection Local Local Local Statistics stats-packets Local connection Local Local Local Statistics get telemetry.temp/volt/volt.i2c; optional GPS history Non-STM32 repeater or room server; I2C command requires a detected voltage monitor; GPS requires a provider; remote access requires administrator Yes Yes Yes Statistics set telemetry.gps; get/set/send telemetry.tx Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator Yes Yes Yes Logging log start; log stop; log erase Storage-backed roles retain data; other roles can return empty data Yes Yes Yes Logging log Local connection Local Local Local Logging get/set usb.logging; unified FULL get/set logging.output Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; nRF52 Full Companion can add a reboot-controlled second CDC Yes Yes No Logging get/set usb.debug Same USB-control availability; saved intent defaults off; actual diagnostics need compiled support and USB master on Yes Yes No Logging get/set usb.watchdog off\\|on\\|auto Current source; recovery requires native USB, live logging, and durable storage; Auto qualifies from USB stats polling Yes Yes Feature Logging get usb.watchdog.last Current source; latest USB watchdog event with advisory timestamp, uptime, action, reasons, and durability; native USB required Yes Yes Feature Radio get radio; set radio ... All text CLI roles Yes Yes Yes Radio get tx; set tx <dbm> Board TX-power limits apply Yes Yes Yes Radio tempradio ...; normalradio Full parser Yes Yes Yes Radio get/set/del radioat; get/set/del tempradioat Full parser Yes Yes Yes Radio get freq; set freq <mhz> Local or authorized LoRa CLI Yes Yes Yes Radio get/set radio.rxgain Supported radio Feature Feature Feature Radio get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Radio get/set radio.fem.txgain Controllable LoRa FEM Feature Feature Feature Radio get/set radio.rxps; get rxps.wd RX power-saving support Feature Feature Feature System get/set name All text CLI roles Yes Yes Yes System get/set lat All text CLI roles Yes Yes Yes System get/set lon All text CLI roles Yes Yes Yes System get/set prv.key get requires a local connection Yes Yes Yes System get password; password <new_password> get requires a local connection; setter requires administrator Yes Yes Yes System get/set guest.password Role with guest administration Yes Yes Yes System get/set owner.info All text CLI roles Yes Yes Yes System get/set adc.multiplier Board ADC override support Feature Feature Feature System send text.flood <message> Repeater Yes Yes Yes System host <text>; get host Repeater with host bridge; host requires a remote administrator and a USB/MQTT service Yes Yes Yes System get/set battery.alert; get battery.alert.region Repeater Yes Yes Yes System get/set battery.alert.low; get/set battery.alert.critical Repeater Yes Yes Yes System get/set rx.watchdog Repeater Yes Yes Yes System get/set system.watchdog nRF52 Yes Yes Yes System get public.key All text CLI roles Yes Yes Yes System get role All text CLI roles Yes Yes Yes System get powersaving; set powersaving on/off Supported repeater board Feature Feature Feature System get/set reboot.interval Full parser Yes Yes Yes Clock sync get/set clock.sync.*; clock.sync.mesh now Repeater, sensor, and room server; clock.sync.internet needs MQTT repeater Yes Yes Yes Routing get/set repeat Forwarding-capable role Yes Yes Yes Routing get/set repeat.trace Repeater only; saved repeat-off trace exception, default off Yes Yes Yes Routing get/set path.hash.mode Role that supports path-hash selection Yes Yes Yes Routing get/set loop.detect Repeater Yes Yes Yes Routing get/set txdelay All text CLI roles, including bridges Yes Yes Yes Routing get/set direct.txdelay Full parser Yes Yes Yes Routing get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Routing get/set dutycycle Full parser Yes Yes Yes Routing get/set af Full parser Yes Yes Yes Routing get/set int.thresh All text CLI roles Yes Yes Yes Routing get/set cad Radio CAD support Feature Feature Feature Routing get/set agc.reset.interval All text CLI roles Yes Yes Yes Routing get/set radio.watchdog MQTT observer No No No Routing get/set multi.acks Full parser Yes Yes Yes Routing get/set flood.advert.interval Repeater Yes Yes Yes Routing get/set advert.interval Advert-capable role Yes Yes Yes Routing get/set flood.max Repeater Yes Yes Yes Routing get/set flood.max.unscoped Repeater Yes Yes Yes Routing get/set flood.max.advert Repeater Yes Yes Yes Routing get/set flood.channel.data; get/set flood.channel.data.hops Repeater Yes Yes Yes Routing get/set/del flood.channel.scope* Repeater Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) Yes Yes Yes Routing get/set/del flood.moderation* Repeater Yes Yes Yes Routing get/set outpath Repeater or room user, own remote-client context Yes Yes Yes Routing get/set altpath Repeater or room user, own remote-client context Yes Yes Yes ACL setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes ACL get acl [page] Repeater, room server, or sensor; admin over LoRa Yes Yes Yes ACL get/set allow.read.only Room server Yes Yes No Room get/set topic Room server Yes Yes No Regions region load; region save Role with region storage Yes Yes Yes Regions region allowf; region denyf Role with region storage Yes Yes Yes Regions region get; region list; region Role with region storage Yes Yes Yes Regions region home; region default Role with region storage Yes Yes Yes Regions region put; region def; region remove Role with region storage Yes Yes Yes Retry get/set direct.retry; get/set direct.retry.heard Role with basic retry support Yes Yes Yes Retry get/set retry.preset Role with retry support Yes Yes Yes Retry get/set flood.retry.count; get/set flood.retry.path; get/set flood.retry.group.path Repeater Yes Yes Yes Retry get/set flood.retry.advert; get/set flood.retry.prefixes; get/set flood.retry.ignore Repeater Yes Yes Yes Retry get/set flood.retry.bridge; get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Retry get/set direct.retry.count; get/set direct.retry.base; get/set direct.retry.step Role with retry support Yes Yes Yes Retry get/set direct.retry.margin; get/set direct.retry.cr Role with retry support Yes Yes Yes Retry get/set/clear recent.repeater; get recent.repeaters Repeater Yes Yes Yes GPS get gps; set gps on/off Compiled GPS Feature Feature Feature GPS gps sync Compiled GPS Feature Feature Feature GPS get/set gps.sync.interval Compiled GPS and provider; 1-336 hours Feature Feature Feature GPS gps setloc Compiled GPS Feature Feature Feature GPS gps advert [none/share/prefs] Compiled GPS Feature Feature Feature Sensors get/set telemetry.access Sensor-capable role Feature Feature Limited Sensors sensor list [start] Compiled sensor manager; reduced OTA trims target-selected optional environmental/ranging drivers Feature Feature Limited Sensors sensor get; sensor set Compiled sensor setting; reduced OTA trims target-selected optional environmental/ranging drivers Feature Feature Limited Sensors io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No Bridge get bridge.type Compiled bridge Feature Feature Feature Bridge get/set bridge.enabled; get bridge.running Compiled bridge Feature Feature Feature Bridge get/set bridge.delay Compiled bridge Feature Feature Feature Bridge get/set bridge.source Compiled bridge Feature Feature Feature Bridge get/set bridge.baud RS-232 bridge Feature Feature Feature Bridge get/set bridge.uart RS-232 bridge Feature Feature Feature Bridge get/set bridge.channel ESP-NOW is ESP32 only No No No Bridge get/set bridge.format ESP-NOW bridge No No No Bridge get/set bridge.secret ESP-NOW is ESP32 only No No No Board get bootloader.ver nRF52 bootloader metadata Yes Yes Yes Board get pwrmgt.support; get pwrmgt.source; get pwrmgt.bootreason; get pwrmgt.bootmv Board power-management implementation Feature Feature Feature Board get/set pwrmgt.bootlock; get/set pwrmgt.cutoff nRF52 power-management boards with a text CLI Feature Feature Feature Board get/set battery.profile; get/set battery.empty; get/set battery.full; get/set adc.multiplier nRF52 power-management boards with a text CLI; relative ADC calibration Feature Feature Feature Ethernet eth.status Ethernet target Feature Feature No Browser OTA start ota [ap]; stop ota ESP32 browser uploader No No No WebConfig start webconfig [ap]; stop webconfig; get/set webui ESP32 WebConfig No No No WiFi get/set wifi.ssid; set wifi.pwd; get wifi.status; get/set wifi.powersave ESP32 WiFi No No No WiFi get/set wifi.cli ESP32 WebConfig No No No LoRa OTA ota help; ota ?; ota h LoRa OTA build No No Yes LoRa OTA ota; ota status; ota st LoRa OTA build No No Yes LoRa OTA ota stats LoRa OTA build No No Yes LoRa OTA ota ls; ota neighbors; ota nbrs; ota updates; ota n LoRa OTA build No No Yes LoRa OTA ota get; ota pull; ota download LoRa OTA build; internal-store nRF52 installs in-place deltas, while matched SD/QSPI nRF52 also accepts full images No No Yes LoRa OTA ota install; ota apply; ota applydelta Compatible bootloader and completed update No No Yes LoRa OTA ota rescue install <base_hash16> Legacy internal-flash nRF52 LoRa OTA build with failed app-side EndF validation; shared bootloader-update builds require valid EndF No No Feature LoRa OTA ota bootloader; ota bootloader install <MID8> <HASH16> Allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2 microSD nRF52 repeater with ABI-3 exact-board OTAFIX No No Feature LoRa OTA ota cancel; ota drop; ota stop LoRa OTA build No No Yes LoRa OTA ota announce; ota adv LoRa OTA build No No Yes LoRa OTA ota self; ota id Firmware with EndF trailer No No Yes LoRa OTA ota qspi; ota storage nRF52 build with raw-QSPI OTA staging Feature Feature Feature LoRa OTA ota folder; ota fold on needs compiled folder transport No No Feature LoRa OTA ota config; ota cfg; ota set LoRa OTA build No No Yes LoRa OTA ota key; ota keys LoRa OTA build No No Yes LoRa OTA ota dev ... Developer diagnostics No No Yes MQTT get/set mqttN.preset MQTT observer No No No MQTT get/set mqttN.server; get/set mqttN.port; get/set mqttN.username; get/set mqttN.password MQTT observer No No No MQTT get/set mqttN.token; get/set mqttN.topic; get/set mqttN.audience MQTT observer No No No MQTT get mqttN.diag MQTT observer No No No MQTT get/set mqtt.origin; get/set mqtt.iata; get mqtt.presets MQTT observer No No No MQTT get mqtt.stats MQTT observer No No No MQTT get/set mqtt.status; get/set mqtt.packets; get/set mqtt.raw; get/set mqtt.interval MQTT observer No No No MQTT get/set mqtt.rx; get/set mqtt.tx MQTT observer No No No MQTT get/set mqtt.neighbors; get/set mqtt.neighbors.interval MQTT observer with compiled neighbor support No No No MQTT get/set mqtt.ntp MQTT observer No No No MQTT get mqtt.ntp.diag Full MQTT observer No No No MQTT get/set timezone; get/set timezone.offset MQTT observer No No No MQTT get/set mqtt.analyzer.us; get/set mqtt.analyzer.eu Legacy MQTT aliases No No No MQTT get/set mqtt.owner; get/set mqtt.email MQTT observer; get requires a local connection No No No MQTT get mqtt.config.valid MQTT observer No No No SNMP get/set snmp; get/set snmp.community MQTT target compiled with SNMP No No No Alerts get/set alert; get/set alert.psk; get/set alert.hashtag; get/set alert.region; get/set alert.wifi; get/set alert.mqtt; get/set alert.interval MQTT observer No No No Alerts alert test [message] MQTT observer with configured alert channel No No No TLS tls.bundletest <host> MQTT target with embedded certificate bundle No No No Manifest OTA ota check MQTT target with OTA_MANIFEST_BASE No No No Manifest OTA ota update MQTT target with OTA_MANIFEST_BASE No No No"},{"location":"cli_command_availability/#esp32","title":"ESP32","text":"Area Command Scope Standard Logging LoRa OTA FULL unified FULL fallback Operational reboot All text CLI roles Yes Yes Yes Yes Yes Operational poweroff; shutdown Board power-off support Yes Yes Yes Yes Yes Operational uf2reset nRF52 only No No No No No Operational clkreboot Full parser Yes Yes Yes Yes Yes Operational clock sync All text CLI roles Yes Yes Yes Yes Yes Operational clock All text CLI roles Yes Yes Yes Yes Yes Operational time <epoch_seconds> Clock only moves forward Yes Yes Yes Yes Yes Operational advert Advert-capable role Yes Yes Yes Yes Yes Operational advert.zerohop Advert-capable role Yes Yes Yes Yes Yes Operational erase Local connection Local Local Local Local Local Display get/set display.mode; display.timeout; display.usb.mode; display.usb.timeout Display-equipped roles; pairing modes require BLE Companion Feature Feature Feature Feature Feature Display get/set display.flip MQTT observer with supported display driver No No No Feature No Information ver All text CLI roles Yes Yes Yes Yes Yes Information board All text CLI roles Yes Yes Yes Yes Yes Diagnostics get storage.layout Detected flash size and live partition table Yes Yes Yes Yes Yes Diagnostics memory ESP32 heap and PSRAM summary Yes Yes Yes Yes Yes Diagnostics sensor Hardware wiring summary Yes Yes Yes Yes Yes Diagnostics powerlog Reset-reason summary Yes Yes Yes Yes Yes Neighbors neighbors Role with a neighbor table Yes Yes Yes Yes Yes Neighbors neighbor.remove [pubkey_prefix] Role with a mutable neighbor table; omit prefix to clear all Yes Yes Yes Yes Yes Neighbors discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Yes Yes Neighbors discover.scopes MQTT observer with compiled neighbor support No No No Feature No Statistics clear stats Full parser Yes Yes Yes Yes Yes Statistics stats-core Local connection Local Local Local Local Local Statistics stats-radio Local connection Local Local Local Local Local Statistics stats-radio-diag Local connection Local Local Local Local Local Statistics stats-packets Local connection Local Local Local Local Local Statistics get telemetry.temp/volt/volt.i2c; optional GPS history Non-STM32 repeater or room server; I2C command requires a detected voltage monitor; GPS requires a provider; remote access requires administrator Yes Yes Yes Yes Yes Statistics set telemetry.gps; get/set/send telemetry.tx Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator Yes Yes Yes Yes Yes Logging log start; log stop; log erase Storage-backed roles retain data Yes Yes Yes Yes Yes Logging log Local connection Local Local Local Local Local Logging get/set usb.logging; unified FULL get/set logging.output Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; every ESP32 Full Companion uses an input-capable single-TTY logging terminal with framed USB Companion disabled while logging Yes Yes No No Yes Logging get/set usb.debug Same USB-control availability; saved intent defaults off; actual diagnostics need compiled support and USB master on Yes Yes No No Yes Logging get/set usb.watchdog off\\|on\\|auto Current source; recovery requires native USB, live logging, and durable storage; Auto qualifies from USB stats polling Yes Yes Feature Yes Yes Logging get usb.watchdog.last Current source; latest USB watchdog event with advisory timestamp, uptime, action, reasons, and durability; native USB required Yes Yes Feature Yes Yes Radio get radio; set radio ... All text CLI roles Yes Yes Yes Yes Yes Radio get tx; set tx <dbm> Board TX-power limits apply Yes Yes Yes Yes Yes Radio tempradio ...; normalradio Full parser Yes Yes Yes Yes Yes Radio get/set/del radioat; get/set/del tempradioat Full parser Yes Yes Yes Yes Yes Radio get freq; set freq <mhz> Local or authorized LoRa CLI Yes Yes Yes Yes Yes Radio get/set radio.rxgain Supported radio Feature Feature Feature Feature Feature Radio get/set espnow.channel Primary ESP-NOW mesh radio Feature Feature Feature Feature Feature Radio get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Feature Feature Radio get/set radio.fem.txgain Controllable LoRa FEM Feature Feature Feature Feature Feature Radio get/set radio.rxps; get rxps.wd Full parser and RX power-saving support Feature Feature Feature Feature Feature System get/set name All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set lat All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set lon All full-parser text CLI roles Yes Yes Yes Yes Yes System get/set prv.key get requires a local connection Yes Yes Yes Yes Yes System get password; password <new_password> get requires a local connection; setter requires administrator Yes Yes Yes Yes Yes System get/set guest.password Role with guest administration Yes Yes Yes Yes Yes System get/set owner.info Full parser Yes Yes Yes Yes Yes System get/set adc.multiplier Board ADC override support Feature Feature Feature Feature Feature System send text.flood <message> Repeater Yes Yes Yes Yes Yes System host <text>; get host Repeater with host bridge; host requires a remote administrator and a USB/MQTT service Yes Yes Yes Yes Yes System get/set battery.alert; get battery.alert.region Repeater Yes Yes Yes Yes Yes System get/set battery.alert.low; get/set battery.alert.critical Repeater Yes Yes Yes Yes Yes System get/set rx.watchdog Repeater Yes Yes Yes Yes Yes System get/set system.watchdog nRF52 only No No No No No System get public.key Full parser Yes Yes Yes Yes Yes System get role All text CLI roles Yes Yes Yes Yes Yes System get powersaving; set powersaving on/off Supported repeater board Feature Feature Feature Feature Feature System get/set reboot.interval Full parser Yes Yes Yes Yes Yes Clock sync get/set clock.sync.*; clock.sync.mesh now Repeater, sensor, and room server; clock.sync.internet needs MQTT repeater Yes Yes Yes Yes Yes Routing get/set repeat Forwarding-capable role Yes Yes Yes Yes Yes Routing get/set repeat.trace Repeater only; saved repeat-off trace exception, default off Yes Yes Yes Yes Yes Routing get/set path.hash.mode Full parser Yes Yes Yes Yes Yes Routing get/set loop.detect Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set txdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Routing get/set direct.txdelay Full parser Yes Yes Yes Yes Yes Routing get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Routing get/set dutycycle Full parser Yes Yes Yes Yes Yes Routing get/set af Full parser Yes Yes Yes Yes Yes Routing get/set int.thresh All text CLI roles Yes Yes Yes Yes Yes Routing get/set cad Radio CAD support Feature Feature Feature Feature Feature Routing get/set agc.reset.interval All text CLI roles Yes Yes Yes Yes Yes Routing get/set radio.watchdog MQTT observer No No No Yes No Routing get/set multi.acks Full parser Yes Yes Yes Yes Yes Routing get/set flood.advert.interval Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set advert.interval Full common parser Yes Yes Yes Yes Yes Routing get/set flood.max Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.max.unscoped Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.max.advert Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set flood.channel.data; get/set flood.channel.data.hops Repeater, full common parser Yes Yes Yes Yes Yes Routing get/set/del flood.channel.scope* Repeater role handler Yes Yes Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater role handler Yes Yes Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater role handler; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) Yes Yes Yes Yes Yes Routing get/set/del flood.moderation* Repeater role handler Yes Yes Yes Yes Yes Routing get/set outpath Repeater or room user, own remote-client context Yes Yes Yes Yes Yes Routing get/set altpath Repeater or room user, own remote-client context Yes Yes Yes Yes Yes ACL setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes Yes Yes ACL get acl [page] Repeater, room server, or sensor; admin over LoRa Yes Yes Yes Yes Yes ACL get/set allow.read.only Room server Yes Yes No Yes Yes Room get/set topic Room server Yes Yes No Yes Yes Regions region load; region save Role with region storage Yes Yes Yes Yes Yes Regions region allowf; region denyf Role with region storage Yes Yes Yes Yes Yes Regions region get; region list; region Role with region storage Yes Yes Yes Yes Yes Regions region home; region default Role with region storage Yes Yes Yes Yes Yes Regions region put; region def; region remove Role with region storage Yes Yes Yes Yes Yes Retry get/set direct.retry; get/set direct.retry.heard Role with basic retry support Yes Yes Yes Yes Yes Retry get/set retry.preset Role with retry support Yes Yes Yes Yes Yes Retry get/set flood.retry.count; get/set flood.retry.path; get/set flood.retry.group.path Repeater Yes Yes Yes Yes Yes Retry get/set flood.retry.advert; get/set flood.retry.prefixes; get/set flood.retry.ignore Repeater Yes Yes Yes Yes Yes Retry get/set flood.retry.bridge; get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Feature Feature Retry get/set direct.retry.count; get/set direct.retry.base; get/set direct.retry.step Role with retry support Yes Yes Yes Yes Yes Retry get/set direct.retry.margin; get/set direct.retry.cr Role with retry support Yes Yes Yes Yes Yes Retry get/set/clear recent.repeater; get recent.repeaters Repeater Yes Yes Yes Yes Yes GPS get gps; set gps on/off Compiled onboard GPS Feature Feature Feature Feature Feature GPS gps sync Compiled onboard GPS Feature Feature Feature Feature Feature GPS get/set gps.sync.interval Compiled GPS and provider; 1-336 hours Feature Feature Feature Feature Feature GPS gps setloc Compiled onboard GPS Feature Feature Feature Feature Feature GPS gps advert [none/share/prefs] Compiled onboard GPS Feature Feature Feature Feature Feature Sensors get/set telemetry.access Sensor-capable full parser Feature Feature Limited Feature Feature Sensors sensor list [start] Compiled sensor manager; no_external_sensors trims target-selected environmental/ranging drivers, not generic I2C Feature Feature Limited Feature Feature Sensors sensor get; sensor set Compiled sensor setting; no_external_sensors trims target-selected environmental/ranging drivers, not generic I2C Feature Feature Limited Feature Feature Sensors io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No No Feature Bridge get bridge.type Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.enabled; get bridge.running Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.delay Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.source Compiled bridge Feature Feature Feature Yes Feature Bridge get/set bridge.baud RS-232 bridge Feature Feature Feature No Feature Bridge get/set bridge.uart RS-232 bridge Feature Feature Feature No Feature Bridge get/set bridge.channel ESP-NOW bridge No No No Feature Feature Bridge get/set bridge.format ESP-NOW bridge No No No Feature Feature Bridge get/set bridge.secret ESP-NOW bridge No No No Feature Feature Board get bootloader.ver nRF52 only No No No No No Board get pwrmgt.support; get pwrmgt.source; get pwrmgt.bootreason; get pwrmgt.bootmv nRF52 only No No No No No Ethernet eth.status Ethernet target Feature Feature No No Feature Browser OTA start ota [ap]; stop ota Compiled browser uploader No No Yes Feature Feature WebConfig start webconfig [ap]; stop webconfig; get/set webui Compiled WebConfig No No No Feature Feature WiFi get/set wifi.ssid; set wifi.pwd; get wifi.status; get/set wifi.powersave MQTT WiFi, or standalone FULL WebConfig; standalone has no get wifi.pwd No No No Yes Feature WiFi get/set wifi.cli Compiled WebConfig No No No Feature Feature LoRa OTA ota help; ota ?; ota h LoRa OTA build No No Yes Yes Yes LoRa OTA ota; ota status; ota st LoRa OTA build No No Yes Yes Yes LoRa OTA ota stats LoRa OTA build No No Yes Yes Yes LoRa OTA ota ls; ota neighbors; ota nbrs; ota updates; ota n LoRa OTA build No No Yes Yes Yes LoRa OTA ota get; ota pull; ota download LoRa OTA build No No Yes Yes Yes LoRa OTA ota install; ota apply; ota applydelta Compatible completed update No No Yes Yes Yes LoRa OTA ota rescue install <base_hash16> Legacy internal-flash nRF52 LoRa OTA build with failed app-side EndF validation; shared bootloader-update builds require valid EndF No No Feature No No LoRa OTA ota bootloader; ota bootloader install <MID8> <HASH16> Allowlisted nRF52 internal/QSPI repeater feature No No No No No LoRa OTA ota cancel; ota drop; ota stop LoRa OTA build No No Yes Yes Yes LoRa OTA ota announce; ota adv LoRa OTA build No No Yes Yes Yes LoRa OTA ota self; ota id Firmware with EndF trailer No No Yes Yes Yes LoRa OTA ota qspi; ota storage nRF52 build with raw-QSPI OTA staging No No No No No LoRa OTA ota folder; ota fold on needs compiled serial or TCP folder transport No No Feature Feature Feature LoRa OTA ota config; ota cfg; ota set LoRa OTA build No No Yes Yes Yes LoRa OTA ota key; ota keys LoRa OTA build No No Yes Yes Yes LoRa OTA ota dev ... Developer diagnostics No No Yes Yes Yes MQTT get/set mqttN.preset MQTT observer No No No Yes No MQTT get/set mqttN.server; get/set mqttN.port; get/set mqttN.username; get/set mqttN.password MQTT observer No No No Yes No MQTT get/set mqttN.token; get/set mqttN.topic; get/set mqttN.audience MQTT observer No No No Yes No MQTT get mqttN.diag MQTT observer No No No Yes No MQTT get/set mqtt.origin; get/set mqtt.iata; get mqtt.presets MQTT observer No No No Yes No MQTT get mqtt.stats MQTT observer No No No Yes No MQTT get/set mqtt.status; get/set mqtt.packets; get/set mqtt.raw; get/set mqtt.interval MQTT observer No No No Yes No MQTT get/set mqtt.rx; get/set mqtt.tx MQTT observer No No No Yes No MQTT get/set mqtt.neighbors; get/set mqtt.neighbors.interval MQTT observer with compiled neighbor support No No No Feature No MQTT get/set mqtt.ntp MQTT observer No No No Yes No MQTT get mqtt.ntp.diag MQTT observer No No No Yes No MQTT get/set timezone; get/set timezone.offset MQTT observer No No No Yes No MQTT get/set mqtt.analyzer.us; get/set mqtt.analyzer.eu Legacy MQTT aliases No No No Yes No MQTT get/set mqtt.owner; get/set mqtt.email MQTT observer; get requires a local connection No No No Yes No MQTT get mqtt.config.valid MQTT observer No No No Yes No SNMP get/set snmp; get/set snmp.community MQTT target compiled with SNMP No No No Feature No Alerts get/set alert; get/set alert.psk; get/set alert.hashtag; get/set alert.region; get/set alert.wifi; get/set alert.mqtt; get/set alert.interval MQTT observer No No No Yes No Alerts alert test [message] MQTT observer with configured alert channel No No No Yes No TLS tls.bundletest <host> MQTT target with embedded certificate bundle No No No Feature No Manifest OTA ota check MQTT target with OTA_MANIFEST_BASE No No No Manifest No Manifest OTA ota update MQTT target with OTA_MANIFEST_BASE No No No Manifest No"},{"location":"cli_command_availability/#supplemental-command-notes","title":"Supplemental command notes","text":"

These short entries cover implemented commands that do not yet have their own section in the primary command reference.

"},{"location":"cli_command_availability/#memory","title":"memory","text":"

ESP32 only. Prints free heap, minimum heap, largest allocation, queue depth, internal heap, and PSRAM totals.

"},{"location":"cli_command_availability/#sensor-hardware-summary","title":"sensor hardware summary","text":"

Prints the configured I2C pins and GPS serial pins. This is a wiring/build diagnostic, distinct from sensor list, sensor get, and sensor set.

"},{"location":"cli_command_availability/#powerlog","title":"powerlog","text":"

Prints the last reset reason. nRF52 also prints the captured shutdown reason and boot voltage.

"},{"location":"cli_command_availability/#stats-radio-diag","title":"stats-radio-diag","text":"

Local-serial diagnostic that prints the extended radio diagnostic counters.

"},{"location":"cli_command_availability/#radiorxps-and-rxpswd","title":"radio.rxps and rxps.wd","text":"

get radio.rxps reports RX power-saving state and timing. set radio.rxps ... changes that configuration. get rxps.wd reports the soft and hard RX power-saving watchdog counters.

"},{"location":"cli_command_availability/#rebootinterval","title":"reboot.interval","text":"

get reboot.interval shows the scheduled reboot interval. Use set reboot.interval <hours> for 1-255 hours, or set reboot.interval 0 to disable it.

"},{"location":"cli_command_availability/#sensor-io","title":"Sensor io","text":"

The simple-sensor role exposes its board GPIO word as hexadecimal. io reads it; io <hex> replaces it; and io r<hex>, io s<hex>, or io t<hex> clear, set, or toggle selected bits.

"},{"location":"cli_command_availability/#get-mqttndiag","title":"get mqttN.diag","text":"

Prints the runtime diagnostic summary for MQTT slot N, including its connection state and failure history.

pub_ack counts actual broker acknowledgments, not merely accepted writes. Recent firmware also shows rec:<count>/<reason> for transport recovery and ack_wait:<seconds>s while an accepted QoS 1 publication awaits acknowledgment. Reasons include error, state, handshake, write, and puback. recover means cleanup is pending, not that the broker is healthy. Optional fields are omitted if they cannot fit completely in the reply. rec counts completed client cleanup; a higher count alone does not prove the subsequent connection or delivery succeeded.

"},{"location":"cli_command_availability/#get-mqttstats","title":"get mqtt.stats","text":"

Prints the MQTT bridge runtime statistics summary.

"},{"location":"cli_command_availability/#get-mqttconfigvalid","title":"get mqtt.config.valid","text":"

Reports whether the saved MQTT configuration has the minimum values needed to run.

"},{"location":"cli_command_availability/#tlsbundletest","title":"tls.bundletest","text":"

tls.bundletest <host[:port]|url> tests the embedded TLS certificate bundle against a remote host without changing the MQTT configuration.

"},{"location":"cli_command_availability/#manifest-ota-check-and-ota-update","title":"Manifest ota check and ota update","text":"

On MQTT targets with OTA_MANIFEST_BASE, ota check fetches and checks the target manifest. ota update downloads and applies the compatible update selected by that manifest.

"},{"location":"cli_commands/","title":"Cli commands","text":"

View this page on MeshCore Docs.

"},{"location":"cli_commands/#cli-commands","title":"CLI Commands","text":"

For SolarXiao Base/Advanced/MPPT automatic detection, manual overrides, live battery/MCU readings and charger controls, see SolarXiao carrier profiles (get solar, get solar readings, set solar.board auto, get mppt, set mppt).

See two LoRa profiles for radio2, tempradio2, scheduling, RX-only mode, cross-transmission policy, and optional radio preambles.

For a saved MediaMesh second profile, use set radio2 mediamesh (911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, automatic preamble), or set radio2 mediamesh-range for 125 kHz. These leave the primary profile and crossing policy unchanged. Both radio2 and radio2.cross default to off. See MediaMesh presets.

For supported Full Companions, see Meshtext channel chat for mt- channels and the explicit send mt.nodeinfo <channel slot> command. Chat remains plain text. A separate GPS-free NodeInfo is sent after the first successful chat transmission per used channel/identity per boot; the explicit command can also request one. Automatic metadata waits for a 250 ms gap after the latest successful MT transmission; explicit requests bypass that gap. MT heard-repeat retries use the Companion's flood.retry.count, with the metadata cap and flood.retry.advert gate for NodeInfo.

See carrier-wave RF checks for cw on [seconds], cw2 on [seconds], and stopping a carrier with off.

For keyed Morse TX, use morse send TEST DE N0CALL and morse stop. Defaults are 902.100 MHz and 25 WPM; set morse.freq <MHz> and set morse.wpm <1\u201360> save separate frequency/speed settings. Small-flash STM32 Companion builds retain Morse TX but do not include Morse RX by default; listen/read/stats/clear return an unsupported-RX error there. The constrained wio-e5-mini_repeater omits Morse TX/RX and CW tools entirely; normal packet TX/RX and radio2 remain available. get morse shows status. See TX-only Morse messages for supported characters, limits, overlap checks and the temporary pause in packet reception. Nothing transmits automatically.

For timed Morse RX, use morse listen <minutes> (fractions allowed, 1 second through 5 minutes). morse stop ends TX or RX. All roles retain the bounded capture in RAM for morse read [offset]; follow each page's next value until next=END. morse clear removes it when idle. A new successful session or reboot replaces/removes the capture. Buffered USB/browser Full Companion ASCII terminals show decoded progress; other terminals and binary clients request it through the CLI, without adding unidentified text to authenticated channel/chat messages. Normal LoRa is paused during listening, so a remote administrator must wait for the selected session to end before reading the capture over LoRa. See timed reception for arming time, real chip bandwidths and truncation behavior.

For copy/paste on/off recipes and the differences from Full Companion, see feature switches by role. The USB web console opens the default ASCII terminal at 115200 baud; it does not require on-device WebConfig or WiFi.

This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.

See CLI Availability by Firmware Build for the role and profile matrix. Commands depend on compiled features; some portable builds omit WebConfig while retaining the complete role CLI and compact WiFi updater.

See CLI Command Availability Matrix for the command-by-command nRF52 and ESP32 build tables.

The first word of a command is case-insensitive, so set, Set, and SET are equivalent, as are get, Get, and the other command verbs. The case of arguments such as node names, passwords, and keys is left unchanged.

Repeater retries carrying the same logical request ID and original command bytes reuse the cached result, even when command parsing trims whitespace or splits arguments in place. Changing the timestamp alone does not execute the operation again. Normal authentication and stale-request checks still apply. Deferred commands and host-service replies remain bound to the full authenticated public key, even if permission changes move ACL entries. Removing or revoking the requester cancels pending execution; a running setperm can still acknowledge its own completed change to the original sender.

Use the site search or your browser's Find command with everyday wording such as tx retries, retry attempts, serial logging, or tx power. Search terms below are alternative wording to help find a command, not additional CLI aliases. Enter the syntax shown under Usage; supported command aliases are listed there explicitly.

"},{"location":"cli_commands/#navigation","title":"Navigation","text":""},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"

Search terms: restart, restart node, reboot device.

Usage: - reboot

Note: No reply is sent.

"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"

Search terms: turn off device, shut down, power off.

Usage: - poweroff, or - shutdown

Note: No reply is sent.

"},{"location":"cli_commands/#enter-the-uf2-bootloader-nrf52-only","title":"Enter the UF2 bootloader (nRF52 only)","text":"

Search terms: bootloader mode, USB firmware update, UF2 mode.

Usage: - uf2reset

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

Note: Reboots directly into the UF2 bootloader on supported nRF52 boards. This includes the Repeater, Room Server, Sensor, Companion, and Terminal Chat local serial command surfaces. It is never accepted as a remote mesh command.

"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"

Usage: - clkreboot

Note: No reply is sent.

"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"

Usage: - clock sync

"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"

Usage: - clock

"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"

Usage: - time <epoch_seconds>

Parameters: - epoch_seconds: Unix epoch time

"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"

Usage: - advert

"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"

Usage: - advert.zerohop

"},{"location":"cli_commands/#ask-a-usb-connected-host-service-over-lora-repeater-only","title":"Ask a USB-connected host service over LoRa (Repeater Only)","text":"

Usage:

host hands one remote LoRa CLI request to a service on the repeater's USB host and returns that service's reply over LoRa. It does not execute the text inside the firmware. The service has 4 seconds to prove the exact request is still pending, then 6 seconds to execute and reply. A second remote command receives busy while either phase is pending.

Request text is limited to 155 UTF-8 bytes, or 152 bytes when a companion uses its legacy three-byte correlation prefix. The complete LoRa reply is limited to 162 bytes. USB records use Base64URL text, a random 64-bit nonce, and the repeater identity's Ed25519 signature. Before executing anything, the service must return a random one-time challenge and receive the repeater's signed live proof. This avoids any dependency on repeater/Pi clock agreement. Every host.reply USB command must carry the matching request ID and nonce; it is not accepted from LoRa or the Ethernet CLI.

The included Raspberry Pi endpoint supports exact commands for help, cpu-temp, hostname, uptime, load, memory, disk-free, and clock status. It also provides strictly validated clock sync and clock set <unix_epoch> recovery actions, opt-in network restart and reboot actions, action status <operation_id>, and run <alias> [arguments] for locally allowlisted executables with typed arguments. Clock changes and host recovery actions are disabled by default, arbitrary executables are never accepted, and no action uses a shell. Clock set accepts only canonical unsigned decimal epochs from 2020 through 2099; a root-owned Unix-socket service revalidates the request and authenticates the local service account with SO_PEERCRED before changing system time. Network restart and reboot use a separate root-owned Unix-socket broker with fixed systemd units; installing clock control does not grant those actions. The endpoint never uses sudo for privileged host actions. See LoRa CLI host service for the complete meshcoretomqtt setup and security model.

"},{"location":"cli_commands/#lora-ota-speed","title":"LoRa OTA speed","text":"
get ota.speed\nset ota.speed <0.05..3>\n

1 preserves the current OTA timing. Values below 1 slow OTA traffic; values above 1 shorten adjustable delays. For example, 0.5 doubles those delays and 3 divides them by three. Decimal values such as .05 work. ota config speed <factor> is an equivalent setter. get ota.speed and ota config show both the saved value and the effective packet pacing factor. Automatic loss response may make packet spacing slower without changing the saved setting. The setting can be changed during a transfer.

Apply it on the source, destination, and relays. It controls all LoRa OTA packet types on both profiles, relay timing, discovery, and OTA adverts. Slower settings extend recovery waits and space queued OTA packets while allowing ordinary traffic through. Faster settings retain the physical retry allowance and the configured airtime budget, so throughput is not guaranteed to rise in direct proportion. Proof turnaround keeps its physical allowance. Ordinary messages, node adverts, and local staged-image verification keep their timing. Available in OTA-enabled builds for all roles, including Companions. See the OTA guide.

"},{"location":"cli_commands/#start-or-stop-an-over-the-air-ota-firmware-update","title":"Start or stop an Over-The-Air (OTA) firmware update","text":"

Search terms: WiFi OTA, wireless firmware update, OTA uploader, update firmware.

Usage: - start ota - start ota ap - stop ota

On nRF52, start ota invokes Bluetooth DFU with the matching bootloader and application DFU ZIP. The WiFi/AP instructions below apply to ESP32.

On ESP32, start ota enables WiFi as needed and serves the web upload page on the station IP when connected to WiFi; otherwise it raises the MeshCore-OTA access point. The reply gives the URL to open and tells you which network to use. start ota ap always selects the access point, including when OTA is already running on a LAN address. This is useful with client-isolated networks.

The runtime browser uploader uses HTTP Basic authentication: username admin, password the node's saved admin password. The OTA hotspot reuses that password for WPA2 when it is 8\u201315 printable ASCII characters; otherwise it is open and the HTTP login still applies. Temporary partition migration and recovery apps retain their documented access policy.

Starting OTA automatically stops WebConfig and reports WebConfig stopped in the reply. The existing LAN connection stays up; a setup hotspot is replaced by MeshCore-OTA. WebConfig's delayed HTTP cleanup cannot stop OTA's WiFi. Use the USB/Bluetooth or LoRa command line to start the uploader; WebConfig's own terminal cannot deliver the handoff reply after shutting itself down. Stop OTA before starting WebConfig again.

FULL ESP32 builds also expose the .mota folder seeder on TCP port 5001 whenever WiFi is usable. This listener is independent of port 80, so a host can run motatool serve --dir ./motas --tcp <node-ip>:5001 while WebConfig is active. The TCP connection auto-attaches and detaches the folder; do not run ota folder on at the same time because that command selects the USB-serial folder transport.

"},{"location":"cli_commands/#browser-configuration-portal-esp32-repeater-and-room-server","title":"Browser configuration portal (ESP32 repeater and room server)","text":"

Search terms: web UI, web interface, WiFi settings page, configuration website, WebConfig.

Usage:

set webui on is the persistent master switch. It starts the portal now and again after future reboots; set webui off closes it and disables that boot start. get webui reports the saved on/off state plus whether the portal is inactive, joining WiFi, serving a setup AP, or serving a LAN URL. The default is off on repeater and room-server builds. On display-equipped repeaters, an otherwise-unused triple click toggles the same saved setting. Consequently, > off, http://192.168.1.130/ means the saved boot setting is off while a temporary WebConfig session is currently active at that URL.

start webconfig is a temporary start that does not change the saved switch. It serves the shared WiFi, radio, flood, loop, and status page and reports its URL. MQTT builds also show the MQTT tab and wizard step; non-MQTT builds remove them entirely. Sign in with the node's admin password. If the node has no saved WiFi SSID, the command starts the MeshCore-Setup-XXXX captive AP at http://192.168.4.1/ instead.

If a saved WiFi network is unavailable and the node falls back to its setup AP, sign in with the existing admin password before changing settings. Joining that AP does not grant permission to replace the password. The hotspot uses the same admin password for WPA2 when it is 8\u201315 printable ASCII characters. Other valid admin passwords leave the hotspot open and require the HTTP admin login. Protected first-time setup without a saved SSID keeps its provisioning flow. ESP32 WiFi Companions also have a saved admin password, initially password. Deliberately passwordless images retain their local no-password policy; use them only on a trusted network.

start webconfig ap forces captive-AP mode. It will not interrupt an active MQTT bridge, so run set mqtt.enabled off first. Use stop webconfig to close either mode for the current boot. (stop webconfig does not change a saved webui on.) LAN mode otherwise remains active until reboot. On expanded FULL builds, an unconfigured automatic setup AP receives one absolute 30-minute window per boot, then powers WiFi off even if a client remains attached; rebooting starts a new automatic window. An administrator can explicitly run start webconfig again without rebooting. Once an SSID is saved, the cutoff no longer applies and the selected WiFi/MQTT mode keeps reconnecting. Other setup sessions retain their profile's idle timeout.

Full Repeater and Room Server suspend unconfigured network bridges during automatic first-boot setup. Automatic ESP-NOW retries remain suspended after the AP closes, so they cannot keep an unused WiFi driver running. Saved bridge choices remain intact; explicitly enabling the bridge resumes it for this boot. Manual WebConfig/WiFi starts preserve an explicitly enabled ESP-NOW bridge, including when set 2.4ghz on restores the previously selected services. Stopping a browser OTA session restores only bridges that session actually paused, rather than starting services from saved intent alone.

Every ESP32 build with WebConfig supports the browser command terminal, including WiFi Companion and Full Companion. Companions use their complete USB/TCP terminal on the trusted LAN, including contact import, chat, recipient selection, and delayed replies. ESP32 roles require their WebConfig admin login before browser terminal access; the HTTP login does not protect TCP.

To add a Companion contact, run import meshcore://<full-contact-card-data>, then list to confirm it appears. Get the complete card with card on the other Companion. A public key alone is insufficient. See the terminal command guide for to, send, login, and cmd examples and session ownership.

The saved wifi.cli setting defaults to on. Use set wifi.cli off to disable the CLI tab. get wifi.cli reports off, on, waiting for WiFi client, or on, active. The saved setting becomes active only in station/LAN mode while the WiFi client is connected. It is deliberately unavailable on the setup access point. The tab displays each reply in the browser, including local diagnostics, infrastructure get password, private-key and WiFi/MQTT credential reads, and erase. ESP32 Companion's get password reports status without the secret. Infrastructure get acl and raw log stream to the requesting browser, with backpressure for slow clients. These maintenance commands retain their LoRa restrictions. set freq works both here and through authorized LoRa CLI. The bounded /api/cli endpoint accepts local maintenance commands; use /api/terminal for complete ACL and log listings. Select Command block to paste up to 100 commands with one command per line. Blank lines are ignored, and every nonblank line must fit the normal 159-byte CLI command limit. The browser sends the lines sequentially and waits for each reply before sending the next line. The queue exists only in that browser page; closing it stops any commands that have not yet been sent. A lost connection also stops the remaining block.

ESP32 WiFi Companions also expose AP-compatibility controls through the same local/framed command parser:

The same reply includes SSID, IP address and RSSI when connected, protocol=<saved>/<boot>/<live bitmap>, mode=<saved>[/<boot>], live modem sleep (ps), Bluetooth coexistence (coex), and ota=off|<port>:listen|<port>:client. The boot mode is omitted only when it matches the saved mode; differing values mean a reboot is needed. No detail argument is required.

Both controls are saved for the next reboot, without interrupting the command connection. bg excludes 802.11n and cannot override an ESP-NOW/LR owner. wifi-only requires wifi.enabled 1 and skips Bluetooth stack initialization, while retaining USB and LoRa. To test without modem sleep, run set wifi.mode wifi-only, set wifi.powersave none, then reboot; none is safely deferred until that reboot if Bluetooth currently owns the radio. Restore with set wifi.mode normal, set wifi.protocol bgn, set wifi.powersave min, then reboot. Keep USB recovery available when testing a WLAN that may not work. The Indicator keeps its existing companion.transport selector instead of wifi.mode. See Companion WiFi compatibility settings.

Up/down arrow keys recall commands from the current browser session in single-command mode. A command that stops WebConfig, changes its WiFi connection, disables wifi.cli, or reboots the node stops the remaining block and can close the page before its reply is collected.

On unified FULL USB + WiFi and FULL logging-fallback ESP32 repeater/room-server builds, get wifi.ssid reports the saved standalone WebConfig network and get wifi.status reports whether WiFi is unconfigured, off, connecting, running the setup AP, failed, or connected. A connected result includes the SSID, LAN IP, and RSSI. When the shared OTA seeder is running, the reply appends OTA TCP 5001: listening or OTA TCP 5001: client connected. WiFi being off is normal while WebConfig is inactive; run start webconfig when a temporary connection is wanted. get wifi.powersave reports the saved standalone setting as none, min, or max. Fresh Cascade-profile builds default to min; target-default builds use none. A saved setting takes precedence after an upgrade.

The WiFi set commands work on MQTT observers and on FULL standalone ESP32 repeater/room-server builds. On a standalone build, changing the SSID or password stops an active WebConfig session; run start webconfig again to use the new credentials. set wifi.pwd with no value selects an open network. Standalone WiFi also accepts an exact 64-character hexadecimal WPA/WPA2 PSK; ordinary passphrases remain limited to 63 characters. Other 64-character values and all longer values are rejected. MQTT observer WiFi passwords retain their fixed 63-character limit. Power-save changes are applied immediately when WiFi is running and otherwise take effect on the next connection. get wifi.pwd returns the saved password on a local connection. It remains unavailable or masked over LoRa, and the configuration form continues to mask stored secrets.

ESP32 WiFi Companion WebConfig exposes the same wifi.powersave values in its WiFi card. Every ESP32 WiFi Companion with WebConfig exposes the standalone wifi.ssid, wifi.status, wifi.powersave, wifi.cli, and WebConfig command families through its USB text terminal. Full Companion exposes the same role-specific terminal on TCP port 5002. Credential writes reply before restarting the WiFi station, so a TCP client should expect to reconnect at the new address; USB password input is masked. Binary Companion clients can use USB, BLE, or TCP port 5000 without the terminal-start token: send command 0x42 (CMD_RUN_CLI_COMMAND) followed by the same CLI text, such as get wifi.powersave, get wifi.pwd, stats-core, or set wifi.powersave min. Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off and Bluetooth on where supported. Legacy WiFi-only builds without another CLI transport retain WiFi on so they are not left unreachable. Use get wifi.enabled and set wifi.enabled 1 to inspect or persistently enable WiFi; set wifi.enabled 0 disables it. Saved enablement survives upgrades, and credentials alone do not turn it on. ESP32 simultaneous-transport builds apply the change from the main loop; exclusive-transport builds and Pico W apply it after reboot. This does not disable a primary ESP-NOW mesh radio or add Bluetooth to a WiFi-only build.

ESP32 WiFi Companions also accept get wifi.tcp over USB, BLE, or the browser CLI. This checks the app's TCP listener (normally port 5000), not just the WiFi association. listening means the socket is open; waiting-ip means no usable station or setup-AP address yet; retrying means listener startup failed and will be retried automatically; off means no enabled listener request. reg=1 confirms the interface is registered for Companion commands. client=1 means a TCP client is connected. acc, rx, and tx count accepted clients, complete received frames, and complete transmitted frames since boot; the value after / is the last frame's hexadecimal opcode. fail, err, and retry show listener failures, the last listener-start socket error, and seconds until retry. drop gives the last client-disconnect reason. These counters contain no message contents or credentials. An open listener does not prove a router or guest-network firewall permits access from the phone.

Listener retries wait 1, 2, 4, 8, 16, then at most 30 seconds. They do not reconnect WiFi, interrupt BLE/WebConfig/OTA, replace a healthy app connection, or re-enable explicitly disabled WiFi. get display.wifi includes tcp=1 only when the listener is actually open; services=1 alone is not proof.

The ESP32 TCP transport services input even while radio logs or other outgoing frames are pending, so unsolicited traffic cannot starve the app handshake. Socket sends are non-blocking. A client that makes no outbound progress for 10 seconds while data is pending is disconnected (drop=tx-timeout), without blocking the other transports. drop=tx-error identifies a fatal send error. Commands wait when all reply slots hold delivery-required data, rather than executing with nowhere to return their reply. A partly sent frame stays pinned until it finishes; input and output remain separate TCP streams.

See local maintenance commands for the complete access rules. WiFi-only Companions accept all three modes. A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects none because coexistence requires modem sleep. Companion device power saving and LoRa radio.rxps remain independent. On an ESP32 Full Companion whose primary mesh radio is ESP-NOW, max is also unavailable: a station using maximum modem sleep can miss ESP-NOW broadcasts, which the access point does not buffer for it. A previously saved conflicting value is capped to and reported as min, and a new conflicting selection is rejected. The primary mesh radio also holds the ESP-IDF WiFi wake reference continuously so unsolicited ESP-NOW frames remain receivable; selecting min does not put that primary receiver to sleep.

SenseCAP Indicator Full is the exclusive-secondary exception. On a WiFi-selected boot, LoRa accepts none|min|max and ESP-NOW accepts none|min. On a BLE-selected boot infrastructure WiFi is not started; LoRa accepts min|max for the saved WiFi setting, while ESP-NOW + BLE requires min. USB and the primary LoRa or ESP-NOW radio remain available in every mode.

"},{"location":"cli_commands/#view-or-change-the-primary-esp-nowwifi-channel","title":"View or change the primary ESP-NOW/WiFi channel","text":"

Usage:

Parameters:

Default: 1

This command is available when ESP-NOW is the node's primary mesh radio. The setting is persisted. A set reply reports that reboot is required; until reboot, the running ESP-NOW radio remains on its previous channel. On builds that also provide ordinary WiFi, its station and setup AP share that channel.

After reboot, ESP-NOW, the setup AP, and the infrastructure-WiFi station use the selected channel. Every primary ESP-NOW node that must communicate with this node, plus the configured router's 2.4 GHz radio, must use the same fixed channel. WiFi power saving does not allow the transports to use different channels. On an ESP32 Full build with primary ESP-NOW, wifi.powersave max is unavailable because maximum modem sleep can miss ESP-NOW broadcasts; use min for coexistence. The firmware keeps the primary ESP-NOW receiver awake while still using the min WiFi/Bluetooth coexistence policy.

espnow.channel is the channel of the primary ESP-NOW mesh transport. It is separate from bridge.channel, which configures only an ESP-NOW bridge on a firmware role whose primary mesh radio is LoRa.

The browser portal is not compiled into the two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt targets because it does not fit while retaining the two app slots required for LoRa OTA. Their normal CLI settings remain available.

"},{"location":"cli_commands/#switch-the-ota-release-channel-observer-builds","title":"Switch the OTA release channel (observer builds)","text":"

The selector is saved only after the preferences transaction succeeds. A queued ota update keeps the channel it checked, even if ota branch is changed before the download starts. Switching channels offers the target image even when its commit matches the running image; build numbers are not comparable across channels.

Before an image is made bootable, its compatibility tag must confirm support for this fork's saved-settings format and required transports. Upstream observer images without the keymind1 capability are refused, including images that use the different /mqtt.json format. A refusal leaves the current image running; select a compatible channel to continue OTA. Production and beta URLs are supplied by the build recipe, not accepted as arbitrary CLI URLs.

"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"

Search terms: factory defaults, reset settings, erase configuration.

Usage: - erase

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

Warning: This is destructive!

"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"

Search terms: nearby nodes, neighbor list, neighbour list.

Usage: - neighbors

Note: The output of this command is limited to the 8 most recent adverts.

Note: Each line is encoded as {pubkey-prefix}:{timestamp}:{snr*4}

"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"

Usage: - neighbor.remove <pubkey_prefix> - neighbor.remove - remove all neighbors

Parameters: - pubkey_prefix: An even-length hexadecimal prefix (up to the full 64-character public key). All neighbors matching it are removed. Invalid keys are rejected.

Omitting the prefix clears the table. An empty or whitespace-only prefix is also accepted for compatibility with the mobile app's Remove All Neighbors action. Newly heard zero-hop adverts can populate the table again.

"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"

Usage: - discover.neighbors

This command is available in every repeater build profile, including portable MQTT, standard, logging, OTA, unified FULL, and FULL logging-fallback artifacts. It does not require MQTT or PSRAM.

"},{"location":"cli_commands/#discover-neighbor-scopes-mqtt-observer-neighbors-feature","title":"Discover neighbor scopes (MQTT observer, neighbors feature)","text":"

Refreshes the zero-hop neighbor table, then queries each neighbor for its region scopes and publishes the assembled table to the MQTT neighbors topic once.

Usage: - discover.scopes

Note: Requires an MQTT observer build with the neighbors feature compiled in (all PSRAM boards, plus non-PSRAM boards built with MQTT_NEIGHBORS_WITHOUT_PSRAM). Elsewhere it replies Err - neighbors not enabled in this build. If a discover.neighbors refresh is already in flight, the scope pass is queued behind it.

"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#list-rate-limited-flood-adverts","title":"List Rate-Limited Flood Adverts","text":"

Usage: get flood.advert [page] or get flood.advert key <index>

Lists tracked advert origins currently subject to a forwarding limit. Page 1 is the default, with three entries per page so replies fit both USB and admin LoRa CLI. Each row shows its index, the first 12 public-key hex characters, the limit reason, and the minimum wait before another new advert can pass the automatic limiter. Ordinary forwarding filters still apply.

get flood.advert\nget flood.advert 2\nget flood.advert key 1\n

Reasons are quota (the prefix's three-hour forwarding allowance is spent), history (distinct receive history is full), and bad (the full key is under the stricter one-advert-per-12-hours rule). A bad key remains listed when wait=0s: one forward is eligible, but its seven-day recovery is not complete. If several limits apply, bad takes precedence over history, then quota; the displayed wait accounts for all applicable limits.

The key form returns the complete 64-hex public key, reason, wait, shared prefix forwarding count/quota, shortest received hop count, and remaining bad-list recovery time. Different full keys with the same 12-hex prefix remain separate entries. Indices refer to the current live list and can change as history expires; read the full key before using a targeted clear.

Queries do not clear history, spend quota, or refresh abuse/recovery timers. Elapsed history expires normally. Origins rejected only because every table slot protects abuse history are not retained and cannot be enumerated. Available on repeaters, room servers, and forwarding sensors through their ordinary local/admin CLI. Companions do not have this table.

"},{"location":"cli_commands/#clear-automatic-flood-advert-history","title":"Clear Automatic Flood Advert History","text":"

Usage: clear flood.advert all or clear flood.advert <64-hex-full-public-key>

Clears automatic advert forwarding quotas, first strikes, and seven-day bad-list history for all keys or one exact full key. Available on repeaters, room servers, and forwarding sensors through their ordinary local/admin CLI (including admin LoRa CLI). A missing selector, a key prefix, or a malformed key is rejected. It does not change saved settings or manual flood rules. Reboot also clears this RAM-only history. See automatic advert limits.

"},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"

Usage: clear stats

"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"

Usage: - stats-core

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"

Search terms: signal strength, signal quality, RSSI, SNR, radio noise.

Usage: stats-radio

last_rssi is the strength of the last received packet. It is not a live signal-strength meter and will not show an unmodulated carrier; see the carrier-wave guide for a separate receiver that can measure one.

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"

Usage: stats-packets

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#read-repeater-and-room-server-telemetry-history","title":"Read repeater and room-server telemetry history","text":"

Repeater and room-server firmware record one UTC-aligned sample every 30 minutes. Temperature and battery voltage retain 336 samples (seven rolling days). Each detected INA219, INA226, INA260, or INA3221 voltage channel retains 192 samples (four rolling days). GPS-capable builds retain three GPS days by default; repeaters request a seven-day default at startup. Builds without a GPS provider omit the GPS history commands to conserve flash. All history and any runtime retention change are held in RAM and reset after a reboot.

Radio noise-floor history is recorded separately every 15 minutes, retaining 768 positions (eight rolling days) per profile. It uses idle RSSI calibration estimates rather than received-packet RSSI. Main-radio and radio2 history are kept separate; failed measurements retain a Missing position in the timeline.

The feature is omitted from flash-constrained STM32 repeater and room images.

Usage:

Parameters:

Local serial and remote administrator CLI sessions can read the history on both roles. Collection uses the MCU temperature, battery voltage, external I2C voltage monitors, and an already-valid onboard GPS fix. It does not wake GPS, so an off, sleeping, or unfixed GPS produces a missing location sample without changing its power-saving schedule.

Noise history combines three fresh estimates collected roughly five minutes apart in each UTC-aligned quarter-hour. If any is at or below -100 dBm, use the minimum. Otherwise, if any is at or above -80 dBm, use the maximum. Otherwise, average all three. Thus a bucket containing both low and high values uses the minimum. Minimums round down, maximums round up, and averages round to the nearest encoded step. Incomplete buckets are Missing. A busy channel can prevent a fresh estimate; old calibration results are not reused as fresh measurements.

External voltage history allocates 360 bytes for each detected monitor channel (1,080 bytes for all three INA3221 inputs). The allocation reserves all detected inputs so a sensor connected later can start recording, but all-zero inputs are omitted from command replies and LoRa transmission.

Packed noise history uses 480 bytes per retained profile, plus its collection state. It does not add a 15-minute transmission schedule. USB and administrator LoRa CLI return the same Base64 format in the existing reply buffer.

Replies contain > followed by standard padded Base64. After decoding, all multi-byte integers are little-endian. Packed fields are written most significant bit first, oldest sample first.

Use the browser-based Telemetry history decoder to turn a reply into a timestamped table or downloadable CSV without uploading the data.

telemetry.tx is disabled by default. Its enabled state and cadence are stored separately from the shared data.tx path and region. data.tx defaults to zero-hop direct plus region=auto, but it does not enable telemetry or any other producer. For compatibility, set telemetry.tx direct|path updates the shared path and enables the default 2d schedule; new configuration should use set data.tx path ... followed by set telemetry.tx schedule .... An automatic run waits until 165 half-hour positions are available, then sends one maximum-size temperature packet, one maximum-size battery-voltage packet, and up to three IVB1 packets for every populated I2C voltage channel, plus the collected noise-floor snapshots. GPS is never included in this raw transmission. The history remains boot-local, so the first scheduled run after a reboot needs about 82.5 hours to fill. On the default two-day interval, each 82.5-hour temperature or battery packet overlaps its predecessor by 34.5 hours. Packets are paced two seconds apart. A queue failure is retried after 30 minutes only for the packet that did not queue; successfully queued packets are not duplicated.

send telemetry.tx now is an administrator-only test action. It queues temperature and battery-voltage snapshots plus the available I2C voltage chunks over the configured path. It also queues one main-radio N1B1 packet once a noise bucket has completed and one N2B1 packet when radio2 is enabled and has a completed bucket. Missing positions are included; an all-Missing history is still sent. It uses all currently available base positions up to 165 and all available I2C positions up to 192 per populated channel, plus the latest 255 positions per noise profile (63 hours 45 minutes). It works while the schedule is off and does not move the next scheduled send time. RAW_CUSTOM direct packets do not enter the normal encrypted direct-message retry mechanism.

Full temperature and battery snapshots are 184-byte RAW_CUSTOM payloads. Manual tests can be shorter while history fills. Both formats are binary, not Base64 and not encrypted:

Bytes Meaning 0-3 ASCII magic TTB1 for temperature or TVB1 for voltage 4-11 First eight bytes of the repeater public key 12-15 First sample UTC epoch, unsigned 32-bit little-endian 16-17 Sample interval in minutes, unsigned 16-bit little-endian (30) 18 Sample count (1-165) 19 onward Oldest-to-newest encoded samples for the selected series

Temperature codes reserve 0 for no reading, 1 for below -50 C, and 2 for above +77 C. Codes 3 through 130 represent exact whole degrees from -50 C through +77 C; decode them as code - 53. Voltage codes use the same encoding as the paged voltage payload documented below.

Noise snapshots reuse this 19-byte telemetry header. Their magic is N1B1 for the main radio or N2B1 for radio2; the interval is 15, the count is 1-255, and data begins at byte 19. Samples are packed 5-bit codes, oldest first and most significant bit first, with zero padding in the last byte. The payload is 19 + ceil(count * 5 / 8) bytes: 179 bytes for 255 positions. No GPS coordinates are included in a noise packet. Noise packets are additional RAW_CUSTOM telemetry snapshots, not a new MeshCore packet type.

External voltage snapshots use a separate packed layout. A full four-day channel takes three 140-byte IVB1 payloads of 64 points each:

Bytes Meaning 0-3 ASCII magic IVB1 4-11 First eight bytes of the source public key 12-15 First sample UTC epoch, unsigned 32-bit little-endian 16-17 Sample interval in minutes, unsigned 16-bit little-endian (30) 18 Cayenne LPP voltage channel 19 Sample count (1-64) 20 onward Oldest-to-newest packed 15-bit voltage codes

Code 0 means missing or disconnected. Codes 1-32767 represent 0.02 V through 655.34 V in 0.02 V steps; decode millivolts as code * 20.

Match bytes 4-11 to the first 16 hex characters of the repeater public key shown by its advert or get public.key. This compact identifier is useful for association but is not authenticated and can be spoofed. An MQTT observer can upload the received raw packet to LetsMesh Analyzer /packets; its packet hex contains the direct-route header and path followed by this payload.

Temperature payload (0x11, 61 bytes):

Bytes Meaning 0 Format/type 0x11 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7-18 48 packed 2-bit temperature statuses 19-60 48 packed 7-bit temperatures

Temperature status codes are 0 none, 1 value, 2 below range, and 3 above range. For status 1, the 7-bit temperature is an exact whole-degree integer from -50 C through +77 C; decode it as code - 50. Low values use code 0, and high values use code 127. The separate status map is required because 7 bits contain exactly 128 codes, leaving no spare code for none, low, or high when the complete range is represented at 1 C resolution. No fractional temperature is stored or transmitted.

Voltage payload (0x12, 55 bytes):

Bytes Meaning 0 Format/type 0x12 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7-54 48 8-bit voltage codes

Voltage codes reserve 0 for no reading, 1 for below 1.88 V, and 255 for above 4.40 V. Codes 2-254 represent 1.88 V through 4.40 V in 0.01 V steps; decode millivolts as 1880 + (code - 2) * 10.

External I2C voltage payload (0x14, 98 bytes):

Bytes Meaning 0 Format/type 0x14 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (48) 7 Cayenne LPP voltage channel 8-97 48 packed 15-bit voltage codes

The codes have the same 0.02 V through 655.34 V meaning as IVB1.

Noise-floor payload (0x15 main radio, 0x16 radio2; up to 117 bytes):

Bytes Meaning 0 Format/type 0x15 or 0x16 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (15) 6 Sample count (1-176) 7 onward Oldest-to-newest packed 5-bit noise-floor codes

The same codes are used by N1B1 and N2B1:

Code Meaning 0 -120 dBm (also clamps lower values) 1 -119 dBm 2-29 -118 through -64 dBm, in 2 dB steps 30 MAX: above -64 dBm 31 Missing: fewer than three fresh, valid estimates

The last byte's unused low bits are zero. A full page encodes as 156 Base64 characters, plus the existing > reply prefix. The decoder keeps radio1 and radio2 separate and rejects wrong lengths, intervals, or padding. MAX is a status, not an invented numeric -62 dBm reading.

When a Companion CLI command includes its three-character correlation prefix (for example 01|), noise replies retain that prefix and omit the > prompt. A full 01|Base64 response then fits in 160 bytes including its terminating NUL. The decoder accepts either reply style.

GPS payload (0x13, 101 bytes):

Bytes Meaning 0 Format/type 0x13 1-4 First sample UTC epoch, unsigned 32-bit 5 Sample interval in minutes (30) 6 Sample count (24) 7-10 Page origin latitude in signed degrees times 10^7 11-14 Page origin longitude in signed degrees times 10^7 15 Origin sample index, or 255 when the page has no GPS fix 16 Flags; bit 0 means at least one differential was clipped 17-100 24 records: signed 14-bit north then signed 14-bit east

GPS differentials use signed 14-bit two's-complement values at 10-meter resolution and are applied to the preceding decoded valid point. The origin sample begins at the header coordinates. A no-fix slot encodes 0,0 and does not advance the reference; a stationary valid fix also quantizes to 0,0. When a page has no fixes, its origin is 0,0, origin index is 255, and all differentials are 0,0. Values outside -8192 through 8191 are clipped and set flag bit 0.

"},{"location":"cli_commands/#set-companion-tx-routing-per-contact-or-channel","title":"Set Companion TX routing per contact or channel","text":"
set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nget tx.channel 0\nset tx.channel #wardriving radio\n

Both command families accept auto, radio, radio2, both, or off. auto uses the global crossing rules. Explicit choices override radio2.cross, but radio2 must still be enabled in rxtx to transmit there. radio2 never falls back to the primary profile; both uses whichever of its selected profiles can transmit. Set a target back to auto to remove its override.

Use an exact contact/channel name, a zero-based channel index, or key:<12-64 hex digits> for a unique contact public-key prefix. Duplicate names/prefixes are rejected. get tx.user and get tx.channel show override counts; queries with a target show its saved choice and current effective profile selection. Choices are saved with the contact/channel and work for phone-app and terminal messages. A contact's off setting also blocks its ACKs and addressed responses.

See Companion routing details and examples for temporary profiles, persistence, and the traffic covered by each setting.

"},{"location":"cli_commands/#companion-meshtext-rebroadcasting","title":"Companion Meshtext rebroadcasting","text":"
get mt.rebroadcast\nset mt.rebroadcast on|off\n

Choose on or off after set. This saved option defaults to off and is also shown in get mt.status. Only broadcast text successfully decoded with a configured channel key can be relayed; unknown-key, non-text and direct messages are excluded. Relays wait five additional actual packet airtimes plus flood jitter, on top of existing radio waits. Hearing another forwarded copy cancels the pending relay. mt.maybe_rebroadcast is an equivalent alias. See optional decoded-text rebroadcasting for hop limits, cancellation and queue behavior.

"},{"location":"cli_commands/#set-companion-media-and-ssh-application-tx-profiles","title":"Set Companion media and SSH application TX profiles","text":"
get tx.media\nset tx.media auto|radio|radio2|both|off\nget tx.ssh\nset tx.ssh auto|radio|radio2|both|off\n

Choose one value after set. Both default to auto and are saved across reboot. Explicit choices override a channel route for the matching application data only; they do not change ordinary text, adverts or contact ACKs. tx.media auto selects only the active rxtx MeshCore second profile, including tempradio2, when available. Otherwise, it uses the normal channel/global route. tx.ssh auto keeps that normal route. radio2 requires an active rxtx MeshCore profile and never falls back to the primary. both uses the available profiles; off blocks sending only. Queries include the effective active= selection and warn if radio2 is unavailable for TX.

Media covers Open TAK's binary photo/voice and fetch packets. SSH requires an application to opt into the new 0x5353 channel-data type or raw 53 53 48 01 envelope; existing untagged SSH/IP tunnel apps are not detected automatically. See the application routing contract for the exact framing, precedence and limits.

"},{"location":"cli_commands/#esp32-full-companion-ethernet-tunnel","title":"ESP32 Full Companion Ethernet tunnel","text":"
get ethernet\nget ethernet.radio\nset ethernet.radio <MHz>,<kHz>,<SF>,<CR>[,<preamble>]\nset ethernet on\nset ethernet off\n

This separate ethernet mode carries the entire raw Ethernet link over radio2, including SSH, while primary radio stays MeshCore. It defaults off, saves its settings, and requires an explicitly configured supported RF tuple. Supported ESP32 Full SX1262/SX1276 builds expose trusted-LAN KISS-over-TCP on station port 5003. It does not add an SSH server, use tx.ssh tags, or bridge Ethernet into MeshCore. Conflicting radio2 owners are not overwritten. See Ethernet setup, host commands and limits.

"},{"location":"cli_commands/#set-repeater-room-server-and-sensor-reply-profiles","title":"Set repeater, room-server, and sensor reply profiles","text":"
get tx.reply\nset tx.reply both\nset tx.reply both force\nset tx.reply radio\nset tx.reply auto\n

Persistent choices: auto, radio, radio2, both (default), and off. Explicit choices override radio2.cross for local replies, including CLI, ACKs, telemetry, room subscription posts, and served LoRa OTA responses. Append force to both or radio2 to permit reply TX on an active RX-only second profile. It never enables a disabled profile or ordinary forwarding. Omitting force removes that override. off suppresses remote replies too.

See reply routing and OTA examples for temporary profiles, receive-only exceptions, and queued reply handling.

"},{"location":"cli_commands/#set-companion-display-rotation","title":"Set Companion display rotation","text":"

SSD1306 Full Companion builds support a persisted runtime orientation:

get display.rotation\nset display.rotation 0\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\n

The values are clockwise degrees. 0 clears the override and restores the board's compiled default. Unsupported display drivers return an error.

"},{"location":"cli_commands/#select-rak-oled-controller","title":"Select RAK OLED controller","text":"

Display-capable RAK4631 and RAK3401 images with OLED controller detection support a saved selection in Companion, Repeater, Room Server and Sensor roles:

get display.controller\nset display.controller auto\nset display.controller ssd1306\nset display.controller sh1106\n

Choose auto for normal use. If a panel is shifted, wraps at its edge or is otherwise incorrectly detected, select its controller explicitly and restart the node. Changes are saved immediately and apply after restart; the current screen keeps its active controller until then. get display.controller reports the configured choice, active choice and whether a restart is needed. A custom firmware build is not needed to change this choice.

Automatic detection lets the same RAK4631 image serve WisMesh Pocket v2 and v3. Both controllers retain rotation support and use one 1 KiB framebuffer.

Detection checks the configured DISPLAY_ADDRESS (default 0x3C). Only repeated SH1106 status readings select that controller; ambiguous responses keep SSD1306, preserving compatibility with older RAK1921 panels. With no saved choice, the default is automatic detection unless the build sets OLED_CONTROLLER_OVERRIDE=1 (SSD1306) or =2 (SH1106). This build-time override remains a fallback for existing custom builds. Any saved selection takes precedence, including auto, which restores detection even when that fallback forces a controller.

See the Pocket v3 issue.

"},{"location":"cli_commands/#show-touchscreen-touch-areas","title":"Show touchscreen touch areas","text":"

All touchscreen Companion builds using the shared touch UI support temporary diagnostic outlines, including both SenseCAP Indicator rendering sizes:

get display.touch\nset display.touch on\nset display.touch off\n

The default is off at every boot. Enabling wakes the display without selecting anything. Dotted yellow borders mark the actual scaled tap targets; the area under a held finger turns green and returns to yellow on release. Disabling removes the borders on the next redraw. Pairing screens suppress the overlay while they consume navigation. This option does not change the font, touch calibration, navigation actions, or saved settings, and is not persisted.

Use the text terminal or Companion framed CLI (0x42) through an available USB, BLE, or TCP connection. Non-touchscreen, missing-display, and legacy UI builds report Error: touchscreen diagnostics unsupported. No Indicator-only build flag is needed.

"},{"location":"cli_commands/#set-display-modes-and-timeouts","title":"Set display modes and timeouts","text":"

Companion message sound, vibration, LED, screen and GPIO rules have separate CLI controls. See the notification builder and CLI reference, including per-contact/channel rules, trusted-contact/room !notify permission, and USB test controls.

Display-equipped Companions, repeaters, room servers, and sensors have four saved settings. The WebConfig Display card exposes the same controls:

Setting CLI Default Battery mode get/set display.mode button-pairing on BLE Companions; button elsewhere Battery timeout, seconds get/set display.timeout 900 on e-paper; 15 elsewhere USB mode get/set display.usb.mode Same as battery mode USB timeout, seconds get/set display.usb.timeout 900 on e-paper; 15 elsewhere

Modes are off, on, button, pairing, button-pairing, and automatic. Pairing modes are available only on builds supporting BLE pairing.

All display-equipped Companion builds show the screen at boot in the default button or button-pairing mode, on battery or USB power. It stays on for the selected profile's timeout (15 minutes on e-paper, 15 seconds elsewhere). Button input restarts that timer. This also applies to previously saved button modes. Saved off and pairing-only modes still suppress the boot screen. Repeaters, room servers, and sensors do not gain this Companion-only normal-UI boot wake.

On display-equipped Companions, repeaters, room servers, and sensors, a small Starting... screen appears as soon as the board, primary filesystem, and display are initialized, before radio initialization and serial-settle delays. If a new identity is needed, it changes to Generating key with an animated spinner during radio entropy collection. OLED/TFT updates are paced at 150 ms; e-paper uses a slower one-second cadence. Saved off and pairing-only modes still suppress this startup screen. The normal UI's boot timeout starts only when that UI is ready, so key generation does not consume it. Filesystem recovery and the display driver's own initialization can still delay the first visible frame; the firmware cannot draw before those operations finish.

Startup shows Loading identity while reading a saved identity. Generating key and its spinner are used only when creating a replacement/new identity; they end before flash persistence, which shows Saving identity. A normal boot with a valid saved identity never regenerates it. An erased filesystem does require a new identity unless one is restored.

On Full Companion screens, an unpaired Bluetooth PIN takes priority over a USB/TCP connected label. Button wake shows the PIN banner without requiring USB to be disconnected; a second navigation press can leave this preview. An actual phone pairing request wakes pairing-enabled display modes and keeps the banner visible until completion or its two-minute expiry. Bluetooth scanning alone is not a pairing request.

Timeouts are whole seconds from 1 to 3600. Use mode on for a permanent display rather than timeout 0. The timeout is retained but unused in off, on, and pairing modes. Settings apply immediately, including while blanked; changing power source applies the other profile without carrying over a wake from the previous profile. Settings survive reboot and filesystem-preserving updates in /display_prefs; erasing the filesystem restores the defaults. The 15-minute e-paper default applies to battery and USB profiles on all roles. Existing saved timeouts stay unchanged. To use 15 minutes on an existing node, set both display.timeout and display.usb.timeout to 900; choose a timed mode such as button or button-pairing if the current mode is on or off. An existing observer timeout that differs from its old 60-second default is migrated once to both power profiles; an old zero timeout becomes mode on.

set display.mode button-pairing\nset display.timeout 15\nset display.usb.mode on\nset display.usb.timeout 60\n

USB profile selection uses the board's external-power detector, with USB-host detection as a fallback. VBUS-capable boards detect chargers without a data connection. Heltec V4 R8 uses either a detected USB host or a calibrated battery voltage above 4.21 V. The voltage estimate is checked every five seconds and clears at 4.20 V or below; host detection takes effect immediately. This estimate can miss a charger while the battery is low, depends on ADC calibration, and cannot distinguish USB from solar charging. Other boards without a power detector may require a USB host to recognize USB power. Wi-Fi/MQTT connection or BLE pairing is not evidence of USB power. E-paper panels can retain their last image when powered off.

"},{"location":"cli_commands/#set-companion-inbox-behavior","title":"Set Companion inbox behavior","text":"

The shared Companion UI (including T114 and R8) defaults to History: the last 32 received text previews remain available on the radio after an app downloads them. Pending separately counts frames waiting for the app, including non-text frames. Downloading a frame clears it from Pending; it does not mean someone read it. The phone's delivery queue is independent of the 32-entry preview history and is never drained by reading on the radio.

get display.inbox\nset display.inbox history\nset display.inbox pending\nset display.inbox unread\n
Mode Display behavior history (default) Shows HISTORY with the retained preview count, plus Pending. Downloaded previews remain browseable. pending Shows INBOX with the app queue count and limits the reader to previews still waiting for the app. Non-text frames count toward Pending but have no text preview. unread Shows UNREAD with the number of retained previews not yet displayed on the radio, plus Pending. Opening a preview on a lit display marks it read; app downloads do not. Read previews remain browseable.

All modes retain up to 32 previews internally; switching from pending back to history makes retained downloaded previews visible again. Evicted or rejected queue entries are not shown as pending. Previews and local read state are in RAM and reset on reboot. The mode applies immediately and survives reboot in /display_prefs; existing version-1 display settings load with history as the default. Legacy and tiny Companion UIs report Error: inbox modes unsupported.

Incoming messages automatically open their preview only when no client is connected. While connected, new messages neither select another page nor move the preview being read; history stays accessible by button or touch. The home screen identifies connected USB and Bluetooth clients (or both), and TCP, Ethernet, or serial clients where supported. Actual BLE pairing prompts retain priority over connection status.

"},{"location":"cli_commands/#set-display-rotation","title":"Set display rotation","text":"

Supported Companion displays and new Heltec V4 R8 TFT Repeater, Room Server, and Sensor images accept a saved rotation:

get display.rotation\nset display.rotation 0\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\n

0 restores the board's compiled orientation. On the R8 TFT, the other values turn clockwise from that default. A quarter turn changes between portrait and landscape, and the observer dashboard adapts to the new screen dimensions. The change applies without rebooting. Changing rotation while the display is off leaves it off; the next wake applies the pending panel rotation without resetting the shared touch controller. WebConfig offers the same choices under Display \u2192 Rotation when the running image supports rotation.

The stock R8 Expansion Kit V2 does not include an accelerometer. Its GNSS and touch controllers do not measure the screen's physical orientation, so this firmware offers manual rotation rather than an automatic orientation option. Automatic orientation would require a supported motion sensor and its mounting orientation.

Companion saves this in its existing node preferences. Infrastructure roles use a separate checked settings file, leaving existing node/MQTT layouts compatible. Old published images retain their original supported commands.

"},{"location":"cli_commands/#set-mqtt-observer-display-flip","title":"Set MQTT observer display flip","text":"

Supported observer displays, including the R8 OLED and ST7789 panels, can also be turned 180 degrees relative to their compiled orientation:

get display.flip\nset display.flip off\nset display.flip on\n

0 and 1 are accepted aliases for off and on. This is intentionally different from display.rotation: observer display.flip is an additional relative 180-degree mounting choice. Use display.rotation to switch between portrait and landscape on new R8 TFT images. The value is harmless on an observer display driver that does not support flipping.

Flip survives reboot and firmware updates that preserve the filesystem; erasing flash restores the off default. The boot log reports the saved flip state on display-enabled observer builds.

"},{"location":"cli_commands/#logging","title":"Logging","text":"

Non-compact builds compiled with MESH_PACKET_LOGGING emit a RAW: line for received radio frames that reach the raw capture hook while usb.logging is on, including Full Companion. Frames rejected by the early route policy are not exposed to raw capture. RAW: contains the complete received frame as uppercase hexadecimal, not only its decoded payload. Packet records do not require verbose USB debug output. Keep the host reader attached and draining the serial port: capture is best effort, not durable storage. Native-USB transports use bounded queues and drop records rather than block radio processing when the host stops reading; DROP:<count> markers report capture loss when output capacity returns.

Successfully parsed received frames also emit the decoded RX summary, including signal, timing, hash, type, route, and payload information. Frames that cannot be decoded, or cannot obtain a packet-pool slot, can still emit their RAW: line. Transmitted packets emit the decoded TX summary. RAW and RX summary are separate records, not an atomic pair; a queue drop or disconnect can leave a summary without its RAW bytes. USB log consumers must not associate a stale RAW record with the next unrelated summary. Size-constrained MESH_PACKET_LOGGING_COMPACT images instead use R<hex> and T<hex> records; that compact stream is not the full ASCII RAW/summary format expected by ordinary USB-to-MQTT parsers.

Ordinary non-OTA artifacts compile packet logging into the canonical image and control its live USB output at runtime; no separate -logging- artifact is emitted. Canonical RAK3401/RAK4631 repeater, room and sensor OTA pairs, plus Tower SD repeater and Tower room pairs, also retain packet logging in both Full-sensor and Reduced-sensor artifacts. Verbose debug stays off by default; get/set usb.logging controls packet output. Their capability manifests must verify both logging.usb.packets and logging.usb.control in the packaged application. Specialized storage/transport aliases and other space-constrained OTA-only targets retain their target-specific logging policy.

For other roles, use the separately named -ota- artifact when required. A -full-usb-wifi-ota- artifact combines USB packet logging, direct WiFi MQTT, LoRa OTA, and the expanded FULL feature set. A -full-logging-ota- artifact is emitted only when that hardware/role has no matching WiFi MQTT environment.

"},{"location":"cli_commands/#control-live-usb-logging","title":"Control live USB logging","text":"

Search terms: serial logging, USB debug log, enable logging, disable logging, debug output.

Usage:

get usb.logging\nset usb.logging on\nset usb.logging off\nset usb.logging on reboot\nset usb.logging off reboot\nget usb.debug\nset usb.debug on\nset usb.debug off\n

Current source also provides get usb.watchdog and saved set usb.watchdog off|on|auto. Missing watchdog state defaults to Auto, which requires 14 continuous healthy logging-client days before saving On. Existing five-minute USB stats polls confirm recent logging-client activity; a plugged cable/open TTY alone does not. See the USB logging watchdog for staged five-minute USB recovery, physical-fault-only MCU resets, persisted backoff, status fields, and safe shutdown deferrals. Older release binaries need an upgrade before these commands exist.

get usb.watchdog.last shows the latest USB watchdog event: recovery action, fault-reason bitmask, advisory node-clock epoch, uptime at the event, sequence, and whether the record passed durable readback. It survives reboot on durable storage. reboot-requested records intent, not proof that a physical reset occurred; a later safety veto records reboot-cancelled. See the watchdog documentation for storage and timestamp limitations.

ESP32 1.17.1.5 USB logging procedure: disable device sleep before enabling the log stream, using separate text commands:

set powersaving off\nset usb.logging on\n

Verify with get powersaving (expect off) and get usb.logging (expect on). Both settings are saved; disabling logging later does not restore power saving. This works around the released USB sleep bug. nRF52 does not need this ESP32 workaround.

These commands are compiled into ordinary USB-loggable artifacts and every Full Companion. usb.logging is the saved USB output master and packet-log gate. usb.debug is a separate saved preference for verbose diagnostics; effective USB debug output requires both switches to be on. Packet RAW and RX/TX summaries require only the master switch. get usb.debug reports saved intent even when the master is off. Turning usb.logging off does not erase that intent; turning it back on restores debug output if usb.debug is on.

CommonCLI roles save these settings in /com_prefs; their fresh USB logging default is on. Full Companion and ordinary USB Companion start with the master off on a fresh installation so diagnostics cannot corrupt framed traffic. Verbose USB debug defaults to off for every role, including when loading older preferences without a debug field. Updates retain an existing saved usb.logging choice. Older binaries without the new usb.debug command retain their previous behavior until upgraded.

For packet-only capture, use these separate text commands:

set usb.debug off\nset usb.logging on\n

Use set usb.debug on temporarily when troubleshooting, then turn it off again to reduce USB traffic. This setting applies immediately and needs no USB-interface reboot. It does not select a firmware variant or enable code omitted from the image: verbose output still requires compiled MESH_DEBUG or other relevant diagnostic support. A packet-only build can retain the preference without gaining missing debug messages.

Debug off suppresses verbose diagnostic chatter, not functional CLI replies, USB port identity/drop markers, or retained operational warnings. Readers must still filter non-packet lines instead of treating every line as a packet.

On Full Companion these lines belong to its text terminal, not meshcli's Binary get/set parameter namespace. Open interface 00, send +++MESHCORE-TERM-START, and then issue the command. Running meshcli ... get usb.logging or get usb.debug directly can instead return Unknown var ... because that is a different protocol operation.

nRF52 Full Companion defaults logging to off and enumerates only USB interface 00, which carries Companion, terminal, and serial mOTA traffic. Enabling logging adds interface 02, its dedicated plaintext logging port, on the next boot. Disabling it removes interface 02 on the next boot. A command without the optional reboot argument saves the choice and reports that a reboot is required when the USB interface count must change. The exact set usb.logging on reboot and set usb.logging off reboot forms save the choice, send their reply, and reboot one second later only when needed. Interface 02 is logging-only, not an input-capable CLI. Send logging/debug commands to interface 00; USB debug uses the same log endpoint as packets.

On every ESP32 Full Companion, enter the USB text terminal and use set usb.logging on (preceded by set powersaving off on 1.17.1.5) to turn that TTY into a logging-repeater-style plaintext stream. Framed Binary Companion is unavailable on USB while logging is on. The TTY remains an input-capable CLI, so set usb.logging off works on the same TTY. After its reply, logging stops and the TTY remains in the normal ASCII terminal, just as it does after a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion. No reboot is needed because the USB interface count does not change.

Turning USB logging off does not disable CLI replies. nRF52 keeps Companion frames active on interface 00; ESP32 resumes the ordinary ASCII/Binary switcher after the logging terminal turns logging off. This setting does not change the node-storage capture controlled by log start and log stop.

For a USB-to-MQTT bridge, enable packet logging but leave usb.debug off. Full Companion now supplies the ASCII RAW/RX/TX packet stream needed by such consumers; it does not provide durable or lossless capture. A compatible host must also support the role's identity/control commands and replies: packet records alone do not make an existing bridge compatible. Companion now answers get public.key with its node public key. ESP32 Full's logging TTY remains input-capable and accepts that query. nRF52 Full's dedicated logging CDC is not a command/reply console (its bounded stats-poll input only renews the watchdog client lease): a host integration must use interface 00 for commands and identity and interface 02 for logs. Existing single-port bridges that require CLI replies cannot simply point at the dedicated log endpoint. This change does not add a two-port bridge driver.

Companion, Repeater, Room Server, and Sensor builds with both MQTT and USB logging provide the same saved selector for both output paths:

get logging.output\nset logging.output off\nset logging.output usb\nset logging.output wifi\nset logging.output both\n

usb emits RAW: packets for a USB-connected service such as meshcoretomqtt. wifi enables the direct MQTT bridge configured by the wifi.* and mqtt.* commands. both intentionally duplicates the radio stream to both consumers; do not point both consumers at the same broker unless the downstream setup deduplicates messages. Fresh unified Full infrastructure installs start in both mode; Full Companion starts with USB logging off. To change only MQTT, use set mqtt.enabled on|off; get mqtt.enabled checks the saved switch and get mqtt.running checks whether the MQTT service is running. Turning MQTT off preserves all broker slots and credentials. The output selector changes the USB master, not saved usb.debug intent; WiFi MQTT logging is independent of the USB debug preference.

On 1.17.1.5 ESP32, run set powersaving off before selecting set logging.output usb or set logging.output both, since those modes enable USB logging. WiFi/MQTT-only logging does not need this workaround while the Repeater/Room Server MQTT bridge is running; the released code already blocks device sleep in that state. Check get mqtt.running, not only get mqtt.enabled. WiFi modem power saving is a separate setting.

"},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"

Search terms: save logs, record received packets, RX logging, stored packet log.

The constrained wio-e5-mini_repeater omits node-storage packet logging: log start, log stop, log erase and log are unavailable. Live USB/MQTT packet logging, set usb.logging on|off and statistics remain. Existing saved log files are not automatically deleted.

Usage: log start

"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"

Usage: log stop

"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"

Usage: log erase

"},{"location":"cli_commands/#print-the-captured-log-to-the-requesting-terminal","title":"Print the captured log to the requesting terminal","text":"

Usage: log

Local connection only: USB, direct network CLI, or Companion binary command 0x42 where the role supports the command; unavailable over LoRa.

"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"

Usage: ver

On Full Companion, the Binary device-info frame retains its legacy fixed-width version field. Use version in the text terminal, or send version through Binary protocol command 0x42 (CMD_RUN_CLI_COMMAND), to read the complete untruncated firmware string together with protocol and build date.

"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"

Usage: board

"},{"location":"cli_commands/#show-the-storage-layout","title":"Show the storage layout","text":"

Usage: get storage.layout

Reports a compact, read-only summary of the storage compiled into the running firmware:

The command does not write, format, or resize storage. A raw-QSPI or SD build may briefly probe, wake, or mount its configured external storage to read its capacity. Remote use follows the existing administrator-command permissions.

"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#select-mcu-and-lora-regulator-modes","title":"Select MCU and LoRa regulator modes","text":"
get mcu.regulator\nset mcu.regulator auto|dcdc|ldo\nget radio.regulator\nset radio.regulator auto|dcdc|ldo\nreboot\n

These are independent controls. auto follows the board's existing default; dcdc requests its switching regulator and ldo requests its linear regulator. Settings are saved for the next reboot. The getter reports the saved choice, active mode, automatic fallback, crash streak and whether recovery is available. Setting a mode again, including the same value, clears that regulator's learned fallback and crash streak. The other regulator keeps its settings.

Choices depend on the board's verified circuit. MCU selection is available on nRF52 boards that already initialize their supported REG1 DC-DC regulator; it does not control REG0, an external PMIC, or ESP32/RP2040 processor regulators. Radio selection covers supported SX1262, LR1110/LR1121 and Meshnology W12 LR2021 implementations. A board with only verified LDO support rejects dcdc. Unverified LR2021 switching-regulator circuits retain their existing mode.

On platforms with retained crash recovery, three consecutive qualifying crashes while a domain is using DC-DC select LDO for that domain on the next boot. Each crash requires a valid last battery sample of at least 50%, no more than 60 seconds old at the last heartbeat. Low/unknown battery readings, stale samples, ordinary reboots, sleep wakes and unknown reset causes break the streak. Five minutes of healthy operation also clears the streak; a later crash starts a new one. LDO fallback stays selected until a mode is explicitly set again; recovery never switches back to DC-DC automatically.

ESP32 WebConfig shows Power regulators under Radio \u2192 Advanced when supported, with separate processor/radio choices and active/fallback status. Only supported options are offered. Clear fallback on restart reapplies the current choice through the normal save flow. Saving either control restarts the node after the save succeeds. See regulator recovery and hardware checks.

"},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"

Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>

Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz. Most targets allow 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500. LR1110 targets allow 62.5, 125, 250, 500. - sf: Spreading factor (5-12) - cr: Coding rate (5-8)

Set by build flag: LORA_FREQ, LORA_BW, LORA_SF, LORA_CR

Default: 869.618,62.5,8,5

get radio returns exactly four comma-separated fields for compatibility with existing Companion apps. On dual-radio builds, use get radio2 and get radio.timing for profile and timing diagnostics.

Note: Requires reboot to apply. If RXPS is enabled and the saved minimum level/preamble cannot safely cover the new radio timing, the command reply reports the effective level and preamble, or RXPS continuous-fast when no level through 10 is safe. Slower settings recalculate from the saved RXPS minimum and return to it exactly when it is safe.

"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"

Search terms: tx power, transmit strength, radio output power, dBm.

Usage: - get tx - set tx <dbm>

Parameters: - dbm: Requested radio-chip power in dBm. The valid range depends on the radio family, selected PA path, and any board-specific external-PA limit.

Set by build flag: LORA_TX_POWER

Default: Varies by board

Notes: This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. Setting a value too high may violate the laws in your country.

The command strictly rejects malformed values and saves the new preference only after the active radio driver accepts it. On LR2021, the chip range is -9 to 22 dBm below 1500 MHz and -19 to 12 dBm above 1500 MHz. A lower board-specific external-PA limit still takes precedence. A saved profile is clamped to the applicable limit when its frequency changes; temporary radio settings use a safe effective power without replacing the saved preference.

"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"

Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins> - normalradio

Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)

Notes: - tempradio is not saved to preferences and clears on reboot. - On repeaters, remote tempradio and normalradio wait for their exact reply copies to finish, with at least one transmitted. This also covers the set alias, case-insensitive verbs, and client correlation prefixes. Failed replies cancel an unconfirmed temporary handoff; hard lease expiry still takes priority. - normalradio cancels pending and active temporary-radio windows, then restores the saved radio tuple after its CLI reply has drained on the current channel. Permanent radioat entries are not removed. - On repeaters, the reply includes total minutes plus NdNhNm (days, hours, minutes) and the watchdog policy for that session. For example:

OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours\nOK - temp params for 3075 mins (2d3h15m); rx.watchdog=12hours\nOK - temp params for 180 mins (0d3h0m); rx.watchdog=off (temp<12hours)\n

Repeater timing overrides apply only while a temporary window is running:

Temporary duration No-RX reboot watchdog Flood advert interval Direct/local advert interval Up to and including 3 hours Off Saved normal interval 1 hour More than 3, less than 12 hours Off 3 hours 1 hour 12 hours or longer 12 hours 3 hours 1 hour

The duration is the requested window length, not the decreasing time remaining. The first temporary adverts are scheduled from successful radio activation; hourly local adverts still run when a flood advert is also due. get advert.interval and get flood.advert.interval report the effective intervals. The web configuration form continues to show and edit the saved normal advert settings. These include any changes made during a temporary session. Expiry, cancellation, reboot, or power loss removes the temporary overrides. Normal mode uses the saved advert intervals and the 24-hour watchdog policy. An explicitly saved normal-mode rx.watchdog off remains off outside temporary sessions.

"},{"location":"cli_commands/#schedule-radio-parameter-changes","title":"Schedule radio parameter changes","text":"

Usage: - set radioat <freq>,<bw>,<sf>,<cr>,<start_time> - get radioat [n|all] - del radioat [n|all] - set tempradioat <freq>,<bw>,<sf>,<cr>,<start_time>,<end_time> - get tempradioat [n|all] - del tempradioat [n|all]

Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - start_time: UTC Unix epoch seconds, or +N for N whole minutes from now - end_time: UTC Unix epoch seconds, or +N for N whole minutes from now - n: Scheduled entry number from get radioat or get tempradioat

Notes: - Both relative endpoints use the same command-time snapshot of the node clock. For example, set tempradioat 910.5,500,8,5,+5,+15 starts in 5 minutes and reverts in 15 minutes (10 minutes active). set radioat 910.5,500,8,5,+5 schedules a saved change in 5 minutes. Bare numbers remain absolute timestamps; absolute and relative endpoints can be mixed. Relative offsets must be positive. The existing scheduler and clock behavior are unchanged. - get radioat and get tempradioat list all entries when n is omitted. - del radioat and del tempradioat delete all entries when n is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - radioat saves the new radio preferences when it fires. tempradioat applies temporarily, then reverts to the saved radio preferences. - tempradioat uses the repeater timing table above, with duration calculated from end_time - start_time. Its acceptance reply includes total minutes, NdNhNm, and the planned rx.watchdog setting. Queuing a future entry does not change the running watchdog or advert intervals; the overrides start only after the temporary radio parameters are successfully applied. If restoration needs retries after expiry, the timing overrides still end with the window. - A successful scheduling reply notes any RXPS effective-level/preamble change required by the scheduled tuple. RXPS continuous-fast means the tuple is accepted but will use continuous receive because no safe duty-cycle level is available. Returning to slower settings recalculates from the saved RXPS minimum.

With the measured RXPS model and the default 1600 us TCXO startup delay (2600 us total transition), a requested minimum level 1 gives:

SF BW (kHz) Wire preamble Effective timing preamble Effective RXPS level RX / sleep 7 500 32 32 2 5594 / 3022 us 6 250 32 32 2 5844 / 3022 us 5 125 32 32 2 5844 / 3022 us 5 250 64 64 1 3829 / 5100 us 6 500 64 64 1 3829 / 5100 us 5 500 128 128 1 2563 / 6647 us 5 62.5 32 16 6 11922 / 2975 us

These values include the actual transmitted preamble, explicit header, and a one-symbol timer margin. SF5/SF6 need two more sync symbols than SF7, so their receive windows differ even when symbol duration and sleep time match.

The wire preamble remains compatible with the existing Cascade convention: 32 symbols at SF5-SF8, extended to 64 for SF5/BW250 and SF6/BW500, or 128 for SF5/BW500; SF9-SF12 use 16. Local RXPS settings and oscillator delays never change the sender's preamble choice. The receiver may use a shorter catch assumption, but its header timeout covers the full transmitted preamble.

Automatic adjustment stops at guarded level 8. If no guarded timing fits, the radio receives continuously until the tuple changes. For example, a board configured with the previous 5000 us TCXO delay uses continuous RX at SF7/BW500; it cannot fit the new guard within that tuple's 32-symbol wire preamble. Experimental levels 9 and 10 require an explicit selection.

Earlier firmware passed 16/16 packets in each direction between Heltec V4 and WisMesh Tag at SF5/BW250/64 and SF5/BW500/128, CR5, 909.950 MHz. That result established the existing wire-preamble convention; it does not validate the new timing model. All senders to an RXPS receiver must use a compatible wire preamble. LoRa OTA treats v1.17.1.5 as that capability boundary.

"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"

Search terms: radio frequency, LoRa frequency, MHz.

Usage: - get freq - set freq <frequency>

Parameters: - frequency: Frequency in MHz

Default: 869.618

Note: Requires reboot to apply

Connections: USB, browser/TCP/Ethernet CLI, Companion binary command 0x42 (USB/BLE/TCP), and authorized LoRa CLI. set freq saves the frequency; bandwidth, spreading factor, and coding rate stay as configured. Infrastructure roles also clamp transmit power to the board's limit for that frequency. Like set radio, it requires permission to configure the destination node.

"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"

Search terms: RX boost, receive gain, receiver sensitivity, boosted reception.

Usage: - get radio.rxgain - set radio.rxgain <state>

Parameters: - state: on|off

Default: Target-specific. Most SX1262 and LR1110 targets default to on; Station G2/G3 targets default to off.

Notes: - The saved setting is applied immediately and persists across reboots. - Periodic AGC resets restore the saved runtime setting; they do not replace it with the target's compile-time default. - Existing installations retain their previously saved value after an upgrade.

"},{"location":"cli_commands/#view-or-change-rx-duty-cycle-power-saving","title":"View or change RX duty-cycle power saving","text":"

Search terms: RX power saving, receiver sleep, radio power saving, RXPS.

Usage: - get radio.rxps - get radio.rxps.config - get radio.rxps.rfrx_disabled - get rxps.wd - set radio.rxps.rfrx_disabled <state> - set radio.rxps off - set radio.rxps on - set radio.rxps conservative - set radio.rxps balanced - set radio.rxps max - set radio.rxps max preamble <16|32> - set radio.rxps overdrive - set radio.rxps overdrive preamble <16|32> - set radio.rxps riskyWorkingMax - set radio.rxps riskyWorkingMax preamble <16|32> - set radio.rxps <1-10> - set radio.rxps level <1-10> - set radio.rxps level <1-10> preamble <16|32> - set radio.rxps <rx_us> <sleep_us>

Parameters: - rx_us, sleep_us: Receive and sleep durations in microseconds (1000-30000000). - level: Guarded levels 1-8, followed by experimental levels 9 and 10. Higher guarded levels allow more sleep. - preamble: RXPS timing assumption in symbols; 16 or 32. This does not change the radio's actual transmitted preamble. - state: on or off.

Notes: - get rxps.wd reports the radio watchdogs' soft and hard recovery counts. See receive calibration and recovery for noise sampling, CAD handling, and the continuous-RX mode check. - get radio.rxps.config adds the persisted level and preamble assumption to the on/off and timing values. Deployment tools use it to restore a level-based preference without converting it to fixed manual timings. - radio.rxps.rfrx_disabled is a runtime-only diagnostic setting and resets to off after reboot. - Its default off state keeps the host-controlled SX1262 receive path enabled during RX duty-cycle mode. Setting it to on reproduces the old missing-RF_RX behavior and can significantly reduce receive sensitivity, making remote commands harder to receive. - radio.rxps.rfrx_disabled is supported only on SX1262 targets with a host-controlled RX enable pin. - on and conservative select level 3, balanced selects level 6, and max selects level 8; each named profile assumes a 16-symbol preamble. - The guarded catch ladders for levels 1-8 are 15,14,13,12,11,10,9,8 symbols for preamble 16 and 24,20,16,14,12,10,9,8 for preamble 32 or longer. Generated sleep leaves another 0.2 symbols of margin. The receive timer covers the actual wire preamble, sync, explicit header, and one extra symbol. - overdrive selects level 9; riskyWorkingMax selects level 10. Both operate outside the guarded timer condition. Upstream bench measurements found packet loss at level 10; neither profile is a general guarantee of reliable delivery. They are never selected by automatic level adjustment. - Named experimental profiles accept preamble 32; numeric levels keep the existing automatic-preamble selection. Manual timings bypass the level model. - MC_TCXO_DELAY_US defaults to 1600 us. Initialization applies it and checks TCXO/calibration errors. A board can override it for its oscillator. - Fresh Cascade-profile builds start with RXPS on at level 8 and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom <rx_us> <sleep_us> timings remain fixed. - get radio.rxps keeps the legacy on/off, RX, and sleep reply. The new get radio.rxps.config reply adds the saved level and preamble assumption. Radio-change replies and the Full Companion terminal status report any effective level/preamble adjustment. Effective preamble 64 or 128 appears only when the active tuple's physical wire preamble has that length. - The selected mode is applied immediately, persisted, and restored after reboot.

"},{"location":"cli_commands/#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM receive-path gain state on supported boards","text":"

Usage: - get radio.fem.rxgain - set radio.fem.rxgain <state>

Parameters: - state: on|off

Notes: - This controls the external LoRa FEM receive-path LNA where the board supports it. - This is separate from radio.rxgain, which controls the radio chip receive gain mode. - On Station G3, transmit temporarily bypasses the LNA. A gain change made during transmission is applied when receive mode resumes; get reports the configured receive preference. Startup and terminal sleep keep the LNA off.

"},{"location":"cli_commands/#view-or-change-the-lora-fem-transmit-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM transmit-path gain state on supported boards","text":"

Usage: - get radio.fem.txgain - set radio.fem.txgain <state>

Parameters: - state: on|off

Notes: - This controls a software-selectable external LoRa FEM transmit gain where the board supports it. - Heltec T096 routes TX through its KCT8103L PA with no populated TX bypass. get radio.fem.txgain reports on (fixed TX PA); set radio.fem.txgain cannot toggle it. Use set tx <dBm> to adjust SX1262 drive. The PA's CSD control shuts down the FEM and is not a lower-gain TX mode. See the T096 schematic and KCT8103L product description. - On Station G3, remove the PA PL1 jumper to allow software control. on selects PA PL1 high/short and off selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. - Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. - The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. get reports the configured state, which may lead the hardware until the node next transmits.

"},{"location":"cli_commands/#select-the-station-g3-physical-radio-slot","title":"Select the Station G3 physical radio slot","text":"
get radio.slot\nset radio.slot auto\nset radio.slot 1\nset radio.slot 2\nreboot\n

Station G3 SX1262 builds support both physical RF sockets in the same image. The default, auto, probes both sockets at boot without transmitting. If both work, it prefers the last successfully used socket; on a fresh installation, it prefers socket 1. A forced 1 or 2 never silently uses the other socket.

get radio.slot reports the saved selection, active socket, detected sockets, and whether a reboot is needed. Changes are saved immediately but applied only after reboot, never during a transmission. The selector does not change keys, node settings, or the time-shared radio2 profile. The legacy _r2 build name means physical socket 2, not a motherboard revision; both build names now support this selector.

If no usable socket is found, startup waits with both LNAs disabled instead of reboot-looping. A limited USB recovery console accepts !g3 get radio.slot, !g3 set radio.slot auto|1|2, !g3 reboot, and !g3 help. The !g3 prefix is required in this recovery console; it is not the normal running CLI. KISS firmware uses binary slot commands during normal operation, documented in the KISS protocol.

If G2 firmware works on a G3 but the G3 image behaves differently, see the G2/G3 compatibility checks for the verified pin assignments, startup fixes, and saved amplifier settings.

"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"

Usage: - get name - set name <name>

Parameters: - name: Node name

Set by build flag: ADVERT_NAME

Default: Varies by board

Note: Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.

"},{"location":"cli_commands/#turn-companion-sound-and-vibration-on-or-off","title":"Turn Companion sound and vibration on or off","text":"

get buzzer and set buzzer on|off control the Companion's buzzer. Devices with a vibration motor also support get vibration and set vibration on|off. Changes take effect immediately, are saved across reboots, and produce no confirmation tone. Muting an output stops an alert already playing.

Fresh settings default to sound off and vibration on. Existing saved choices are retained. The same preferences are used by the device buttons and UI. Unsupported hardware reports an error.

These are the device mute controls. Programmable notify.sound and notify.vibration rules also respect them; an enabled rule cannot bypass a muted output. The notification output masks still select which alert outputs a rule may use. Explicit button mute/unmute confirmations retain their existing behavior.

Adapted from upstream PR #3603.

"},{"location":"cli_commands/#turn-indicator-leds-on-or-off","title":"Turn indicator LEDs on or off","text":"
get leds\nset leds off\nset leds on\n

Available in Companion, repeater, room-server, and sensor firmware. The setting is saved across reboots and defaults to on. off suppresses software-controlled transmit, Bluetooth, status, and notification LEDs. on restores their normal behavior; it does not leave every LED lit. A build with DISABLE_BLE_LED keeps the Bluetooth indicator disabled.

Supported outputs include ordinary and inverted GPIO LEDs, addressable RGB LEDs, and board-controlled expander LEDs on nRF52, ESP32, RP2040, and STM32 boards. Screen backlights and manually controlled torches keep their own controls. Power and charging LEDs wired directly to a charger cannot be switched off by firmware. Bootloader and early startup indicators may still appear before the saved setting is loaded.

This extends the SenseCAP Solar LED switch from upstream PR #3605. With LEDs enabled, RAK-family Companions blink their Bluetooth LED while advertising and leave it off once connected, following upstream PR #2694.

"},{"location":"cli_commands/#turn-wifi-esp-now-or-all-24-ghz-services-on-or-off","title":"Turn WiFi, ESP-NOW, or all 2.4 GHz services on or off","text":"
get wifi\nset wifi off\nset wifi on\nget espnow\nset espnow off\nset espnow on\nget 2.4ghz\nset 2.4ghz off\nset 2.4ghz on\nset 2.4ghz on all\n
Control Effect wifi Infrastructure WiFi, setup AP, TCP/WebUI, and WiFi MQTT services. ESP-NOW remains available. espnow The ESP-NOW bridge or primary ESP-NOW mesh transport in this build. Infrastructure WiFi remains available. 2.4ghz off Stop WiFi, Bluetooth, and ESP-NOW; remember which were enabled. LoRa and wired transports remain available. 2.4ghz on Restore the states saved by the first off. Repeated off preserves that snapshot. With no previous off, keep the current states. 2.4ghz on all Enable every service available in this build and boot. 2.4ghz on force is an alias.

These commands work in Companion, repeater, and room-server firmware where the services are available. Changes last until reboot; saved WiFi credentials, Bluetooth preferences, and bridge settings stay intact. get 2.4ghz lists each service as on, off, or unavailable. An nRF52 Companion controls its Bluetooth and reports WiFi/ESP-NOW as unavailable. Exclusive WiFi/Bluetooth builds retain their boot transport restriction; on all cannot reclaim a released Bluetooth stack.

An off command requires a management connection that will survive it. Bluetooth can provide the fallback for wifi off; USB, Ethernet, or a CLI request over LoRa can provide it for 2.4ghz off. WiFi association, a listening socket, and USB power alone do not count. On USB hardware without an observable open-client session, send the command through USB itself. To bypass the guard:

set wifi off force\nset espnow off force\nset 2.4ghz off force\n

Shutdown starts after the reply has had time to drain (normally 250 ms, up to two seconds for pending transport I/O). Network services stop before the shared WiFi driver. A normal shutdown is cancelled if its fallback connection disappears before it starts. The get commands report pending changes and startup/teardown failures. After 2.4ghz off, use 2.4ghz on or on all before enabling an individual service. The all/force suffix on on applies only to 2.4ghz.

WiFi and ESP-NOW share one hardware channel. A running ESP-NOW bridge pins setup and station connections to bridge.channel; starting the bridge alongside an existing WiFi connection requires that same channel. A mismatch reports a failed start and preserves the WiFi connection.

"},{"location":"cli_commands/#turn-bluetooth-on-or-off-companion","title":"Turn Bluetooth on or off (Companion)","text":"
get bluetooth\nset bluetooth off\nset bluetooth off force\nset bluetooth on\n

get ble and set ble ... are aliases. This controls the running Bluetooth service on ESP32 and nRF52 Companions: off stops advertising and disconnects Bluetooth clients; on enables it again. Both choices are saved and survive reboots. USB remains available when Bluetooth is off. Older saved preferences without this setting retain the previous default of Bluetooth on.

Successful local commands report (saved). If saving fails, the command reports an error and leaves Bluetooth unchanged. When requested over Bluetooth, off first reports (save pending) so its reply can drain; the firmware rechecks the alternate connection and saves the preference before disconnecting. If that connection disappears or the save fails, shutdown is cancelled. get bluetooth reports the current runtime state. Broad 2.4ghz controls still apply to the current boot only and do not change this saved Bluetooth preference.

Plain off requires another active management connection. A connected USB terminal or binary client, TCP client, or browser terminal can provide that connection. WiFi association, a listening server, and USB power alone do not. On USB hardware without an observable open-host session (HWCDC or a UART bridge), issue the command through that USB client so the request itself proves it is active.

Append force to off to permit disconnecting the only active connection. Use USB or another management transport and set bluetooth on to regain access afterward. Reboot alone will not re-enable saved-off Bluetooth. force is accepted only as the final argument to off.

Commands sent through Bluetooth allow its reply to drain before shutdown (normally 250 ms, at most two seconds). get bluetooth shows off pending during that wait. A normal shutdown is cancelled if the other management connection disappears before it applies; force bypasses that check too.

Bluetooth must be available in the current build and boot transport mode. An exclusive-WiFi boot still requires set companion.transport ble and a reboot before Bluetooth can be enabled.

"},{"location":"cli_commands/#view-or-change-the-independent-bluetooth-name-companion","title":"View or change the independent Bluetooth name (Companion)","text":"

Usage:

get ble.name and set ble.name ... are accepted aliases. This command is specific to Companion firmware. The default is MeshCore-<advert name>; default (or clear) removes a custom override. Names are limited to 31 valid UTF-8 bytes without control characters and take effect after reboot.

A binary Companion client should carry the same text in command 0x42 (CMD_RUN_CLI_COMMAND), which works over USB, BLE, or TCP without entering USB terminal mode. See Companion radio binary protocol for the frame and reply format.

"},{"location":"cli_commands/#set-the-bluetooth-pairing-pin-companion","title":"Set the Bluetooth pairing PIN (Companion)","text":"

Usage:

Choose a six-digit code for a fixed Bluetooth pairing PIN. For example:

set pin 654321\nreboot\n

The command saves the PIN across reboots and replies > pin is now 654321. Reboot to activate the new code; the current Bluetooth session keeps its existing PIN until then. A failed save reports an error and retains the previous saved setting.

set pin 0 clears the saved override, rather than setting the pairing code to 000000 or disabling Bluetooth. After reboot, the build's default policy applies: normally 123456 without a display, or a generated PIN on eligible display-equipped builds.

This command is available in the Companion text terminal, binary command 0x42 (CMD_RUN_CLI_COMMAND), and authorized LoRa CLI; a Bluetooth-capable build is needed to use the PIN. Existing bonded clients may reconnect without being asked for a PIN. Forget the device on the phone and pair again if you need to enter the new code.

"},{"location":"cli_commands/#view-or-change-the-bluetooth-address-ble-companion","title":"View or change the Bluetooth address (BLE Companion)","text":"

Usage:

get ble.mac and set ble.mac ... are accepted aliases. The setting is available on every Companion build that includes Bluetooth.

Value Result after reboot C2:11:22:33:44:55 Save and use that custom address. random Generate one random address now, save it, and reuse it on later boots. random-every-boot Generate a new address once at each boot. random everyboot is also accepted. random-after-connect Keep one random address through unused boots. After an authenticated connection, arm a new address for the next boot. default or clear Remove the override and use the chipset's factory Bluetooth address.

A custom value must be a valid BLE random-static address in XX:XX:XX:XX:XX:XX form. Its first byte must be from C0 through FF, and you are responsible for keeping addresses unique. Hyphens are also accepted on input; status output uses uppercase colons.

Every change requires a reboot and invalidates the phone's previous view of the device. Forget the old device entry and pair again. random-every-boot also clears the Companion's saved peer bonds during each startup, so pairing and PIN entry are expected again after every reboot.

random-after-connect keeps the same address across any number of reboots when nobody successfully authenticated. As soon as an authenticated BLE connection succeeds, the next boot rotates the address and clears stale bonds. The marker is saved while connected, so a normal reboot, power switch, or battery pull after that connection all produce the same next-boot rotation.

The address setting does not enable or disable stealth. Use the separate bluetooth.stealth flag below. default changes only the address policy.

"},{"location":"cli_commands/#view-or-change-bluetooth-stealth-ble-companion","title":"View or change Bluetooth stealth (BLE Companion)","text":"
get bluetooth.stealth\nset bluetooth.stealth on\nset bluetooth.stealth off\n

get ble.stealth and set ble.stealth on|off are short aliases. This flag defaults to off and is independent of the Bluetooth address policy. Enabling or disabling it keeps the configured custom/random/factory address policy unchanged and requires a reboot. Repeating on keeps an existing stealth pairing; repeating off does not alter address settings.

For a custom address with stealth:

set bluetooth.mac C2:11:22:33:44:55\nset bluetooth.stealth on\nreboot\n

Use set bluetooth.mac random instead of the first line for one saved random address. Both combinations keep their address and bond across later boots. Rotating policies still rotate: random-every-boot reopens pairing at every boot, while random-after-connect reopens it on the first boot after an authenticated connection. Stealth stays enabled; each new address begins with a fresh pairing window so it cannot get hidden behind an unusable bond.

With stealth on, the node is normally discoverable until the first authenticated pairing. After that, nRF52 uses directed advertising to the saved peer. ESP32 uses a minimal unnamed advertisement and a controller allowlist because its common Arduino BLE API does not expose the directed-advertising peer field. Both reject connection and scan requests from other peers. BLE cannot be completely radio silent and still accept a connection, so a passive radio sniffer may still observe packets. To deliberately clear the saved stealth peer, send set bluetooth.stealth off, then set bluetooth.stealth on, then reboot over USB or an existing authenticated connection. To leave stealth, set it off and reboot. If the saved node-side bond is lost, recovery reopens pairing. The former set bluetooth.mac stealth command is no longer accepted.

This changes only the running MeshCore Companion application. On nRF52, the OTAFIX bootloader still uses its own address, which is visible only while the board is in BLE DFU/recovery mode. USB and LoRa updates are unaffected. Bluetooth update tools must follow that separate bootloader address after the handoff; they must not assume that a custom application address remains the DFU address. See the hardware observations for the tested XIAO handoff and host-cache caveats.

"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"

Usage: - get lat - set lat <degrees>

Set by build flag: ADVERT_LAT

Default: 0

Parameters: - degrees: Latitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"

Usage: - get lon - set lon <degrees>

Set by build flag: ADVERT_LON

Default: 0

Parameters: - degrees: Longitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"

Usage: - get prv.key - set prv.key <private_key>

Parameters: - private_key: MeshCore expanded private key in hex format (128 hex characters)

Local connection only: get prv.key. Companion also requires ENABLE_PRIVATE_KEY_EXPORT=1; use binary command 0x42 or the text terminal. Infrastructure set prv.key retains its administrator access rules.

Infrastructure repeaters with ENABLE_PRIVATE_KEY_EXPORT=1 also support backup prv.key <16-hex-nonce> for authenticated remote administrators. It returns KEYBACKUP <nonce> <128-hex-private-key> only as an end-to-end encrypted direct CLI reply. This separate command does not make get prv.key remotely available. Treat the reply as a secret: clients must not put it in chat history or debug logs and should encrypt any saved backup. An administrator password alone is not a safe backup-file encryption key. Firmware does not retain this reply in its remote CLI retry cache. Companion firmware must advertise get key.backup.transport as ephemeral-routed-v1 before the phone requests it; that build delivers the reply only to the live attached client that requested it, without offline queuing or terminal echo. Older Companion firmware is not a safe transport for this backup flow.

Note: Requires reboot to take effect after setting

"},{"location":"cli_commands/#view-or-change-this-nodes-admin-password","title":"View or change this node's admin password","text":"

Usage: - get password - password <new_password>

Parameters: - new_password: New admin password (on ESP32, 1\u201315 UTF-8 bytes without CR or LF)

Set by build flag: ADMIN_PASSWORD

Default: password

get password returns the current admin password only on a local connection: USB, Ethernet, or the authenticated LAN browser CLI. It is rejected over LoRa. ESP32 WiFi Companion instead reports management-password status without revealing the secret. Its password is stored in NVS, survives a filesystem erase, and defaults to password. Other Companion builds retain their existing no-admin response. This command does not report the Bluetooth PIN.

On ESP32, empty and overlong new passwords are rejected rather than truncated. Leading spaces are part of the password. For ESP32 setup WiFi protection, choose 8\u201315 printable ASCII characters; other accepted passwords use an open hotspot with an HTTP admin login. WebConfig and runtime browser OTA share this admin credential (OTA's HTTP Basic username is admin).

A confirmed password change invalidates existing browser sessions. The page that submitted it keeps a save receipt, tries the new password once, and reads the same result again after login; it does not resend commands. If another setting fails, the result remains visible and reboot is withheld. An unconfirmed password save keeps the previous runtime password active, but the saved state after reboot may differ; the error reports this uncertainty.

Trusted TCP consoles and delegated remote CLI remain administrative paths and can change this password. The HTTP credential does not protect those ports or replace radio ACL permissions.

Note: The password setter replies with confirmation.

Note: Any node using this password will be added to the admin ACL list.

"},{"location":"cli_commands/#enable-or-disable-repeater-guest-logins","title":"Enable or disable repeater guest logins","text":"

Availability: Repeater firmware. Local administrative consoles and authenticated remote admins can use these commands.

get guest\nset guest on\nset guest off\n

Default: off, including when upgrading from firmware without this setting. The choice is saved and takes effect without a reboot. set guest off also removes existing guest sessions immediately. Turning it back on requires guests to log in again; admin and assigned ACL access are unaffected.

When enabled, guests must supply the configured guest.password. Its default is blank, so enabling guests without setting a password permits passwordless guest access. A guest can read repeater status and basic telemetry but cannot run administrative commands. This setting does not disable ordinary packet forwarding or public discovery queries.

For example:

set guest.password a-private-code\nset guest on\nget guests\nset guest off\n

Room-server login settings are unchanged.

"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"

Usage: - get guest.password - set guest.password <password>

Parameters: - password: Guest password

Set by build flag: ROOM_PASSWORD (Room Server only)

Default: <blank>

Repeater guest logins must also be enabled with set guest on. Changing this password takes effect immediately; use set guest off to remove existing guest sessions. Setting the password to off makes it the literal password off.

"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"

Usage: - get owner.info - set owner.info <text>

Parameters: - text: Owner information text

Default: <blank>

Note: | characters are translated to newlines

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#calibrate-the-battery-reading-against-a-meter","title":"Calibrate the battery reading against a meter","text":"

Availability: Repeater, Room Server, Sensor, Companion and Terminal Chat firmware on boards with a supported battery ADC. Use an administrative CLI connection; infrastructure roles also accept authenticated remote commands.

battery calibrate 4.18\nget adc.multiplier\nbattery calibrate reset\n

Measure the battery voltage with a voltmeter and enter that reading in volts. The firmware calculates new multiplier = current multiplier * meter voltage / reported voltage, applies it and saves it for restart. A reference is required; the command does not assume a chemistry or a 4.2 V full battery. Enter the voltage of the battery input the board actually measures, including pack voltage where that input supports a pack. For example, battery calibrate 13.2 supplies a 13.200 V reference; it does not change the board's measurement range.

Calibration supports the ESP32, nRF52, RP2040 and STM32 boards with implemented battery-voltage ADC conversions, including the Wio Tracker L2's external ADS1115. An unsupported board or unavailable/saturated reading returns an error. Digital PMU/fuel-gauge and battery-module readings retain their hardware scaling; boards without a battery-voltage input cannot be calibrated through this command.

The meter reference must be within 50% of the reported voltage. Newly supported ADCs accept a relative multiplier from 0.500 to 1.500, where 1.000 retains their original conversion. Existing board-specific absolute multipliers and saved values keep their established meaning. The five legacy absolute drivers are Heltec V4, V4 R8, RC32, Meshnology W12 and ProMicro.

battery calibrate reset restores and saves the board's default conversion. set adc.multiplier 0 also resets it. A successful numeric calibration reply shows the effective multiplier, including any rounding by the board. A failed save reports an error and attempts to restore the previous runtime setting.

Calibration corrects voltage reporting and the battery percentages/protection that use that reading. On nRF52 boards with voltage policy it also applies to the boot reading and running cutoff. It does not adjust charger hardware or create a current measurement. This is a single-point scale correction, not an ADC offset or nonlinear calibration.

"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"

Usage: - get adc.multiplier - set adc.multiplier <value>

Parameters: - value: ADC multiplier (0.0-10.0)

Default: 0.0 (value defined by board)

Note: On newly supported ADC boards this is a relative 0.500-1.500 correction, with 0 restoring the default. Existing absolute-multiplier boards retain their coefficient units and accept 0 or a positive value up to 10. On nRF52 boards with power management and a boot-voltage setting, this is a saved relative calibration: 1.000 keeps the board conversion, 1.050 raises all reported battery millivolts by 5%, and 0 resets to 1.000. The allowed range is 0.500-1.500. This affects boot protection, running cutoff, telemetry, battery alerts, and percentage. Calibrate against a voltmeter before enabling a low cutoff. Boards without a supported ADC return unsupported.

"},{"location":"cli_commands/#send-a-repeater-flood-text","title":"Send a repeater flood text","text":"

Usage: - send text.flood <message>

Parameters: - message: Text to send to the shared #repeaters flood channel, prefixed with this node's name. Any : in the node name is sent as ; so the prefix delimiter stays unambiguous.

Example:

send text.flood checking ridge link\n
"},{"location":"cli_commands/#view-or-change-battery-alert-state","title":"View or change battery alert state","text":"

Usage: - get battery.alert - get battery.alert.region - set battery.alert on [region] - set battery.alert off

Parameters: - region: Optional named region scope. When omitted, the repeater selects the single deepest (most narrow) named region in the configured hierarchy. If multiple regions tie for deepest, specify one explicitly.

Defaults: - battery.alert: off - battery.alert.region: <unset>

Notes: - Enabling fails until at least one usable named region is defined. Alerts are never sent as unscoped floods. If the selected region is later removed, alerts stop until battery alerts are enabled again with a valid region. - Region hierarchy edits are not persistent until region save is run. After region def west pnw wa w-wa sea, run region save before enabling the alert if the hierarchy must survive a reboot. - A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected. - The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the #repeaters channel in the selected region. - Once an alert finishes transmitting, another battery alert is suppressed for at least 12 hours. A queue rejection, stale-queue drop, or radio send failure does not start the cooldown. Battery recovery or toggling alerts off and back on does not bypass a completed alert's cooldown during the same boot. - With region def west pnw wa w-wa sea, set battery.alert on selects sea; set battery.alert on w-wa overrides that default. - get battery.alert.region returns the selected scope, for example > sea. - The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a pending alert keeps the repeater awake until the packet is handled.

"},{"location":"cli_commands/#view-or-change-battery-alert-thresholds","title":"View or change battery alert thresholds","text":"

Usage: - get battery.alert.low - set battery.alert.low <1-100> - get battery.alert.critical - set battery.alert.critical <0-99>

Defaults: - battery.alert.low: 20 - battery.alert.critical: 10

Note: The low threshold must be greater than the critical threshold. Alerts at or below the critical threshold use CRITICAL BATTERY in the message; both severities use the same 12-hour resend cooldown.

"},{"location":"cli_commands/#pause-forwarding-when-the-battery-is-low","title":"Pause forwarding when the battery is low","text":"

Search terms: low battery repeating, battery flood cutoff, save battery, automatic repeat off.

Repeater, Room Server, and Sensor firmware can automatically pause forwarding below a configured battery percentage. Both controls default to off, including when upgrading. They are independent of battery alerts.

wio-e5-mini_repeater omits these controls and the adjustable battery voltage range below to fit its application partition. Battery alerts and basic battery readings remain available.

Usage:

set repeat.battery on enables the saved repeat threshold, initially 10%. set flood.battery on enables the saved flood threshold, initially 20%. A number both enables the control and sets its threshold. off disables it while retaining that threshold for the next on. Changes are saved and take effect without a reboot.

Below the repeat threshold, the node stops LoRa retransmissions of received traffic. Below the flood threshold, it stops LoRa retransmissions of received flood traffic but may still relay routed traffic if repeating is otherwise allowed. Neither control stops the node's own transmissions or replies to administration requests. The saved manual repeat flag, flood limits, and forwarding rules remain unchanged; a battery recovery does not override a manual restriction.

Received packets entering from a bridge are subject to the same LoRa transmit limits. Exporting radio reception to WiFi, ESP-NOW, or RS232 is unchanged.

Forwarding resumes automatically after the measured percentage reaches two percentage points above the applicable threshold, capped at 100%. With the initial thresholds, flood forwarding pauses below 20% and resumes at 22%; all relay forwarding pauses below 10% and resumes at 12%. This recovery margin avoids repeatedly switching forwarding on and off near a threshold.

Battery voltage is sampled every 30 seconds while either control is enabled, and immediately when its configuration changes. get battery.forward reports the voltage, estimated percentage, whether the reading is valid, and which battery controls are currently suppressing forwarding. A missing or invalid reading does not suppress forwarding: accepted readings are 2000-20000 mV. Voltage alone does not establish that a battery is physically connected.

To enable both default thresholds:

set repeat.battery on\nset flood.battery on\nget battery.forward\n

To choose different thresholds and later disable the feature:

set repeat.battery 15\nset flood.battery 25\nset repeat.battery off\nset flood.battery off\n
"},{"location":"cli_commands/#set-the-battery-percentage-voltage-range","title":"Set the battery percentage voltage range","text":"

The forwarding thresholds use a voltage-derived percentage. On infrastructure builds without the nRF52 voltage policy, the saved range is adjustable with:

get battery.empty\nget battery.full\nset battery.empty 3000\nset battery.full 4200\n

Values are millivolts, from 2000 to 20000, with full at least 100 mV above empty. The default range comes from the board's compiled battery endpoints, normally 3000 mV for 0% and 4200 mV for 100%; boards with a two-cell pack retain their compiled pack range. The percentage is interpolated between those endpoints and limited to 0-100%. These settings describe the battery range; they do not calibrate the voltage measurement or alter a charger.

Supported nRF52 power-management builds use their existing saved battery.profile, battery.empty, and battery.full values instead. Their endpoint range remains 2000-4200 mV, with at least 100 mV between endpoints. See nRF52 battery protection for profiles and their separate boot-lock and cutoff effects. set adc.multiplier adjusts voltage calibration on boards that support it; see battery reading calibration.

Percentage remains an estimate: a voltage reading under transmit load may differ from an idle reading. Check the reported voltage against a voltmeter before relying on automatic forwarding limits.

"},{"location":"cli_commands/#enable-or-disable-the-rx-inactivity-watchdog-repeater-only","title":"Enable or disable the RX inactivity watchdog (Repeater Only)","text":"

Usage: - get rx.watchdog - set rx.watchdog on - set rx.watchdog off

Default: on, with a 24-hour no-RX timeout in normal mode.

Notes: - In normal mode, the repeater reboots after 24 hours without a received packet passing MeshCore parsing. Each such reception restarts this timeout; checks use a sliding deadline rather than waiting for another 24-hour checkpoint. - While tempradio or tempradioat is running, windows of at least 12 hours use a 12-hour no-RX timeout. Shorter windows disable this reboot watchdog for the duration of the session. This temporary policy also overrides a saved normal-mode off setting. - Startup, a successful temporary-radio activation/replacement, expiry, cancellation, or manually changing this setting starts a fresh observation window. Expiry is processed before the watchdog, so a temporary window ending exactly at its watchdog deadline restores normal timing first. - get rx.watchdog reports both the effective on/off state and timeout or short-session disabled reason. set rx.watchdog on|off saves the normal-mode preference; temporary-session rules remain active until that session ends. Existing saved preferences are retained when upgrading; the new default applies to fresh settings or older preference files without this field. - The check uses the last successful MeshCore parse time, recorded before receive delays and routing filters. It does not require the packet to be addressed to this repeater or its encrypted payload to be decrypted; duplicate receptions also count. This checks MeshCore packet structure, not sender authentication. - It does not poll, sample, or wake the radio or CPU. A due check waits for the next normal loop/wake. With RX power saving enabled, qualifying packets received during normal listening windows count as activity; the watchdog does not alter the RX/sleep timing. - Transmitting, radio interrupts, and raw LoRa packets rejected by the MeshCore parser do not reset the timer. Packets dropped before parsing because the packet pool is full do not count either. This whole-board reboot setting is separate from the automatic radio-only recovery watchdog, which still measures successful raw radio reads.

"},{"location":"cli_commands/#enable-or-disable-the-nrf52-system-watchdog","title":"Enable or disable the nRF52 system watchdog","text":"

Usage: - get system.watchdog - set system.watchdog on - set system.watchdog off

Default: on

Notes: - This nRF52-only hardware watchdog resets the device if the application loop stops for 60 seconds, including an indefinite SoftDevice flash-write wait. - Enabling takes effect without a reboot. - The nRF52 hardware cannot stop a watchdog after it has started. Disabling is persisted immediately, then the current firmware stops feeding it so the board performs one watchdog restart within 60 seconds. It remains off after that restart. - This setting is nRF52-only. The SoftDevice flash deadlock does not apply to ESP32, whose existing watchdog behavior is unchanged.

"},{"location":"cli_commands/#estimate-and-correct-infrastructure-node-time-after-startup","title":"Estimate and correct infrastructure-node time after startup","text":"

Usage: - get clock.sync - get clock.sync.status - get clock.sync.status.table - get clock.sync.status.<1-16> - get clock.sync.mesh - set clock.sync.mesh <on|off> - get clock.sync.mesh.edge - set clock.sync.mesh.edge <on|off> - clock.sync.mesh now - get clock.sync.internet - set clock.sync.internet <on|off> - get clock.sync.drift - set clock.sync.drift <30-86400> - get clock.sync.samples - set clock.sync.samples <3-16>

Defaults: - clock.sync.mesh: on for all repeater, sensor, and room-server builds - clock.sync.mesh.edge: on - clock.sync.internet: off - clock.sync.drift: 600 seconds (10 minutes) - clock.sync.samples: 9

When either source is enabled, the node makes its first clock-bootstrap attempt after 30 minutes of uptime, or immediately when the configured number of fresh evidence sources has been collected, whichever comes first. A successful estimate changes the RTC only when the absolute difference is greater than clock.sync.drift; correction can move the clock forward or backward. A valid estimate within the threshold counts as a successful sync without changing the clock. Seven days after each successful estimate, the node evaluates time again; the seven-day deadline therefore starts from the last successful estimate rather than from boot. This is a lazy uptime deadline: the check runs on the first normal loop/wake after it becomes due and does not wake the device by itself. If no source or consensus is available, the node retries every 30 minutes, and newly collected evidence triggers another immediate evaluation once the configured source count is present. Every reboot starts with the initial bootstrap attempt. An existing saved setting always overrides the platform default.

clock.sync.mesh now bypasses the startup/seven-day deadline and queues a LoRa-only consensus evaluation on the next normal loop, even when the internet source is also enabled. It uses any currently fresh samples without clearing the 16-slot table. If there is not yet enough evidence, mesh collection remains open and the next attempt follows the normal 30-minute retry. The command requires clock.sync.mesh to be on and does not bypass CLI, GPS, or NTP suppression; it also retains the normal quorum, timestamp-validity, and drift checks. The separate clock command only displays the current RTC and does not request a synchronization attempt.

clock.sync.mesh collects signature-verified advert timestamps and MAC-valid, decrypted Public-channel plain-text timestamps. In normal path mode, collection occurs only after the packet passes every forwarding filter. Sources are deduplicated by advert public key or case-insensitive Public-channel display name. Every fresh sample must also have a different full received path; all direct, zero-hop receptions count as the same empty path. This prevents repeated packets or multiple names arriving over one route from increasing the vote count.

For a node at the edge of the network where every packet arrives through one relay path, set clock.sync.mesh.edge on changes the evidence requirement from distinct receive paths to distinct sources. Signature-verified adverts are deduplicated by public key, and Public-channel timestamps are deduplicated by case-insensitive display name. Repeated packets from one source still count once. Edge mode observes this verified evidence on the receive path, independently of the forwarding decision, so repeat off and forwarding filters do not prevent clock collection. Other packet types are observed normally but cannot be clock evidence because they do not provide a suitable authenticated Unix timestamp. Changing edge mode clears the in-memory sample table so evidence collected under the other policy is not reused. The setting is persistent and defaults on.

At least the configured number of distinct fresh evidence sources (nine by default) and a strict majority of all fresh samples must fall within ten minutes of the median. In normal mode each source must use a distinct receive path. In edge mode signed adverts are distinct by public key and Public-channel messages are distinct by display name, but all may use the same receive path. The effective quorum is therefore the larger of clock.sync.samples and half the fresh sample count plus one. For example, a 9-vs-7 split can succeed but an 8-vs-8 split cannot. The median is used. clock.sync.samples accepts 3 through 16; samples older than two hours are ignored. Status reports mode=paths or mode=edge and labels the collected evidence as paths or sources. It reports reason=need-more-paths or reason=need-more-sources when fewer than the configured number exist, and reason=no-consensus when enough evidence exists but the effective quorum does not agree. Mesh collection begins immediately after boot. Following a successful estimate, it resumes two hours before the next seven-day deadline so only evidence that can still be fresh at evaluation time is processed.

get clock.sync.status reports whether the clock was set and a reason when it was not. Common reasons include waiting-deadline, need-more-sources, need-more-paths, no-consensus, within-drift, mesh-off, and suppression by CLI, GPS, or internet time. It also reports whether collection is active, the fresh evidence count, the number of occupied table slots, and the next evaluation deadline.

get clock.sync.status.table shows the active sample table in compact form. Each item is slot:type:id-prefix:age, where type A is a signed advert and type P is a Public-channel message. A trailing ! marks a stale sample. If the compact reply is truncated, query any slot with get clock.sync.status.<1-16>. The detail view reports the full source and path hashes, age-adjusted epoch, difference from the local clock, and freshness.

A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by build.sh and that time plus ten calendar years. Direct developer builds that bypass build.sh fall back to the compiler timestamp. Validation happens before a slot is selected or written, so an advert or Public-channel timestamp outside that window is not recorded as a clock sample.

Before voting, the node advances each packet timestamp by an estimated transit time. The estimate sums the radio airtime at the original packet length and at each progressively longer relay length, plus the expected midpoint of the random flood-forward delay at every prior hop. The normal elapsed time since the radio recorded local receipt is then added when consensus is evaluated, so local signature/decryption/filter processing time is included too. This is better than using hop count alone because LoRa airtime changes with packet length and radio settings. Transit compensation is capped at the ten-minute consensus window. It cannot know sender queueing, channel contention, or a remote relay's non-matching txdelay, so the consensus window and median still absorb residual error.

If a clock sync or time <epoch> CLI command successfully sets the clock, or a GPS provider writes a valid GPS time, LoRa-derived clock collection and correction are suppressed for the rest of that boot. Turning clock.sync.mesh off and back on does not clear this safety latch; only a reboot does. get clock.sync.mesh and get clock.sync.status report whether CLI or GPS time caused the suppression. On a WiFi MQTT build, a successful NTP sync is authoritative and also suppresses LoRa correction for the rest of that boot. Source selection starts fresh after a reboot, so LoRa remains the fallback when NTP cannot obtain internet time during that boot.

Public-channel display names are not authenticated and can be spoofed. Received path hashes are also truncated, unauthenticated routing hints; requiring unique paths prevents ordinary duplicate-route inflation but is not a cryptographic identity check. Signed adverts authenticate the advert contents but do not prove that the advertising node's own clock is correct. Mesh time is therefore a consensus estimate, not an authoritative time service. Edge mode intentionally gives up receive-path diversity. Public-channel display names can be spoofed, so one sender can claim multiple names and inflate the edge-mode vote count.

clock.sync.internet is available on WiFi MQTT repeater-observer builds. Its initial and seven-day queries run on the MQTT/WiFi task and are read-only until the repeater applies the configured drift test. Failed queries retry after 30 minutes. On other infrastructure-node builds, the preference can be stored but status reports that internet time is unavailable. MQTT builds retain their existing startup NTP behavior required for MQTT/TLS/JWT operation; this setting controls the additional delayed drift checks. Startup NTP is always preferred when it succeeds, regardless of this setting.

Sensor and room-server builds support mesh clock consensus and report clock.sync.internet as unavailable. Changing any clock.sync.* setting starts a new attempt for the current boot. Settings are persistent in /clock_sync; samples and schedule state are not.

A backward correction is intentionally allowed, but peers that already recorded a later timestamp from this node may temporarily reject its lower timestamps as replays until corrected time passes the previously observed value.

Example:

set clock.sync.drift 600\nset clock.sync.samples 3\nset clock.sync.mesh on\nset clock.sync.mesh.edge on\nclock.sync.mesh now\nget clock.sync.status\n
"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"

Usage: get public.key

Returns this node's identity public key without changing it or exposing its private key. Companion supports the query through its text terminal and framed CLI command 0x42 (CMD_RUN_CLI_COMMAND); its reply is > followed by exactly 64 uppercase hexadecimal characters. Private-key export support is not required. On nRF52 Full Companion, send the query to control interface 00, not the output-only logging interface 02.

"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"

Usage: ver

"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"

Usage: get role

"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag","title":"View or change this node's power saving flag","text":"

Search terms: battery saver, device power saving, low power mode.

Usage: - get powersaving - set powersaving on - set powersaving off

Parameters: - on: enable power saving - off: disable power saving

Default: on for fresh Cascade-profile builds and Companion firmware; off for other infrastructure profiles

Note: get powersaving, set powersaving on, and set powersaving off are shared by Companion and infrastructure. The saved preference controls whether power saving is allowed; active USB, logging and network services may keep the hardware awake. Actual sleep depends on the board.

On ESP32 infrastructure boards using an external USB-to-UART chip, such as the Heltec V3's CP2102, the native USB host guard cannot detect an attached computer. Current source blocks MCU light sleep while the raw UART console driver is enabled, preserving the first web-console command after idle even with all bridges and USB logging stopped. This guard has no idle timeout and also covers a computer's first connection after boot. These builds enable that console at startup, so MCU light sleep is unavailable while it remains enabled, including on battery. set powersaving on still enables sensor/GPS power-saving policies; the CPU continues normal idle yielding while keeping the console available. A running MQTT, RS232 or ESP-NOW bridge keeps infrastructure awake even when get powersaving reports on; that readback alone does not prove MCU sleep. Negotiated RS232 standby can stop an unresponsive UART owner without changing its saved enabled flag; see rs232.autosleep below. Other sleep blockers still apply. Adding UART wake alone would still discard the characters that trigger wake, so it cannot preserve an unmodified web console's first command. See the ESP-IDF UART wake documentation.

For the 1.17.1.5 G3 USB-disconnect report, use set powersaving off as the workaround. The released ESP32 sleep code can lose native USB after two minutes when the terminal is closed, even with a computer attached. The source fix blocks ESP32 light sleep while a native USB host is attached or live USB logging is enabled; ordinary CPU idle/yield remains available. The G3 button also wakes the device for at least two minutes. See the G3 release note for verification steps and the distinction between the fix and the published binaries.

Companion firmware defaults this setting to on. Full Companion accepts the command from its local USB terminal and exposes the same setting in WebConfig. On ESP32, it lowers the CPU clock to 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. USB and each active wireless transport remain available; SenseCAP Indicator Full keeps only its selected BLE or infrastructure-WiFi secondary transport active. set powersaving off restores the board's normal CPU clock and disables the GPS duty cycle. This device setting is separate from LoRa RXPS (radio.rxps) and WiFi modem power save (wifi.powersave). Infrastructure uses the same commands; its hardware and active-service sleep guards determine when the node can sleep.

"},{"location":"cli_commands/#diagnose-what-prevents-power-saving","title":"Diagnose what prevents power saving","text":"

Companion, Repeater, Room Server and Sensor provide on-demand diagnostics:

set powersaving.diag on\nget powersaving.diag\nget powersaving.diag calls\nget powersaving.diag time\nget powersaving.diag usb.session\nget powersaving.diag bridge\nget powersaving.diag 3\nset powersaving.diag off\nclear powersaving.diag\n

Diagnostics default to off and use only RAM. They do not change the saved power-saving flag, USB/DTR behavior, radio settings, logging or bridge state, and are lost on reboot. on starts a new window after off; repeated on keeps the current window. off freezes the counters for later inspection. clear resets them without changing whether collection is enabled. There is no automatic diagnostic log stream or flash write.

The default/state page shows the last actual sleep decision (why, mask), its age, reasons seen, total blocked time and eligible time in milliseconds. Before the first observed decision it reports unobserved. Pages 3 through 8 list every reason's transition count and blocked milliseconds; use an exact reason name for a separate readback. Role guards preserve their normal short-circuit order, so they report the first guard actually checked that prevented sleep, not an exhaustive simultaneous inventory. Inner board vetoes can add another reason. Reason times can overlap; blocked_ms on the state page counts their union instead of adding them twice.

Reasons include policy.off, startup, usb.session, usb.logging, usb.recovery, wireless, button, morse, radio2, ota, calibration, radio.watchdog, queue, retry, scheduled, serial, bridge, bridge.cleanup, bridge.wake, network, mesh, tracker, board and no.wake. Not every board/role has every guard. mesh includes pending protocol/storage work; scheduled can include a deadline too close for the existing whole-second sleep policy. RS232 merely being compiled or saved as enabled is not a blocker while its UART is suspended; running, cleanup and due wake are distinguished where those guards apply. USB power from a charger is not a host session. Logging enabled alone is not an nRF52 MCU sleep veto; on ESP32, the existing logging and raw-console guards can veto light sleep.

ESP32 pages 9 and 10 count SDK call entries in sixteen 16-second buckets starting at start_ms. They also report entries outside the first 256 seconds and flags for outside entries (1), saturation (2) or missing timestamps (4). Use a whole bucket contained within an independently timed interval to prove SDK calls occurred during that interval; whole-window totals are insufficient. See power-saving diagnostics for the timing limits.

The calls page counts actual instrumented wait calls, returns, short returns, board vetoes and SDK errors. time reports time spent inside those wait calls in microseconds, including SDK and interrupt work, not CPU sleep residency or electrical current. Returns under 100 us are marked short, not failed. An eligible decision with many short calls can indicate wake/event activity; an eligible ESP32 automatic-sleep path may yield without a direct instrumented wait, so zero calls alone is not proof of a fault. detail records the last board veto/error detail (SDK error code, or board-specific guard detail). Counters saturate instead of wrapping; observation timestamps must be sampled within one millis() wrap (about 49 days).

Opening USB to read the results can itself add a host/serial blocker. For a battery test, enable collection, close the terminal, wait, then read the accumulated pages. Stop collection promptly if preserving that window. A meter is still needed to diagnose radio RX current, RXPS duty cycle, FEM/LNA gain, GPS, displays, regulator losses and other loads that can consume power even when the MCU sleeps correctly. See power-saving diagnostics.

Native USB ESP32 Full Companions can also use short light-sleep intervals when the BLE/WiFi drivers, ESP-NOW, GPS UART, logging, and USB host sessions are inactive and no work is pending. Compiling those transports into Full does not itself prevent sleep. Turning off BLE advertising alone leaves its controller running and therefore retains idle behavior rather than manual light sleep.

An active radio2 requires continuous software scanning. With power saving on, ESP32 Companion, Repeater, Room and Sensor builds do not insert the ordinary 1 ms idle delay while dual-radio scanning is active. This avoids delaying sub-millisecond scan visits; it does not make active dual-radio scanning a sleep mode.

The verified Meshnology W12 layout enables the LR2021 radio's DC-DC regulator after LoRa and Morse receive initialization, including recovery. This setting is independent of powersaving and does not remove radio features. Other LR2021 layouts retain their existing regulator behavior unless their recipe explicitly enables LR2021_USE_DCDC after confirming the required inductor; the X1 is not opted in without that hardware confirmation.

"},{"location":"cli_commands/#view-or-set-the-reboot-interval-repeater-and-room-server","title":"View or set the reboot interval (Repeater and room server)","text":"

Usage: - get reboot.interval - set reboot.interval <hours>

Parameters: - hours: 0-255; 0 disables scheduled reboots.

Default: 0 (disabled)

"},{"location":"cli_commands/#control-an-exposed-gpio","title":"Control an exposed GPIO","text":"

Availability: ESP32 Repeater, Room Server, Bridge, and Sensor firmware. Companion firmware does not expose these commands. On nRF52, the commands are enabled only for Sensor builds on the Heltec T096, ProMicro, RAK3401, and RAK4631. GPIO expanders are not supported.

Usage:

Examples:

An integer duration has seconds as its default unit, so 5 means 5 seconds. Add ms for milliseconds (5ms); an explicit s suffix is also accepted (5s). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). on or off without a duration remains in that state until another command, reset, or reboot.

reset changes the pin to an input with no pull resistor (high impedance). It does not reboot the node. It also cancels any pending timer for that pin. A new timed command for the same pin cancels and replaces the previous timer. GPIOs begin in reset; states and timers are not saved and are lost on reboot.

The immediate reply confirms the applied state and any pending transition. When a timed transition finishes, a command issued over the authenticated remote CLI receives a second report such as > GPIO 16 timer complete: off. Retries of the same authenticated timed command are recognized and do not restart its countdown.

The pin number is the Arduino pin number used by that target (the normal GPIO number on ESP32 and the board's D/pin index on nRF52). The available-pin list is build-specific. Radio, flash/PSRAM, USB, serial console, display, GPS, I2C, buttons, LEDs, battery measurement, power control, bridge, Ethernet, watchdog, and other pins claimed by the firmware are rejected. A pin must also be physically broken out on your board; get gpio cannot detect wiring or an attached peripheral that is not represented by the firmware configuration.

Electrical warning: GPIOs use 3.3 V logic and have limited drive current. Do not power a relay, motor, solenoid, or other load directly from a GPIO. Use a suitable transistor, MOSFET, optocoupler, or driver with the required protection components.

"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-set-the-direct-path-override-for-the-current-remote-client","title":"View or set the direct path override for the current remote client","text":"

Usage: - get outpath - get outpath path - set outpath <hop1_hex,hop2_hex,...> - set outpath path - set outpath direct - set outpath clear - set outpath flood

Parameters: - hopN_hex: Hop hash with 2, 4, or 6 hexadecimal characters. Every hop must use the same width.

Notes: - These commands require remote client context and update the caller's ACL entry. - Room Server readers and writers may manage their own outpath and altpath without administrator permission. See room return routes for the command list, session persistence and alternate delivery behavior. - get outpath path reports the reciprocal PAYLOAD_TYPE_PATH received after the caller's latest flood login without changing the selected output route. Because that packet is asynchronous, an immediate query can report > path pending; retry shortly. The observation window expires after one minute, and the captured login path is not automatically selected. - set outpath path copies that observed route to outpath and saves it. It returns an error without changing outpath if no route was received. - direct selects a zero-hop route for a directly reachable caller. - clear forgets the override, replies > outpath cleared, and allows normal path discovery to repopulate it. - flood forces replies to use flood packets until the client logs in again.

"},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"

Search terms: enable repeating, disable repeating, packet forwarding, stop relaying.

Usage: - get repeat - set repeat <state>

Parameters: - state: on|off

Default: repeat on.

repeat off disables relay forwarding, not locally generated traffic or configuration replies. Repeaters can separately opt in to the routed-trace exception below.

"},{"location":"cli_commands/#allow-routed-traces-while-repeating-is-disabled-repeater-only","title":"Allow routed traces while repeating is disabled (repeater only)","text":"
get repeat.trace\nset repeat.trace on\nset repeat.trace off\n

Default: off, including upgrades from older preference files. The setting is saved and takes effect without reboot. Failed saves leave the active choice unchanged. Room Server, Sensor, and Companion do not gain this exception.

To keep ordinary forwarding disabled while allowing routed trace diagnostics:

set repeat off\nset repeat.trace on\n

Only direct TRACE packets with a matching next hop may be relayed. Ordinary messages, adverts, group data, ACKs, and control packets remain subject to repeat off; flood traces remain invalid. Existing trace length, path-capacity, duplicate, radio-profile, and channel/duty-cycle checks still apply. This is a diagnostics opt-in, not authenticated-admin-only forwarding: trace packets retain the protocol's existing authentication behavior.

get repeat and the disabled status bit continue to report repeating off. With repeat on, traces use the ordinary forwarding policy regardless of repeat.trace. Changing the exception to off while repeating is off blocks subsequent relayed trace transmissions, including queued copies and retries; a trace already on air is allowed to finish. Locally initiated traffic and management replies are not cancelled.

The dynamic configuration field is repeat.trace_off (0 or 1). It is appended to /com_prefs; all field offsets in the published upstream layout stay unchanged.

"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"

1.17.1.5 infrastructure bug: affected Repeater, Room Server, and Sensor images accept get path.hash.mode but report unknown config for the setter. The command was not renamed. The source correction restores the setter; installed images need an updated build. See the setting-dispatch audit.

Usage: - get path.hash.mode - set path.hash.mode <value>

Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)

Default: 0

Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14

Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.

"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"

Usage: - get loop.detect - set loop.detect <state>

Parameters: - state: - off: no loop detection is performed - minimal: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) - moderate: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - strict: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)

Default: off

Note: When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14

Example: If preference is loop.detect minimal, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"

Search terms: flood forwarding delay, flood retransmit delay, flood TX delay.

Usage: - get txdelay - set txdelay <value>

Parameters: - value: Transmit delay factor (0-2)

Default: 0.5

Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"

Search terms: direct forwarding delay, direct retransmit delay, direct TX delay.

Usage: - get direct.txdelay - set direct.txdelay <value>

Parameters: - value: Direct transmit delay factor (0-2)

Default: 0.3 (Repeater) - 0.2 (Room Server, Sensor)

Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.

"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"

Usage: - get rxdelay - set rxdelay <value>

Parameters: - value: Receive delay base (0-20)

Default: 0.0

Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.

"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"

Usage: - get dutycycle - set dutycycle <value>

Parameters: - value: Duty cycle percentage (1-100)

Default: 50% (equivalent to airtime factor 1.0)

Examples: - set dutycycle 100 - no duty cycle limit - set dutycycle 50 - 50% duty cycle (default) - set dutycycle 10 - 10% duty cycle - set dutycycle 1 - 1% duty cycle (strictest EU requirement)

Note: Added in firmware v1.15.0

"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"

Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.

Usage: - get af - set af <value>

Parameters: - value: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: - af = 1 -> ~50% duty - af = 2 -> ~33% duty - af = 3 -> ~25% duty - af = 9 -> ~10% duty You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).

Default: 1.0

"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"

Usage: - get int.thresh - set int.thresh <value>

Parameters: - value: Interference threshold value

Default: 0.0

"},{"location":"cli_commands/#enable-or-disable-hardware-channel-activity-detection-cad","title":"Enable or disable hardware Channel Activity Detection (CAD)","text":"

Usage: - get cad - set cad <on|off>

Description: When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of int.thresh - either, both, or none may be active.

Fresh Cascade-profile builds default CAD to off across all roles, including Companion. Saved CAD settings take precedence after an upgrade. Use set cad off to disable it on an existing node or set cad on to enable it explicitly. The repeater applies the saved toggle to the radio during its periodic noise-floor service. With one runnable packet, a busy result uses the normal CAD retry delay and allows roughly four seconds of continuous busy results. As the runnable transmit queue grows, both delays are divided by its depth: retry spacing will not fall below 50 ms and the busy ceiling will not fall below 500 ms. Reaching the busy ceiling records a CAD-timeout error and attempts the next queued transmission rather than waiting indefinitely. Future-scheduled packets do not accelerate CAD. get cad includes the hardware busy-result count.

Parameters: - on|off: Enable or disable hardware CAD

Default: off

"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"

Usage: - get agc.reset.interval - set agc.reset.interval <value>

Parameters: - value: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable.

Default: 0.0

With device powersaving on and a nonzero AGC interval, Companion, Repeater, Room Server, and Sensor firmware use adaptive reset scheduling. The saved interval is the starting/minimum interval. After every two admitted maintenance attempts without an observed radio recovery, the interval doubles, capped at 60 seconds (or the saved interval if it is already longer). For example, 8 -> 16 -> 32 -> 60 seconds, with two attempts at each step. An observed radio recovery returns the schedule to the saved interval. Changing the interval or power-saving mode starts a fresh schedule; adaptive history is not persisted.

get agc.reset.interval still reports the saved starting interval, not the current adaptive interval. 0 disables periodic AGC maintenance even with power saving on. With power saving off, fixed-interval behavior is retained. This follows device powersaving, independently of LoRa radio.rxps.

Adaptive scheduling does not treat a quiet mesh, strong packets, a stable noise floor, or a successful transmission as proof of full receiver sensitivity. TX cannot continually postpone adaptive maintenance. Pending RX/TX, a BUSY chip outside known RX duty cycling, carrier/Morse ownership, and an active noise-floor block can defer it; deferred attempts do not advance backoff and normally retry after 50 ms. When a failed receive start already has a later retry deadline, adaptive AGC waits at least until that deadline too, allowing sleep during persistent receive-start failures. The 60-second bound is a scheduling interval, not permission to interrupt a packet. An armed RXPS radio uses the existing explicit wake/reset path, avoiding retry polling that could miss every short listen window. Existing radio recovery watchdogs remain active. These conservative intervals have host regression coverage, but are not hardware-qualified sensitivity or battery-life results; noisy sites may require a short fixed interval with device power saving off.

"},{"location":"cli_commands/#periodic-transmit-only-text-beacons","title":"Periodic transmit-only text beacons","text":"

See TX beacons for the 12 independent MeshCore Public or Meshtext slots. set beacon.interval 12|24|36|48 selects the shared cycle; the default is 24 hours. Slots are staggered and disabled until configured.

"},{"location":"cli_commands/#view-or-change-the-radio-watchdog-interval-mqtt-observer-only","title":"View or change the radio watchdog interval (MQTT observer only)","text":"

Usage: - get radio.watchdog - set radio.watchdog <minutes>

Parameters: - minutes: 0 to use the standard 30-minute soft recovery interval, or 1-120 minutes to override it

Default: 5

Note: This setting belongs to the MQTT observer runtime and is not available on a standalone FULL repeater. It sets the interval without a successfully received packet before soft radio recovery. Transmissions and interrupts alone do not restart the timer. Setting it to 0 removes the observer override; automatic radio recovery remains active. The normal hard recovery threshold is 12 hours, where the radio supports it, and recovery does not reboot the whole board. On quiet meshes, increasing the soft interval can reduce unnecessary recoveries when no traffic is expected.

"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"

Usage: - get multi.acks - set multi.acks <state>

Parameters: - state: 0 (disable) or 1 (enable)

Default: 0

"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"

Usage: - get flood.advert.interval - set flood.advert.interval <hours>

Parameters: - hours: Interval in hours (3-168)

Default: 47 (Repeater, Room Server) - 0, disabled (Sensor)

"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"

Usage: - get advert.interval - set advert.interval <minutes>

Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)

Default: 2 on a factory-fresh node, then 0 (disabled) once configured.

Note: A new install ships with a 2 minute zero-hop advert interval. Saving any setting resets an interval below the 60 minute minimum to 0, on the assumption that the node has now been deliberately configured. To keep zero-hop adverts running, set an explicit value in the 60-240 range.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"

Search terms: hop limit, maximum hops, max hops, flood distance.

Usage: - get flood.max - set flood.max <value>

Parameters: - value: Maximum flood hop count (0-64)

Default: 64

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"

Usage: - get flood.max.unscoped - set flood.max.unscoped <value>

Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)

Default: 0xFF - indicates it hasn't been set, will track flood.max until it is.

Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"

Usage: - get flood.max.advert - set flood.max.advert <value>

Parameters: - value: Maximum flood hop count (0-64) for an advert packet

Default: 8

"},{"location":"cli_commands/#forward-flood-group-data-packets-on-repeaters","title":"Forward flood group data packets on repeaters","text":"

Usage: - get flood.channel.data - get flood.channel.data.hops - set flood.channel.data <on|off> - set flood.channel.data.hops <all|1-7>

Parameters: - on: Retransmit received flood GRP_DATA channel packets. - off: Do not retransmit received flood GRP_DATA channel packets. - all: When flood.channel.data is off, block GRP_DATA at any received flood hop count. - 1-7: When flood.channel.data is off, repeat GRP_DATA at this hop count or lower and block longer paths.

Default: flood.channel.data on; flood.channel.data.hops h=all

Forwarding behavior: Repeater firmware only. The repeater still receives and logs the packet when logging is enabled; this only blocks retransmission. On generalized repeaters these commands manage an ordinary visible FPF7 type=grp_data drop row. off with all maps to hops=all; off with N maps to hops=N+1+. The 240 KB compact FPF6 profiles retain the legacy hard gate. Because it is an ordinary FPF7 row, a matching higher-priority stop rule can exempt traffic from it. The compact get flood.filter list marks the managed row with ~data. Flood group text (GRP_TXT) is unaffected by this setting.

get flood.channel.data includes the active hop gate as h=all or h>N.

"},{"location":"cli_commands/#bridge-and-radio-crossover-filters","title":"Bridge and radio crossover filters","text":"

mode= selects where a generalized flood.rule / flood.filter row applies:

Mode Where the rule applies radio Ordinary LoRa forwarding; flood-only unless route=direct or route=any is selected. This is the default mode. bridge Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. cross Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. bridge,cross Both transport paths using one rule. cross,bridge is equivalent.

The third built-in repeater rule blocks authenticated #wardriving text and data at all hops on bridge and crossover paths, including temporary-radio sessions. It leaves same-profile traffic subject to the separate four-hop rule. set radio2.cross on does not override this filter.

# Factory slot: inspect before replacing an existing rule.\nget flood.rule.3\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n# On an existing device, add it without overwriting an occupied slot.\nset flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\n

Saved tables are preserved on upgrade; new defaults are seeded only when the table is first initialized. Bridge/cross modes accept drop, rate, priority and stop actions; scope rewrites, retry actions and TX timing are rejected. These modes cover direct as well as flood packets, in both crossing directions. The default authenticates the channel key, avoiding false drops from short channel-hash collisions. FULL room servers support explicit transport rules; companions and compact FPF6 profiles do not expose this table.

See filtering documentation for persistence, queue/rate semantics, exceptions, and how to remove one or both blocks.

"},{"location":"cli_commands/#block-selected-flood-channels-with-fpf7","title":"Block selected flood channels with FPF7","text":"

The separate flood.channel.block command and 15-row table have been retired. Generalized repeaters use the 63-row FPF7 forward phase for authenticated channel blocks:

set flood.rule type=any channel=#test hops=all drop\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule type=any channel=9cd8fcf22a47333b591d96a2b848b73f hops=4+ drop\nget flood.rule\ndel flood.rule.2\n

type=any with a channel condition can authenticate only GRP_TXT and GRP_DATA, so it does not match other payload types. Use hops=all to block at every received hop count. To preserve the old h=N meaning of repeating through N hops and blocking longer paths, use hops=N+1+; old h=4 is therefore hops=5+.

New generalized repeater tables seed slot 2 with the second example. Existing FCB2 rows are imported once into free FPF7 slots and the retired file is then removed. The fixed-size STM32WL FPF6 build cannot match authenticated channels.

"},{"location":"cli_commands/#force-a-transport-scope-onto-floods","title":"Force a transport scope onto floods","text":"

Usage: - get flood.channel.scope - get flood.channel.scope.<n> - set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - del flood.channel.scope.<n> - del flood.channel.scope all

fs is the short family name for all these commands. Short options are p=bl (path=blacklist), p=b1 through p=b6 (path=bucket:1 through path=bucket:6), and tx=s/tx=f (tx=slow/tx=fast).

Parameters: - n: Slot number within the table compiled for the target. Roomy ESP32 builds provide 1-255; classic ESP32 repeaters and nRF52/other normal constrained builds provide 1-31; very-tight STM32WL repeaters provide 1-15; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides 1-4. - channel: public, a public #channel, or a 128/256-bit channel key in hex. - txt:*: Unauthenticated fallback for otherwise-unmatched GRP_TXT and GRP_DATA. Plain * is an alias for txt:*. - login:*: Type-based wildcard for the remote-login/admin family: REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH (0x00, 0x01, 0x02, 0x07, and 0x08). It classifies the outer type; a transit repeater cannot authenticate whether a packet is actually part of a login session. - other:*: Type-based wildcard for every remaining flood payload type, including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved types, and raw custom. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - scope=<name>: Regionless alternative to region. The public name is normalized with a leading #, and its 128-bit transport key is derived directly from that hashtag exactly as for flood.filter scope=<name>. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - path=blacklist: Optional. Require the received path to match the passive flood.filter.blacklist ID table. No flood.filter drop row needs to be enabled. One exact listed ID qualifies a 3-byte path. A 2-byte path requires two matching received path entries, while a 1-byte path never qualifies. - path=bucket:<1-6>: Optional alternative to path=blacklist. Match IDs in the selected persistent flood.retry.bucket. Each bucket holds up to 17 three-byte IDs and remains usable when flood.retry.bridge is off. It uses the same 3-byte, 2-byte, and 1-byte thresholds as path=blacklist. recent.repeater freshness and flood.retry.ignore do not affect this passive match. - tx=slow: Optional. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and schedule retransmission with the maximum supported txdelay factor of 2.0 after changing the scope. The default is fast; tx=fast may be supplied explicitly when replacing a slow row.

Default: No forced scopes.

Remote ACL permission 4 (region/scope manager) can use all get, set, and del flood.channel.scope forms. Filter managers and other non-admin roles cannot change this table.

Without .n, set updates the row for the same exact channel key or wildcard class with the same path selector, otherwise it uses the first empty slot. This permits an ordinary fallback and separate blacklist or bridge-bucket rows for the same channel. With .n, it replaces that slot. The three wildcard classes are independent and consume one slot each. get flood.channel.scope reports active/total slot counts; use the numbered form for row detail. Keyed rows are displayed by the first four bytes of their derived channel hash because channel secrets are never returned. Regionless targets are displayed with their normalized leading #.

This acts on received ROUTE_TYPE_FLOOD and ROUTE_TYPE_TRANSPORT_FLOOD packets. An unscoped packet gains the configured scope; an already-scoped packet has its existing transport codes replaced. For GRP_TXT and GRP_DATA, all exact channel-key rows are tried first and must validate the packet MAC/decryption. Matching path-qualified exact rows are tried before ordinary exact fallback rows. A region-backed row whose target is missing or unusable is skipped; later exact rows and then txt:* are tried. Exact keyed rows with a usable target therefore beat txt:* regardless of slot number. Within each wildcard class, path-qualified rows similarly precede ordinary fallback rows. login:* and other:* select their non-overlapping outer-type families without decrypting the payload. The lowest usable slot wins within each priority tier.

Standard traceroute is direct-routed and is therefore outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and region/unknown-code gates still apply.

On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region or direct hashtag key and packet payload, and sets transport code 1 to zero. This occurs before region enforcement, forwarding filters, and the seen-packet lookup. For an already-scoped packet, the selected code replaces both incoming transport-code fields. Direct routes are never rewritten. A packet converted from unscoped is no longer subject to flood.max.unscoped; all rewritten packets remain subject to normal payload handling, flood.max, flood.filter, loop detection, and moderation. Assigning a scope does not make a packet type forwardable if the core would otherwise reject it. By default, if the selected scope differs and the rewritten packet is accepted for forwarding, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Adding tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), keeps the ordinary queue priority, and uses the maximum txdelay factor of 2.0 for the retransmission. As with ordinary txdelay, the actual transmit delay is randomized from zero through the resulting window; factor 2.0 gives a maximum of ten packet airtimes. Neither mode preempts an active radio transmission or bypasses CAD and airtime-budget limits. Selecting the scope already carried by the packet is a no-op and does not grant special transmit treatment.

A region-backed target must be locally flood-allowed and remains subject to the normal region gate. A scope=<name> target is trusted for this matched receive pass even though it has no region-list entry, matching the behavior of flood.filter scope=<name>. It does not create a region, consume a region slot, or change the allow/deny state for unrelated packets carrying the same transport code.

If a region-backed row's target has been removed or has no usable key, the repeater tries the next applicable row. For group packets this means later authenticated exact rows followed by txt:*; wildcard duplicates likewise fall through to the next usable slot. When no usable mapping exists, the packet retains its original unscoped or scoped route.

LoRa OTA remains functional when other:* is configured. OTA packets are given that target's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. A region target must allow flooding; a direct target follows the regionless trust behavior above. The OTA core itself is dormant outside that window; no default flood filter row is needed for that behavior. Forced scope does not make OTA operate outside the window.

Capacity cost: Each rule slot retains its 36-byte runtime and persistent record. A separate 32-byte-name table holds up to the smaller of the rule count or 32 distinct regionless targets; very-tight STM32WL builds hold one reusable direct target. Region-backed targets do not consume this table. The four-slot minimum uses 272 bytes RAM and a 278-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 572 bytes RAM and a 578-byte file. The 31-slot table uses 2,108 bytes RAM and a 2,114-byte file. Roomy ESP32 builds use 255 rule slots and 32 direct-target slots: 10,204 bytes RAM and a 10,210-byte file. Classic ESP32 LoRa-OTA builds that cannot afford the 255-rule table use 31 slots instead. Both configured regions and regionless targets can be reused by any number of rules.

Duplicate behavior: Mesh dedup hashes payload type and payload bytes; it does not hash route type, transport codes, or the ordinary flood path. Adding a transport scope therefore does not create a new duplicate identity. If the same payload later arrives scoped, unscoped, or through a different region, it is still the same seen packet. TRACE is the exception only in that its encoded path_len byte is also hashed.

While equivalent non-TRACE flood copies are waiting in rxdelay, the normal receive-quality timing still selects the packet to process, but that winner receives a scope from the queued scoped copies with the same dedupe identity. If the copies carry different locally allowed scopes, the scope from the shortest received path wins. Unknown and denied transport codes are not candidates and therefore cannot overwrite an unscoped winner. With equal path lengths, the deeper child region wins because it is narrower. A remaining tie keeps queue order. The winner's own path, SNR reading, and scheduled time are not changed, and an already-scoped winner may have its code replaced by the better queued scope.

The comparison is deferred until dequeue so each copy retains its original scope and path for arbitration. It can only use copies still present in rxdelay; it cannot replace a packet that already won the dedupe race. Flood-form TRACE participates in this arbitration; direct traceroute does not enter the flood queue.

A packet that matches a fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses the inbound rxdelay queue. A tx=slow row remains in that queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.

Examples:

region put west\nregion save\nset flood.channel.scope #local west\nset flood.channel.scope.2 txt:* west tx=slow\nset flood.channel.scope.3 login:* west\nset flood.channel.scope.4 other:* west\nget flood.channel.scope\nget flood.channel.scope.1\ndel flood.channel.scope.2\n

A regionless exact mapping needs no region command:

set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope\nget flood.channel.scope.1\n

For example, if an authenticated #rgdata packet arrives carrying scope #usa, that rule replaces #usa with #BlackHole86 before forwarding. The rule also assigns #BlackHole86 when the packet is unscoped or carries any other scope; it is a channel-to-target mapping, not an incoming-scope filter.

To use bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, while assigning west to every other authenticated public packet:

set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n

Additional 3-byte IDs may be added to bucket 1 later; any one exact hit qualifies the east row. This use is passive and does not require flood.retry.bridge to be enabled. The separate blacklist selector remains available for tables shared with flood.filter path=blacklist rules.

"},{"location":"cli_commands/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"

Usage: - get flood.channel.scope.require - get flood.channel.scope.require.<n> - set flood.channel.scope.require <public|#channel|128/256-bit-key> - set flood.channel.scope.require.<n> <public|#channel|128/256-bit-key> - del flood.channel.scope.require.<n> - del flood.channel.scope.require all

fsr is the short family name, with the same arguments and region/scope-manager permissions.

Default: Empty; normal global region enforcement remains active.

Once this table contains a row, received flood GRP_TXT and GRP_DATA packets use selective region enforcement. A packet authenticating against a listed channel key must already carry a transport scope matching a locally flood-allowed region. Listed channels arriving unscoped, with an unknown code, or with a denied region are not retransmitted. This tests the original incoming scope before any flood.channel.scope or flood.filter scope= rewrite. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.

Other group channels bypass the region/unknown-code gate while the table is active. They remain subject to every other forwarding control, including repeat, flood.max*, packet filters, loop detection, payload validation, and moderation. Non-channel flood payload types retain normal global region enforcement.

Channel matching validates the packet MAC/decryption with the configured key; the visible one-byte channel hash is only a prefilter. Public hashtag channels use their derived public key. Without .n, setting an existing key updates it and a new key uses the first empty slot. Numbered set replaces that slot. get ...<n> reports a four-byte derived prefix and key size without exposing the key. The table uses the same build-dependent slot count as flood.channel.scope.

Remote ACL permission 4 can manage this table. Deleting its final row restores normal global region enforcement for group channels.

Example:

set flood.channel.scope.require #bot\nget flood.channel.scope.require\nget flood.channel.scope.require.1\n
"},{"location":"cli_commands/#change-persistent-flood-rules-in-the-field","title":"Change persistent flood rules in the field","text":"

For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Flood Filtering and Moderation.

Usage:

Generalized rows also accept sender=, text=, len=, route=, prob=, enabled=, and anchored path= chains. These use the same table and ordering; see the parameters and short forms below.

The extended table is available on repeaters with the rule engine enabled and on FULL-profile ESP32 room servers. A FULL room server exposes fr, flood.rule, and flood.filter, has 31 slots, and requires an administrator for remote changes. It does not have the repeater's passive path blacklist, so flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= match on a room server. Standard room-server profiles do not compile this table.

Parameters:

The payload names follow the MeshCore packet-format allocation:

Value Short name Full name 0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA (this fork's LoRa OTA extension; reserved upstream) 0x0D 13 reserved 0x0E 14 reserved 0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM

Route scope: mode=radio defaults to the two flood route values: ROUTE_TYPE_TRANSPORT_FLOOD (0x00, flood plus transport codes) and ROUTE_TYPE_FLOOD (0x01, unscoped flood). Direct routes 0x02 and 0x03 require an explicit route=direct or route=any on a radio row. Bridge/cross rules cover both route classes by default at their transport boundaries.

Behavior: Match fields within one row are ANDed. Radio rows are matched against the same immutable receive-time packet, before any rule changes its scope. Bridge/cross rows match the packet at their admission boundary; a TX packet can already have an appended hop or rewritten scope. Each mode evaluates only its applicable rows, processed in descending priority. At equal numeric priority, authenticated channel matches precede raw hashes, which precede an unrestricted channel matcher; lower slot wins after that. The first matching stop row is included and all lower-order FPF7 matches are discarded. A stop cannot undo an earlier drop or bypass hard forwarding gates or the other policy phases. A row with path=blacklist must meet the path condition as well as its other conditions; blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. In contrast, prefix= begins at the first received path entry and preserves order. A matching drop row prevents retransmission. The highest-order remaining matching scope/region row wins; matching drop and rate rows remain independent and can still block the rewritten packet. Scope rewriting happens before region enforcement and is trusted even when its name is absent from the local region list. It does not bypass repeat, flood.max, other drop rows, loop detection, or moderation.

Retry selection uses that same ordered, stop-truncated match set. With at least one active retry row, a received flood starts a retry sequence only when one of those matching rows includes retry. This selector cannot override flood.retry.count, path/type attempt caps, flood.retry.advert, disabled forwarding, a drop decision, or any other hard forwarding gate. Locally originated floods retain the normal global retry behavior.

With require=region, a failed check makes that scope row ineligible. It leaves the packet unchanged and does not set the filter-scope trust bypass, so an unknown or denied incoming region is rejected normally unless another independent scope rule rewrites it. Later eligible filter scope rows may still match.

By default, when a scope row will change the packet's transport codes, the packet bypasses inbound rxdelay; its retransmission then uses zero txdelay and the highest outbound queue priority. With tx=slow, the rewrite instead uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), normal queue priority, and the maximum txdelay factor of 2.0. The actual randomized transmit wait ranges from zero to ten packet airtimes. Selecting the scope already carried is a no-op and does not grant special treatment. An active radio transmission is not preempted, and CAD and airtime-budget limits still apply.

The packet is still received and can still be logged. Rules are persistent data and can be changed over serial or authenticated remote CLI without an OTA or reboot. flood.rule and flood.filter address the same table on extended builds; FPF6 files are migrated in memory and the next save writes FPF7. New sender/text, length, route, probability, enabled-state, or extended-path fields use FPF8 instead. Old firmware without FPF8 support cannot load that table; remove the new fields before downgrading and keep a copy of the rules. FPF1-FPF5 files are rejected and filtering fails open. FPF7/FPF8 store canonical region names rather than transient numeric region IDs. Removing, reordering, or reusing a region ID therefore cannot silently retarget a rule. If a saved input or target region name is absent, that input match or rewrite is inert; restoring the same region name reactivates it. While either tempradio or tempradio2 is active, only rows explicitly marked suspend=tempradio are skipped. Temporary radio is a radio state, not an OTA mode; normal payload types can also use the temporary channel. Other rows remain in force, and suspended rows resume after both temporary sessions end. A malformed persisted table fails open (no general rules are applied).

Within one receive evaluation, rows using the same channel key share the initial authentication verdict; content patterns also decrypt into temporary scratch for matching. The cache and plaintext are discarded after the packet. Raw hash:XX rows skip authentication and compare only the visible byte.

Per-channel retry examples:

# Use bridge-bucket retry only for authenticated Public and #hamradio traffic.\nset flood.retry.bridge on\nset flood.rule.2 type=any channel=public retry\nset flood.rule.3 type=any channel=#hamradio retry\n\n# If the channel key/name is unavailable, select visible channel hash A7.\n# This is an unauthenticated 1-byte hint, not a channel identity.\nset flood.rule.4 type=any channel=hash:A7 retry\n

Because Public's visible hash is 0x11, a bare hash:11 drop row also matches Public. To exempt authenticated Public while dropping other packets that carry the same visible byte, put the exact Public rule first and stop lower-priority rules after it:

# Retry and preserve authenticated Public; drop other channel-hash 11 packets.\nset flood.rule.2 type=any channel=public retry stop\nset flood.rule.3 type=any channel=hash:11 drop\n

The Public row matches only after its MAC/decrypt check succeeds. A colliding channel therefore misses that stop and reaches the raw-hash drop row. Both rows use the default numeric priority, but authenticated-channel specificity automatically orders Public first even if its slot number is higher. Without stop, both rows match Public and the sticky drop action wins. Omit retry from the Public row when only the forwarding exemption is wanted. An operator can deliberately reverse this order by assigning the hash row a higher numeric priority.

Deleting or replacing the last active retry row restores the legacy global retry eligibility. Firmware that predates the retry/hash:XX FPF7 extension cannot preserve tables containing those rows; remove them before downgrading.

Default rows: Repeater firmware and FULL ESP32 room-server firmware seed a new flood-filter table with ota all suspend=tempradio in slot 1. This blocks repeated LoRa OTA (0x0C) floods at every received hop unless either temporary radio profile is active. The OTA core independently refuses OTA receive, relay, and transmit outside temporary radio. The row is editable and deletable; once the table is saved, deletion is persistent. Restore the exact seeded row with:

set flood.filter.1 0x0C all suspend=tempradio\n

Omitting all is equivalent. Omit .1 as well to reuse an identical rule or the first empty slot instead of replacing slot 1.

Generalized repeaters also seed slot 2 with the authenticated #wardriving radio hop limit and slot 3 with the bridge/crossover block:

set flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n

These numbered commands replace those slots. Saved tables are preserved on upgrade; use the unnumbered third command to add the new rule without replacing an occupied slot. See restoring the three defaults.

Remote-admin lockout warning: There are no hidden payload-type or short-hop exceptions. FPF7 drop and rate rows may block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so. Transit repeaters cannot decrypt these outer types to distinguish an admin exchange from ordinary peer traffic. Keep a serial or other recovery path and stage broad deny/rate rules carefully.

Without .n, set reuses an identical rule or uses the first empty slot. With .n, it replaces that slot, which is the intended way to change a row's match or action. get flood.filter or get flood.rule gives a compact list. Use the numbered form for full details, including channel, prefix, original-scope condition, action, timing, rate, and temporary-radio suspension.

fr is a shorter alias for flood.rule, with the same explicit-action requirement. Short and long field names can be mixed in either command:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\nget fr.3\n

The two setters above save the same row. get fr.3 returns the complete short setter; get fr lists the table. Unnumbered set fr ... reuses an identical row or adds one to the first empty slot. del fr.N and del fr all have the same behavior as their long forms.

Content, exceptions, and state changes use that same setter:

set fr grp_txt c=#local x=\"^BEACON\" d\nset fr grp_txt c=#local n=\"^Alice$\" pri=200 a\nset fr grp_txt c=#local d\nset fr.4 e=off\nset fr.4 e=on\n

The Alice rule stops lower-order general drops; separate moderation and hard forwarding gates still apply. Sender names remain unverified display text. Saved older prefix rules keep their start-anchored semantics; when retyping an old path=A1,B2, use prefix=A1,B2 or pa=^A1>B2.

Full token Short token type=... Positional type or t=...; * means any hops=... Positional range or h=...; * means all mode=radio/bridge/cross/bridge,cross m=r/b/c/bc (choose one value) channel=..., prefix=... c=..., p=... path=[^]ID>ID...[$] pa=... sender=..., text=... n=..., x=... len=N/N+/N-M l=... route=flood/direct/any rt=f/d/a prob=N, enabled=on/off pr=N, e=1/0 hashbytes=any/1/2/2+/3 pb=*/1/2/2+/3 (choose one value) path=blacklist p=bl (repeater only) in=any/none/scoped/allowed/unknown i=*/n/s/a/u (choose one value) in=scope:name, in=region:name i=s:name, i=r:name drop, stop, retry d, s, r allow, forward a (same action as stop) scope=name, region=name s=name, r=name rate=N/min, priority=N q=N, pri=N tx=slow, suspend=tempradio, retry f=s, f=t, f=r; combine as f=str

All existing validation and permissions apply. m=cb, m=b,c, and m=c,b also mean bridge and crossover.

Full family Short family Commands flood.moderation fm get, set, del flood.channel.scope fs get, set, del flood.channel.scope.require fsr get, set, del flood.filter.blacklist fb get, set, del flood.advert fa get, clear

Slot arguments, output, validation, and permissions are shared with the full family names. For example, get fa key 1 inspects the full advert key and clear fa all clears advert history.

The numbered long getters use this complete short setter when descriptive fields would exceed a reply. Defaults are omitted and specific payload types are printed numerically. If it still cannot fit, the reply is Err - compact command exceeds reply size, never a partial setter. Private channel keys are shown as key:XXXXXXXX; that reference can be reused while an active rule on the same device holds the key. Copying it to another device requires the original key, unless that device already has a matching rule. Unknown or ambiguous references are rejected. See the shorthand guide for examples.

On generalized repeaters, filter rows, scope-rewrite rows, the shared blacklist, and flood.channel.data compatibility state are committed in one atomic FPF7/FPF8 image. Compact FPF6 profiles retain separate files. Replacing or deleting the blacklist does not delete rows containing path=blacklist; such rows remain dormant while the list is empty. Path hashes are truncated routing identifiers, not authenticated identities, so this is a forwarding signal rather than proof that a particular repeater handled a packet.

A common use is containment of bulk internet-to-mesh dumping: list the path IDs associated with the offending gateways, then add a broad type=any hops=all path=blacklist drop row. This prevents this repeater from retransmitting matching floods; it does not delete them from local logs or prove who originated them.

The unnumbered blacklist set replaces the whole list and accepts up to 18 IDs so it fits every CLI transport. Numbered set writes up to 18 consecutive entries beginning at an existing slot or exactly the next slot, allowing an ESP32 list to grow to 255 entries in batches. Numbered deletion compacts subsequent slots. The unnumbered get reports the total and as many leading IDs as fit in one reply; use numbered get to inspect entries beyond that reply.

Standard traceroute is direct-routed and therefore outside flood.filter entirely. A custom flood-form trace participates normally: type=any, explicit trace, rewrite, rate, drop, and stop rows can all apply.

Examples:

set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\nset flood.rule.4 type=any prefix=860C rate=10/min\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\nget flood.rule.2\nset flood.filter grp_data 4+\nset flood.filter.2 PAYLOAD_TYPE_ADVERT 6+\nset flood.filter ota 2-4\nset flood.filter.1 0x0C all suspend=tempradio\nset flood.filter grp_data all suspend=tempradio\nset flood.filter grp_txt all scope=local\nset flood.filter grp_data all scope=local require=region\nset flood.filter grp_data all path=blacklist scope=local tx=slow\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter.blacklist.19 778899,AABBCC,DDEEFF\nset flood.filter any all path=blacklist\nget flood.filter.blacklist\nget flood.filter.blacklist.4\nset flood.filter any 12+\nget flood.filter\nget flood.filter.2\ndel flood.filter.2\n

The first rule authenticates #rgdata, requires more than three received hops, and adds #BlackHole86 only when no scope was present. The second rewrites the exact incoming #usa scope. The third demonstrates a two-byte pbyte source prefix and a global per-row rate cap. The fourth authenticates #rgdata at zero through two hops, applies no FPF7 action of its own, and stops lower-order FPF7 forward rows; hard gates and the rewrite/moderation phases still apply.

The fixed 240 KB STM32WL profiles leave MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the compact, persistent FPF6 flood.filter and blacklist commands. They still filter floods, but do not expose the flood.rule alias or its extended channel, prefix, input-scope, region-action, or rate fields, nor sender/text patterns, length, route, probability, enabled-state, or anchored chains. Their compiled legacy families still accept the fm/fs/fsr/fb/fa aliases; fm requires moderation support in that build. No partition size changes are required.

"},{"location":"cli_commands/#moderate-flood-group-text-by-channel-sender-and-source-path","title":"Moderate flood group text by channel, sender, and source path","text":"

Usage: - get flood.moderation - get flood.moderation.<n> - set flood.moderation <channel> <sender> <action> [action...] - set flood.moderation.<n> <channel> <sender> <action> [action...] - del flood.moderation.<n> - del flood.moderation all

fm is the short family name. Options can use d (drop), q=N (rate=N/min), h=N (hops=N), and p=... (path=...). The short rate also accepts q=N/min or q=N/m; duplicate rate/drop options remain invalid.

Parameters: - n: Moderation slot from 1 to 16. - channel: - public: Built-in Public channel. - #channel: Derive the well-known hashtag-channel key. - A 128-bit or 256-bit channel key in hex, for any other/private channel. - sender: Exact group-text display name. Matching is ASCII case-insensitive. Quote names containing spaces, for example \"Field User\". - drop: Do not forward matching messages. Equivalent to rate=0/min. - rate=X/min: Forward at most X matching messages per 60-second local window. This option requires an exact sender rather than *. - hops=N: Do not forward a matching message whose received flood path count is N or higher. hops=all removes this constraint. - path=H1[,H2,H3]: Match the start of the flood path. One to three hashes are accepted; every hash must have the same 1-, 2-, or 3-byte width. - path=*: Match any source path (the default).

At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. Rate counters are local to this repeater and rule, use a 60-second window beginning with the first matching message, and reset on reboot.

Configured exact names are limited to 31 bytes. A wildcard * with a hop restriction also matches longer valid sender names. The FGM1 storage format is unchanged; saves use staging, readback, and backup recovery. A save reported as unconfirmed may already have published, so rebooted settings can differ from the restored live table.

Decode and identity behavior: Moderation applies only to flood PAYLOAD_TYPE_GRP_TXT. The repeater first checks the packet's channel-hash byte, then validates and decrypts with the configured key. It extracts the text before the first : from the standard <sender>: <message> plaintext. The channel key is stored locally but is never printed by get.

The group-text sender is an unverified display name, not a public key. It can be spoofed. Combining it with the first one to three path hashes makes a more useful moderation signal, but path hashes are truncated and are not proof of the originating user. A path-qualified rule begins matching only after the packet contains all configured starting hops; it cannot identify a first hop on a zero-hop packet.

As with general filtering, matching messages are still received/logged; only retransmission is denied. There are no moderation rules by default.

Examples:

set flood.moderation public \"Noisy User\" rate=5/min\nset flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation.3 00112233445566778899AABBCCDDEEFF alice rate=10/min hops=4 path=71CE82\nset fm public \"Noisy User\" q=5\nget flood.moderation\nget flood.moderation.3\ndel flood.moderation.3\n
"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"

Usage: - setperm <pubkey> <permissions>

Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin - 4: Region/scope manager (repeater delegated region and forced-scope management) - 5: Filter manager (repeater delegated forwarding-filter management)

Filter manager scope: Permission 5 can use an explicit allowlist of non-secret operational/filter status commands and can change the forwarding controls repeat, loop.detect, flood.max*, flood.channel.data*, flood.filter*, flood.rule*, fr[.N], and flood.moderation*, including the fm, fb, and fa aliases. It cannot read guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or other admin configuration. Permission 4 is limited to region commands, flood.channel.scope* (including fs and fsr), and the same non-secret status allowlist. Both delegated manager roles are protected from least-recently-active ACL eviction like administrators.

The same filter aliases work inside enrolled signed fleet commands, including quoted sender/text patterns. Fleet signature, expiry, replay, and command-size checks still apply; ordinary channel text cannot authorize a change.

Note: Removes the entry when permissions is omitted

"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"

Usage:

Available on repeaters, room servers, and sensors. Over LoRa, an authenticated admin session is required; guests, read-only/read-write clients, and delegated region/filter managers cannot read the ACL.

Each page starts with ACL <page>/<pages> and contains up to two entries. Each entry is a two-digit hexadecimal permission byte followed by the full 64-character public key. For example, use get acl 2 for the second page. An empty list returns ACL: empty. Deleted/guest entries with permission byte 00 are omitted, matching the local listing. Pages reflect the current live ACL, so entries can move between pages if permissions or clients change.

Bare get acl keeps the full streamed listing on USB and direct network CLI, or via Companion binary command 0x42 where the role supports the command. The listing contains public keys and permissions, not passwords or shared secrets, and does not change the ACL.

"},{"location":"cli_commands/#view-repeater-guest-sessions","title":"View repeater guest sessions","text":"

Usage:

Available on repeaters through local administrative consoles or an authenticated remote admin session. Delegated region/filter managers and guest clients cannot read this list.

Each page starts with Guests <page>/<pages> (<total>) and shows one guest's full public key followed by last=<UTC epoch seconds>. An empty list returns Guests: empty. A last=0 value can occur before the node's clock is set.

These are live RAM entries, separate from the persistent ACL shown by get acl. They indicate an accepted session, not whether the guest is currently online. Entries can move between pages as clients change. A reboot or set guest off clears them.

"},{"location":"cli_commands/#view-repeater-login-attempts","title":"View repeater login attempts","text":"

Usage:

Available on repeaters through local administrative consoles, an authenticated remote admin session, or enrolled signed fleet commands. Guests and delegated region/filter managers cannot read this list.

The repeater keeps the 16 most recently attempted public keys in RAM, with the newest attempt first. Each row shows only the first 12 hexadecimal characters of the key, the latest attempt's UTC epoch time, its result, and an attempt count. There are up to two rows per page. Attempts for the same full key update its existing row and count; the oldest key is replaced when the list is full. The count stops at 4,294,967,295. Two different keys can share the displayed prefix; their records and counts remain separate.

Logins 1/1 (2)\nABCDEF123456 n=3 last=1792600000 accepted\n123456ABCDEF n=1 last=1792599900 guests-disabled\n

Results are accepted, wrong-password, guests-disabled, replay/state, and acl-unavailable. An accepted login can be an admin, an assigned ACL client, or a guest. This history records actual login requests after packet authentication; anonymous owner, region, and clock requests, incomplete requests, and packets that fail authentication do not appear. Passwords and shared secrets are never recorded.

This is recent attempt history, not a list of online users or a complete audit log. Pages can change as attempts arrive. A reboot clears it; set guest off does not. A zero timestamp means the clock was not set when the attempt arrived.

"},{"location":"cli_commands/#recover-a-repeaters-future-dated-replay-timestamp","title":"Recover a repeater's future-dated replay timestamp","text":"

This is an explicit recovery operation after correcting a bad clock, not a contact deletion. It lowers selected replay timestamps to the repeater's current UTC epoch only when they are later than that epoch. Earlier values, public keys, permissions, stored paths, and historical identity records remain. The replay file is committed before the live table changes. No extra 60-second login reservation is added by this command.

First set/synchronize and verify the repeater's clock (clock); a build-default clock without a manual or observed synchronization is not accepted. Also fix the companion's clock before its next login.

USB console:

replay reset <full-64-hex-public-key>\nreplay reset all CONFIRM\n

Authenticated LoRa admin, including a resumed admin session:

replay reset <full-64-hex-public-key>\n

The reply shows now=<epoch>, ttl=<remaining-seconds>s, and a confirmation command containing the same full key and a one-use 32-hex-character token. Verify the displayed time and send that command before the original 300-second deadline. During the first 120 seconds, repeated requests for the same key by the same admin return the same token without restarting either timer. From 120 through 300 seconds, the token is retained for confirmation only: requests do not resend or replace it. This leaves at least 180 seconds to deliver a confirmation after the last permitted token response is generated (radio transit time still counts toward expiry). At 300 seconds it expires and a new request can receive a new token. Confirmation can succeed immediately; there is no requirement to wait for the resend window to close. The displayed TTL decreases on retries and is measured when the reply is generated, not when it reaches the companion.

It can target the caller's own key or another exact key; prefixes, self, wildcards, and all are not allowed over LoRa. Tokens are bound to both the requesting admin and target, expire on reboot, and are invalid after use or a clock correction outside the five-second confirmation tolerance. A failed write requires a new confirmation. Challenge requests consult live token state instead of replaying cached challenge text; completed confirmation results remain cacheable without executing the reset again. Normal packet freshness checks still apply to retries.

Only the physical serial console grants all access. Ethernet, browser and internal command callbacks do not count as USB. Guest, read-only, region-manager and filter-manager roles cannot reset replay state. This command is implemented in repeater firmware; room-server and sensor CLI are unchanged.

Normal login and command admission checks remain in force: a fully locked-out caller that cannot send an accepted admin command needs another working admin or USB access. A corrupt/unreadable replay file fails closed and is not erased or formatted by this operation. Unknown keys do not create records.

Security trade-off: lowering a replay boundary can admit previously captured future-dated login/command packets above the new boundary, including packets that could raise it again. The one-use token prevents the recovery command itself from being repeatedly executed; it does not replace the protocol's timestamp-based replay protection. Use recovery only after verifying clocks. Setting a clock alone never automatically resets this table.

"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"

Usage: - get allow.read.only - set allow.read.only <state>

Parameters: - state: on (enable) or off (disable)

Default: off

Make a room read-only while allowing selected writers:

set allow.read.only on\nsetperm <PUBKEY_USER_1> 2\n

With read-only mode on, a client without a valid room password can join as a read-only guest, including with a blank password. Empty configured admin and room passwords disable those password credentials; they never grant Admin or Read-Write access. Permission 2 gives the specified public key read-write access. Replace <PUBKEY_USER_1> with the user's full public key and repeat setperm for every approved writer.

An existing ACL identity keeps its assigned role when logging in with a blank, stale, or room password. A valid nonempty admin password can explicitly upgrade it to Admin. This prevents a guest-password login from demoting an admin, and prevents a room password from overriding an explicit read-only assignment. Only Read-Write (2) and Admin (3) clients can post.

This does not remove write access from clients that already know the room password. Keep that password private, or replace it, if write access should be limited to the explicitly permitted users.

"},{"location":"cli_commands/#view-or-change-the-room-topic","title":"View or change the room topic","text":"

Availability: Room Server firmware. Local consoles and authenticated remote admins can use these commands.

set topic Welcome to the Bellevue room. Please keep posts local.\nget topic\nset topic\n

set topic <text> saves an announcement of up to 151 UTF-8 bytes. Individual command transports can impose a smaller input limit. get topic returns the current text prefixed with >. set topic with no text clears it.

The topic survives reboot and appears as a normal room post authored by the server, using the existing app message format. It does not require an app update or provide a separate pinned-header display. Older unread retained posts are delivered before the topic, so the companion's automatic catch-up cursor cannot jump past them. A topic ACK updates only topic delivery state, leaving the server's message-history cursor unchanged.

The server waits six seconds before delivering a new topic and attempts an unacknowledged announcement up to three times per client. It then continues ordinary post delivery; reconnecting or changing the topic renews that budget. Changing a topic while its predecessor is awaiting an ACK does not mark the replacement as received. Clearing the topic stops further announcement pushes; a packet already queued for transmission can still arrive.

Topic saves are verified before publication. A failed save reports an error and retains the previous live topic. Corrupt, unreadable, or unsupported stored topic files are preserved and block replacement until their storage issue is repaired. Ordinary room history uses the bounded RAM ring by default. Enable optional retention of the newest 32 posts with set room.history on.

See Room history, moderation, and information board for persistent history, saved ACL roles, full-key bans, per-user post/request limits, the ESP32 /room browser page, and versioned article downloads.

"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"

Usage: - region load - region load <name> [flood_flag]

Parameters: - name: A name of a region. * represents the wildcard region

Note: flood_flag: Optional F to allow flooding

Note: Indentation creates parent-child relationships (max 8 levels)

Note: region load with an empty name will not work remotely (it's interactive)

"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"

Usage: - region save

"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"

Usage: - region allowf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * allows packets without region transport codes

"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"

Usage: - region denyf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * drops packets without region transport codes

"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"

Usage: - region get <name>

Parameters: - name: Region name (or * for wildcard)

"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"

Usage: - region home - region home <name>

Parameters: - name: Region name

"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"

Usage: - region default - region default {name|<null>}

Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"

Usage: - region put <name> [parent_name]

Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)

Note: In firmware v1.15.0 and later, region put enables flooding for that region by default (you do not need a separate region allowf <name> after each put). On v1.14.0 and earlier, new regions may still require region allowf for flooding-see region allowf.

"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"

Usage: - region def <token> [<token> ...]

Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.

Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. The command is transactional: invalid names, unknown or ambiguous jumps, table overflow, and hierarchy cycles return Err - ... without changing the existing tree.

Existing regions: region def does not clear the existing tree - if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.

Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | - region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.

Example - linear chain (each token becomes a child of the previous):

region def a b c d e\nregion save\n

Example - branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):

region def a b c d|b e f\nregion save\n

Example - transactional error:

region def a b c|nope d\n

The reply is Err - unknown or ambiguous jump: nope. The existing tree is unchanged; re-run with a corrected jump.

Example - flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:

region def a|* b|* c|* d|* e|* f\nregion save\n
"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"

Usage: - region remove <name>

Parameters: - name: Region name

Note: Must remove all child regions before the region can be removed

"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"

Usage: - region list <filter>

Serial Only: Yes

Parameters: - filter: allowed|denied

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"

Usage: - region

Serial Only: For firmware older than 1.12.0

"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"

Example 1: Using F Flag with Named Public Region

region load\n#Europe F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a region named #Europe with flooding enabled - Packets from this region will be flooded to other nodes

Example 2: Using Wildcard with F Flag

region load \n* F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * with flooding enabled - Enables flooding for all regions automatically - Applies only to packets without transport codes

Example 3: Using Wildcard Without F Flag

region load \n*\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * without flooding - This region exists but doesn't affect packet distribution - Used as a default/empty region

Example 4: Nested Public Region with F Flag

region load \n#Europe F\n  #UK\n    #London\n    #Manchester\n  #France\n    #Paris\n    #Lyon\n<blank line to end region load>\nregion save\n

Explanation: - Creates #Europe region with flooding enabled - Adds nested child regions (#UK, #France) - All nested regions inherit the flooding flag from parent

Example 5: Wildcard with Nested Public Regions

region load \n* F\n  #NorthAmerica\n    #USA\n      #NewYork\n      #California\n    #Canada\n      #Ontario\n      #Quebec\n<blank line to end region load>\nregion save\n

Explanation: - Creates wildcard region * with flooding enabled - Adds nested #NorthAmerica hierarchy - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules

"},{"location":"cli_commands/#direct-retry","title":"Direct Retry","text":"

Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets.

The shared state, count, base, and step controls work on repeater, room-server, and sensor firmware. Recent-repeater/SNR controls are repeater-only because the other roles do not keep the repeater reachability table they require.

"},{"location":"cli_commands/#view-or-change-direct-retry-state","title":"View or change direct retry state","text":"

Search terms: enable tx retries, disable tx retries, stop direct retries, turn off retransmissions.

Usage: - get direct.retry - set direct.retry <state>

Parameters: - state: on|off

Default: on

Notes: - New installs and older preference files without direct retry settings default to on with the rooftop preset.

Examples:

get direct.retry\nset direct.retry on\nset direct.retry off\n
"},{"location":"cli_commands/#view-or-change-direct-retry-heard-table-gate","title":"View or change direct retry heard-table gate","text":"

Usage: - get direct.retry.heard - set direct.retry.heard <state>

Parameters: - state: on|off

Default: on

Note: This command is repeater-only. When enabled, the recent repeater table is the direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked.

Examples:

get direct.retry.heard\nset direct.retry.heard on\nset direct.retry.heard off\n
"},{"location":"cli_commands/#view-or-apply-a-retry-preset","title":"View or apply a retry preset","text":"

Search terms: retry profile, retry defaults, rooftop retries, mobile retries, infrastructure retries.

Usage: - get retry.preset - set retry.preset <preset>

Parameters: - preset: infra|rooftop|mobile

Notes: - Applies shared direct retry and flood retry defaults. - infra: fewer, slower retries for stable fixed infrastructure. - rooftop: default long retry window for weak rooftop links. - mobile: long retry count with shorter spacing for moving or changing links; flood retry count is 15. - Changing direct.retry.count, direct.retry.base, direct.retry.step, direct.retry.margin, flood.retry.count, flood.retry.path, or flood.retry.group.path makes the preset report as custom.

Examples:

get retry.preset\nset retry.preset infra\nset retry.preset rooftop\nset retry.preset mobile\n
"},{"location":"cli_commands/#flood-retry","title":"Flood Retry","text":"

Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater.

The count, path, group-data path, and advert controls work on repeater, room-server, sensor, and Companion firmware. Infrastructure forwarding must also be enabled for retries to run. Prefix, ignore, bridge, and bucket controls are repeater-only.

"},{"location":"cli_commands/#hop-zero-flood-retry-coding-rate","title":"Hop-zero flood retry coding rate","text":"

Flood packets with no path hops recorded yet use the same CR escalation ladder as direct retries. The starting point is the active radio CR, since a flood has no single next-hop repeater whose SNR can select a starting CR. Each retry uses its attempt number and the active radio setting; it does not restart the ladder from the previous retry's override. Forwarded packets with one or more recorded hops use the active radio CR on every retry, including bridge retries whose effective path length discounts ignored hops.

The initial transmission uses the active radio CR. This table lists retries after that initial transmission; any later retries remain at CR8:

Starting CR Retry 1 Retry 2 Retry 3 Retry 4 Retry 5+ CR4 CR4 CR5 CR7 CR7 CR8 CR5 CR5 CR7 CR7 CR8 CR8 CR6 CR6 CR7 CR7 CR8 CR8 CR7 CR7 CR7 CR8 CR8 CR8 CR8 CR8 CR8 CR8 CR8 CR8

The ladder is shared across presets. Presets change the retry budget, not the CR steps. At CR5, the maximum hop-zero schedules are:

Preset Configured flood count Hop-zero retries CR schedule after the initial send infra 1 2 CR5, CR7 rooftop 3 6 CR5, CR7, CR7, CR8, CR8, CR8 mobile 15 15 CR5, CR7, CR7, then CR8 for retries 4-15

These budgets apply to group text and the origin login/message family (RESPONSE, TXT_MSG, ANON_REQ, PATH). Existing payload caps, echo cancellation, and retry eligibility still apply: REQ and OTA do not use generic flood retries; other payloads allow at most one retry. An eligible advert at CR5 therefore gets only its first CR5 retry.

set flood.retry.count 0 continues to disable all automatic flood retries, including hop zero. It leaves the initial transmission and all direct-retry settings and CR schedules unchanged. Direct retry's direct.retry.cr switch and SNR thresholds continue to control direct retries only. Per-packet CR overrides are restored after transmission and do not change the saved radio setting or physical preamble.

"},{"location":"cli_commands/#view-or-change-flood-retry-count","title":"View or change flood retry count","text":"

Companion firmware supports flood.retry.count, flood.retry.path, flood.retry.group.path, and flood.retry.advert through its text terminal and framed CLI. These settings persist across reboot. Companion retains its pre-existing defaults when upgrading an older preferences file: count 15, general path gate 1, group path gate off, and self-origin advert retries on. Companion uses the configured count directly, without the infrastructure role's hop-zero and one-hop multipliers. The shared payload caps below still apply. Companion only retries its own adverts; neighbor adverts remain ineligible even with flood.retry.advert on.

The infra, rooftop, and mobile defaults and retry.preset command in this section apply to infrastructure roles. Companion does not expose repeater bridge or prefix retry filters.

On builds with Meshtext channel chat, flood.retry.count also limits retries of the node's own MT channel text. Retries reuse the original encoded packet and stop after hearing a matching forwarded copy with a lower remaining hop limit. This is not a recipient ACK or a delivery guarantee. NodeInfo permits at most one additional transmission, only with nonzero retry count and flood.retry.advert on, and waits an extra minute beyond its airtime-based delay. These settings never enable relaying of received MT traffic or apply MeshCore prefix/bridge rules to MT packets. set mt.hops 0 disables heard-repeat retries, without suppressing the original chat or its first-use NodeInfo. get mt.retry reports retry/heard/exhausted counters, pending work and the configured count without changing settings.

Search terms: flood tx retries, flood retry attempts, flood retransmissions, broadcast retries.

Usage: - get flood.retry.count - set flood.retry.count <count>

Parameters: - count: Base retry attempts after the original send, from 0 to 15. 0 disables flood retry.

Note: Infrastructure roles first calculate their retry count: path count 0 uses count * 2, path count 1 uses count * 1.5 rounded up, and path count 2 and higher uses the configured base count, with a hard cap of 15. Companion uses count directly. A shared payload policy then applies to every build: REQ never retries; GRP_TXT keeps the role-calculated count; remote-login-critical RESPONSE, TXT_MSG, ANON_REQ, and PATH packets keep up to 15 at the originating node (path count 0) and cap at 2 after entering the path; all other flood payload types cap at 1. These caps never raise a lower role-calculated count. Setting count to 0 immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.

Forwarded neighbor adverts have an additional loop guard independent of the advert retry setting. After this node completes an advert transmission and hears a downstream copy with a longer path, it does not forward that same advert again while the advert's signed timestamp is less than six hours old. Self-originated adverts, adverts without a heard echo, and adverts six hours old or older are unaffected.

An enabled self-originated advert retry waits at least one additional minute beyond the normal airtime-aware retry delay. Once a newer self advert has successfully entered the outbound queue, its retry sequence replaces queued or future retries for older self adverts; an older advert already transmitting is allowed to finish. Companion firmware permits this single slow retry for its own adverts while continuing to block retry attempts for neighbor adverts it forwards.

Defaults: - infra: 1 - rooftop: 3 - mobile: 15

Examples:

get flood.retry.count\nset flood.retry.count 0\nset flood.retry.count 15\n
"},{"location":"cli_commands/#view-or-change-flood-retry-path-gate","title":"View or change flood retry path gate","text":"

Usage: - get flood.retry.path - set flood.retry.path <count|off>

Parameters: - count: Maximum flood path hash count eligible for retry, from 0 to 63. - off: Disable the path-length gate.

Defaults: - infra: 1 - rooftop: 2 - mobile: 1

Examples:

get flood.retry.path\nset flood.retry.path 1\nset flood.retry.path off\n
"},{"location":"cli_commands/#view-or-change-the-group-data-flood-retry-path-gate","title":"View or change the group-data flood retry path gate","text":"

Usage: - get flood.retry.group.path - set flood.retry.group.path <count|off>

Parameters: - count: Maximum flood path hash count eligible for retry for group data packets (PAYLOAD_TYPE_GRP_DATA/type 6), from 0 to 63. - off: Disable only the group-data-specific gate. The general flood.retry.path gate still applies.

Default: 1 for infra, rooftop, and mobile presets.

Note: The stricter of flood.retry.path and flood.retry.group.path is used. A value of 1 allows retry sequences at path counts 0 and 1; group data at path count 2 or higher is still forwarded normally but does not start a flood retry sequence. A value of 0 allows retries only at the originating sender.

On infrastructure roles, setting flood.retry.path to 0 also sets flood.retry.group.path to off because the general zero-hop gate is already stricter. While the general gate remains 0, attempts to set the group-data gate keep it off. Applying a named retry preset restores the group-data default of 1. Companion retains the saved group gate, but the stricter general gate still wins.

Examples:

get flood.retry.group.path\nset flood.retry.group.path 1\nset flood.retry.group.path off\n
"},{"location":"cli_commands/#view-or-change-flood-retry-advert-handling","title":"View or change flood retry advert handling","text":"

Usage: - get flood.retry.advert - set flood.retry.advert <on|off>

Parameters: - on: Retry node advert floods. - off: Do not retry node advert floods.

Default: off

Examples:

get flood.retry.advert\nset flood.retry.advert off\n
"},{"location":"cli_commands/#view-or-change-flood-retry-target-prefixes","title":"View or change flood retry target prefixes","text":"

Usage: - get flood.retry.prefixes - set flood.retry.prefixes <prefixes|none|off>

Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.

Note: When set, non-bridge flood retry only accepts same-packet echoes whose last hop matches one of these prefixes. When unset, any non-ignored last hop can cancel the retry.

Examples:

get flood.retry.prefixes\nset flood.retry.prefixes A58296,860CCA,425E5C\nset flood.retry.prefixes none\n
"},{"location":"cli_commands/#view-or-change-flood-retry-ignored-prefixes","title":"View or change flood retry ignored prefixes","text":"

Usage: - get flood.retry.ignore - set flood.retry.ignore <prefixes|none|off>

Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.

Note: Non-bridge flood retry does not cancel on same-packet echoes whose last hop matches this list. Bridge mode also excludes these prefixes from bucket and other hits.

Examples:

get flood.retry.ignore\nset flood.retry.ignore 71CE82,C7618C\nset flood.retry.ignore none\n
"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-mode","title":"View or change flood retry bridge mode","text":"

Usage: - get flood.retry.bridge - set flood.retry.bridge <on|off>

Note: Bridge mode retries until each configured fresh bucket, plus the non-source other bucket, has been heard or the retry count is exhausted. If prefixes in different buckets share their first byte, configuration commands return a warning because a 1-byte path cannot distinguish those buckets. The configuration remains valid: an ambiguous source is treated as belonging to every matching source bucket, and an ambiguous echo credits every matching target bucket so it cannot force retry exhaustion.

Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random 0-200% of one additional packet airtime on every attempt. This de-synchronizes repeaters that may have missed the same echo while capping the added wait at two frames.

Only one active retry sequence is kept for a given logical flood packet. An identical flood can still transmit normally, but it does not create a second sequence of extra attempts. Retry state is released if a queued packet is evicted, and the final echo window retains metadata without reserving a packet-pool entry.

Bridge reachability learned from earlier hops in a successful echo is cached separately from recent.repeater. Only the final RF hop updates recent.repeater and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.

Examples:

get flood.retry.bridge\nset flood.retry.bridge on\n
"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-buckets","title":"View or change flood retry bridge buckets","text":"

Usage: - get flood.retry.bucket.<n> - set flood.retry.bucket <n> <prefixes|none|off>

Parameters: - n: Bucket number from 1 to 6. - prefixes: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket. - none or off: Clear the bucket.

Examples:

get flood.retry.bucket.1\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 none\n
"},{"location":"cli_commands/#view-or-change-direct-retry-count","title":"View or change direct retry count","text":"

Search terms: tx retries, transmit retries, direct retries, retry count, retry attempts, retransmission count, message retries, DM retries.

Usage: - get direct.retry.count - set direct.retry.count <count>

Parameters: - count: Maximum retry attempts after the original send, from 1 to 15.

Default: 15 with the rooftop preset

Note: This setting limits retries for eligible direct-routed packets such as traces, requests, responses, and ACKs. Direct-routed text messages (type 2) are an exception: they allow up to 21 retries after the original send, regardless of direct.retry.count or the repeater short-path cap. For example, set direct.retry.count 1 limits eligible non-text packets to one retry, but does not reduce the text-message limit.

These are maximums, not a fixed number of transmissions. Retries stop early when the node hears the next hop forward the packet. The special final-hop retry sends only one duplicate because the destination does not forward the packet. Repeater non-text retries are also capped at 8 for a retry path of up to 3 hops, 12 for 4 hops, and 15 for longer paths; these caps never increase a lower configured count.

Use set direct.retry off to disable this node's direct retries, including text-message retries, and set direct.retry on to enable them again. A count of 0 is not supported. Sending apps and other nodes may have their own retry behavior; this setting does not change it.

Examples:

get direct.retry.count\nset direct.retry.count 1\nset direct.retry.count 4\nset direct.retry.count 15\n
"},{"location":"cli_commands/#view-or-change-direct-retry-base-delay","title":"View or change direct retry base delay","text":"

Search terms: retry delay, retry timeout, retransmission timeout, wait between retries.

Usage: - get direct.retry.base - set direct.retry.base <ms>

Parameters: - ms: First retry wait in milliseconds, from 10 to 5000.

Default: 175 with the rooftop preset

Explanation: - The first retry waits for base + packet-length add-on + random forwarding jitter after the preceding transmission completes. - TRACE and ANON_REQ/type 7 packets use a 3x line-time add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured base with the same packet-type add-ons. - Larger values reduce channel pressure and give slow repeaters more time. - Smaller values recover faster but create tighter retry bursts.

Examples:

get direct.retry.base\nset direct.retry.base 175\nset direct.retry.base 275\nset direct.retry.base 500\n
"},{"location":"cli_commands/#view-or-change-direct-retry-step-delay","title":"View or change direct retry step delay","text":"

Search terms: retry backoff, increasing retry delay, retry interval step.

Usage: - get direct.retry.step - set direct.retry.step <ms>

Parameters: - ms: Extra milliseconds added for each subsequent retry, from 0 to 5000.

Default: 100 with the rooftop preset

Explanation: - Retry delay is base + packet-length add-on + random forwarding jitter + attempt_index * step. - TRACE and ANON_REQ/type 7 packets use a 3x packet-length add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured step with the same packet-type add-ons. - With base=175 and step=100, the fixed portion is 175, 275, 375, 475 ms, and so on, before the packet-length add-on and random jitter. - step=0 keeps every retry at the same delay. - Larger steps spread retries over time and are safer on busy channels.

Examples:

get direct.retry.step\nset direct.retry.step 0\nset direct.retry.step 50\nset direct.retry.step 100\nset direct.retry.step 250\n
"},{"location":"cli_commands/#view-or-change-direct-retry-snr-margin","title":"View or change direct retry SNR margin","text":"

Search terms: retry signal threshold, retry SNR threshold, retry signal margin.

Usage: - get direct.retry.margin - set direct.retry.margin <snr_db>

Parameters: - snr_db: Extra SNR margin above the SF receive floor, from 0 to 40.

Default: 5.00 with the rooftop preset

Notes: - This command is repeater-only. - Unknown repeaters are still retried. - Known repeaters below the receive floor plus this margin are skipped. - Failed attempts lower the recent repeater SNR estimate by 0.25 dB.

Examples:

get direct.retry.margin\nset direct.retry.margin 0\nset direct.retry.margin 2.5\nset direct.retry.margin 5\nset direct.retry.margin 10\n
"},{"location":"cli_commands/#view-or-change-adaptive-direct-retry-coding-rate","title":"View or change adaptive direct retry coding rate","text":"

Usage: - get direct.retry.cr - set direct.retry.cr off - set direct.retry.cr on (room-server and sensor) - set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>

Parameters: - cr4_min: Minimum SNR in dB to retry at CR4. - cr5_min: Minimum SNR in dB to retry at CR5. - cr7_min: Minimum SNR in dB to retry at CR7. - cr8_max: Maximum SNR in dB that forces CR8.

Default: 10.00,7.50,2.50,2.50

Explanation: - Higher SNR uses faster coding rates. - Lower SNR uses more robust coding rates. - Repeater retry attempts escalate from the adaptive starting CR. CR4 starts as CR4, CR5, CR7, CR7, then CR8. CR5 starts as CR5, CR7, CR7, then CR8. CR7 gets two attempts, then CR8. - Repeater adaptive CR selection intentionally skips CR6. - Non-repeater retry packets start at the current radio CR and follow the same escalation pattern, clamped at CR8. With the normal CR5 radio setting this is CR5, CR7, CR7, then CR8. - Room-server and sensor firmware accept on or off; numeric SNR thresholds are repeater-only because those roles do not keep recent-repeater SNR data. - off disables per-packet direct-retry CR overrides and uses the current radio CR. - Retry packets may use a different coding rate, but they keep the radio's normal physical preamble. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - A failed unknown repeater is seeded at +2.75 dB. - Each later failure lowers the SNR estimate by 0.25 dB.

Examples:

get direct.retry.cr\nset direct.retry.cr off\nset direct.retry.cr on\nset direct.retry.cr 10.0,7.5,2.5,2.5\nset direct.retry.cr 12.0,8.0,4.0,1.0\nset direct.retry.cr 8.0,5.0,1.5,0\nset direct.retry.cr 6.0,3.0,0,-2.0\nset direct.retry.cr 20.0,12.0,6.0,2.0\nset direct.retry.cr 4.0,2.0,0,-4.0\n

Example profiles: - Conservative weak-link profile:

set direct.retry.cr 12.0,8.0,4.0,1.0\n
set direct.retry.cr 10.0,7.5,2.5,2.5\n
set direct.retry.cr 6.0,3.0,0,-2.0\n
set direct.retry.cr 20.0,12.0,6.0,2.0\n
"},{"location":"cli_commands/#view-seed-or-clear-the-recent-repeater-table","title":"View, seed, or clear the recent repeater table","text":"

Usage: - get recent.repeater - get recent.repeater <page> - get recent.repeaters <page> - get recent.repeaters search <prefix> [page] - set recent.repeater <prefix> [snr_db] - clear recent.repeater

Parameters: - prefix: Repeater path-hash prefix as 2, 4, or 6 hex characters. - snr_db: Optional SNR in dB. If omitted or invalid, defaults to 3.0. - page: 1-based result page.

Note: These commands are repeater-only.

The default capacity is 256 entries on classic ESP32, 2,048 on other ESP32 chips, 512 on nRF52, and 64 on other platforms. Builds can override it with MAX_RECENT_REPEATERS. Classic ESP32's history uses 3,072 bytes of startup heap instead of 24,576 bytes, leaving more memory for the packet pool and Wi-Fi.

Output order: - get recent.repeater lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes. - Within each prefix length, entries are sorted from highest SNR to lowest SNR. - search returns every overlapping path-hash entry. For example, searching 860C can return 86, 860C, and 860CCA; it does not return a different branch such as 86D0. - Search rows include the monotonic age of the entry's most recent recording, compacted to a whole s, m, or h field. Search pages contain up to six rows so the result remains within the remote CLI reply limit.

SNR details: - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: 75% existing SNR and 25% new heard SNR, rounded up. - Direct retry success also feeds the heard echo SNR back into the same weighted table. - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by 0.25 dB. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at +2.75 dB. - set recent.repeater <prefix> [snr_db] seeds a missing prefix or adds another weighted sample for an existing prefix. - Successful set recent.repeater replies include the stored prefix and SNR, for example OK - set A1B2C3 at 3.0 SNR. - Entries strictly older than 24 hours are removed during a sweep every three hours, so an entry can remain for at most approximately 27 hours.

Examples:

get recent.repeater\nget recent.repeater 2\nget recent.repeaters search 86\nget recent.repeaters search 860C page 2\nset recent.repeater A1B2C3 8.5\nset recent.repeater 71CE82 -3.25\nset recent.repeater A1B2C3\nclear recent.repeater\n
"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"

Search terms: enable GPS, disable GPS, turn GPS on, turn GPS off.

Usage: - get gps - set gps <state>

Parameters: - state: on|off

Default: off

Note: These command names are shared with GPS-capable Companion builds. Companion reports on or off; infrastructure includes receiver details: - off, {powered|unpowered} when GPS is manually disabled - on, {powered|unpowered}, {fix|no fix}, {sat count} sats when GPS is enabled

powered describes the supply, not proof that the receiver is tracking or asleep. Shared-power hardware can remain powered while GPS processing is off.

On repeaters, gps off cancels any acquisition already in progress. A later authorized location-telemetry request may wake the receiver, but only until its first valid fix is cached; GPS processing then stops again. Whether its supply can also turn off depends on the board's power policy below.

"},{"location":"cli_commands/#rak4631-shared-peripheral-power","title":"RAK4631 shared peripheral power","text":"

Usage:

get peripherals.mode\nset peripherals.mode gps-only\nset peripherals.mode shared\nreboot\n

Default: gps-only on RAK4631. The saved mode takes effect only after reboot; setting it does not switch power immediately. get peripherals.mode reports the saved choice, which may differ from the current boot's mode.

Ethernet/PoE builds and detected external NOR storage automatically keep the rail on, even in gps-only mode or when the network service is disabled. RAK3401 permanently protects the same rail because its radio frontend needs it; peripherals.mode is a RAK4631 setting, not an override of that protection.

On a powered shared rail, RAK12501/L76K cannot be independently switched off: GPS-off stops MCU parsing, not physical GNSS operation. Other receivers may use internal power-saving commands, but shared does not promise physical sleep. UART1 bridge ownership remains protected; changing this mode does not grant permission to connect two transmitters to the GPS UART.

"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"

Usage: - gps sync

The GPS must be enabled. When GPS power saving has put an enabled receiver to sleep, this command schedules a sync and wakes it; after set gps off, it reports gps is off without scheduling work.

On repeaters with device power saving enabled, the automatic GPS cycle wakes for up to 10 minutes and repeats seven days after GPS sleeps. A valid GPS time can correct the RTC either forward or backward. With device power saving off, an enabled GPS refreshes the RTC every 30 minutes. These are the legacy defaults; gps.sync.interval below overrides them.

"},{"location":"cli_commands/#set-the-automatic-gps-clock-sync-interval","title":"Set the automatic GPS clock-sync interval","text":"
get gps.sync.interval\nset gps.sync.interval 1\nset gps.sync.interval 24\nset gps.sync.interval 336\n

The interval is a whole number of hours, from 1 to 336 (two weeks). Larger numbers are capped, not rejected. For example:

set gps.sync.interval 999\nOK - GPS sync interval 336 hours (saved)\n

Zero, negative numbers, fractional hours, and nonnumeric values are rejected. The setting is saved across reboots and supported by GPS-equipped repeater, room-server, sensor, and Companion builds. Until it is explicitly configured, the getter reports default (board GPS sync policy) and existing board timings are retained.

The selected interval controls periodic clock acquisitions both with GPS power saving and with a continuously powered receiver. Changing it reschedules a sleeping receiver without immediately powering it on. A GPS fix can take time; an unsuccessful acquisition retains the existing bounded awake window, then backs off before trying again. Device GPS/power switches still apply.

This does not change position-update gps_interval, location telemetry/cache timings, or mesh/NTP time synchronization. Location requests can still power GPS for a fix, but do not bypass the configured clock-sync interval. Explicit gps sync remains an immediate acquisition request where that command exists.

"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"

Usage: - gps setloc

"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"

Usage: - gps advert - gps advert <policy>

Parameters: - policy: none|share|prefs - none: don't include location in adverts - share: share gps location (from SensorManager) - prefs: location stored in node's lat and lon settings

Default: prefs on every repeater, room-server, and sensor build that uses the common advert policy. A previously saved none, share, or prefs choice still overrides the first-boot default after an update.

"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-telemetry-access-mode","title":"View or change telemetry access mode","text":"

Usage: - get telemetry.access - set telemetry.access <mode>

Parameters: - mode: all|acl - all: allow telemetry requests using the requester-provided telemetry mask - acl: require ACL read-only or higher for telemetry, including GPS

Default: all

Note: all matches the previous sensor telemetry behavior.

"},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"

Usage: sensor list [start]

Parameters: - start: Optional starting index (defaults to 0)

Note: Output format: <var_name>=<value>\\n

"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"

Usage: - sensor get <key> - sensor set <key> <value>

Parameters: - key: Sensor setting name - value: The value to set the sensor to

"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":"

For a two-repeater setup and diagnostic flow, see the ESP-NOW bridge setup guide or RS-232/UART bridge guide. For an existing WiFi network, see the eight-peer UDP bridge guide.

"},{"location":"cli_commands/#wifi-udp-bridge","title":"WiFi UDP bridge","text":"

Available on supported ESP32 repeater images with WITH_UDP_BRIDGE. UDP starts off on new/upgraded installs. It requires an existing station WiFi connection, and pauses during AP/AP+STA operation, disconnection, browser OTA and ownership transitions. It does not start a network or require an MQTT broker.

get udp.enabled\nset udp.enabled on\nset udp.enabled off\nget udp.port\nset udp.port 4404\nget udp.peer.1\nset udp.peer.1 192.168.1.51:4404\nset udp.peer.1 off\nget udp.status\nget udp.stats\nget udp.stats rx\nget udp.stats tx\nget udp.stats drops\nget udp.stats test\n

Peer slots are udp.peer.1 through udp.peer.8. Each destination is one numeric unicast IPv4 address plus a port 1..65535. The listening port defaults to 4404. Multicast, loopback, unspecified/broadcast and duplicate endpoints are rejected. Both boards must list each other's listening address/port. Reserve DHCP addresses so the stored destinations remain valid.

The transport reuses bridge.secret for authenticated sessions and packet integrity; no additional password is introduced. UDP payloads are not encrypted. Counter/status output does not reveal the secret. Config writes use the existing authenticated CLI policy and a separate checked preferences file; a successful save reports intent rather than reachability.

Local-only diagnostic actions:

udp test 1\nudp test 1 A1B2C3D4\nudp resetstats\n

The peer number is 1..8; the optional ID is exactly eight hexadecimal digits without 0x. A queued/sent probe is not a delivery acknowledgement. On the destination, check get udp.stats test for an increased rx and matching rxid, then reverse the test. An authenticated peer session must be ready. The status ready count records established sessions, not current liveness; use a fresh received probe to confirm reachability. Incoming UDP enters the local LoRa mesh and is not relayed to other UDP peers. Probes do not echo, retry, allocate mesh packets or transmit on LoRa. Resetting statistics does not write preferences or restart the bridge.

Both set usb.logging on and set usb.debug on enable bounded runtime USB summaries without requiring a special debug build. Restore both previous settings after capture. CLI pages/probes work with logging disabled.

WiFi client isolation and firewall rules must allow peer-to-peer UDP; the firmware cannot bypass an isolating guest network. Isolation is an optional network policy, not an intrinsic WiFi restriction. See the setup and troubleshooting guide.

"},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"

Usage: get bridge.type

"},{"location":"cli_commands/#diagnose-the-esp-now-bridge","title":"Diagnose the ESP-NOW bridge","text":"

Available when the optional ESP-NOW bridge is compiled in. These are runtime diagnostics; no BRIDGE_DEBUG build flag is required.

Read-only commands:

get espnow.status\nget espnow.stats\nget espnow.stats rx\nget espnow.stats tx\nget espnow.stats drops\nget espnow.stats test\n

Status reports local state (off, teardown-error, driver-error, channel-mismatch or ready), then these compact fields:

Field Meaning ch Configured/actual 2.4 GHz channel; actual 0 means unavailable. fmt, src Packet format and configured LoRa packet source (rx or tx). wifi Shared WiFi enable setting, not proof of WiFi association. mode, ps, proto WiFi SDK mode and power-save integers, and protocol bitmask in hexadecimal. An unavailable mode or power-save query returns -1. q Current receive/transmit queue depths. last Last recorded SDK error stage/code, which may remain after recovery.

espnow.running on means the local bridge initialized; it does not prove a peer is reachable. The statistics pages use these keys:

Page Keys and meaning Default rx: received callback frames; mesh: inbound MeshCore packets queued; tx: outbound MeshCore packets queued; sent: successful frame callbacks. rx frames: received callback frames; decoded: parsed MeshCore packets; mesh: queued for mesh processing; crc: checksum failures; bad: malformed frames; parse: packet parse failures. tx req: mesh TX requests; queued: mesh packets queued; start: SDK-accepted frames; ok: successful frame callbacks; err: SDK start failures; cbfail: failed frame callbacks; timeout: callback timeouts. drops rxq: RX queue drops; foreign: foreign-group packets; filter/dupe: inbound filter/duplicate drops; txq: recorded local mesh TX rejection/final-failure decisions; txfilter/txdupe: outbound filter/duplicate drops; big: oversize TX drops. test queued, start, ok, fail, timeout: probe TX stages; rx, bad: valid/invalid probes received; txid, rxid: last queued/received IDs in hexadecimal.

Mesh admission means queued for normal processing, not confirmed LoRa forwarding or end-to-end delivery. Packet and frame counters have different units: fragmentation can produce several frames for one mesh packet. Counters survive bridge stop/start; reboot or the explicit reset starts a new window. Stopping or restarting the bridge clears pending queue work without classifying its delivery. These counters are not a complete end-to-end delivery ledger.

Local-only actions:

espnow test\nespnow test A1B2C3D4\nespnow resetstats\n

The optional test ID is exactly eight hexadecimal digits, case-insensitive, without 0x, spaces or trailing arguments. Omitting it uses uptime in milliseconds. The reply OK - test queued id=...; confirm on peer confirms local queueing only. Read the other updated board's get espnow.stats test and match rxid, with an increased rx, to establish one-way probe receipt. Send a different ID from the other board to test the reverse direction.

A probe is unencrypted, CRC-checked transport diagnostic data, not a MeshCore packet or a security token. It does not verify packet-format compatibility or bridge.secret, authenticate the peer or establish normal packet delivery. There is no automatic echo, retry or LoRa forwarding; older firmware ignores it. Resetting statistics does not restart the bridge or write preferences. Test/reset commands are rejected over LoRa; use a local console. Every reply fits the bounded CLI frame, and diagnostics do not print from WiFi callbacks.

For a short USB debug capture, record both get usb.logging and get usb.debug, then enable both set usb.logging on and set usb.debug on. Run the test and restore both original settings. The ordinary image emits bounded ESPNOW: updates from its main loop at most once per second when diagnostic state changes, even with BRIDGE_DEBUG=0. Updates can be skipped when USB is busy; read the counter pages for exact values. The CLI counter pages and probes do not require either logging gate.

See the two-board diagnostic flow. Do not include get bridge.secret or WiFi credentials in shared debug logs.

"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"

Search terms: enable bridge, disable bridge, serial bridge, RS232 bridge.

Usage: - get bridge.enabled - get bridge.running - set bridge.enabled <state>

Parameters: - state: on|off

bridge.enabled is the saved intent. bridge.running reports whether the bridge actually started in this boot; they can differ after a hardware conflict, missing credentials, or a transient initialization failure. Normal merged repeater images default to off; dedicated bridge images may default to on.

Full MQTT repeaters with RS-232 expose three independent transports. Use set rs232.enabled on|off and get rs232.enabled for saved UART intent; get rs232.running reports the actual UART state. mqtt.enabled and espnow.enabled select the other two transports. In MQTT images the historical bridge.enabled alias controls ESP-NOW; in non-MQTT UART images it controls RS-232. bridge.baud and bridge.uart change only UART configuration, without restarting MQTT or ESP-NOW. The new UART mode starts off when upgrading old MQTT preferences and retains explicit settings across reboots.

"},{"location":"cli_commands/#diagnose-the-rs232-bridge","title":"Diagnose the RS232 bridge","text":"

Available on repeater builds containing the optional RS-232 bridge. These runtime diagnostics require no BRIDGE_DEBUG build flag. Builds without the transport do not gain UART support from these commands.

Read-only commands:

get rs232.transport\nget rs232.stats\nget rs232.stats rx\nget rs232.stats tx\nget rs232.stats drops\nget rs232.stats test\n

The existing get rs232.status keeps its heartbeat/automatic-standby report. If no UART bridge object exists, such as after explicitly disabling it, the transport and counter commands report RS232 diagnostics unavailable; use rs232.enabled, rs232.running and rs232.status to check saved intent and power state. get rs232.transport reports the local transport state (ready, off, standby, probing or retrying) and these fields:

Field Meaning baud Configured serial baud rate. pins Selected RX/TX pin numbers. src Configured LoRa packet source, rx or tx. rxpart, rxready Bytes held in the current RX frame and whether a complete frame awaits mesh-pool space. tx Bytes remaining in the owned TX frame. heartbeat on initiates heartbeats; passive can still answer a valid ping. last Last recorded diagnostic error: none, pins, driver, test.stopped, test.busy or test.timeout. It can remain after recovery.

A running UART means local initialization succeeded, not that a peer is reachable. Counter pages separate ordinary mesh traffic from explicit probes:

Page Keys and meaning Default rx: received data frames; mesh: inbound packets admitted to mesh processing; tx: outbound mesh packets queued; sent: mesh frames fully accepted by the local UART. rx bytes: all consumed UART bytes; frames: checksum-valid data frames; crc: checksum failures; length: invalid lengths; parse: mesh-packet parse failures; timeout: incomplete-frame expiry; pool: complete frames held for mesh-pool space. tx req: mesh TX requests; queued: mesh frames accepted into the owned buffer; sent: fully UART-accepted mesh frames; bytes: all UART-accepted bytes; stall, short: blocked drain attempts or short-write events. drops noise: synchronization rejects; filter/dupe: inbound filter/duplicate drops; busy/stopped: mesh TX rejected by transport state; txfilter/txdupe: outbound filter/duplicate drops; big: invalid or oversize TX packets. test queued, sent, fail: probe TX stages and recorded failures; a late write can count as both failed and sent; rx, bad: valid/invalid probes received; txid, rxid: last queued/received IDs in hexadecimal.

Mesh admission means queued for normal processing, not confirmed LoRa forwarding or end-to-end delivery. Local write acceptance does not prove the bytes reached the peer. Byte totals include data, heartbeat and test traffic; RX bytes also include discarded noise. A frame can cause several write stalls. Counters survive same-object UART stop/start, including automatic standby. Disabling or reconfiguring the bridge can delete and replace that object, starting a new window; reboot and explicit reset also clear the counters. Stopping a transport can discard unfinished work without determining delivery, so the pages are not a complete delivery ledger.

Local-only actions:

rs232 test\nrs232 test A1B2C3D4\nrs232 resetstats\n

The optional ID is exactly eight hexadecimal digits, case-insensitive, without 0x, spaces or trailing arguments. Omitting it uses uptime in milliseconds. OK - test queued id=...; confirm on peer confirms local queueing only. On the other updated board, get rs232.stats test must show an increased rx and matching rxid. Use a different ID in the reverse direction.

Probes use a reserved UART-local control frame. They do not allocate a mesh packet, enter duplicate tracking, refresh peer liveness, authenticate a peer, or get forwarded onto LoRa. There is no automatic echo or retransmission. A probe that misses its write deadline records one failure. If no byte was accepted by the UART, it is cancelled. If a prefix was already accepted, the bridge retains and drains only the original suffix, with bounded work per main-loop pass, before another frame can start. This preserves framing for older receivers. A later completion also increments sent; the historical failure is retained and neither result proves peer receipt. A stopped, transitioning or busy UART, including a due heartbeat, rejects the test rather than waking or taking over the port. Older firmware ignores the extension. The test and counter reset do not write preferences and are rejected over LoRa; use a local console.

For a short USB capture, record both get usb.logging and get usb.debug, then enable both set usb.logging on and set usb.debug on. The ordinary image emits bounded RS232: updates from its main loop at most once per second when diagnostic state changes. Busy USB output can skip updates; use the counter pages for exact values. Restore both original settings afterward. The CLI pages and test probes need neither logging gate.

See the two-board serial diagnostic flow.

"},{"location":"cli_commands/#rs232-heartbeat-and-automatic-standby","title":"RS232 heartbeat and automatic standby","text":"

Available on repeater builds with an RS232 bridge. This is a UART-local control exchange, not an advert or a LoRa packet. Room builds without RS232 are unaffected.

Usage:

Heartbeat and negotiated automatic standby are on by default, including upgrades. An active UART pings every 30 seconds (52 bytes per ping). Stock bridges ignore that traffic and still receive the original data frames; they are not automatically put to sleep: standby is armed only after a fresh heartbeat reply proves support. Fresh, session/challenge-matched replies prove a bidirectional peer; ordinary valid incoming data can keep a legacy link awake but cannot prove heartbeat support. The status separates saved intent, actual UART state, standby/probe phase, and confirmed versus legacy/unknown peer liveness.

Enable on both updated peers:

set rs232.enabled on\nset rs232.heartbeat on\nset powersaving on\nset rs232.idle 300\nset rs232.autosleep on\nget rs232.status\n

With heartbeat, autosleep, and global powersaving all on and the peer's heartbeat support confirmed, when no peer responds for the idle interval the UART is soft-stopped while rs232.enabled stays on. It wakes after 30 seconds for a 2-second probe; every eighth probe listens for 40 seconds. That longer window spans the other side's sleep interval, so two sleeping updated peers can reconnect without manual intervention (normally within about five minutes with working wiring and timely loop servicing). Healthy peers remain running; this is dead-link power saving, not an idle-data duty cycle.

Stock receivers ignore these control frames; they are deliberately invalid zero-length stock frames with an ASCII-only remainder. They do not allocate a mesh packet or enter LoRa forwarding. Stock peers do not answer heartbeats, so default negotiated standby never arms on a stock-only link. Valid legacy data after heartbeat replies cease also disarms negotiated standby, allowing a replacement stock peer to stay awake. Use set rs232.autosleep force only to sleep an absent/unconfirmed UART peer; this explicitly bypasses stock protection and can miss one-off stock packets. If a previously confirmed updated peer is silently replaced with stock while the local UART is already sleeping, its first packet can be missed before replacement is detected. Set autosleep off before that change, or restart the local bridge to clear the old negotiated session. While the UART is stopped it cannot receive a wake byte; mesh traffic during standby is not queued for later UART delivery. Turning autosleep or global powersaving off resumes the saved-enabled UART; rs232.enabled off remains an explicit off and never automatically wakes. rs232.heartbeat off stops initiating heartbeats and prevents standby. An updated active peer can still answer a valid incoming ping in passive mode.

"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"

Usage: - get bridge.delay - set bridge.delay <ms>

Parameters: - ms: Delay in milliseconds (0-10000)

Default: 500

"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"

Usage: - get bridge.source - set bridge.source <source>

Parameters: - source: - rx: bridges received packets - tx: bridges transmitted packets

Default: tx

Note: For MQTT, use mqtt.rx and mqtt.tx to control each direction independently. Both can be enabled together. Use mqtt.enabled as the MQTT master switch; bridge.* configures RS232 and ESP-NOW bridges.

"},{"location":"cli_commands/#view-or-change-the-mqtt-master-switch","title":"View or change the MQTT master switch","text":"

Search terms: enable MQTT, disable MQTT, MQTT on, MQTT off, WiFi logging.

Usage:

These commands are the same on MQTT-capable Companion, Repeater, Room Server, and Sensor builds. The master switch is saved; turning it off disconnects MQTT while keeping broker presets and credentials. Turning it on allows those brokers to reconnect. get mqtt.running checks the service's runtime state; get mqtt.status reports individual broker connections. Enabling MQTT does not imply that a broker is connected.

Custom mqtts:// and wss:// brokers require an image with the embedded CA bundle. Without it, the slot stays disconnected and sends no credentials; use a build with the bundle or a supported preset with its pinned CA. Portable MQTT images retain those presets but do not include the general bundle for custom TLS brokers. Explicit mqtt:// and ws:// configurations remain available for trusted networks and use plaintext transport.

On ESP32 Repeater and Room Server images, set mqtt.enabled off acknowledges the stop request while the MQTT task disconnects and destroys its clients. get mqtt.stopping stays on until that cleanup completes; mqtt.running also stays on while those resources are still owned. Wait for mqtt.stopping to become off before enabling MQTT again. UART, ESP-NOW, and the radio continue running during this cleanup. OTA still waits for a completed MQTT shutdown before it can write firmware.

USB logging remains independent. On images with both outputs, set logging.output off|usb|wifi|both selects USB and MQTT together.

"},{"location":"cli_commands/#view-or-change-mqtt-rx-packet-uplinking","title":"View or change MQTT RX packet uplinking","text":"

Search terms: MQTT receive logging, MQTT RX capture, publish received packets.

Usage: - get mqtt.rx - set mqtt.rx <on|off>

Parameters: - on: uplink received (RX) packets to MQTT brokers - off: disable RX packet uplinking

Default: on

"},{"location":"cli_commands/#view-or-change-mqtt-tx-packet-uplinking","title":"View or change MQTT TX packet uplinking","text":"

Search terms: MQTT transmit logging, MQTT TX capture, publish sent packets.

Usage: - get mqtt.tx - set mqtt.tx <on|off|advert>

Parameters: - on: uplink all transmitted (TX) packets to MQTT brokers - advert: uplink only this node's own advert packets (self-originated advertisements only - forwarded adverts from other nodes are filtered out) - off: disable TX packet uplinking

Default: advert

Note: mqtt.rx and mqtt.tx take effect immediately - no restart required. Both can be enabled simultaneously.

"},{"location":"cli_commands/#view-or-change-mqtt-advert-signature-filtering","title":"View or change MQTT advert signature filtering","text":"

Usage:

Parameters:

Default: off

Note: Adverts are uploaded before the mesh validates them, so a frame corrupted over the air that still passes the LoRa CRC is published with a mangled name or location. Leaving this off keeps that corruption visible to collectors; turning it on hides it from them. Dropped adverts are counted as badsig= in get mqtt.stats. Takes effect immediately.

On Companion builds, enabling this setting opts into a marked MQTT preference record. Before rolling back to firmware without this setting, turn it off to restore the older record format and retain the saved MQTT setup. Ordinary default-off updates keep the existing record size.

"},{"location":"cli_commands/#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature","title":"View or change periodic neighbors publishing (MQTT observer, neighbors feature)","text":"

Usage: - get mqtt.neighbors - set mqtt.neighbors <on|off>

Parameters: - on: periodically discover neighbor scopes and publish the neighbor table to the neighbors topic - off: disable periodic neighbors publishing

Default: off

Note: Requires a build with the neighbors feature compiled in (all PSRAM boards, plus non-PSRAM boards built with MQTT_NEIGHBORS_WITHOUT_PSRAM); elsewhere this replies Err - neighbors not enabled in this build. Non-PSRAM builds publish at most 20 neighbours per pass to bound internal-DRAM use, and set truncated with the true total_neighbors when the table is larger. The setting is read live by the mesh loop - no restart required; enabling it triggers a discovery on the next pass. While enabled, get mqtt.status gains a trailing nbr: <next>/<last> field (time to next publish, and how the last publish went).

"},{"location":"cli_commands/#view-or-change-the-neighbors-publish-interval-mqtt-observer-neighbors-feature","title":"View or change the neighbors publish interval (MQTT observer, neighbors feature)","text":"

Usage: - get mqtt.neighbors.interval - set mqtt.neighbors.interval <hours>

Parameters: - hours: how often to publish the neighbor table (12-336, default 24)

Default: 24 (hours)

Note: Out-of-range values are rejected (not clamped). Requires a build with WITH_MQTT_NEIGHBORS; PSRAM boards enable it automatically and selected non-PSRAM variants opt in with MQTT_NEIGHBORS_WITHOUT_PSRAM.

"},{"location":"cli_commands/#view-or-change-the-ntp-server-mqtt-observer-only","title":"View or change the NTP server (MQTT observer only)","text":"

Usage: - get mqtt.ntp - set mqtt.ntp <hostname> - set mqtt.ntp none

Description: Sets the primary NTP server used for clock sync (required for JWT MQTT auth). On set, the device attempts an immediate sync of the just-configured server (primary only, so a typo fails fast) when WiFi is connected and the MQTT bridge is running.

Fallbacks: If the primary fails, the firmware tries pool.ntp.org, time.google.com, time.cloudflare.com, time.aws.com, and time.nist.gov in order (skipping duplicates).

Default: pool.ntp.org (when unset or none)

"},{"location":"cli_commands/#diagnose-ntp-server-connectivity-mqtt-observer-only","title":"Diagnose NTP server connectivity (MQTT observer only)","text":"

Usage: - get mqtt.ntp.diag

Description: Probes every configured NTP server (the custom primary, if set, plus the built-in fallbacks) and reports whether each responds. This is a pure connectivity diagnostic - it does not change the system clock.

Requires WiFi connected and the MQTT bridge running.

"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"

Usage: - get bridge.baud - set bridge.baud <rate>

Parameters: - rate: Integer baud rate from 9600 through the board's compiled BRIDGE_MAX_BAUD (commonly 500000); for example 115200. Stop the bridge with set rs232.enabled off in a combined MQTT image, or set bridge.enabled off in a non-MQTT UART image, before changing it. Enable the UART again with the matching switch.

Default: 115200

"},{"location":"cli_commands/#view-or-change-the-uart-used-by-the-bridge-rs-232-only","title":"View or change the UART used by the bridge (RS-232 only)","text":"

Usage: - get bridge.uart - set bridge.uart <port>

Parameters: - port: Hardware UART number compiled for the board. Most boards expose one fixed UART. RAK4631 accepts 1 or 2; UART 2 is the default so UART 1 can remain available to the RAK12501/L76K GPS. RAK12500 GPS uses I2C rather than this UART, but the explicit legacy Serial1 bridge omits the combined GPS provider and therefore does not expose either GPS path.

The setting is persistent and restarts an enabled bridge immediately. Normal repeater artifacts start with bridge.enabled off; configure the UART and baud rate before running set bridge.enabled on. Combined MQTT images use rs232.enabled for that independent UART setting. On the canonical RAK4631 runtime image, UART 1 is reserved even if the bounded boot probe hears no RAK12501. Silence cannot prove that a cold L76K is physically absent, and that module remains powered by the shared WB_IO2/3V3_S rail. Use UART 2. UART 1 requires an explicit no-GPS/dedicated Serial1 bridge image. This fail-closed reservation also applies when the detected GPS is an I2C RAK12500; that receiver does not electrically use UART 1, but its presence cannot rule out another silent UART module.

"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"

Usage: - get bridge.channel - set bridge.channel <channel>

Parameters: - channel: Channel number from 1 through 13 in either format. This matches the primary-ESP-NOW policy and the default regulatory range used by the supported ESP32 targets.

This controls the optional ESP-NOW bridge transport; it does not change a node's primary ESP-NOW mesh channel. Primary-ESP-NOW firmware uses get espnow.channel and set espnow.channel <1-13> instead.

"},{"location":"cli_commands/#view-or-change-the-esp-now-bridge-wire-format","title":"View or change the ESP-NOW bridge wire format","text":"

Usage: - get bridge.format - set bridge.format <format>

Parameters: - format: - wrapped: the original ESP-NOW bridge framing (magic, checksum, and XOR using bridge.secret) - raw: the exact serialized MeshCore packet used by primary ESPNOWRadio firmware

Default: wrapped

The setting is persistent and restarts an enabled bridge immediately. Receive parsing is strict: wrapped accepts only wrapped frames and raw accepts only raw MeshCore frames. Coordinate the format and channel on every ESP-NOW peer; there is no automatic dual-format receive mode because it would permit ambiguous, asymmetric bridge deployments.

Use raw to connect a LoRa-primary *_repeater_bridge_espnow node to Generic_ESPNOW, SenseCapIndicator-ESPNow, or another primary-ESP-NOW node. Raw mode enables the ESP-NOW LR PHY and sends at the same LR rate used by those targets. All nodes must use the same 1-13 channel. On a primary-ESP-NOW node, change that side with set espnow.channel <1-13> and reboot; on the bridge, use set bridge.channel <1-13>.

Raw mode ignores bridge.secret and removes the bridge wrapper's lightweight network isolation. MeshCore's own packet authentication/encryption still applies where the packet type provides it, but public frames and routing metadata remain visible. The wrapped format's XOR is isolation, not strong cryptography. Bridge duplicate tracking remains active in both formats, but deploying multiple gateways between the same LoRa and ESP-NOW coverage areas can still increase duplicate traffic while their seen-packet tables converge.

One ESP-NOW frame carries at most 250 payload bytes. In raw mode, serialized MeshCore packets up to that size remain byte-for-byte compatible with existing raw endpoints. Updated raw endpoints split 251-255-byte transport units into two versioned fragments and reassemble them by source MAC, length, and CRC; both endpoints must include this support for those sizes. Current valid MeshCore packet geometry reaches 254 bytes, while 255 is retained as transport headroom. Wrapped mode retains its legacy 246-byte maximum after the four-byte magic and checksum overhead, and drops larger packets instead of truncating them.

"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"

Usage: - get bridge.secret - set bridge.secret <secret>

Parameters: - secret: ESP-NOW bridge secret, 1-15 characters

Default: Varies by board

This setting is used only by bridge.format wrapped; raw mode ignores it.

"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"

Usage: get bootloader.ver

Applies to every nRF52 board/role that exposes this getter, including Companion, repeater, and room-server firmware. It reads the installed bootloader region selected by the MBR/UICR boot address and bounded by the chip's flash geometry; it does not search application firmware or staged update files.

The reader preserves a complete Adafruit/UF2 version string, including OTAFIX, preview, and vendor/build suffixes. Manufacturer two-component versions such as Seeed's 1.00 are preserved without inventing a patch number. If that string is absent or ambiguous, it uses the existing whole-image CRC and unique BLMF/BLM2 validation to read the embedded OTAFIX version (for example, > OTAFIX2.4.6). Some current release images, including MeshTower V2 SD 2.4.6, omit the UF2 text entirely. As a last OTAFIX-specific fallback, it accepts one unambiguous standalone version token such as v0.11.0-OTAFIX2.4.9 from only the first 40 KiB of the identified bootloader region. A corrupt canonical bootloader manifest cannot be bypassed by that printable token. The Adafruit core's startup-captured version remains the final fallback and is explicitly labelled (base); it cannot identify an OTAFIX/fork revision.

unknown remains possible for a bootloader exposing none of these version sources, invalid geometry/vectors, or unresolved ambiguity. There is no universal version field shared by every third-party bootloader. This is diagnostic only: displaying a version neither grants OTA capability nor bypasses install checks. The fix is in MeshCore application firmware; installing another bootloader alone does not update an older application's version reader.

See the stock bootloader audit for tested manufacturer images and the boards still requiring untouched factory dumps.

"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"

Usage: get pwrmgt.support

"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"

Usage: get pwrmgt.source

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"

Usage: get pwrmgt.bootreason

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"

Usage: get pwrmgt.bootmv

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#configure-battery-charge-voltage","title":"Configure battery charge voltage","text":"

T-Beam SX1262/SX1276 and T-Beam S3 Supreme support persistent charger-voltage selection through the fitted AXP192 or AXP2101 PMU:

get charge.voltage.options\nget charge.voltage\nset charge.voltage 4.1\n

Values are exact supported targets in volts. Neither T-Beam PMU supports 3.65 V. Heltec Mesh Solar's CN3795 charge voltage is set by its Li-ion or LiFePO4 hardware version, so these commands report that limitation without changing BMS settings. See charge-voltage behavior and supported targets.

"},{"location":"cli_commands/#configure-nrf52-battery-protection","title":"Configure nRF52 battery protection","text":"

Usage: get pwrmgt.bootlock, set pwrmgt.bootlock <2500-4200>

Usage: get pwrmgt.cutoff, set pwrmgt.cutoff <off|2500-4200>

Values are battery millivolts and are saved in internal storage. The boot lock defaults to each board's compiled threshold and takes effect on the next boot. Boards whose compiled boot lock is disabled also accept set pwrmgt.bootlock off. The running cutoff defaults to off; when enabled, three valid low readings at 30-second intervals shut the node down. Both checks are bypassed while externally powered. A nonzero cutoff cannot exceed the boot lock, and the boot lock cannot be disabled while a cutoff is active. A fixed 2000 mV pre-check protects flash on boards with a nonzero compiled boot lock before the saved setting can be loaded. In the custom profile, the lower limit for the boot lock and running cutoff is 2000 mV. Firmware warns when any saved voltage is below 2500 mV; that experimental range may brown out the board or interrupt flash writes. These commands are available on nRF52 boards with NRF52_POWER_MANAGEMENT and a boot-voltage configuration, in roles with a text CLI. The KISS modem has no text CLI for changing these settings.

battery.profile selects saved voltage-to-percentage endpoints and protection settings. liion uses 3000 mV = 0%, 4200 mV = 100%, the board's original boot lock, and running cutoff off. lifepo4 uses 2700 mV = 0%, 3550 mV = 100%, 2900 mV boot lock, and 2700 mV running cutoff. custom starts with the current values and allows endpoint overrides. These percentages are simple voltage estimates; they are not an accurate state-of-charge measurement under load.

WARNING: SELECTING LIFEPO4 DOES NOT CHANGE ANY BOARD'S CHARGING CIRCUIT. A 4.2 V LI-ION CHARGER WILL STILL CHARGE TOWARD 4.2 V AND CAN DESTROY A LIFEPO4 CELL. USE A LIFEPO4-COMPATIBLE CHARGER OR DISABLE/ISOLATE ON-BOARD CHARGING BEFORE CONNECTING A LIFEPO4 CELL. Check the charging hardware on every board before using this profile; firmware battery-profile and cutoff settings cannot make a Li-ion charger safe for LiFePO4.

get battery.profile\nset battery.profile lifepo4\nset battery.profile liion\nset battery.profile custom\nget battery.empty\nset battery.empty 2700\nget battery.full\nset battery.full 3550\nget adc.multiplier\nset adc.multiplier 1.050\n

The custom empty/full endpoints may be 2000-4200 mV and must be at least 100 mV apart. Setting either endpoint automatically selects custom; boot lock and cutoff remain separately adjustable. get pwrmgt.bootlock and get pwrmgt.cutoff show the values in force after selecting a profile.

For a RAK3401/RAK13302 LiFePO4 cell:

set battery.profile lifepo4\n

For example, the RAK19007 base-board datasheet specifies 4.2 V charging for a Li-ion battery. The profile does not change it.

The 2.7 V cutoff is a cell-protection choice, not a guarantee of 1 W RF output. The RAK13302 datasheet specifies 3.0 V minimum at VBAT and lists 5 V / 1 A peak for 30 dBm output. Use a cutoff at or above 3.0 V if keeping the radio within its published battery-input range matters; verify the actual voltage under transmit load.

The shutdown wake comparator has a separate fixed hardware reference. Changing these millivolt settings does not change that wake threshold; a unit may need to charge above it or have USB power restored before it wakes.

"},{"location":"cli_commands/#ethernet-when-ethernet-support-is-compiled-in","title":"Ethernet (when Ethernet support is compiled in)","text":"

Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the _ethernet firmware variants (for example, RAK_4631_repeater_ethernet) for the normal Ethernet-only configuration.

RAK_4631_companion_radio_full is a distinct combined profile: it can compile both physical RAK13800 Ethernet and an optional RAK15002 SD mOTA archive. They share CS26, so they are mutually exclusive. In that Full profile Ethernet is off at boot and is deliberately controlled only for the current boot:

get eth\nset eth on\nset eth off\nget sdcard [free]\nota cache [on|off]\n

Before set eth on, remove the SD card and finish/cancel any active OTA capture, transfer, serving, or host-folder work. The command rejects an inserted card or a busy archive instead of taking the shared bus. Turn Ethernet off before accessing or inserting the card. An insertion while Ethernet is live fails closed and still needs set eth off; it is not a supported hot-swap flow. This combined hardware path is not yet qualified.

The SD-capable Ethernet-room receiver, RAK_4631_room_server_ethernet_lora_ota_no_external_sensors, uses the same shared CS26 safety rule but has its own cooperative Ethernet control. It starts off on every reboot and is never persisted. From USB or an already established Telnet session, use either spelling:

eth on\n# or: set eth on\neth.status\neth off\n

eth on rejects a fitted card, active/cache/staged OTA work, or a retained NOR owner. eth off waits for a safe release and refuses while OTA work is busy. Remove the card before enabling Ethernet and do not insert it while Ethernet is active.

"},{"location":"cli_commands/#view-ethernet-connection-status","title":"View Ethernet connection status","text":"

Usage: - eth.status

Output: - ETH: <ip>:<port> when connected (e.g. ETH: 192.168.1.50:23) - ETH: not connected when Ethernet is not active

Notes: - Available on repeater and room server firmware only. The normal _ethernet Companion image does not expose a CLI; the combined RAK4631 Full profile above instead exposes get eth / set eth on|off. - The normal _ethernet variants obtain an IP address via DHCP automatically on boot. The combined RAK4631 Full profile does not: Ethernet starts off until set eth on succeeds. - A TCP server listens on port 23 (default) for CLI connections. - Connect with any TCP client (e.g. nc, PuTTY) to access the same CLI available over serial.

"},{"location":"cli_settings_audit/","title":"Cli settings audit","text":"

View this page on MeshCore Docs.

"},{"location":"cli_settings_audit/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_contact_cache/","title":"Companion contact cache","text":"

View this page on MeshCore Docs.

"},{"location":"companion_contact_cache/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"companion_contact_cache_results/","title":"Companion contact cache results","text":"

View this page on MeshCore Docs.

"},{"location":"companion_contact_cache_results/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_lost_reply/","title":"Companion lost reply","text":"

View this page on MeshCore Docs.

"},{"location":"companion_lost_reply/#companion-lost-status-auto-response","title":"Companion lost-status auto response","text":"

Development Companion builds containing this feature can automatically answer a private Am I lost? message with Yes or No. You choose the saved answer. The default is off, and the firmware does not infer lost status from GPS, distance, or a missing phone connection.

This is included in nRF52, ESP32 and RP2040 Companion profiles, including T1000-E and RAK Tag. Compact STM32 Companion images omit it to preserve their existing application and filesystem boundaries. A custom build can explicitly enable MESH_ENABLE_LOST_REPLY=1 when its flash budget permits; the normal flash and RAM checks still apply.

Configure it through a directly connected local Companion CLI:

get lost.reply\nset lost.reply no\nset lost.reply yes\nset lost.reply off\n

The setting survives reboot and takes effect immediately. USB, BLE and other local Companion transports can use framed command 0x42. Builds with a text terminal also accept these commands there. LoRa CLI cannot read or change this local setting. A failed save restores the previous answer and reports an error. Older saved preference images load the new setting as off.

ESP32 Companions with WebConfig also have a Lost-status auto reply selection on the radio configuration page. Choose Off, Reply No or Reply Yes and save. The control is shown only on Companion firmware with this capability. The existing mobile app protocol needs no new message type; a native app toggle is not added by this firmware change.

"},{"location":"companion_lost_reply/#dog-tracker-example","title":"Dog tracker example","text":"

The owner's Companion stores the tracker as a normal radio contact. Set the answer to no during normal operation. If the dog is lost, set it to yes. When the tracker sends Am I lost? privately to that reachable Companion, it receives the selected answer, even when the owner's phone is disconnected. The same configured answer applies to all eligible contacts on this Companion.

The opt-in Sensor dog tracker implements periodic fresh GPS acquisition, low power sleep, and owner check-ins. It uses a tagged encrypted request and response rather than parsing plain Yes or No text, so old answers cannot change a newer check-in. Store the Sensor as a persistent Sensor contact on the owner's Companion. The same saved lost.reply setting answers those checks; off returns Unknown. A fresh accepted GPS position updates the contact location through normal contact notifications.

Other clients can still use the plain text question. They must stay awake for the separate private answer; a delivery ACK is not that answer. Plain text answers have no request ID. The responding Companion must be powered on and reachable. Personal room mailboxes provide store-and-forward mail; automatic mailbox polling is not yet implemented in the Sensor tracker.

"},{"location":"companion_lost_reply/#matching-and-traffic-limits","title":"Matching and traffic limits","text":"

Only an authenticated ordinary private message from a known persistent radio contact is eligible. The contact must be an ordinary Companion/chat contact; room-server and repeater contacts are excluded. Temporary unknown senders, public channels, room chat posts and CLI messages do not trigger a response.

The exact phrase am I lost is matched without regard to ASCII letter case, with an optional final question mark and surrounding whitespace. Extra words or commands do not match. The reply is ordinary encrypted private text, Yes or No, and neither answer can trigger another automatic reply.

Automatic responses are limited to one per contact per minute and one globally per ten seconds. A bounded four-peer cache uses full public keys and the incoming request timestamp to suppress retries. Its state clears on reboot. The same retained request is not answered again; a fresh check uses a new timestamp. An old retry can receive another answer after a newer request replaces its saved timestamp or its peer slot is evicted. Queueing an answer records the request even if later delivery fails. An unanswered poll means unknown, and the tracker should trust replies only from its configured Companion's full public key.

The responder leaves packet capacity for normal receive/ACK work, uses a free ACK-tracking entry, and does not replace pending user messages or their delivery deadlines. Busy packet or ACK capacity, or a failed send, can prevent an answer; the tracker should retry later. A full offline app inbox does not disable the responder. Normal message reception and notification behavior remain in place.

"},{"location":"companion_offline_queue/","title":"Companion offline queue","text":"

View this page on MeshCore Docs.

"},{"location":"companion_offline_queue/#companion-offline-message-queue","title":"Companion Offline Message Queue","text":"

Companion firmware keeps received channel data, channel messages, and direct messages in one pending queue until a Companion client requests them with the sync-next-message command. This is volatile RAM, not message history in flash. A reboot clears it.

"},{"location":"companion_offline_queue/#default-capacities","title":"Default capacities","text":"Platform or memory profile Pending frames ESP32 with configured PSRAM 512 ESP32 without PSRAM 256 nRF52840 with at most 300 contact slots 256 nRF52840 with more than 300 contacts and the contact cache (non-mOTA-shared queue) 256 total: up to 15 held DMs use slots until accepted RAK WisMesh Tag ordinary BLE Companion 240 total: up to 15 held DMs use slots until accepted nRF52840 with more than 300 contacts but no contact cache 208 ordinary frames plus a separate 15-DM queue nRF52 Full Companion with the memory correction 256 normally; 128 while mOTA owns shared storage RP2040 256 STM32 16 (one-key DM holding is omitted on flash-constrained STM32WL builds) Known constrained classic ESP32 target override 128 Meshadventurer Full Companion 16 Constrained Full ESP32 fallback 16

An explicit target OFFLINE_QUEUE_SIZE overrides the platform default. The Heltec V2 and TLora V2 Full Companion profiles, for example, use 16 frames so their combined WiFi, BLE, and LoRa mOTA image retains enough internal DRAM. Meshadventurer SX1262 and SX1268 Full Companion use 16 frames together with 100 contacts and 30 group channels; their ordinary transport-specific images keep 128 frames and 40 channels.

The nRF52 Full memory correction applies queue sharing to every nRF52 Full Companion. It keeps 256 frames normally, retaining each board's contacts, channels, and all Full transports. The upper 128 slots temporarily hold the mOTA context when a source or TempRadio discovery session starts. Stopping or disconnecting the USB/Bluetooth source returns all 256 slots; a discovery-only session returns them when TempRadio ends. This shares a fixed memory region and avoids heap fragmentation from resizing.

Existing unread messages retain their order. If more than 128 frames are pending, mOTA refuses the loan and asks you to sync messages with a Companion app first. While the loan is active, the overflow policy below applies at 128 frames. The original 26303793 1.17.1.5 builds reserve the queue and mOTA state separately. Sharing recovers about 19 KiB on nRF52 Full. The ST7735 color-display Full profiles require at least 72 KiB of heap space at link time; OLED and headless profiles use their own memory budgets.

Standard, logging, MQTT, and Cascade build overlays retain the selected target capacity. On cache-enabled high-contact nRF52 Companions, including the RAK4631 USB and BLE profiles, ordinary messages and consent-pending DMs share one 256-frame RAM pool. With no held DMs, all 256 slots are available to ordinary messages; with 15 held DMs, up to 241 are. The app sees only ordinary messages until a sender is accepted. The oldest held DM rolls off when a sixteenth decryptable DM arrives. The shared pool is still volatile and clears on reboot. Uncached high-contact profiles keep the conservative 208-frame default and separate held queue. An explicit target OFFLINE_QUEUE_SIZE still takes precedence.

The ordinary RAK WisMesh Tag BLE profile uses 240 frames to retain the runtime RAM reserve for Bluetooth, filesystem buffers, and the main task. It keeps 350 contact slots and 40 group channels. All 240 slots are available when no DMs are held; 15 held DMs leave 225 ordinary slots. This saves 2,832 bytes compared with 256 frames. The Tag Full Companion retains 256 frames normally and 128 while mOTA borrows queue storage.

Each queue slot currently costs 177 bytes. A 256-frame queue reserves 45,312 bytes, while a 512-frame queue reserves 90,624 bytes. There is no 256-frame protocol limit: the queue length and indexes can represent 512 or more. The practical limit is available RAM and the heap and stack headroom required by the transports and display.

On ESP32 boards marked with BOARD_HAS_PSRAM, the queue is allocated from PSRAM before WiFi and BLE start. A failed 512-frame allocation retries at 256, then 128, and finally uses a 16-frame internal fallback. Full Companion prints the capacity actually allocated in its startup memory line as offline_queue=<frames>.

"},{"location":"companion_offline_queue/#full-queue-behavior","title":"Full queue behavior","text":"

The capacity is shared across Public, other channels, channel data, and direct messages. It is not a per-channel count. When the queue is full, firmware replaces the oldest queued channel frame so newer traffic can still arrive. If the full queue contains only direct messages, a new frame is dropped rather than deleting a direct message.

Queue order is preserved. Removal uses a ring index, so delivering one pending message no longer copies every remaining frame; only the less-common removal of an old channel frame from a full queue may shift entries.

Capacity is selected when firmware is compiled. There is no CLI or Companion protocol setting to resize it at runtime.

"},{"location":"companion_protocol/","title":"Companion protocol","text":"

View this page on MeshCore Docs.

"},{"location":"companion_protocol/#companion-protocol","title":"Companion Protocol","text":"

The command and response catalogs track examples/companion_radio/MyMesh.cpp. Applications should negotiate the protocol and validate lengths because older firmware exposes a subset.

This document is a practical guide to MeshCore's binary companion protocol. The same protocol frames can be carried by the enabled BLE, USB serial, Wi-Fi, or Ethernet companion interface; connection details differ by build.

On builds exposing more than one transport, delivery-required replies follow the interface which supplied the command. The multi-frame contact-list response holds that route until END_OF_CONTACTS; best-effort asynchronous observations may still be broadcast to enabled clients. Treat the device as one Companion session rather than as independent per-transport sessions.

The examples focus on BLE, but the packet formats are transport-independent.

"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"

Please see the following repos for existing MeshCore Companion Protocol libraries.

"},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"

All secrets, hashes, and cryptographic values shown in this guide are example values only.

"},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
  1. BLE Connection
  2. Packet Structure
  3. Commands
  4. Channel Management
  5. Message Handling
  6. Response Parsing
  7. Example Implementation Flow
  8. Best Practices
  9. Troubleshooting
"},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"

MeshCore Companion devices expose a BLE service with the following UUIDs:

An nRF52 Full Companion also exposes a separate LoRa mOTA source service. It does not replace or multiplex the normal Companion UART service:

All three mOTA attributes require an encrypted, MITM-authenticated connection using the Companion's six-digit PIN. The source remains inactive until the client subscribes to Device Request and explicitly starts it with command 0x4B. See Bluetooth LoRa mOTA source.

ESP32 and nRF52 Companion UART characteristics require the same PIN-protected, MITM-authenticated link. ESP32 advertises DisplayOnly capability so a central must enter the PIN shown by the Companion; a Just Works bond is insufficient.

"},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
  1. Scan for Devices

    • Scan for BLE devices advertising the MeshCore Service UUID
    • Optionally filter by device name (typically contains \"MeshCore\" prefix)
    • Note the device MAC address for reconnection
  2. Connect to GATT

    • Connect to the device using the discovered MAC address
    • Wait for connection to be established
  3. Discover Services and Characteristics

    • Discover the service with UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
    • Discover the RX characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
      • Your app writes to this, the firmware reads from this
    • Discover the TX characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
      • The firmware writes to this, your app reads from this
  4. Enable Notifications

    • Subscribe to notifications on the TX characteristic to receive data from the firmware
  5. Send Initial Commands

    • Send CMD_APP_START to identify your app to firmware and get radio settings
    • Send CMD_DEVICE_QUERY to fetch device info and negotiate supported protocol versions
    • Send CMD_SET_DEVICE_TIME to set the firmware clock
    • Send CMD_GET_CONTACTS to fetch all contacts
    • Send CMD_GET_CHANNEL multiple times to fetch all channel slots
    • Send CMD_SYNC_NEXT_MESSAGE to fetch the next message stored in firmware
    • Setup listeners for push codes, such as PUSH_CODE_MSG_WAITING or PUSH_CODE_ADVERT
    • See Commands section for information on other commands

Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.

"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"

When writing commands to the RX characteristic, specify the write type:

Platform-specific:

Recommendation: Use write with response for reliability.

"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:

  1. Request Larger MTU: Request MTU of 512 bytes if supported
    • Android: gatt.requestMtu(512)
    • iOS: peripheral.maximumWriteValueLength(for:)
    • Python (bleak): MTU is negotiated automatically
"},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"

Critical: Commands must be sent in the correct sequence:

  1. After Connection:

    • Wait for BLE connection to be established
    • Wait for services/characteristics to be discovered
    • Wait for notifications to be enabled
    • Now you can safely send commands to the firmware
  2. Command-Response Matching:

    • Send one command at a time
    • Wait for a response before sending another command
    • Use a timeout (typically 5 seconds)
    • Match response to command by type (e.g: CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO)
"},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"

For reliable operation, implement a command queue.

Queue Structure:

Error Handling:

"},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"

The MeshCore protocol uses a binary format with the following structure:

Most packets follow this format:

[Packet Type (1 byte)] [Data (variable length)]\n

The first byte indicates the packet type (see Response Parsing).

"},{"location":"companion_protocol/#commands","title":"Commands","text":"

The first byte selects the command. This is the current protocol-v14 command catalog; bytes 0x2C-0x31 are parked and 0x35 is unused.

Byte Firmware name Purpose 0x01 CMD_APP_START Start an app session and request self information. 0x02 CMD_SEND_TXT_MSG Send text to a contact. 0x03 CMD_SEND_CHANNEL_TXT_MSG Send channel text. 0x04 CMD_GET_CONTACTS Enumerate contacts, optionally modified since a timestamp. 0x05 / 0x06 CMD_GET_DEVICE_TIME / CMD_SET_DEVICE_TIME Read or set the device clock. 0x07 / 0x08 CMD_SEND_SELF_ADVERT / CMD_SET_ADVERT_NAME Advertise self or change the advertised name. 0x09 CMD_ADD_UPDATE_CONTACT Add or update a contact. 0x0A CMD_SYNC_NEXT_MESSAGE Dequeue the next pending message. 0x0B / 0x0C CMD_SET_RADIO_PARAMS / CMD_SET_RADIO_TX_POWER Set radio parameters or transmit power. 0x0D CMD_RESET_PATH Reset a contact's learned path. 0x0E CMD_SET_ADVERT_LATLON Set advertised coordinates. 0x0F CMD_REMOVE_CONTACT Remove a contact. 0x10 / 0x11 / 0x12 CMD_SHARE_CONTACT / CMD_EXPORT_CONTACT / CMD_IMPORT_CONTACT Share, export, or import contact data. 0x13 CMD_REBOOT Reboot after the required confirmation body. 0x14 CMD_GET_BATT_AND_STORAGE Read battery and storage usage. 0x15 CMD_SET_TUNING_PARAMS Set tuning parameters. 0x16 CMD_DEVICE_QUERY Negotiate protocol support and read device information. 0x17 / 0x18 CMD_EXPORT_PRIVATE_KEY / CMD_IMPORT_PRIVATE_KEY Export or import identity key material when enabled. 0x19 CMD_SEND_RAW_DATA Send an application raw-data packet. Known media and opt-in SSH envelopes honor application TX routing; other raw bytes retain normal routing. 0x1A-0x1D CMD_SEND_LOGIN through CMD_LOGOUT Manage a server connection. 0x1E CMD_GET_CONTACT_BY_KEY Look up a contact by public-key prefix. 0x1F / 0x20 CMD_GET_CHANNEL / CMD_SET_CHANNEL Read or write a channel slot. 0x21-0x23 CMD_SIGN_START through CMD_SIGN_FINISH Stream data for identity signing. 0x24 CMD_SEND_TRACE_PATH Trace a direct route. 0x25 CMD_SET_DEVICE_PIN Set or clear the device PIN. 0x26 CMD_SET_OTHER_PARAMS Set telemetry, location, ACK, and related preferences. 0x27 CMD_SEND_TELEMETRY_REQ Send the legacy telemetry request. 0x28 / 0x29 CMD_GET_CUSTOM_VARS / CMD_SET_CUSTOM_VAR Read or set custom variables. 0x2A CMD_GET_ADVERT_PATH Read a cached advertisement path. 0x2B CMD_GET_TUNING_PARAMS Read tuning parameters. 0x32 CMD_SEND_BINARY_REQ Send an application binary request. 0x33 CMD_FACTORY_RESET Factory-reset after the required confirmation body. 0x34 CMD_SEND_PATH_DISCOVERY_REQ Request path discovery. 0x36 CMD_SET_FLOOD_SCOPE_KEY Select scoped or unscoped flood behavior. 0x37 CMD_SEND_CONTROL_DATA Send zero-hop control data. 0x38 CMD_GET_STATS Read core, radio, or packet statistics. 0x39 CMD_SEND_ANON_REQ Send an anonymous request. 0x3A / 0x3B CMD_SET_AUTOADD_CONFIG / CMD_GET_AUTOADD_CONFIG Write or read automatic-contact policy. 0x3C CMD_GET_ALLOWED_REPEAT_FREQ Read allowed client-repeat frequency ranges. 0x3D CMD_SET_PATH_HASH_MODE Set path-hash width mode. 0x3E CMD_SEND_CHANNEL_DATA Send a channel binary datagram. 0x3F / 0x40 CMD_SET_DEFAULT_FLOOD_SCOPE / CMD_GET_DEFAULT_FLOOD_SCOPE Write or read the default flood scope. 0x41 CMD_SEND_RAW_PACKET Queue a fully encoded raw mesh packet. 0x42 CMD_RUN_CLI_COMMAND Run a local CLI command (protocol v14+). 0x4A CMD_EXEC_LOCAL_OTA_CONTROL Run one bounded local TempRadio or OTA command when the Companion includes the OTA CLI. 0x4B CMD_BLE_MOTA_SOURCE Query, start, or stop an nRF52 Full Companion's Bluetooth-backed LoRa mOTA source. 0x78-0x7F Deprecated hardware-setting aliases Receive-only compatibility for clients shipped before command 0x42 became the canonical settings path.

The sections below detail the most common frames. Refer to the source named above for command bodies that are not expanded here.

"},{"location":"companion_protocol/#private-text-when-only-the-sender-knows-the-recipient","title":"Private text when only the sender knows the recipient","text":"

Clients continue to send an ordinary CMD_SEND_TXT_MSG (0x02) with TXT_TYPE_PLAIN; no client protocol change is needed. For a chat contact without a recent confirmed DM, Companion firmware first sends a signed, encrypted one-key introduction that includes its full public key, then queues the normal private text after the introduction. It sends the introduction again on an application retry, which recovers if the recipient removed the sender contact. An ACK confirms that the recipient can decrypt normal DMs, so subsequent first attempts omit the extra packet until the sender reboots or the small recent-ACK cache is replaced.

The receiving Companion verifies the introduction signature. By default, dm.one_key is off: the sender is offered to the app as a synthetic PUSH_CODE_NEW_ADVERT (0x8A) but is not added as a contact. The app/user accepts through the normal add-contact command. Up to 15 verified, decryptable plain DMs are held in RAM and delivered after acceptance; the oldest rolls off first. The queue is lost on reboot. Cache-enabled high-contact nRF52 Companions share a 256-frame pool between ordinary offline messages and up to 15 held DMs, so ordinary capacity is 256 when none are held and 241 when all 15 are held. The held frames are not exposed by ordinary message sync before acceptance. Uncached high-contact nRF52 builds retain 208 ordinary frames and a separate held queue. The device sends a signed DMR1 refusal, and the sender pushes 0x91 plus the full rejecting key to a connected app, then stops further introductions for that peer until reboot. Directly attached clients may use get dm.one_key or set dm.one_key on|off; get dm.held reports the number of held DMs. on auto-accepts verified senders. The flash-constrained STM32WL Companion builds omit one-key DM support to preserve their existing filesystem boundary; these commands report Error: one-key DMs unsupported on this build there. Ordinary contact-based private messages remain available. A recipient running older firmware still needs the sender contact before it can decrypt a normal DM. This flow uses a second radio packet for the initial DM; RESP_CODE_SENT includes that scheduling delay in its timeout.

For a two-radio regression test, run tools/hil/one_key_dm.py with the USB serial paths for --sender and --recipient. --reset-contact removes the sender from recipient contacts, and --zero-hop sets the sender's route to zero hop when the devices are nearby. The test checks the synthetic advert, refusal, manual acceptance, held-message delivery, and matching ACK. --invalid-signature-first also checks that a decryptable introduction with an invalid Ed25519 signature does not add a contact.

CMD_RUN_CLI_COMMAND is followed by the local CLI text with an optional final NUL, as sent by the official app. Clients that omit it remain supported. The device returns RESP_CODE_CLI_REPLY (0x1D) followed by the reply text. This is separate from sending a remote on-air CLI command with CMD_SEND_TXT_MSG and TXT_TYPE_CLI_COMMAND. The body must contain at least one byte and must not contain an embedded NUL. An unknown command is returned as the normal CLI reply text Unknown command, not as an error frame. Clients may prefix the CLI text with any two-character correlation tag and | (for example, A7|get radio.rxgain); the reply preserves that prefix.

Direct USB, BLE, WiFi TCP, and Ethernet clients can also use this command for stats-core, stats-radio, stats-radio-diag, stats-packets, erase, and get prv.key (when private key export is enabled). WiFi/MQTT builds accept get wifi.pwd, get mqttN.password, and get mqttN.token. On ESP32 WiFi Companion, get password reports whether the management password is available without revealing it. password <new-password> changes the saved password shared by setup WiFi, WebConfig and browser OTA. Other Companion builds report that they have no admin password; infrastructure roles return their own admin password on a local CLI connection. These direct-client maintenance and secret-read privileges do not apply to CLI commands relayed over LoRa. Existing trusted Companion transports and contacts explicitly granted remote CLI access retain their command privileges. The browser login does not add a password handshake to those transports. ESP32 WiFi Companion builds expose the official app's get/set wifi.enabled, get/set wifi.ssid, and get/set wifi.pwd controls. Credentials use the shared WiFi store, and enablement is saved in node preferences. WiFi/BLE builds apply enablement from the main loop; exclusive WiFi/BLE builds apply it after reboot. get wifi.status reports the connection state and get wifi.ip reports the station address, the setup AP address, or (not connected). get wifi.tcp reports the actual app listener, interface registration, client connection and completed frame counters. It is read-only and available through the same binary CLI command over BLE or USB when TCP is unreachable. Listener startup failures recover automatically without resetting a healthy client or the WiFi connection; see the CLI diagnostics. set freq <MHz> works through command 0x42 and authorized LoRa CLI, like set radio; it saves the frequency and requires reboot to apply. See local maintenance commands for role differences and the text-terminal equivalents.

Full Companion clients can send version through this command to receive the untruncated build identity, for example Companion 1.17.1.5-... (protocol 14, build 31-Aug-2026). This deliberately supplements rather than changes the 20-byte legacy version field in RESP_CODE_DEVICE_INFO, so existing clients keep the same frame layout.

Firmware from this fork predating the upstream 0x42 allocation used 0x42-0x49 for these eight settings. This firmware accepts those values as deprecated inbound aliases so existing clients continue to work. A one-byte 0x42 frame is the legacy FEM-gain GET; 0x42 followed by text is the official CMD_RUN_CLI_COMMAND. New clients should use CMD_RUN_CLI_COMMAND for all of these settings, rather than allocating additional command bytes. For example, send 0x42 followed by get radio.rxgain or set radio.rxgain on. The reply is RESP_CODE_CLI_REPLY followed by the normal CLI reply text.

Two deprecated binary alias blocks remain receive-only for compatibility:

Setting Original alias Later fork alias GET body/reply SET body/reply FEM receive gain 0x42 / 0x43 0x78 / 0x79 No body; OK, state One byte 0/1; OK Radio receive gain 0x44 / 0x45 0x7A / 0x7B No body; OK, state One byte 0/1; OK WiFi power save 0x46 / 0x47 0x7C / 0x7D No body; OK, mode One mode byte 0-2; OK Bluetooth name 0x48 / 0x49 0x7E / 0x7F No body; OK, custom, name Zero to 31 UTF-8 bytes; OK

Each pair lists GET then SET. Here OK is RESP_CODE_OK; the remaining reply bytes have the same meanings as the CLI settings below. A bare 0x42 is the old FEM GET, while 0x42 plus at least one text byte is CMD_RUN_CLI_COMMAND. New clients must use the framed CLI form; these aliases exist only so deployed clients do not break after a firmware update.

The equivalent framed CLI commands are:

Setting Commands Radio receive gain get radio.rxgain; set radio.rxgain on|off FEM receive gain get radio.fem.rxgain; set radio.fem.rxgain on|off WiFi power save get wifi.powersave; set wifi.powersave none|min|max Bluetooth name get bluetooth.name; set bluetooth.name <name|default> Bluetooth address get bluetooth.mac; set bluetooth.mac <address|random|random-every-boot|random-after-connect|default> Bluetooth stealth get bluetooth.stealth; set bluetooth.stealth on|off

The framed form works over the normal binary USB, BLE, or TCP transport and does not need the USB terminal-start token. Unsupported settings return the same explanatory text as the local CLI.

WiFi power-save modes are:

Value Mode 0 min - minimum modem sleep 1 none - no modem sleep 2 max - maximum modem sleep

A Full Companion that runs BLE and infrastructure WiFi simultaneously rejects WiFi mode none because coexistence requires modem sleep. A Full Companion using ESP-NOW as its primary mesh radio also rejects max, because maximum modem sleep can miss broadcasts that the access point cannot buffer. If an older image saved a conflicting value, the effective mode is capped to and reported as min. Device power saving remains independent.

The SenseCAP Indicator Full profiles run exactly one secondary wireless Companion transport per boot. Their active-mode constraints are:

Indicator mode Accepted wifi.powersave values LoRa + infrastructure WiFi none, min, max LoRa + BLE min, max; infrastructure WiFi is not started ESP-NOW + infrastructure WiFi none, min; max conflicts with primary ESP-NOW ESP-NOW + BLE min; infrastructure WiFi is not started and primary ESP-NOW remains active

The Bluetooth name can be configured over USB, BLE, or TCP. Use set bluetooth.name default to restore MeshCore-<advert name>; an empty CLI value is rejected. (clear is also accepted as an alias for default.) A custom name is limited to 31 valid UTF-8 bytes and takes effect after reboot.

Bluetooth Companion builds also accept bluetooth.mac (ble.mac is an alias). A literal address must be a BLE random-static address; random generates and saves one, random-every-boot rotates it at each startup, and random-after-connect retains it through unused boots but rotates it on the first boot after an authenticated connection. default or clear restores the factory address. The change takes effect after reboot.

bluetooth.stealth on|off (ble.stealth is an alias) is a separate flag, defaulting to off. It preserves the address policy and can be combined with custom, saved random, rotating random, or factory addresses. With stealth on, the node advertises normally until the first authenticated pairing, then permits only that bonded peer to reconnect. Address changes clear the saved peer and reopen pairing without disabling stealth. Rotating policies retain their normal rotation triggers. Repeating on does not clear an existing bond. Both flag changes require reboot. The old MAC value stealth is not accepted.

BLE still transmits directed or allowlisted advertisements; stealth is not radio silence. These are application settings; an nRF52 OTAFIX bootloader continues to advertise its separate DFU identity.

"},{"location":"companion_protocol/#bluetooth-lora-mota-source","title":"Bluetooth LoRa mOTA source","text":"

Protocol v14 lets a phone control an install-capable Companion's own LoRa OTA session. An nRF52 Full Companion can additionally act as the source for a remote repeater update without a USB computer. The normal Companion service still carries contacts, repeater login, CLI messages, and these two control commands. The separate mOTA service carries only host-folder request/response frames.

CMD_EXEC_LOCAL_OTA_CONTROL (0x4A) is followed by 1-174 printable ASCII bytes. A Companion built with the OTA CLI accepts only these local command families:

tempradio <freq_kHz>,<bw_kHz>,<sf>,<cr>,<minutes>\nnormalradio\nota ...\n

ota folder ... is deliberately rejected because USB and Bluetooth source ownership must not be changed through the wrong transport. Embedded NUL, CR, LF, other control bytes, non-ASCII bytes, empty commands, and oversized frames return ERR_CODE_ILLEGAL_ARG. A recognized command replies with RESP_CODE_OK, one unsigned reply-length byte, and exactly that many printable result bytes. Shell metacharacters are rejected as well; the text is dispatched only to the in-firmware parser and is never passed to a host shell. Firmware without the OTA CLI feature returns ERR_CODE_UNSUPPORTED_CMD.

CMD_BLE_MOTA_SOURCE (0x4B) has one action byte:

Action Meaning 0 Read status without changing it. 1 Attach and enumerate the subscribed Bluetooth host's .mota catalog. 2 Detach the Bluetooth source.

Current firmware returns eleven bytes (legacy protocol-v14 previews returned the seven-byte prefix only):

00 action flags offered_le16 advertised_le16 source_packets_sent_le32\n

Flag bit 0x01 means the encrypted GATT channel is connected and Device Request notifications are enabled. Bit 0x02 means the Bluetooth catalog is attached. Bit 0x04 means USB or another folder transport currently owns the source slot. Start without a ready subscription, or while another source link owns the slot, returns ERR_CODE_BAD_STATE. A non-nRF52 Full Companion returns ERR_CODE_UNSUPPORTED_CMD. source_packets_sent is a per-attachment count of OTA packets accepted by the Companion's LoRa transmit adapter, including catalog/manifest traffic, data, proofs, and retries. It wraps as an unsigned 32-bit value. Clients should accept the legacy seven-byte response and display the packet counter as unavailable.

After a successful start, the device sends the same bounded seeder frames used by motatool serve on Device Request:

device -> host: 'M' 'S' op args... xor(op || args)\nhost -> device: 'm' 's' op status payload... xor(all prior bytes)\n

Device requests are at most 11 bytes. A source response is at most 197 bytes. The host may split one response across multiple write-with-response operations when the negotiated ATT payload is smaller; it must preserve byte order and must not interleave another response. Bad checksums, partial frames, overflow, unsubscribe, loss of encryption, or disconnect fail closed. The firmware then detaches the catalog and stops advertising its entries. USB and Bluetooth folder sources are mutually exclusive.

A Linux reference controller and seeder is provided at tools/ble_mota/ble_mota_seeder.py. It verifies every input with motatool before offering it. A mobile implementation should apply the same complete container verification before serving files.

"},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"

Purpose: Initialize communication with the device. Must be sent first after connection.

Command Format:

Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n

Example (hex):

01 00 00 00 00 00 00 00 6d 63 63 6c 69\n

Response: PACKET_SELF_INFO (0x05)

"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"

Purpose: Query device information.

Command Format:

Byte 0: 0x16\nByte 1: Highest companion protocol version understood by the app\n

Example (hex):

16 0E\n

Response: PACKET_DEVICE_INFO (0x0D) with device information

"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"

Purpose: Retrieve information about a specific channel.

Command Format:

Byte 0: 0x1F\nByte 1: Channel index (0 through max_channels - 1)\n

Example (get channel 1):

1F 01\n

Response: PACKET_CHANNEL_INFO (0x12) with channel details

On builds with MESH_ENABLE_MESHTEXT_CHAT=1, a capable client can append a third request byte of 32 (1F <index> 20) to retrieve the complete 32-byte secret for an mt- channel. Ordinary two-byte requests, and requests for MeshCore channels, retain the original 16-byte secret response.

"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"

Purpose: Create or update a channel on the device.

Command Format:

Byte 0: 0x20\nByte 1: Channel index (0 through max_channels - 1)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n

Total Length: 50 bytes

Channel index: - Slot count is build-specific. Read max_channels from byte 3 of PACKET_DEVICE_INFO; current profiles commonly expose 1, 8, or 40 slots. - No slot number has an intrinsic public/private meaning.

Channel Name: - UTF-8 encoded - Maximum 32 bytes - Padded with null bytes (0x00) if shorter

Secret Field (16 bytes): - Supply the exact 16-byte channel key. A private channel normally uses a cryptographically random key; known public and hashtag channels use their defined or derived key. - An all-zero key is not the public-channel key.

Example (create channel \"SMS\" at index 1 with secret):

20 01 53 4D 53 00 00 ... (name padded to 32 bytes)\n    [16 bytes of secret]\n

Meshtext-only extension: With MESH_ENABLE_MESHTEXT_CHAT=1, a 66-byte SET_CHANNEL frame can supply 32 secret bytes at offsets 34\u201365 for an mt- channel. Non-MT channels and feature-disabled builds reject this variant with PACKET_ERROR; the standard 50-byte MeshCore command is unchanged. The MT backend uses AES-256 when the upper 16 bytes are nonzero, otherwise AES-128.

A legacy 50-byte SET of an existing MT channel preserves its upper 16 key bytes when the supplied lower half is unchanged. This permits an old client's GET/edit/SET round trip without silently truncating a provisioned AES-256 key. To explicitly replace or downgrade that key, send the full 32-byte variant (zero the upper half for AES-128). A client using only the legacy GET cannot export a complete AES-256 key. Stock app editors do not gain 256-bit provisioning or sharing from this firmware extension.

For local CLI provisioning without implementing the binary extension, use set mt.channel with a 32- or 64-digit hexadecimal PSK; see the private-channel commands.

MT channel changes can also fail with a bad-state error for conflicting presets, an unsuitable manual radio2 profile, ambiguous region or a busy automatic owner. See Meshtext channel chat.

Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure

"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"

Purpose: Send a text message to a channel.

Command Format:

Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel index (0 through max_channels - 1)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n

Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian)

Example (send \"Hello\" to channel 1 at timestamp 1234567890):

03 00 01 D2 02 96 49 48 65 6C 6C 6F\n

Response: PACKET_MSG_SENT (0x06) on success

"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"

Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp - applications needing either must encode them inside the binary payload.

Command Format:

Byte 0:                         0x3E\nByte 1:                         Channel index (0 through max_channels - 1)\nByte 2:                         Encoded path descriptor (0xFF = flood)\nBytes 3+:                       Encoded path bytes (omitted for 0xFF)\nNext 2 bytes (little-endian):   Data Type (`data_type`, uint16)\nRemaining bytes:                Binary payload (variable length)\n

For a direct send, the descriptor's low six bits are the hash count and its high two bits are the hash size minus one. Current mesh packets accept one-, two-, or three-byte hashes; the four-byte code is reserved. The following path therefore occupies hash_count * hash_size bytes; the descriptor itself is not a raw byte count.

Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):

3E 01 FF FF FF A1 B2 C3\n

Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Registered application/community namespaces occupy 0x0100-0xFEFF; the remaining nonzero ranges are reserved for internal or development use. See the Registered data_type values table below.

Limits: - Maximum payload length is MAX_GROUP_DATA_LENGTH = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3 = 165 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).

Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) - unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) - invalid path_len, reserved data_type (0x0000), or payload larger than MAX_GROUP_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) - outbound send queue is full; retry later

Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.

"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered data_type values","text":"

data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents - data_type is transported opaquely.

Value Constant Purpose 0x0000 DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 - 0x00FF - Reserved for internal use 0x0100 - 0xFEFF - Registered application namespaces (see number_allocations.md) 0xFF00 - 0xFFFE - Testing/development; no registration required 0xFFFF DATA_TYPE_DEV Developer/experimental namespace

To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.

"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"

Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.

Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):

Byte 0:                 0x1B (packet type)\nByte 1:                 SNR (signed int8, scaled x4 - divide by 4.0 to recover dB)\nBytes 2-3:              Reserved (clients MUST ignore)\nByte 4:                 Channel index (0 through max_channels - 1)\nByte 5:                 Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7:              Data Type (uint16 little-endian)\nByte 8:                 Data Length\nBytes 9 .. 8+data_len:  Payload\n

Path bytes are not forwarded: Only path_len is reported in the receive frame - the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6-7, regardless of path_len.

Path Length semantics differ between send and receive:

Direction path_len = 0xFF path_len != 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = hash_count x hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded pkt->path_len field as observed (no path bytes follow)

In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only - never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.

Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.

Parsing Pseudocode:

def parse_channel_data_recv(data):\n    if len(data) < 9:\n        return None\n    snr_byte = data[1]\n    snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n    channel_idx = data[4]\n    path_len = data[5]\n    data_type = int.from_bytes(data[6:8], 'little')\n    data_len = data[8]\n    if 9 + data_len > len(data):\n        return None\n    payload = data[9:9 + data_len]\n    return {\n        'snr': snr,\n        'channel_idx': channel_idx,\n        'path_len': path_len,\n        'data_type': data_type,\n        'payload': bytes(payload),\n    }\n
"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"

Purpose: Request the next queued message from the device.

Command Format:

Byte 0: 0x0A\n

Example (hex):

0A\n

Response: - PACKET_CHANNEL_MSG_RECV (0x08) or PACKET_CHANNEL_MSG_RECV_V3 (0x11) for channel messages - PACKET_CONTACT_MSG_RECV (0x07) or PACKET_CONTACT_MSG_RECV_V3 (0x10) for contact messages - PACKET_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - PACKET_NO_MORE_MSGS (0x0A) if no messages available

Note: Poll this command periodically to retrieve queued messages. The device may also send PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.

"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"

Purpose: Query device battery voltage and storage usage.

Command Format:

Byte 0: 0x14\n

Example (hex):

14\n

Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information

"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
  1. Public Channel
    • Uses a publicly known 16-byte key: 8b3387e9c5cdea6ac9e5edbaa115cd72
    • Anyone can join this channel, messages should be considered public
    • Used as the default public group chat
  2. Hashtag Channels
    • Uses a secret key derived from the channel name
    • It is the first 16 bytes of sha256(\"#test\")
    • For example hashtag channel #test has the key: 9cd8fcf22a47333b591d96a2b848b73f
    • Traffic is encrypted on air, but anyone who knows or guesses the channel name can derive the key. Hashtag channels should not be treated as private.
    • Used as a topic based public group chat, separate from the default public channel
  3. Private Channels
    • Uses a randomly generated 16-byte secret key
    • Messages should be considered private between those that know the secret
    • Users should keep the key secret, and only share with those you want to communicate with
    • Used as a secure private group chat
"},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
  1. Set Channel:
    • Read max_channels from device info, fetch those slots, and choose an unused slot (normally an empty name and zeroed key)
    • Generate or provide a 16-byte secret
    • Send CMD_SET_CHANNEL with name and a 16-byte secret
  2. Get Channel:
    • Send CMD_GET_CHANNEL with channel index
    • Parse RESP_CODE_CHANNEL_INFO response
  3. Delete Channel:
    • Send CMD_SET_CHANNEL with empty name and all-zero secret
    • Or overwrite with a new channel
"},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"

Messages are received via the TX characteristic (notifications). The device sends:

  1. Channel Messages:
  2. PACKET_CHANNEL_MSG_RECV (0x08) - Standard format
  3. PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR

  4. Contact Messages:

  5. PACKET_CONTACT_MSG_RECV (0x07) - Standard format
  6. PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR

  7. Notifications:

  8. PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued
"},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"

Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):

Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n

V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):

Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_contact_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x10:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    pubkey_prefix = data[offset:offset+6].hex()\n    offset += 6\n\n    path_len = data[offset]\n    txt_type = data[offset + 1]\n    offset += 2\n\n    timestamp = int.from_bytes(data[offset:offset+4], 'little')\n    offset += 4\n\n    # If txt_type == 2, skip 4-byte signature\n    if txt_type == 2:\n        offset += 4\n\n    message = data[offset:].decode('utf-8')\n\n    return {\n        'pubkey_prefix': pubkey_prefix,\n        'path_len': path_len,\n        'txt_type': txt_type,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x10 else None\n    }\n
"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"

Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):

Byte 0: 0x08 (packet type)\nByte 1: Channel index (0 through max_channels - 1)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n

V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):

Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel index (0 through max_channels - 1)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_channel_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x11:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    channel_idx = data[offset]\n    path_len = data[offset + 1]\n    txt_type = data[offset + 2]\n    timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n    message = data[offset+7:].decode('utf-8')\n\n    return {\n        'channel_idx': channel_idx,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x11 else None\n    }\n
"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"

Use the SEND_CHANNEL_MESSAGE command (see Commands).

Important: - The shared text envelope permits up to 160 UTF-8 bytes. For channel text, firmware prepends <sender name>: inside that envelope, so the available message body is 160 - prefix_bytes and varies with the configured name. - Count encoded UTF-8 bytes, not Unicode characters. Split a longer message at valid UTF-8 boundaries. - Include a chunk indicator (e.g., \"[1/3] message text\")

"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"

This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:

Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.

"},{"location":"companion_protocol/#response-types","title":"Response types","text":"Value Firmware name Description 0x00 RESP_CODE_OK Command succeeded. 0x01 RESP_CODE_ERR Command failed; byte 1 is the error code. 0x02 RESP_CODE_CONTACTS_START Contact enumeration started. 0x03 RESP_CODE_CONTACT One contact record. 0x04 RESP_CODE_END_OF_CONTACTS Contact enumeration ended. 0x05 RESP_CODE_SELF_INFO Device self-information. 0x06 RESP_CODE_SENT Send accepted, with route/tag/timeout data. 0x07 / 0x08 RESP_CODE_CONTACT_MSG_RECV / RESP_CODE_CHANNEL_MSG_RECV Queued legacy-format message. 0x09 RESP_CODE_CURR_TIME Current device time. 0x0A RESP_CODE_NO_MORE_MESSAGES Offline queue is empty. 0x0B RESP_CODE_EXPORT_CONTACT Exported contact bytes. 0x0C RESP_CODE_BATT_AND_STORAGE Battery and storage values. 0x0D RESP_CODE_DEVICE_INFO Protocol and build information. 0x0E RESP_CODE_PRIVATE_KEY Exported identity key, when enabled. 0x0F RESP_CODE_DISABLED Requested sensitive feature is disabled. 0x10 / 0x11 RESP_CODE_CONTACT_MSG_RECV_V3 / RESP_CODE_CHANNEL_MSG_RECV_V3 Queued message with SNR fields. 0x12 RESP_CODE_CHANNEL_INFO Channel slot information. 0x13 / 0x14 RESP_CODE_SIGN_START / RESP_CODE_SIGNATURE Signing capacity or completed signature. 0x15 RESP_CODE_CUSTOM_VARS Custom-variable data. 0x16 RESP_CODE_ADVERT_PATH Cached advertisement path. 0x17 RESP_CODE_TUNING_PARAMS Tuning parameters. 0x18 RESP_CODE_STATS Requested statistics subtype. 0x19 RESP_CODE_AUTOADD_CONFIG Automatic-contact policy. 0x1A RESP_ALLOWED_REPEAT_FREQ Allowed repeat-frequency ranges. 0x1B RESP_CODE_CHANNEL_DATA_RECV Queued channel datagram. 0x1C RESP_CODE_DEFAULT_FLOOD_SCOPE Default flood-scope data. 0x1D RESP_CODE_CLI_REPLY Text returned by CMD_RUN_CLI_COMMAND."},{"location":"companion_protocol/#asynchronous-push-types","title":"Asynchronous push types","text":"Value Firmware name Description 0x80 PUSH_CODE_ADVERT Advertisement received. 0x81 PUSH_CODE_PATH_UPDATED A contact path changed. 0x82 PUSH_CODE_SEND_CONFIRMED A sent message was acknowledged. 0x83 PUSH_CODE_MSG_WAITING One or more offline frames are waiting. 0x84 PUSH_CODE_RAW_DATA Raw application data received. 0x85 / 0x86 PUSH_CODE_LOGIN_SUCCESS / PUSH_CODE_LOGIN_FAIL Server login result. 0x87 PUSH_CODE_STATUS_RESPONSE Server status response. 0x88 PUSH_CODE_LOG_RX_DATA Radio receive log data. 0x89 PUSH_CODE_TRACE_DATA Completed trace data. 0x8A PUSH_CODE_NEW_ADVERT New advert; a verified one-key DM may use this as an addable synthetic advert without storing the contact. 0x8B PUSH_CODE_TELEMETRY_RESPONSE Telemetry response. 0x8C PUSH_CODE_BINARY_RESPONSE Binary request response. 0x8D PUSH_CODE_PATH_DISCOVERY_RESPONSE Path-discovery response. 0x8E PUSH_CODE_CONTROL_DATA Control/discovery data. 0x8F PUSH_CODE_CONTACT_DELETED Oldest contact was deleted while making room. 0x90 PUSH_CODE_CONTACTS_FULL Contact storage is full. 0x91 PUSH_CODE_ONE_KEY_DM_REJECTED One-key DM refused; bytes 1\u201332 contain the refusing contact's full public key."},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"

PACKET_OK (0x00):

Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n

PACKET_ERROR (0x01):

Byte 0: 0x01\nByte 1: Error code (optional)\n

PACKET_CHANNEL_INFO (0x12):

Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n

Note: The normal response contains the 16-byte channel secret. An opt-in 32-byte MT GET described above extends the secret through byte 65; response code, index and name fields remain unchanged.

PACKET_DEVICE_INFO (0x0D):

Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: Active BLE PIN (32-bit little-endian; includes a generated session PIN)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n

Parsing Pseudocode:

def parse_device_info(data):\n    if len(data) < 2:\n        return None\n\n    fw_ver = data[1]\n    info = {'fw_ver': fw_ver}\n\n    if fw_ver >= 3 and len(data) >= 80:\n        info['max_contacts'] = data[2] * 2\n        info['max_channels'] = data[3]\n        info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n        info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n        info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n        info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n    if fw_ver >= 9 and len(data) >= 81:\n        info['client_repeat'] = data[80] != 0\n    if fw_ver >= 10 and len(data) >= 82:\n        info['path_hash_mode'] = data[81]\n\n    return info\n

PACKET_BATTERY (0x0C):

Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n

Parsing Pseudocode:

def parse_battery(data):\n    if len(data) < 3:\n        return None\n\n    mv = int.from_bytes(data[1:3], 'little')\n    info = {'battery_mv': mv}\n\n    if len(data) >= 11:\n        info['used_kb'] = int.from_bytes(data[3:7], 'little')\n        info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n    return info\n

PACKET_SELF_INFO (0x05):

Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n

Parsing Pseudocode:

def parse_self_info(data):\n    if len(data) < 36:\n        return None\n\n    offset = 1\n    info = {\n        'adv_type': data[offset],\n        'tx_power': data[offset + 1],\n        'max_tx_power': data[offset + 2],\n        'public_key': data[offset + 3:offset + 35].hex()\n    }\n    offset += 35\n\n    lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n    lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n    info['adv_lat'] = lat\n    info['adv_lon'] = lon\n    offset += 8\n\n    info['multi_acks'] = data[offset]\n    info['adv_loc_policy'] = data[offset + 1]\n    telemetry_mode = data[offset + 2]\n    info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n    info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n    info['telemetry_mode_base'] = telemetry_mode & 0b11\n    info['manual_add_contacts'] = data[offset + 3] > 0\n    offset += 4\n\n    freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n    bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n    info['radio_freq'] = freq\n    info['radio_bw'] = bw\n    info['radio_sf'] = data[offset + 8]\n    info['radio_cr'] = data[offset + 9]\n    offset += 10\n\n    if offset < len(data):\n        name_bytes = data[offset:]\n        info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_MSG_SENT (0x06):

Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n

PACKET_SEND_CONFIRMED (0x82):

Byte 0: 0x82\nBytes 1-4: ACK code (32-bit little-endian)\nBytes 5-8: Round-trip time (32-bit little-endian, milliseconds)\n
"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"

PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:

Code Constant (firmware) Description 1 ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2 ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3 ERR_CODE_TABLE_FULL Internal queue or table is full - retry later 4 ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5 ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6 ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)

Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.

"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"

BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.

"},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
  1. Command-Response Pattern:
  2. Send command via RX characteristic
  3. Wait for response via TX characteristic (notification)
  4. Match the response by the expected response type; frames do not carry a general command sequence number
  5. Handle timeout (typically 5 seconds)
  6. Use command queue to prevent concurrent commands

  7. Asynchronous Messages:

  8. Device may send messages at any time via TX characteristic
  9. Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command
  10. Parse incoming messages and route to appropriate handlers
  11. Validate frame length before decoding

  12. Response Matching:

  13. Match responses to commands by expected packet type:

    • APP_START -> PACKET_SELF_INFO
    • DEVICE_QUERY -> PACKET_DEVICE_INFO
    • GET_CHANNEL -> PACKET_CHANNEL_INFO
    • SET_CHANNEL -> PACKET_OK or PACKET_ERROR
    • SEND_CHANNEL_MESSAGE -> PACKET_MSG_SENT
    • GET_MESSAGE -> PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS
    • SEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERROR
    • GET_BATTERY -> PACKET_BATTERY
  14. Timeout Handling:

  15. Default timeout: 5 seconds per command
  16. On timeout: Log error, clear current command, proceed to next in queue
  17. Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds)
  18. Consider longer timeout for channel operations

  19. Error Recovery:

  20. On PACKET_ERROR: Log error code, clear current command
  21. On connection loss: Clear command queue, attempt reconnection
  22. On invalid response: Log warning, clear current command, proceed
"},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n
"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16)  # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = choose_unused_slot(max_channels)\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n
"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n
"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n    packet_type = data[0]\n\n    if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n        message = parse_channel_message(data)\n        handle_channel_message(message)\n    elif packet_type == PACKET_MESSAGES_WAITING:\n        # Poll for messages\n        send_command(rx_char, build_get_message())\n
"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
  1. Connection Management:
  2. Implement auto-reconnect with exponential backoff
  3. Handle disconnections gracefully
  4. Store last connected device address for quick reconnection

  5. Secret Management:

  6. Always use cryptographically secure random number generators
  7. Store secrets securely (encrypted storage)
  8. Never log or transmit secrets in plain text

  9. Message Handling:

  10. Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received
  11. Implement message deduplication to avoid displaying the same message twice

  12. Channel Management:

    • Fetch all channel slots even if you encounter an empty slot
    • Ideally save new channels into the first empty slot
  13. Error Handling:

  14. Implement timeouts for all commands (typically 5 seconds)
  15. Handle RESP_CODE_ERR responses appropriately
"},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":""},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":""},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":""},{"location":"companion_radio_full/","title":"Companion radio full","text":"

View this page on MeshCore Docs.

"},{"location":"companion_radio_full/#full-companion","title":"Full Companion","text":"

Configure message alerts with the Companion notification builder. It generates CLI rules for contacts, rooms and channels, previews patterns, and can apply/test them over USB. Trusted contacts and rooms can send !notify strings with no GPIO and a 15-second playback limit.

For runtime on/off commands and board exceptions, see feature switches by role. Open the USB web console for the default ASCII terminal.

For practical on/off commands and update directions, see Full Companion feature switches.

companion_radio_full combines every qualified Companion transport for its platform into one artifact and acts as a host-backed LoRa mOTA source for updating other nodes. Most targets can run those transports simultaneously; the SenseCAP Indicator exception selects one secondary wireless transport per boot as described below.

Fresh Full Companion installs default to Bluetooth on and infrastructure WiFi off. WiFi remains compiled in: use set wifi.enabled 1 through the USB or Bluetooth CLI to enable it, or set wifi.enabled 0 to disable it persistently. Existing saved WiFi enablement is honored after an upgrade. The SenseCAP Indicator requires a reboot to apply its exclusive transport selection; primary ESP-NOW mesh operation is unaffected by the infrastructure-WiFi default.

The full Companion is deliberately not a LoRa OTA destination: it has no installable firmware staging store, refuses ota install, and never advertises its own firmware as an mOTA image. The RAK4631 Full variant can optionally keep a source-only RAK15002 SD archive of peer .mota files; that does not make the Companion an update destination.

Preference changes through the Companion protocol, WebConfig, or USB terminal report a storage error if saving fails, and restore their previous values. Settings that also change GPS or radio power-saving state restore that state; a busy radio rollback remains pending for recovery instead of reporting success.

Capability ESP32 full nRF52 full USB Binary Companion Yes Yes BLE Binary Companion Yes Yes USB ASCII terminal Yes Yes Dedicated USB plaintext logging No - shares the one USB TTY Yes Host-backed LoRa mOTA source WiFi TCP 5001 Exclusive USB mode or encrypted BLE; RAK4631 Full can additionally cache/serve peer images from optional RAK15002 SD WiFi Companion/WebConfig Yes No - nRF52840 has no WiFi Hardware serial Companion On targets with assigned serial pins On targets with assigned serial pins Ethernet Companion On targets with an Ethernet module On RAK4631 with RAK13800; the RAK15002 combination is explicitly one-at-a-time LoRa self-update No No Optional self-update WiFi on existing dual-app layouts; USB on single-app layouts Bluetooth DFU with a compatible bootloader; USB always supported"},{"location":"companion_radio_full/#rak4631-full-companion-optional-rak15002-archive-and-rak13800-ethernet","title":"RAK4631 Full Companion: optional RAK15002 archive and RAK13800 Ethernet","text":"

RAK_4631_companion_radio_full can cache and serve peer mOTA files from an optional RAK15002 card. This remains a source-only archive: ota install and bootloader installation are unavailable on the Companion, whether or not a card is inserted. Check the archive with ota cache [on|off]; use get sdcard [free] for card space.

The optional RAK13800 Ethernet module and RAK15002 share CS26. Ethernet is therefore off by default in this combined Full image and is controlled only for the current boot:

get eth\nset eth on\nset eth off\n

Startup and shutdown are asynchronous. get eth reports starting, retrying, online, stopping, or a failure state; accepting set eth on does not prove a DHCP lease. USB, BLE, and radio service continue while network operations wait. After requesting OFF, wait until get eth reports off before accessing SD. An already-entered vendor DHCP or renewal call cannot be cancelled midway; OFF keeps the SPI reservation until that call finishes and cleanup is safe.

Remove the SD card and end/cancel active OTA cache, transfer, serve, or host folder activity before set eth on. The command refuses unsafe states. Turn Ethernet off before inserting or accessing a card; an insertion during Ethernet use fails closed and still requires set eth off. This is not a supported hot-swap workflow. The combined RAK15002/Ethernet hardware path has not yet been hardware-qualified; see nRF52 RAK15002 storage for the receiver's separate signed-application path and loader prerequisite.

"},{"location":"companion_radio_full/#screenless-nrf52-spoken-button-confirmations","title":"Screenless nRF52 spoken button confirmations","text":"

Full Companion builds for T1000-E, RAK WisMesh Tag, MeshTracker X1, Muziworks R1 Neo and ThinkNode M3 use prerecorded spoken confirmations for their shared single-button controls. Other roles, legacy USB/BLE-only builds, screened boards and all Wio profiles retain their existing audio behavior. Set MESH_GPS_VOICE=0 at build time to retain tones on an eligible Full build.

User-button gesture Action / spoken confirmation One press Acknowledge an active notification: \"Notification cleared\"; otherwise \"Ready\" Two presses \"Advert queued\" or \"Advert failed\" Three presses \"Sound on\" or \"Sound off\" after the preference is saved Four presses \"GPS on\" or \"GPS off\" when the GPS setting is available Hold more than three seconds \"Shutting down\"; shutdown remains nonblocking while audio drains Hold during the first eight seconds of UI startup \"USB setup\" for terminal/rescue mode

MeshTracker X1's three-press gesture instead cycles \"Sound and vibration\", \"Sound only\", \"Vibration only\", and \"Silent\". The new setting must save before either live alert output changes. A failed save restores both preferences and announces \"Action failed\". An explicit mute or alert-mode change permits one courtesy confirmation without unmuting ordinary notifications. The four additional mode clips are compiled only for HAS_DRV2605 builds. Notification timers cannot cancel control speech, and the committed alert mode remains authoritative for vibration even if a notification-mask save fails.

Power-on and wake wiring remain board-specific. Speech is nonblocking and uses the existing nRF52 PWM player; each buzzer still needs a physical listening test for intelligibility. All spoken button, alert-mode and GPS recordings use Microsoft Zira Desktop with the selected compressed Companion voice profile. GPS on/off use three stored audio spans: one shared encoded GPS prefix and two phrase tails. Advert queued/failed also share an identical encoded prefix; Sound off/Sound only share one only on HAS_DRV2605 boards. The tails are not independently decodable words. Playback treats each prefix and tail as one continuous compressed stream, without resetting its codec, bit position, interpolation or DMA at the seam. The approved full-phrase PCM and PWM output remain bit-for-bit identical, including pronunciation and pauses.

The IMA audio payload is 3,881 bytes smaller on ordinary voice boards, or 4,971 bytes smaller on vibration-equipped boards, before the small segmented descriptor/decoder overhead. Tiny shared silence prefixes are not split. A representative Cortex-M4 GCC 14.2 size-optimized/LTO player-and-catalog build saves 3,732 or 4,792 bytes respectively after that overhead; final firmware savings can vary with toolchain and surrounding code. The existing generators still synthesize the two complete GPS phrases and button confirmations, then share byte-identical encoded prefixes; they do not try to identify word boundaries or splice independently spoken words. Windows Zira synthesis still requires Windows' local System.Speech tooling. R1 Neo's buzzer pin aliases its optional QSPI clock, so voice is not default-enabled when QSPIFLASH is selected. ThinkNode M4 is deliberately excluded: its former buzzer definitions incorrectly claimed I2C SDA and a battery-status LED, not verified audio hardware. Its Companion recipes no longer drive those pins as a buzzer.

The optional ESP32 RC32-without-display recipe is not part of this nRF52 extension: its fitted audio hardware needs verification and its ui-new controls require an ESP32 playback backend, not the nRF52 PWM player.

"},{"location":"companion_radio_full/#experimental-g726-speech","title":"Experimental G.726 speech","text":"

Ordinary builds still use the approved Zira IMA recordings. G.726 is a hardware test option only: set both MESH_BUTTON_AUDIO_HIL=1 and MESH_GPS_VOICE_G726_BITRATE=16 or 24. Invalid bitrates or a missing test gate are rejected. The generated catalog selects exactly one bitrate, retains the original sample counts, and omits the X1-only mode prompts on other boards.

The decoder needs 52 bytes of state, no heap or full-clip buffer, and about 1.2 KiB of Cortex-M4 code/tables. Its packing, resets and PWM lifecycle are checked against independently pinned FFmpeg output. At 24 kbps the audio payload is 25% smaller; at 16 kbps it is 50% smaller with more waveform error. Neither software comparisons nor successful builds qualify physical buzzer intelligibility or the refill deadline under Bluetooth/radio load.

scripts/generate_g726_voice.py regenerates both experimental catalogs from the approved mono 8 kHz Zira WAVs using FFmpeg's MSB-first g726 encoder. Production IMA headers cannot be selected as its output. The decoder's Sun provenance, attributed FFmpeg MIX interpolation and LGPL license are retained in src/helpers/ui/g726; no claim of independent ITU conformance is made.

"},{"location":"companion_radio_full/#nrf52-bluetooth-reconnect-qualification","title":"nRF52 Bluetooth reconnect qualification","text":"

Full Companions persist the paired client's notification subscriptions (CCCDs) alongside its bond keys. A RAM-only subscription cache is insufficient: after reboot, a phone can retain its bond and subscription while the node has disabled notifications. Bluetooth then shows Connected, but Companion replies do not reach the app. The RAM cache remains useful for warm reconnects; the persistent bond record restores subscriptions after reboot.

The nRF52 build force-includes SoftDeviceSvcCompat.h in both application and framework compilation. It prevents LTO from discarding memory reads and writes performed by Nordic supervisor calls, including queries used to save CCCDs. test/test_nrf52_svc_compat.py compiles a real ARM regression with the build's LTO and pointer-analysis options and compares it with the unprotected wrapper.

On an already paired test node, run the hardware regression with bleak:

python3 scripts/check_nrf52_ble_reconnect.py \\\n  --address D1:A0:86:CD:BB:C6 --board \"Seeed Tracker T1000-E\" \\\n  --version <expected-firmware-hash> --reboot\n

This checks app-style NUL-terminated CLI commands, reboots the selected node, and reads the saved subscription before the host subscribes again. It also observes release of a secured link with no Companion traffic, followed by a normal reconnect and working CLI. It does not clear pairing keys or change preferences. Fresh pairing with the current PIN is a separate initial gate. A link drop alone does not prove the firmware watchdog fired: confirm the cause in the node debug log or Bluetooth trace. Link timeout reasons 0x08 and 0x22 must not be counted as a confirmed firmware-requested disconnect (0x13 at the central).

"},{"location":"companion_radio_full/#sensecap-indicator-transport-and-rendering-exception","title":"SenseCAP Indicator transport and rendering exception","text":"

Both SenseCAP Indicator Full layouts keep USB available and start exactly one secondary wireless Companion transport per boot: BLE or infrastructure WiFi. The saved companion.transport selection takes effect after reboot. BLE mode does not start infrastructure WiFi, WebConfig, MQTT, TCP Companion, or network OTA services. WiFi mode never initializes BLE and releases its controller and host memory. LoRa remains the primary radio on the LoRa layout. ESP-NOW remains the primary mesh radio in both ESP-NOW modes, including BLE mode; only the infrastructure-WiFi services are omitted.

The requested 4-bit internal render profiles are:

Primary radio Secondary Companion transport Canvas LoRa Infrastructure WiFi Native 480x480 LoRa BLE Native 480x480 ESP-NOW Infrastructure WiFi Native 480x480 ESP-NOW BLE 320x320, scaled 1.5x to the 480x480 panel

All four modes retain the same 160x160 logical UI. If a contiguous native DMA-capable block is unavailable, startup keeps the device usable with the 320x320 emergency fallback and reports the canvas actually retained. Such a fallback is not a successful native-480 validation result.

"},{"location":"companion_radio_full/#build-and-install","title":"Build and install","text":"

The target is synthesized by build.sh only for an exact board recipe that has passed the combined-transport size check:

The measured ESP32 additions are M5Stack Unit C6L; XIAO C6/S3, Meshimi, WHY2025 Badge, LilyGo T-LoRa C6, T3S3 SX1262/SX1276, T-Deck, TETH Elite, classic T-Beam SX1262/SX1276, and T-Beam S3 Supreme; Heltec Wireless Tracker, Wireless Paper, E213, E290, T190, CT62, and V4 expansion-kit TFT; Generic ESP-NOW; SenseCAP Indicator ESP-NOW/LoRa; Ebyte EoRa-S3; and Meshadventurer SX1262/SX1268. The measured nRF52 additions are GAT562 Mesh Watch13, LilyGo T-Echo Lite, LilyGo T-Impulse Plus, and Wio Tracker L1 E-Ink. Their old transport-specific names remain available for explicit compatibility builds, but the Full image is the canonical release artifact.

Every ESP32 Full image includes ordinary WiFi Companion, WebConfig, and the TCP mOTA services, even when its historical build base was USB- or BLE-only. On Generic_ESPNOW and SenseCapIndicator-ESPNow, ESP-NOW is also the primary mesh radio and shares the same 2.4 GHz hardware. Those two Full images retain B/G/N for ordinary clients alongside ESP-NOW LR. Their ESP-NOW mesh, setup AP, and infrastructure connection all use one persisted channel, which defaults to 1. Configure the router's 2.4 GHz radio and every other primary ESP-NOW node for that same fixed channel. Turning Companion WiFi off stops its TCP/AP services but deliberately leaves the ESP-NOW mesh radio running on the selected channel.

Every ESP32 Full Companion with a station connection synchronizes its UTC clock from NTP after boot and explicitly refreshes it every 24 hours. The common WiFi path owns this for non-MQTT and runtime-unconfigured nodes; a configured MQTT bridge owns the same boot-and-daily schedule. Successful sync updates an attached hardware RTC as well as the ESP32 clock and suppresses LoRa clock fallback for that boot. Reconnecting WiFi preserves the existing daily deadline. A timeout leaves the existing clock and fallback intact and retries without blocking radio work.

Inspect or change the shared channel from the Full Companion text terminal:

get espnow.channel\nset espnow.channel 6\nreboot\n

The accepted range is 1 through 13; use only a channel permitted in your region and supported by the router. The setter persists the selection, but the running radio stays on its current channel until reboot. Coordinate the change across every primary ESP-NOW node and the router before rebooting, or the node will lose one or both links. This primary-radio setting is not bridge.channel; that command belongs to the separate ESP-NOW bridge feature. To attach a LoRa-primary *_repeater_bridge_espnow gateway to these nodes, match its bridge.channel and select set bridge.format raw; the bridge's backward-compatible default is the distinct wrapped format. WiFi power saving cannot put infrastructure WiFi and ESP-NOW on different channels. On these two primary-ESP-NOW Full targets, max is unavailable because maximum modem sleep can make the station miss ESP-NOW broadcasts; use min for WiFi/BLE/ESP-NOW coexistence. The firmware also holds the ESP-IDF RF wake reference for the primary mesh radio, so the ESP-NOW receiver remains continuous even though the reported coexistence setting is min.

List the available targets:

bash build.sh list | grep companion_radio_full\n

Build by using one exact listed name:

bash build.sh build-firmware heltec_v4_r8_companion_radio_full \\\n  --firmware-version v1.17.0\n\nbash build.sh build-firmware RAK_4631_companion_radio_full \\\n  --firmware-version v1.17.0\n

To build every canonical full Companion target, select the corresponding interactive menu item or run:

bash build.sh build-full-companion-firmwares \\\n  --firmware-version v1.17.0\n

Canonical Companion bulk builds also omit legacy _ps and _femoff aliases. Power saving and controllable FEM receive gain are persisted runtime settings; the old names remain available through an explicit build-firmware command for compatibility. Full Companion replaces separate USB, BLE, ordinary WiFi, hardware-serial, Ethernet Companion, Terminal Chat, and USB-only packet-logging release artifacts whenever the exact board supports those combined transports. Direct builds of the legacy targets remain available. RAK4631 repeater and room server Ethernet builds remain separate because they are different standalone roles, not Companion transports. nRF52 separates framed traffic and logs; ESP32 makes those modes mutually exclusive on its one USB TTY. In WebConfig, use the FEM RX boost switch. From the text terminal (USB, or TCP 5002 on ESP32), use:

get radio.rxgain\nset radio.rxgain off\nset radio.rxgain on\nget radio.fem.rxgain\nset radio.fem.rxgain off\nset radio.fem.rxgain on\nget radio.fem.txgain\nset radio.fem.txgain off\nset radio.fem.txgain on\n

radio.rxgain controls the radio chip's boosted receive-gain mode; the FEM commands control the external receive and transmit paths. The selected states are applied immediately and retained after reboot. FEM TX gain is reported as unsupported on boards without software-selectable PA gain.

SSD1306 display builds also persist a runtime orientation. This replaces the separate rotated Full Companion release image:

get display.rotation\nset display.rotation 90\nset display.rotation 180\nset display.rotation 270\nset display.rotation 0\n

0 resets the screen to that board's compiled default orientation.

The display clock accepts decimal hour offsets, including half-hour and quarter-hour timezones, from -12 through +14:

get tz.offset\nset tz.offset 5.75\n

This Companion display setting is separate from MQTT's timezone.offset. Invalid values leave the previous setting unchanged; a failed save restores its exact previous fractional value.

Preference migration retains the published signed one-byte hour at offset 225 and every following legacy field. Integer settings keep the existing image format. Fractional settings append a 10-byte TZF version-1 footer after the existing board and TXR footers: a little-endian 32-bit float and CRC-16/CCITT covering the first eight bytes, stored most-significant byte first. The old byte contains the hour truncated toward zero. A missing footer uses that legacy integer; malformed, truncated, nonfinite or out-of-range footers reject the complete image before changing live preferences. Older firmware with strict image-length validation cannot read a fractional image; set an integer offset and save before downgrading.

Heltec E290 and T190 now use their Full Companion artifacts for simultaneous USB, BLE, and WiFi. Their older usb_ble, USB-only, and BLE-only names remain available only as explicit compatibility builds.

Heltec V3 and base OLED V4 Full Companion also include the former direct WiFi MQTT Companion capability. Configure and enable MQTT at runtime through WebConfig; the canonical release therefore publishes the Full image instead of a second companion_radio_wifi_mqtt image. V4 TFT and expansion-kit layouts remain separate hardware images because their display and I2C wiring differs.

Device power saving is separate from LoRa RXPS. It can be changed in WebConfig with the Device power saving switch or from the text terminal:

get powersaving\nset powersaving on\nset powersaving off\n

On ESP32, WiFi modem power saving is a third independent setting. Select it in the WebConfig WiFi card, or use the Full Companion text terminal:

get wifi.ssid\nget wifi.status\nget wifi.powersave\nget webui\nget password\npassword my-admin-pass\nset wifi.ssid MyNetwork\nset wifi.pwd my-password\nset wifi.powersave min\nset wifi.powersave max\nstart webconfig\nstop webconfig\n

ESP32 WiFi Companions with WebConfig, including Full Companion, expose a CLI tab at the node's LAN address. It defaults to on; use get wifi.cli, set wifi.cli on, or set wifi.cli off. Older builds that report the browser terminal as unavailable need a firmware update. The setup AP does not expose this tab.

ESP32 WebConfig and runtime browser OTA share the saved node admin password, initially password; the OTA HTTP Basic username is admin. get password reports status without revealing it. The password is stored in NVS and survives a filesystem erase. New values must be 1\u201315 UTF-8 bytes without CR or LF. Use 8\u201315 printable ASCII characters to protect setup WiFi with the same password; otherwise the hotspot is open and requires an HTTP admin login. The display's WiFi join QR uses the hotspot's current credential. After a successful change, the submitting page keeps its save result and tries the new login once; other browser sessions must sign in again. An unconfirmed save keeps the previous runtime password active, but the saved state after reboot may differ.

The browser accepts Companion configuration commands, including get powersaving, set usb.logging on|off, WiFi controls, and MQTT settings. set mqtt.enabled on|off controls MQTT without erasing broker settings; get mqtt.enabled, get mqtt.running, and get mqtt.status distinguish the saved switch from current connections. On MQTT builds, get mqtt.stats reports free/largest heap and accepted/error publish counts. get mqttN.diag reports the connection state, transport errors, and pub_ack, the count of actual broker acknowledgments for that client. Accepted publishes are separate from broker acknowledgments. Runtime slot capacity depends on the hardware profile; the V4 OLED Full profile runs one slot even though six broker configurations can be saved. set logging.output off|usb|wifi|both selects both outputs together. The browser also uses the complete Companion terminal for card, import meshcore://..., list, to, send, login, cmd, and delayed replies, with the same syntax as USB and TCP port 5002. See the terminal command guide for contact import and session ownership. USB MOTA session commands require the USB connection; WiFi host folders use port 5001.

On the two primary-ESP-NOW Full targets, the same terminal also provides get espnow.channel and set espnow.channel <1-13>. A channel change is persisted and requires a reboot, unlike a WiFi power-save change.

SSID and password writes return their reply first, then restart the Companion WiFi station with the saved credentials. A TCP terminal therefore disconnects shortly after either write; reconnect to the IP reported by the new network. Password entry is masked over USB. get wifi.pwd reads the saved password from any local connection, including binary command 0x42; LoRa cannot read it. It may be empty for an open network, an ordinary passphrase of up to 63 characters, or an exact 64-character hexadecimal WPA/WPA2 PSK. Other 64-character values and all longer values are rejected.

The normal binary Companion connection can also read or write this setting over USB, BLE, or TCP port 5000 without entering terminal mode. The mode values are 0 for min, 1 for none, and 2 for max; see the Companion protocol. A Full Companion that runs WiFi and BLE simultaneously rejects none because coexistence requires modem sleep. A primary-ESP-NOW Full target also rejects max; unlike infrastructure traffic, peer ESP-NOW broadcasts cannot be buffered by the access point while the station sleeps. If an older image saved a conflicting value, firmware applies and reports min instead. The SenseCAP Indicator uses the active-mode constraints in the exception above: LoRa + WiFi accepts all three values, ESP-NOW + WiFi accepts none|min, LoRa + BLE accepts min|max for the inactive WiFi setting, and ESP-NOW + BLE requires min. Fresh Cascade builds select min, and an existing valid saved selection takes precedence.

On radios with RX duty-cycle support, WebConfig and the text terminal also expose the persisted RXPS setting:

get radio.rxps\nset radio.rxps off\nset radio.rxps on\nset radio.rxps level 8 preamble 16\nset radio.rxps 65625 60000\n

Fresh Cascade-profile Full Companion builds start with RXPS on at level 8 and a 16-symbol preamble. Changing it takes effect immediately and remains selected after reboot.

Companion firmware defaults device power saving to on. Version 1.17.1.2 also turns it on once when upgrading an older Companion preference file, including one written by the short-lived default-off regression. After that one-time migration, an explicit set powersaving off selection remains persistent.

On ESP32, enabling it lowers the CPU clock to 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. Disabling it restores the normal CPU clock and keeps GPS awake. Active Full Companion transports remain available in both states; on the SenseCAP Indicator this means USB plus the selected BLE or infrastructure-WiFi secondary transport. WiFi modem sleep stays enabled when BLE and the ESP32 WiFi radio are active together because coexistence requires it. Changing device power saving does not overwrite the saved WiFi power-save mode. While a native-USB host is enumerated, the platform sleep attempt is held off so USB CDC remains responsive; detaching the host releases that guard. CPU, radio-modem, and GPS power-saving settings remain active, and USB power from a charger alone does not create a Companion session. The selected state is retained after reboot.

On the LilyGo T-Beam 1W Full Companion, press the physical BOOT button once to turn the ESP32 WiFi radio and all WiFi services off or on. The screen confirms WiFi: OFF or WiFi: ON, and the selected state is retained after reboot. When WiFi is off, TCP ports 5000-5002, WebConfig, and MQTT are stopped; USB, BLE, the display, GPS, and LoRa continue to operate. Press BOOT again to restore WiFi, including the saved station or setup-AP mode. On boot, BLE starts two seconds after WiFi/WebConfig so their peak startup allocations do not overlap.

Artifacts are written to out/ by default.

Full Companion behavior is selected with independent capability macros for TempRadio, the OTA CLI, the TCP terminal, USB folder seeding, and memory diagnostics. The legacy COMPANION_RADIO_FULL flag remains an input for older target recipes, but application behavior no longer uses that umbrella as an unrelated compile guard. In particular, ESP32 WiFi/WebConfig terminal controls are compiled from their actual WiFi/WebConfig capability. On targets that run ESP32 WiFi and BLE simultaneously, BLE is started first to avoid heap fragmentation. The SenseCAP Indicator starts only its selected secondary transport. Compile-time prerequisite checks reject inconsistent feature flags. After linking, the capability sidecar verifies USB, BLE, the OTA CLI, TempRadio, and each platform's host-folder transport; it also verifies the TCP terminal, WebConfig, and WiFi seeder on ESP32, plus dedicated logging on nRF52.

On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit in one artifact. The T-Beam 1W Full Companion uses that same LilyGo factory-compatible boot layout on its 16 MB flash because this source-only role does not install updates into a second app slot. Flash the generated -merged.bin when first installing this partition layout. Other boards with 8 MB or more retain dual application partitions. Heltec V2 and TLora V2 use 100 contacts, 8 group channels, and a 16-frame offline queue in this combined profile because of internal DRAM limits. Meshadventurer SX1262 and SX1268 use 100 contacts, 30 group channels, and a 16-frame queue. These capacities preserve the required 8 KiB static internal-DRAM reserve alongside MOTA and all Full transports; their ordinary transport-specific images retain 160 contacts, 40 channels, and 128 queued frames.

The 1.17.1.5 memory replacements for Generic ESP-NOW, Heltec Wireless Paper, Wireless Tracker, CT62, V3, Tracker V2, and XIAO C3 Full Companion use 150 contacts and retain their 256-frame queue and Full transports. Export contacts before updating if you have more than 150; entries beyond the new limit may be unavailable and a later save may omit them.

Full Companions normally retain 256 pending Companion message frames. ESP32 boards with configured PSRAM retain 512 and allocate that queue from PSRAM before WiFi and BLE start. If PSRAM is unavailable at runtime, allocation falls back through 256 and 128 frames, then to a 16-frame internal buffer. The Full Companion startup memory line reports the capacity actually allocated as offline_queue=<frames>. The queue is volatile and shared by all channels and direct messages; it is not flash-backed history. See Companion offline message queue for all platform defaults and full-queue behavior.

Every corrected nRF52 Full profile keeps 256 offline frames normally and temporarily lends 128 slots to mOTA to leave room for Bluetooth tasks, displays and UI allocations. Queue sharing preserves the board's contacts, channels, and USB/Bluetooth mOTA sending. See the memory correction. If an explicit OTA source, discovery, download, install, or temporary-radio request needs that workspace while more than 128 unread frames are queued, the oldest volatile frames are consumed until 128 remain. The command reports the number removed. Read-only OTA status queries do not remove messages. Sync the queue to an app before starting OTA when those unread messages matter.

The nRF52 target inherits the board's ordinary USB Companion installation format and adds BLE plus the serial mOTA source. It does not enable an SD cache or any other board-specific storage behavior; host files are streamed as they are requested. Its image is bounded by the board's normal application region, not the smaller OTAFIX in-place workspace reserved for firmware that can update itself.

"},{"location":"companion_radio_full/#interfaces","title":"Interfaces","text":"Platform Interface Purpose Both USB, 115200 baud ASCII after boot or observable USB session reset; automatically switches on the first complete Binary Companion frame Both BLE Binary Companion; display builds show a random session PIN, while headless builds default to 123456 ESP32 TCP 5000 Binary Companion over WiFi ESP32 HTTP 80 Companion WebConfig and first-boot WiFi setup ESP32 TCP 5001 Host .mota folder from motatool serve --tcp ESP32 TCP 5002 Full Companion text terminal; same role commands as the USB terminal nRF52 USB mOTA mode Host .mota folder from motatool serve --serial nRF52 Encrypted BLE mOTA service Paired phone/tablet/Linux host .mota catalog

Delivery-required replies are returned only to the interface which supplied the latest command. A contact-list stream keeps that route locked from CONTACTS_START through END_OF_CONTACTS; commands waiting on another interface are read after the stream finishes. Best-effort asynchronous observations such as adverts remain broadcast so passive clients can refresh their views. Companion session state is device-wide, so use one active Companion application at a time. On nRF52, BLE remains available while USB is in terminal or mOTA mode.

The Companion TCP listener starts only after a usable WiFi station or setup-AP address exists. If opening the socket fails, it closes any failed-start socket and retries with a delay capped at 30 seconds, without restarting the WiFi connection or disturbing BLE, WebConfig, OTA, or a healthy accepted TCP client. Turning WiFi off or suspending it cancels the listener request; retry service cannot turn it back on. Use get wifi.tcp through BLE, USB, or the browser CLI to inspect listener state, registration, accepted clients, completed RX/TX frames, and errors without a PC port probe. get display.wifi also shows the actual tcp listener flag independently of the network-services flag. A listening socket alone cannot detect LAN firewall or guest-network isolation.

ESP32 TCP sends are non-blocking, and inbound commands are serviced alongside outgoing data rather than waiting for an empty transmit queue. Unsolicited radio logs therefore cannot keep the app handshake unread. A live client with pending output is disconnected after 10 seconds without write progress; other transports continue running. Required replies retain their queue slots, so a new command waits if no reply can be admitted. Partially sent frames remain pinned to preserve framing, while already queued replies keep the same reconnection rules below.

On WiFi TCP port 5000, reconnecting from the same IP retains queued outgoing frames. A different IP takes over immediately, but never receives the previous IP's replies: up to four queued frames are parked separately for 30 seconds from takeover. The old IP can reconnect within that window to retrieve them. Only one displaced IP's backlog is retained; if another client also needs that slot, the older backlog is discarded so the new connection is not delayed. This reuses an existing fixed buffer instead of allocating additional frame storage. Pending commands, signing sessions, and contact streams belonging to the displaced WiFi session are canceled immediately; already accepted radio message transmissions are not canceled. Queued frames are retained, not a resumable command session, and IP matching is not authentication. Turning WiFi off clears both queues. Stored message history is unaffected.

Ethernet also retains complete queued frames for the same IP. Unlike WiFi, it has no spare backlog buffer: a different IP cancels the old owner's pending operations and discards its queued frames before the new socket is exposed. This keeps Ethernet's existing four-frame memory limit. On both transports, a live same-IP replacement preserves pending operations; an observed disconnect, disabled transport, or rejected input cancels them. Already queued frames can still be replayed on a same-IP reconnect, but canceled operations are not resumed.

Partial TCP writes retain the unsent bytes without interleaving later frames. After a reconnect, an incomplete frame restarts from its header; the new TCP connection must not receive only the old connection's trailing bytes.

Binary WiFi/Ethernet frames must declare 1\u2013176 payload bytes. Illegal lengths close the connection immediately, without waiting for the announced body or executing a truncated command. An incomplete binary header or payload has a five-second deadline once reception starts; reconnect to send a fresh frame. RAK13800 worker-owned SPI intervals pause this deadline without changing the connection or its queued replies. The remaining receive budget resumes when the worker hands the controller back; an already-expired frame is not revived. Ethernet raw-line terminal input has no typing deadline, but an overlong line also closes the connection instead of executing its prefix.

USB Binary output is queued as complete length-prefixed frames. Temporary CDC or UART backpressure pauses the contact stream; a frame may drain through a smaller hardware FIFO in ordered chunks, but its remainder is retained and no later frame can interleave with it or cause it to be discarded.

Binary login, status, telemetry, path-discovery, binary-request, and trace results also have bounded producer retention when that transport queue is full. One request result and one trace result can wait independently, on their original captured interfaces. Their required SENT frames are retained and admitted before the final result. A first valid radio result gets up to ten seconds for whole-frame queue admission; a still-blocked SENT keeps its earlier ten-second deadline. Duplicate callbacks cannot replace the retained result or extend these deadlines. Disconnect, cancellation, or expiry drops an unadmitted result. Queue admission is not proof of physical host receipt.

Reflected request tags are matched against the complete peer public key. Recent request and trace history has eight slots each, reserved before radio transmission; live entries are never evicted to accept a newer request. A full history or a quarantined identifier returns ERR_BAD_STATE, so clients must back off. Retired reflected tags and trace tag/auth tuples are suppressed for at least ten seconds and through the original radio deadline plus ten seconds when longer. Login retries to the same peer have at least a thirty-second quarantine. Positively matched newer status/telemetry/binary replies still work after login, but otherwise unsolicited responses from that login peer can be suppressed during the quarantine.

These guards are finite and clear at reboot. Login replies carry a server timestamp, not an echoed request nonce; sufficiently late replies or a timestamp collision cannot always be distinguished without a protocol change. Trace tag/auth fields are correlation data, not cryptographic authentication. The guards do not authenticate a host process or retract frames already accepted by a transport. In particular, the same-IP TCP queue replay described above remains unchanged, including its lack of authentication.

When a BLE client requests pairing, a display-equipped build wakes the screen, switches to the first home page, and keeps the active six-digit PIN visible until Bluetooth connects or the two-minute pairing window expires. USB, WiFi, Ethernet, and hardware-serial connections do not suppress this screen. With no saved BLE PIN, display builds generate a new PIN at boot; builds without a physical display use 123456. Use set pin 654321 in the Companion CLI to save a fixed pairing PIN, then reboot to activate it. set pin 0 restores the default/generated PIN policy. A PIN saved through the Companion protocol also takes effect after reboot. See the PIN command reference.

On T1000-E, a USB data host automatically pauses Bluetooth, including an existing phone connection. Unplug USB to resume Bluetooth after 500 ms; a saved off setting stays off. A wall charger without a USB data host does not pause Bluetooth. See the T1000-E transport policy for status and saved-setting behavior.

The Bluetooth device name is independently configurable. In the text terminal, use get bluetooth.name and set bluetooth.name <name>; use set bluetooth.name default to restore MeshCore-<node name>. The WebConfig Node card exposes the same optional field on Bluetooth-capable ESP32 builds. Custom names replace the complete Bluetooth label rather than inheriting the prefix, accept up to 31 valid UTF-8 bytes, and take effect after reboot. This does not change the node's mesh advertisement name.

Every Bluetooth Companion also supports get bluetooth.mac and set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>. A literal address is a BLE random-static identity, random creates one persistent identity, random-every-boot rotates it on startup, and random-after-connect rotates it on the first boot after an authenticated connection while leaving unused boots unchanged. default (or clear) restores the chipset address. None of these values changes the stealth flag. Reboot and re-pair after changing modes or after a rotation. Address rotation clears stale local bonds. On nRF52 this affects the MeshCore application only, not the OTAFIX bootloader's separate BLE DFU identity.

get bluetooth.stealth and set bluetooth.stealth on|off control an independent flag, defaulting to off. Combine it with a custom address or random to retain the address and bond across boots. With stealth on, normal discovery ends after the first authenticated pairing and only the saved peer may reconnect. Rotating MAC policies still rotate and reopen first pairing when they do; stealth remains enabled. Repeating on keeps an existing bond. Toggle off, then on, then reboot to reset first pairing. All flag changes need reboot. The node still sends directed or allowlisted BLE packets; it is not radio silent.

ESP32 TCP ports 5000, 5001, and 5002 remain trusted-network services. The HTTP admin login protects WebConfig and runtime browser OTA, and does not protect these ports or delegated remote CLI. Administrative console access can change the admin password. Expose TCP services only on a trusted LAN or temporary setup network. Temporary migration/recovery apps retain their documented access policy. See WiFi setup for credential setup and reconnect behavior.

"},{"location":"companion_radio_full/#usb-binary-and-text-terminal-modes","title":"USB Binary and text terminal modes","text":"

Full Companion USB starts in the ASCII terminal after boot or an observable USB session reset. MeshCore apps and meshcli send a <-prefixed framed command, which automatically hands the untouched frame to the Binary Companion parser:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

The automatic probe runs only at an empty prompt. A complete frame confirms binary mode; an incomplete probe returns to ASCII after one second. Binary mode then remains selected until an observable USB session reset, reboot, or the explicit terminal start token. See Full Companion USB CLI and binary switcher for the byte-level state machine, logging and mOTA ownership, recovery paths, and known limitations.

Immediately after boot, an ordinary terminal can issue ASCII commands without a start token. If the device is already in Binary Companion mode, open the port with the terminal start token sent automatically:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

The input map prevents any Binary Companion control bytes received during the mode transition from changing the local terminal's character set or display state while leaving UTF-8 emoji intact. The banner confirms that terminal mode is active; do not enter the start token again after it appears.

The terminal supports Companion chat commands, including channels, channel <name-or-slot> <message>, remote administration with login <admin-password> and cmd <remote-command>, and routed trace [recipient-name-or-prefix], plus local ota, tempradio, and normalradio controls. board reports the exact hardware target. ESP32 Full Companion also provides memory, which reports heap, internal allocation, PSRAM, and offline-queue usage; it is omitted from non-ESP32 help. ESP32 builds also provide local WiFi credential, status, WebConfig, and power-save controls. Every Full Companion provides persistent get/set usb.logging and starts with logging off on a fresh installation.

"},{"location":"companion_radio_full/#esp32-single-usb-serial-port","title":"ESP32 single USB serial port","text":"

Every ESP32 Full Companion exposes one USB TTY with two exclusive modes. It starts as the ASCII terminal unless a saved logging-on preference boots directly into the logging terminal. If it is already binary, enter its text terminal with +++MESHCORE-TERM-START. For an ESP32 1.17.1.5 USB logging session, run these separate commands:

set powersaving off\nset usb.logging on\n

The first command is the release's USB power-saving workaround. The same TTY then emits plaintext packet/debug logs and continues accepting CLI commands, including set usb.logging off. Framed Binary Companion is unavailable on USB while logging owns the TTY. Turning logging off sends the command reply, stops the logs, and leaves that TTY in the normal ASCII terminal, matching a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion afterward. A saved logging-on preference boots directly into this input-capable logging terminal. BLE and Wi-Fi Companion remain available while USB is logging on simultaneous targets. On the SenseCAP Indicator, only the selected BLE or Wi-Fi Companion transport remains available alongside the logging terminal.

ESP32 Full Companion uses the repository's Arduino-ESP32 2.x platform base where the board supports it. RC32 and ESP32-C6 retain their board-required Arduino 3.x platform, but follow the same one-TTY policy. No ESP32 Full image creates an optional second CDC interface. The single-TTY behavior applies to native-USB ESP32-S3 boards and boards using a USB-UART bridge.

"},{"location":"companion_radio_full/#nrf52-dual-usb-serial-ports","title":"nRF52 dual USB serial ports","text":"

Current nRF52 Full Companion firmware can expose two CDC ACM serial interfaces on one physical USB cable:

A fresh Full Companion starts with USB logging off and therefore enumerates only interface 00. Use set usb.logging on to save logging on; the reply says that a reboot is required. Use set usb.logging on reboot to save it and have the node reboot automatically after the reply. The second interface appears after that reboot. Likewise, set usb.logging off reboot removes interface 02. The optional reboot word is accepted only in these exact command forms and triggers a reboot only when the USB logging mode or descriptor needs to change.

For serial bridges that use one port for both commands and packets, including unmodified Cisien/meshcoretomqtt, use set usb.logging stream reboot instead. This saved packet-stream mode keeps only interface 00 and emits plaintext RAW/RX/TX records together with repeater-style -> CLI replies. Configure the bridge with the stable *-if00 path. Binary Companion and serial mOTA are unavailable on that port while logging is enabled; set usb.logging off stops the stream and leaves the ASCII terminal active. BLE remains available. get usb.logging reports stream and the primary port after reboot. To return to separate logging, use set usb.logging on reboot.

These are Full Companion text-terminal commands. The superficially similar meshcli ... get usb.logging command uses the Binary Companion parameter registry and can report Unknown var usb.logging; it does not forward that line to the text terminal. Enter terminal mode on interface 00 with the +++MESHCORE-TERM-START token as described above, then issue the command.

When logging is on, Linux normally shows two /dev/ttyACM* devices. Match the stable /dev/serial/by-id/*-if00 and *-if02 links, or use a udev rule matching ID_USB_INTERFACE_NUM, rather than assuming which tty number is assigned. On Windows they appear as two COM ports; identify them by USB interface instead of depending on a particular COM number. The nRF52 bootloader temporarily exposes its normal DFU serial interface during an update.

Opening the nRF52 logging port prints MeshCore USB logging port followed by its portable identity, USB CDC 1; interface 02; Linux stable suffix: -if02. get usb.logging reports the same endpoint. Firmware cannot print the exact /dev/ttyACM* or COM* name because Linux, macOS, or Windows assigns that name after USB enumeration; use the *-if02 link on Linux to obtain the exact path. For example, readlink -f /dev/serial/by-id/*-if02 prints the host-assigned /dev/ttyACM* name.

Every ESP32-S3 Full Companion image uses DIO flash mode, including the RAK3112 and RC32 profiles. The S3 ROM supports DIO while loading the software bootloader, and some flash configurations fail before the application starts when a merged image inherits QIO. DIO trades some maximum flash-read throughput for compatibility; it does not change a board's PSRAM type or any ordinary non-Full firmware profile.

On nRF52, point MeshCore Companion software, meshcli, and motatool at interface 00. When enabled and rebooted, point a plaintext reader or USB-connected MQTT service at interface 02. On ESP32, use its only USB TTY for Binary Companion while logging is off, or for the plaintext CLI/logger while logging is on; close one consumer before switching modes. Turning logging off returns to ASCII, so the normal stop token or a valid framed probe is still required before Binary Companion owns the port.

ESP32 Full Companion exposes this same text terminal on TCP port 5002. Connect with nc DEVICE_IP 5002; no USB control token is needed. USB terminal mode and the TCP terminal share recipient, login, command, trace, and display state, so only one may own the terminal at a time. The idle startup USB prompt yields to a TCP connection when no USB data client or partial command is present, and is restored when TCP disconnects. An active USB session rejects TCP; entering USB terminal mode later closes an active TCP session. On USB-Serial-JTAG and USB-to-UART hardware, the firmware cannot observe an idle host open, so actual buffered USB activity\u2014not the physical cable alone\u2014claims ownership. Disconnecting TCP clears pending terminal-only state without cancelling Binary Companion delivery or radio retries.

Both terminal transports accept reboot. The reply is sent first and the device reboots one second later, so a script can distinguish an accepted reboot from an abruptly lost connection.

Port 5002 is plaintext and has no device-local login gate. A remote-admin password entered with login is sent across the LAN connection as typed even though the terminal does not echo it. Use port 5002 only on a trusted LAN or a temporary setup network.

For example:

channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nshow\nshow channels on\nto Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\nlogin my-admin-password\ncmd ver\ntrace\n

The terminal list [n] command displays favorite contacts first and orders each favorite/non-favorite group by its most recent advertisement. This does not alter the binary Companion contact-list protocol.

Unsolicited terminal output starts in a quiet mode: advertisements and ordinary channel messages are hidden, while #emergency messages remain visible. Use show adverts on|off, show channels on|off, and show emergency on|off to control each category independently; plain show reports their state. These runtime filters affect terminal printing only and reset to their defaults after reboot.

The to command selects the remote-administration target. path shows its saved outgoing route; path direct, path clear, or a list separated by spaces, commas, or both changes the route used by subsequent login, send, and cmd commands. Every hop must use the same 2-, 4-, or 6-digit hexadecimal width. Login passwords are masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait for the asynchronous login result before using cmd; command replies appear as CLI -> from <name> and use a response window of 300% of the route estimate. DIRECT via path <hop,...> displays the exact saved prefixes copied into the packet. Remote ACL permissions determine which commands the target accepts. The matching reply reports its round-trip time from local queueing through result reception, including radio transit and remote execution. Only one terminal cmd can be pending at a time.

Incoming unicast replies are labeled ROUTED. Their exact return prefixes are not available at the destination because each forwarder consumes its prefix; use trace to verify the return route.

With no argument, trace uses the current to recipient. A name-prefix argument traces that contact directly without changing the current recipient. The contact must already have a known direct path; results show the SNR at each hop, or a timeout if the round trip does not return.

An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces, commas, and mixed separators are accepted:

trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n

The entered route must include both the outward and return prefixes. Exact three-byte traces are not supported.

Return to Binary mode with:

+++MESHCORE-TERM-STOP\n

An observable USB session reset restores ASCII, including after a Binary client disconnects. Native USB with DTR can detect a terminal closing; ESP32 hardware USB Serial/JTAG detects bus resets and physical host loss. UART bridges usually cannot report a terminal closing, so use the start token if they remain in Binary mode. A different baud rate, including 57600, does not select ASCII.

On an ESP32 Full Companion built with OTA_FOLDER_SERIAL, motatool can keep the USB serial port open as an mOTA folder source when WiFi is unavailable:

motatool serve --serial /dev/ttyACM0 --dir ./motas -v\n

The tool sends the exact completed ota folder on line when it opens the port. Firmware recognizes it in startup ASCII or idle Binary Companion mode, then transfers the port to exclusive USB mOTA ownership before binary request/reply frames begin. The text terminal and Binary Companion are unavailable on that TTY until motatool detaches the folder. This matches the nRF52 ownership behavior described below. TCP port 5001 remains the preferred unattended source transport.

"},{"location":"companion_radio_full/#nrf52-usb-mota-mode","title":"nRF52 USB mOTA mode","text":"

The nRF52 full target has a third, exclusive USB mode for the host folder. Unmodified motatool serve --serial sends ota folder on when it opens the port. The startup ASCII terminal recognizes that exact completed line, leaves terminal mode, and gives the stream directly to exclusive mOTA handling. If the port is already in Binary Companion mode, the idle binary parser recognizes the same control sequence. The sequence is not examined inside a framed Binary Companion packet. See the switcher guide for the ownership transitions.

While mOTA mode owns USB:

No modified motatool build, terminal token, or preliminary mode change is required.

"},{"location":"companion_radio_full/#nrf52-bluetooth-mota-source","title":"nRF52 Bluetooth mOTA source","text":"

Protocol v14 also lets a phone, tablet, or Bluetooth-capable Linux host feed the .mota catalog to an nRF52 Full Companion. Normal Companion commands stay on the Nordic UART service. Firmware data uses a separate GATT service, so binary app traffic cannot be mistaken for a firmware block.

The client must pair with the Companion PIN, subscribe to the mOTA Device Request characteristic, and send CMD_BLE_MOTA_SOURCE action start over the normal Binary Companion connection. The source is available only while that encrypted MITM-authenticated connection remains active. Disconnecting, unsubscribing, overflowing a frame, or receiving malformed data automatically detaches the catalog. USB and BLE source modes are mutually exclusive.

The included Raspberry Pi reference client validates each .mota with motatool, schedules the local TempRadio window, serves until interrupted, then detaches and restores the normal radio tuple:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion \\\n  --dir ./motas \\\n  --local 'tempradio 909.950,250,5,5,120'\n

Prefer this relative tempradio form when the phone/Pi and radio clocks may disagree. It starts a duration on the Companion and does not compare their wall clocks. Use the absolute tempradioat scheduler only after synchronizing the participating nodes.

Use --pair when the Linux host has not already bonded. BlueZ must have an agent capable of entering or confirming the six-digit PIN. Use --source status without --dir for a read-only channel/status check. The complete UUID, frame, action, and status definitions are in the Companion protocol.

This reference process stands in for the phone application. A mobile app can use the same sequence while retaining its normal contact and Repeater Admin UI: log in to the destination, put each required node on the same bounded TempRadio tuple, start the local Bluetooth catalog, then send the normal remote ota ls, ota pull, and ota install commands. The destination still checks container geometry, hardware identity, hashes, signature policy, and the OTAFIX bootloader before installation.

"},{"location":"companion_radio_full/#serve-mota-images-manually","title":"Serve mOTA images manually","text":"

First put the destination, required relays, controller, and source on the same bounded TempRadio tuple. The example frequency below is not legal everywhere; choose a legal tuple supported by every participating radio.

"},{"location":"companion_radio_full/#esp32-source","title":"ESP32 source","text":"

Use the local console to start TempRadio:

nc 192.168.1.50 5002\n
tempradio 909.950,250,5,5,120\nota status\n

Then start the dedicated TCP seeder:

motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v\n
"},{"location":"companion_radio_full/#nrf52-source","title":"nRF52 source","text":"

Use the USB terminal briefly to schedule TempRadio, then return to Binary mode and close the terminal:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM1\n
tempradio 909.950,250,5,5,120\n+++MESHCORE-TERM-STOP\n

After sending the stop token, exit picocom with Ctrl-A, Ctrl-X.

Start the serial seeder on that same port:

motatool serve --dir ./motas --serial /dev/ttyACM1 --baud 115200 -v\n

motatool switches the port into mOTA mode automatically. Stop it with Ctrl-C to detach the folder. Reopen the terminal and use normalradio if the source should return early; otherwise the saved radio settings return when the bounded window expires.

As a cable-free alternative, keep the normal Companion BLE session open and run the Bluetooth reference client shown in the nRF52 Bluetooth section. Do not run the USB seeder at the same time.

Both platforms intentionally refuse firmware installation commands such as:

ota pull <id> flash\nota install\nota dev ...\n
"},{"location":"companion_radio_full/#script-a-complete-update","title":"Script a complete update","text":"

The Bash and PowerShell wrappers accept a release ZIP or ready .mota, set up TempRadio, run motatool, monitor the exact image, install it on the destination, and restore the radio path. Use a separate Companion as the controller.

For an ESP32 full source:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-tcp 192.168.1.50:5002\n

For an nRF52 full source, the script automatically detects the token-switched terminal and uses the same source port sequentially for control and seeding:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n
$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof Node' `\n  --controller-serial COM7 `\n  --source-serial COM8\n

See the start-to-finish LoRa OTA guide for package selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior.

"},{"location":"companion_usb_ascii_validation/","title":"Companion usb ascii validation","text":"

View this page on MeshCore Docs.

"},{"location":"companion_usb_ascii_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"companion_usb_client_validation/","title":"Companion usb client validation","text":"

View this page on MeshCore Docs.

"},{"location":"companion_usb_client_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"docs/","title":"Docs","text":"

View this page on MeshCore Docs.

"},{"location":"docs/#local-documentation","title":"Local Documentation","text":"

This document explains how to build and view the MeshCore documentation locally.

"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n
"},{"location":"double_frequency_v4_validation/","title":"Double frequency v4 validation","text":"

View this page on MeshCore Docs.

"},{"location":"double_frequency_v4_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"esp32_memory_budget/","title":"Esp32 memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_memory_budget/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"esp32_partition_catalog/","title":"Esp32 partition catalog","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_partition_catalog/#esp32-release-partition-lookup-table","title":"ESP32 release partition lookup table","text":"

The canonical LUT is firmware/esp32_partition_catalog.json in this fork. scripts/precompute_esp32_partitions.py regenerates it from GitHub release assets. MeshCore Open bundles an identical copy for offline phone updates. There is no need to run a command on the phone or download old firmware there.

"},{"location":"esp32_partition_catalog/#smallest-partition-by-board","title":"Smallest partition by board","text":"

The table shows each board's smallest known application partition across all cataloged releases, roles, and profiles. It does not count smaller NVS or filesystem partitions. Sizes use MiB (1,048,576 bytes), with exact bytes below. Mixed layouts means some releases have dual-slot OTA and others do not. These are historical factory-image minimums, not measurements of your device; use get storage.layout to check its actual installed layout.

Board Open JSON Download JSON

Loading catalog JSON...

BoardSmallest app partitionOTA layouts Show JSON details JSON view Snapshot and partition layouts Release inventory Complete catalog (large)
Expand this section to view the JSON.

With JavaScript disabled, use the complete JSON file.

"},{"location":"esp32_partition_catalog/#coverage","title":"Coverage","text":"

The initial snapshot enumerates every published release (including prereleases, excluding drafts) in these repositories, without a recent-release cutoff:

Repository Releases Inspected ESP32 factory images meshcore-dev/MeshCore 94 1,998 IoTThinks/EasySkyMesh (PowerSaving) 14 336 mikecarper/MeshCore (including keymindCascade releases) 69 7,808

All 10,142 standalone -merged.bin / -cleanInstall.bin images were parsed, with nine distinct partition tables and no unresolved selected assets. PowerSaving release tags can contain several firmware patch versions; the LUT uses each asset's version, not merely its containing release tag.

Eleven releases have no standalone ESP32 factory image. These include nRF52 update chains, RP2040/nRF52-only releases, an empty release, checksum-only metadata, and the ZIP-only Station G3 KISS modem package. They remain in the release inventory with an explicit note, but supply no inferred ESP32 layout. Unpublished builds, custom partition tables, and partition tables inside ZIP archives are not covered. This is an inventory of published factory layouts, not a claim that every Git revision has a known layout.

"},{"location":"esp32_partition_catalog/#matching-and-safety","title":"Matching and safety","text":"
  1. Ask the device for get storage.layout internally. Complete, valid OTA slot measurements take precedence over the LUT. A truncated reply is not proof that a slot is absent.
  2. Otherwise match board, role, and complete firmware version. Prefer the exact build hash when reported and present. Preserve profile candidates; do not conflate PowerSaving, upstream, and fork version suffixes or V4/R8 boards.
  3. Compare the chosen application length against both OTA slots (use the smaller slot so the next update also fits). If multiple profiles disagree, only make a fit/expansion recommendation when all candidates agree.
  4. An oversized image needs a supported exact-board two-stage migration bundle. A single-app layout cannot accept the bridge via normal dual-slot OTA and needs cable installation. Unknown layouts remain explicitly unknown.

Version-based results are estimates. OTA usually replaces only the app, so a newer version can run on an older partition table. Never use this LUT alone to authorize a partition-table write. The migration bridge must validate the actual flash geometry and identity recovery on the device before the final application upload. See two-stage migration.

The phone blocks known oversized regular uploads and opens its two-step migration section. Companion updates use the same capacity guard, but the Companion screen does not yet perform partition migration; it offers a smaller exact-board image or cable migration when necessary.

"},{"location":"esp32_partition_catalog/#schema-1","title":"Schema 1","text":"

The generator reads at most the first 36 KiB of each image, including partition tables at merged-file offset 0x8000 or 0x7000. It validates partition bounds, overlaps, duplicate app subtypes and the table MD5 when supplied. GitHub asset digests are recorded as provenance; a prefix read does not verify the full asset SHA-256. This LUT is not a substitute for firmware download verification.

"},{"location":"esp32_partition_catalog/#refresh-and-tests","title":"Refresh and tests","text":"

Use an authenticated gh CLI and Python 3. The generator has bounded downloads, retries, and a disposable prefix cache keyed by asset identity/update metadata. No secrets or firmware binaries are committed.

python3 scripts/precompute_esp32_partitions.py \\\n  --output firmware/esp32_partition_catalog.json \\\n  --cache-dir /tmp/meshcore-partition-catalog-cache \\\n  --copy-to ../meshcore-open-android-5.1.1/assets/firmware/esp32_partition_catalog.json\npython3 test/test_partition_catalog.py\n

--cached-inventory deliberately reuses the saved release listing for a resumable, reproducible snapshot. Omit it to discover newly published assets. Review coverage counts and unresolved after each refresh before shipping the LUT. No scheduled refresh job or release upload is installed by this script.

"},{"location":"esp32_wifi_partition_migration/","title":"Esp32 wifi partition migration","text":"

View this page on MeshCore Docs.

"},{"location":"esp32_wifi_partition_migration/#esp32-legacy-partition-migration-over-usb-wi-fi-or-lora","title":"ESP32 legacy-partition migration over USB, Wi-Fi or LoRa","text":"

The partition-migration bridge lets a supported ESP32 node move from a small dual-OTA layout to a verified larger dual-OTA layout without cable flashing or a full-chip erase. It fits a legacy 1.25 MiB slot. The Wi-Fi version exposes an updater for the Full image after migration. The older identity-only LoRa handoff preserves a verified old OTA receiver so it can fetch the Full image into the expanded inactive slot. On 8/16 MiB flash that bridge restores identity before handing off; its 4 MiB route keeps identity staged until the Full image's first boot. Configuration-preserving Partition Expanders instead receive the Full image themselves after restoring identity and supported settings.

The distinct USB version performs verified identity/configuration restoration before the host writes the final app. It keeps application USB CDC disabled during flash operations and prevents Arduino startup from automatically erasing damaged/full NVS. S3/C3 return to ROM download automatically once restoration settles; original ESP32 reports matching-MAC settled UART status before the host uses DTR/RTS. Neither route needs physical BOOT access, but classic boards require their working USB-UART automatic-reset circuit.

The host must verify the same physical USB device and original chip MAC, the exact target table, CRC-valid NVS restored/no-pending markers, and current OTA metadata before flashing the unchanged final app. USB reappearance, a timer, or settled status alone is not recovery proof. An interrupted migration retains its pending host record and device-side restoration markers for a safe retry.

It has explicit, byte-for-byte generated target plans for the 4 MiB MeshCore dual-OTA Full table and Arduino ESP32 default_8MB.csv and default_16MB.csv. It does not guess a table for other flash sizes. Adding a flash size later means adding one reviewed target plan and its generated table-prefix test; the bridge core is otherwise shared.

Flash size Target table app0 / app1 size Typical board 4 MiB variants/dual_ota_full_4MB.csv 0x1F0000 / 0x1F0000 ThinkNode M2/M5, LilyGo T3S3, Nibble, Heltec CT62, Waveshare LR1121 observers 8 MiB default_8MB.csv 0x330000 / 0x330000 Seeed XIAO ESP32-S3, Heltec V3 16 MiB default_16MB.csv 0x640000 / 0x640000 Heltec V4 / V4.3, Station G2

These are the supported migration plans. The 8/16 MiB tables retain more filesystem storage and a coredump partition; their application slots are not the absolute maximum permitted by the physical flash capacity.

"},{"location":"esp32_wifi_partition_migration/#bridge-wi-fi-credentials","title":"Bridge Wi-Fi credentials","text":"

Join MeshCore-Migrate using the admin password saved on the old node. This applies to the Wi-Fi bridge and the recovery AP of LoRa bridges and Partition Expanders. The firmware stores at most 15 password characters; the bridge can use a saved password of 8 to 15 printable ASCII characters for WPA2.

If the saved password is shorter than 8 characters, contains other characters, or cannot be read, the bridge uses the published fallback meshcore-migrate. Missing saved preferences, including a node still using an unsaved built-in default, also cause fallback. Anyone nearby who knows that published password can then join and upload firmware when the updater is enabled. A saved default or other public admin password is also usable, so choose a strong, nonpublic password before migration. On the old node, use password <new password> and verify that it remains valid after a normal reboot before uploading the bridge.

The Wi-Fi bridge and configuration-preserving Partition Expanders restore the saved password before starting their AP in the expanded layout. The older identity-only LoRa handoff does not restore saved settings, so its Wi-Fi recovery AP after the table change normally uses the fallback. Follow the exact route in the package README. If a device remembers MeshCore-Migrate from an earlier bridge, forget that saved network or update its password before joining. Bridge packages built before this credential change use meshcore-migrate.

"},{"location":"esp32_wifi_partition_migration/#ordinary-full-releases-and-installation","title":"Ordinary Full releases and installation","text":"

Ordinary ESP32 node releases use Full for each exact board and firmware role. The existing RAM and application-size guards still apply: a failed Full qualification fails that release target rather than publishing a smaller ordinary image. Direct --standard builds and reduced identities remain compatibility/development tools outside the ordinary release selection.

An app-only update cannot change the installed partition table. If a device already has the required layout, use the matching Full application through a supported updater. If its layout needs expansion, use a verified migration ZIP for its exact entry in --list-boards and an updater supported by the old image. Otherwise, install the exact board/role's Full -merged.bin through USB/serial using that board's flashing procedure; the merged image contains the bootloader, partition table, and application. Never upload a merged image through an app-only browser or LoRa updater.

The new Full defaults outside the migration catalog, including ESP32-C6 boards and other boards introduced with min_spiffs.csv, have no reviewed wireless expansion package in this recipe. If their Full app fits the unchanged live layout and update identity, an ordinary app update can suffice. A table change requires USB/serial until a matching migration package is reviewed. A Full image fitting physical flash does not by itself prove it can be installed through an old updater. Full Companion images with a single app slot likewise need USB/serial for local firmware replacement; their host-backed LoRa sending role does not provide a second local update slot.

"},{"location":"esp32_wifi_partition_migration/#waveshare-esp32-s3-lr1121-observer-migration","title":"Waveshare ESP32-S3 LR1121 observer migration","text":"

Only the Waveshare repeater observer and room-server observer select the larger 4 MiB table. Ordinary repeater, room, Companion and KISS recipes retain min_spiffs.csv. The two exact migration selections are:

sh scripts/build_esp32_partition_migration.sh \\\n  --board waveshare-s3-lr1121-observer-repeater \\\n  --board waveshare-s3-lr1121-observer-room-server \\\n  --version v1.18.0-test --radio-preset usa-cascadia --profile cascade --dry-run\n

Remove --dry-run only to build and verify the packages from a clean commit; this command does not flash hardware. Each observer role has its own exact-target Partition Expander. Both its Wi-Fi application and first-stage LoRa package use that same configuration-preserving bridge, not the older identity-only LoRa handoff. It accepts a reviewed source layout, stages and verifies identity and supported configuration before destructive writes, and refuses unknown layouts or saved data that cannot be preserved safely. The final table has two 1984 KiB application slots and only 64 KiB SPIFFS.

The bridge can migrate from either existing OTA slot. After reboot it must verify the new table, private key and saved configuration before enabling the LoRa receiver. It uses the saved primary radio profile and fetches only the exact observer role's Full image. The node name, ACL, radio 1/radio 2 and saved GPS preferences remain part of the configuration handoff; temporary sessions are not converted into permanent preferences. Already-expanded boards use the existing verified-image/no-handoff safety checks rather than migrating again.

The recipes reuse the real LR1121 board implementation, including its 1.8 V TCXO and HP-PA/RF-switch wiring. The first-stage image must pass the legacy 1.25 MiB size limit and both final-image and mOTA staging guards. A failed guard must fail the package, not produce a reduced image. These are build/host-test contracts, not physical qualification: test both A/B placements, restored settings and interruptions on a sacrificial Waveshare before remote deployment. The ESP32's single partition-table-sector erase/write is still not atomic; power loss in that window can require cable recovery.

"},{"location":"esp32_wifi_partition_migration/#repeatable-esp32-build-recipe","title":"Repeatable ESP32 build recipe","text":"

From a clean commit, run the shell menu in WSL/Linux:

sh scripts/build_esp32_partition_migration.sh\n

Choose one exact board/role, or all listed board/role pairs. The menu also offers all repeaters, all room servers, or all sensors separately. It asks for the firmware version, radio preset, and profile, then builds Full application images first, the available bridge(s) for each selected board, and verifies each ZIP. Only one PlatformIO process runs at a time; build.sh can clean the shared .pio/build tree between Full targets. The same menu can be run non-interactively:

sh scripts/build_esp32_partition_migration.sh --board heltec-v4 \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\nsh scripts/build_esp32_partition_migration.sh --all \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\nsh scripts/build_esp32_partition_migration.sh --role sensor \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

--all creates one verified ZIP per listed board/role pair and a release ZIP containing all of them, their hashes, and a manifest. This is a local release bundle, not an upload to GitHub Releases. It is not an archive of old firmware binaries. --list-boards shows the configured choices. The release fails instead of publishing a partial bundle if one board package is absent or fails verification.

The underlying Python recipe remains available:

python3 -B scripts/build_esp32_partition_migration.py \\\n  --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

The default builds all configured board/role recipes. Add --board heltec-v4 or --board xiao-s3-wio to build one; repeat --board for a selected set. Use --dry-run to inspect the command order without building. The resulting ZIPs appear under .releases/esp32-expanded-<commit>/packages/. The recipe requires a clean checkout so each ZIP identifies the firmware commit it was built from. It creates files only; it does not flash a device or erase flash.

"},{"location":"esp32_wifi_partition_migration/#independent-usb-firmware-set","title":"Independent USB firmware set","text":"

The USB intermediate firmware has six distinct chip/flash-capacity builds:

Chip Flash capacities USB completion Original ESP32 4, 8 MiB Settled UART status, then host DTR/RTS ESP32-C3 4 MiB Automatic ROM return ESP32-S3 4, 8, 16 MiB Automatic ROM return

To build only this complete set, without rebuilding or relabeling any Full firmware release, run serially in WSL/Linux:

python3 -B scripts/build_esp32_partition_migration.py --usb-only\n

This creates meshcore-usb-partition-bridges.zip under .releases/esp32-usb-bridges-<commit>/. Its manifest records each binary's chip, capacity, bytes and SHA-256, plus the utility source commit, dirty-tree state and runtime/configuration source hashes. All six are mandatory, and both the USB migration and NVS no-autoerase guard markers are verified. Existing Wi-Fi and LoRa artifacts remain unchanged. Exact new board/role migration packages also include their matching usb-bridge.bin.

A host can pair this standalone set with an older, exact Full migration ZIP; the Full image retains its original release/source identity. Meshfirmware downloads a versioned, checksum-pinned published USB set when a qualified older package has no USB bridge. It does not use an untested latest bridge, and an ordinary update that fits does not download migration utilities. For an offline or explicitly selected set, use MESHFIRMWARE_USB_MIGRATION_SET, or place the ZIP beside the flasher at tools/meshcore-usb-partition-bridges.zip. The original Full package must still match the selected final image and prove the target layout. No app-only upload or manual partition-table write substitutes for that proof.

The catalog covers canonical ESP32 repeater, room-server, and sensor roles on 4, 8, and 16 MiB chip-family plans, including historical 1.25 MiB candidates now built with expanded tables. This includes ESP32-S3, original ESP32, and ESP32-C3 boards with an evidenced old default layout. --list-boards is the authoritative list of exact targets; specialty ESP-NOW/observer aliases are separate identities and are not packaged under their old IDs. With Wi-Fi OTA they can migrate to the same physical board's canonical role image. The Wi-Fi bridge verifies and restores its supported saved settings; other settings may need to be recreated. The LoRa route still requires an exact old target ID. The catalog omits boards with no evidenced legacy default-layout build, such as boards introduced with min_spiffs.csv or another non-1.25 MiB layout. Some listed boards use larger slots today but had historical default-layout builds. A listed package is usable only when the installed old image supports the selected updater and the bridge verifies its live source table. Menu inclusion does not override either check.

Every listed board/role has Wi-Fi and LoRa bridge recipes. Heltec V4 and XIAO S3 WIO repeaters retain their board-specific bridge names; the other entries use matching original-ESP32, ESP32-S3, or ESP32-C3 chip-family bridges. The older identity-only LoRa handoff requires a verified same-target old receiver, and its 4 MiB route requires the slot-B-only sequence below. Configuration-preserving Partition Expanders use the route described in their package README instead. A 4 MiB Full build offers 1,984 KiB per slot. The packager verifies both the Full application's slot fit and its mOTA staging overhead. It supports at most 4096 blocks of 2 KiB for the Full transfer, and records the seeder proof-scratch requirement in the manifest. Older 4 KiB-scratch seeders are limited to 1024 blocks. A package fails if its required Full LoRa transfer cannot fit the supported staging/block limits.

Other ESP32 boards need a reviewed source updater, chip/flash-size bridge, verified Full image, and board entry before they can be added. The LoRa route also needs an exact old OTA target ID and a non-overlapping receiver handoff. Matching flash capacity alone is not enough to claim a package is safe.

"},{"location":"esp32_wifi_partition_migration/#what-the-bridge-preserves","title":"What the bridge preserves","text":"

Before it touches the partition table, the bridge reads the historical /identity/_main.id SPIFFS file. It requires all 96 bytes (32-byte public key followed by its 64-byte private key), stages them in an NVS namespace that is unchanged by the source and target layouts, then changes the table. The Wi-Fi bridge and 8/16 MiB LoRa bridge restore the staged value into expanded SPIFFS, read it back byte-for-byte, and only then clear the NVS staging record. On the 4 MiB identity-only LoRa handoff, the Full application's first boot performs this restoration.

The Wi-Fi bridge also stages and verifies its supported saved configuration: node name and preferences, radio profiles, ACL and login replay state, region keys, and selected network/OTA, filtering, display, and telemetry settings. It refuses migration if the saved files cannot fit or verify in NVS. The older LoRa handoff route preserves only the private identity, so saved settings may reset. The board/role-specific Partition Expander routes preserve their supported configuration and automatically fetch the exact-target Full image; follow the package README for those routes.

The old SPIFFS image is deliberately not raw-copied into an expanded partition: SPIFFS is not safe to resize that way. Files outside the supported preservation set, including logs and temporary radio sessions, are not preserved.

Migration does erase the 4 KiB partition-table sector and the destination app sectors. Expanded SPIFFS can be reformatted. It never issues a full-chip erase.

"},{"location":"esp32_wifi_partition_migration/#eligible-source-layout","title":"Eligible source layout","text":"

The bridge only accepts a source table that has all of the following:

This means the source app-slot and SPIFFS sizes can vary; the bridge validates the live geometry instead of carrying board-name rules. It also ensures it is running from one source OTA slot. If it arrived in B, it CRC-copies itself to the future A address, selects A in OTA metadata, writes the table, and restarts. The Wi-Fi route therefore works whether the browser OTA updater placed the bridge in legacy A or B. Single-app source layouts cannot use this route.

Keep power stable while the bridge is replacing the 4 KiB table sector. A loss of power before that point leaves the old table and source intact. After that small critical write, the selected A copy of the bridge is the recovery path. The stock ESP32 bootloader reads one partition-table sector; rewriting that sector is not atomic. A power loss during its erase/write window can leave an unreadable table and require cable recovery. This recipe must not be called power-loss-proof on a device whose USB/serial flash port is inaccessible.

"},{"location":"esp32_wifi_partition_migration/#migrate-a-seeed-xiao-esp32-s3","title":"Migrate a Seeed XIAO ESP32-S3","text":"

Use --board xiao-s3-wio with the recipe above. Its Wi-Fi bridge is a legacy-slot application image; its Full image keeps the old repeater's exact LoRa OTA target ID. Upload wifi-bridge.bin from the ZIP through the legacy node's existing Wi-Fi browser updater. Then join MeshCore-Migrate using the node's saved admin password, following the shared credential and fallback rules, and wait for Expanded layout ready. Upload full-application.bin at /update. Do not upload an -merged.bin: merged images include bootloader and table offsets for cable flashing, not browser OTA.

xiao_s3_partition_legacy_seed is a disposable-hardware test image only. It models the legacy table and an identity file; it is not a deployable repeater.

"},{"location":"esp32_wifi_partition_migration/#lora-only-migration-of-a-listed-role","title":"LoRa-only migration of a listed role","text":"

This section describes the older identity-only LoRa handoff. A package whose LoRa first stage is a configuration-preserving Partition Expander, including the Waveshare observer packages, uses its package README's self-hosted route.

The exact old board/role must already support MeshCore LoRa mOTA and have a valid EndF image identity. The LoRa bridge checks the old app's target ID and body hash before any partition-table write; an unsupported or damaged receiver is refused. Keep a capable seeder on the old receiver's compiled default radio profile before starting: this route may reset saved radio settings. Verify that the seeder lists the Full image and has the manifest's required proof scratch before installing the first-stage bridge.

On 8/16 MiB flash the bridge can run from either legacy A or B. It copies legacy B to the future B address, boots the bridge once to restore the private key in expanded SPIFFS, then boots the preserved old receiver. That receiver can fetch the Full image into its now-expanded inactive slot.

On 4 MiB flash the expanded app1 overlaps old app1, so that copy cannot be used. A verified old same-target receiver must remain in old A and the bridge must be installed in old B; a bridge in A refuses migration. If the old node is running B, first install and boot a working same-target old receiver in A, then install the bridge in B. After expansion the old receiver in A may use a temporary identity. The original key stays staged in NVS until the Full image restores it on first boot. Complete the second update before treating that device as the migrated node.

The board-specific LoRa bridges are heltec_v4_partition_migrator_lora_repeater and xiao_s3_partition_migrator_lora_repeater; the other listed roles use their configured chip-family LoRa bridges. Build the final application with bash build.sh build-firmware <target> --full-exact so its mOTA target ID still matches the old role. The repeatable recipe already uses this selector. scripts/package_esp32_partition_migration.py checks the images, partition tables, hashes and mOTA containers and creates one Wi-Fi/LoRa migration ZIP for each selected exact board/role. The ZIP README gives the exact ota ls, ota pull <id> flash, ota install sequence.

An old image without LoRa mOTA cannot use the LoRa-only route. Use its working Wi-Fi updater with a listed package, or USB/serial if no supported updater or package is available.

"},{"location":"esp32_wifi_partition_migration/#heltec-v4-v43","title":"Heltec V4 / V4.3","text":"

Build heltec_v4_partition_migrator, upload it through the legacy Wi-Fi updater, then upload the Full V4 app-only image after the bridge reports ready. The 16 MiB target table creates two 6.25 MiB OTA slots. See the V4 build notes for its build command.

"},{"location":"espnow_bridge_setup/","title":"Espnow bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"espnow_bridge_setup/#esp-now-bridge-simple-setup","title":"ESP-NOW bridge: simple setup","text":"

Use two ESP32 LoRa boards to join two MeshCore networks with different radio settings. Each board repeats on its own LoRa network, and the boards exchange packets directly over 2.4 GHz ESP-NOW. No router, internet connection, or manual peer pairing is needed.

LoRa network A <--> Repeater A <-- ESP-NOW --> Repeater B <--> LoRa network B\n
"},{"location":"espnow_bridge_setup/#1-choose-the-firmware","title":"1. Choose the firmware","text":"

Install the exact board's Full repeater image on each board. ESP-NOW is included as a runtime transport on supported ESP32 Full repeaters. Newly combined ordinary repeaters leave it off until explicitly enabled; that choice is saved across reboot. Boards with measured capacity exceptions keep separate _repeater_bridge_espnow images. Historical dedicated target names remain available for direct builds with their original bridge-on defaults. Full Companion is a different role. See the firmware picker guide.

Open each board's USB console at 115200 baud and run:

get bridge.type\n

The answer can be espnow, mqtt+espnow, rs232+espnow, or mqtt+rs232+espnow, depending on the image. Confirm the independent ESP-NOW control is present with get espnow.enabled and get espnow.running. The espnow commands select this transport without changing another bridge in a combined image.

"},{"location":"espnow_bridge_setup/#2-set-each-boards-lora-network","title":"2. Set each board's LoRa network","text":"

Configure one board at a time over USB. Replace the placeholders below with the radio settings used by the network on that board's side. Use Bridge-A for the first board and Bridge-B for the second, or choose your own names.

set espnow.enabled off\nset name Bridge-A\nset radio <freq>,<bw>,<sf>,<cr>\nreboot\n

Do not type the angle-bracket placeholders literally.

Radio option Meaning freq LoRa frequency in MHz. Match the local network on this side. bw LoRa bandwidth in kHz. Match that network's bandwidth. sf Spreading factor, 5-12. Match that network's setting. cr Coding-rate value, 5-8. Match that network's setting. name A label to tell the two repeaters apart. It does not pair them.

set radio requires a reboot to apply. After reconnecting, get radio shows the saved settings. Use permanent radio settings for this setup; tempradio sessions expire.

"},{"location":"espnow_bridge_setup/#3-configure-the-esp-now-link-on-both-boards","title":"3. Configure the ESP-NOW link on both boards","text":"

Run this block on both boards. Replace YourSharedKey with the same case-sensitive secret on each board, using 1-15 characters. Channel 6 is an example; both boards must use the same channel.

set espnow.enabled off\nset bridge.channel 6\nset bridge.format wrapped\nset bridge.secret YourSharedKey\nset bridge.source tx\nset bridge.delay 500\nset repeat on\nset espnow.enabled on\n

These settings are saved across reboot. Bridge changes apply immediately; changing an enabled bridge's channel, format, or secret restarts the bridge. Stopping it first lets you finish configuring both sides before connecting them.

"},{"location":"espnow_bridge_setup/#what-each-bridge-option-does","title":"What each bridge option does","text":"Option Choices and purpose espnow.enabled on starts ESP-NOW; off stops it and keeps its saved settings. Independent of MQTT or RS-232. rs232.enabled On combined UART images, starts or stops RS-232 independently of MQTT and ESP-NOW. Use get rs232.running to check the live UART. bridge.channel The 2.4 GHz channel, 1-13. Match it on both boards. This is separate from the LoRa frequency in set radio. bridge.format wrapped is the default for this two-repeater setup and uses the shared secret. raw connects to primary-ESP-NOW nodes such as Generic_ESPNOW or SenseCapIndicator-ESPNow; it ignores the secret. Both peers must use compatible formats. bridge.secret A matching, case-sensitive value of 1-15 characters for wrapped mode. It separates bridge groups; it is not strong encryption. MeshCore's own message encryption remains in place. bridge.source tx crosses packets this repeater transmits on LoRa, after its forwarding decisions; use this for the setup above. rx crosses received packets before those decisions and can pass traffic the local repeater would not forward. bridge.delay Wait before processing a packet received from the other bridge, in milliseconds. Range: 0-10000. Start with the default 500. repeat on enables normal LoRa forwarding; off disables forwarding. Keep it on on both bridge repeaters. Normal routing, scope, and packet-filter rules still apply. bridge.type Read-only firmware capability, including combined transports. espnow.running Read-only ESP-NOW runtime state. on means the local bridge started; it does not prove the other board is reachable.

This fork uses set bridge.source tx or rx. Some upstream guides use logTx and logRx; those are not the command values used here.

For a LoRa gateway connected to primary-ESP-NOW Companions instead of another LoRa repeater, see raw ESP-NOW compatibility.

"},{"location":"espnow_bridge_setup/#4-check-the-link","title":"4. Check the link","text":"

Run on both boards:

get espnow.enabled\nget espnow.running\nget espnow.status\nget bridge.channel\nget bridge.format\nget bridge.source\nget bridge.delay\nget repeat\nget radio\n

For the example above, expect enabled and running on, matching channel and format, source tx, delay 500, repeat on, and the intended LoRa settings on each side. An existing bridge.source rx setup is also valid; diagnose the selected source rather than changing it just to match this example. Compare the shared secret privately; omit get bridge.secret from logs you share. get espnow.status distinguishes the configured channel from the actual 2.4 GHz channel. Its last error stage/code is historical and may remain after recovery. A running bridge is not proof that the other board received anything.

Send a message from a Companion on network A to one on network B, then reply from B to A. Check both directions; configuration readback alone does not test delivery.

If it does not work:

If those saved settings already match, use the bounded test below before changing them again.

"},{"location":"espnow_bridge_setup/#test-the-24-ghz-path-without-lora-forwarding","title":"Test the 2.4 GHz path without LoRa forwarding","text":"

Updated ESP-NOW bridge firmware includes runtime diagnostics in the ordinary image. No BRIDGE_DEBUG build flag or continuous USB logging is needed. Use the USB console so a WiFi change cannot disconnect your diagnostic session.

On both boards, start a fresh observation window:

espnow resetstats\nget espnow.status\nget espnow.stats test\n

On A, send one probe:

espnow test A1B2C3D4\n

OK - test queued id=A1B2C3D4; confirm on peer means queued locally. On B, run get espnow.stats test: rx must increase and rxid must be A1B2C3D4. Then send espnow test B1C2D3E4 on B and confirm that ID on A. The two commands are two one-way tests; there is no automatic reply or retry. Sender completion alone does not prove peer receipt. The optional ID is exactly eight hexadecimal digits, without 0x; omitting it uses the current uptime in milliseconds as a correlation ID.

These small, unencrypted CRC-checked probes test transport reachability only. They do not validate packet-format compatibility or the wrapped shared secret, authenticate the peer or prove MeshCore packet delivery. They are never injected into the mesh or forwarded onto LoRa. Older firmware ignores them and cannot report the received ID; use an updated peer for confirmation. Sending a probe and resetting statistics require a local connection; neither changes saved preferences.

"},{"location":"espnow_bridge_setup/#locate-a-mesh-forwarding-failure","title":"Locate a mesh forwarding failure","text":"

If both probe directions work, send one fresh identifiable message from a Companion on A's LoRa network and read these pages on both bridge boards:

get espnow.stats\nget espnow.stats rx\nget espnow.stats tx\nget espnow.stats drops\n

Repeat in the opposite direction with a new message. Counters accumulate until espnow resetstats or reboot and survive an ESP-NOW stop/start. Inspect changes across the one message, rather than assuming old totals describe the current attempt. Stopping or restarting clears pending queue work without classifying delivery; the TX drop counter records local rejection/final-failure decisions, not every undelivered packet.

An absent counter change narrows a stage; these checks do not assume a cause for a particular pair of boards.

If both bridge boards also hear the same message directly on LoRa, normal duplicate suppression can discard its later bridged copy. That alone does not show a broken ESP-NOW link. This is a possibility to check with the counters, not an established cause of a reported delivery failure.

"},{"location":"espnow_bridge_setup/#optional-short-usb-debug-capture","title":"Optional short USB debug capture","text":"

For a brief observation window, run get usb.logging and get usb.debug and record both original settings. Enable both gates with set usb.logging on and set usb.debug on. The bridge emits bounded ESPNOW: status/counter updates from its main loop, at most once per second when diagnostic state changes. No BRIDGE_DEBUG build flag is required, and WiFi callbacks do not print to USB. Send the two probes or one fresh message, then restore both original settings. The CLI counter pages and probes do not require either logging gate. Use the counter pages for exact values; a busy USB console can skip a debug update. Do not include secrets or WiFi credentials in a shared capture.

A running bridge keeps the device awake, even with device power saving enabled. ESP-NOW shares the WiFi radio: use the connected WiFi network's channel when WiFi is active. Browser OTA pauses ESP-NOW on ordinary non-MQTT repeaters, reports that pause, and resumes saved-enabled ESP-NOW when OTA is stopped. A failed OTA start restores the bridge. The RS-232 transport remains available on combined boards while ESP-NOW is paused.

For every command's full details, see the bridge CLI reference.

For a broader comparison with a Linux-based bridge, see the MeshCore Nexus bridging guide.

"},{"location":"espnow_bridge_setup/#keep-wardriving-traffic-off-the-bridge","title":"Keep wardriving traffic off the bridge","text":"

New generalized repeater filter tables include a third built-in rule that blocks authenticated #wardriving text and data in both bridge directions, at every hop count. The same row blocks crossover between radio profiles:

get flood.rule.3\nset flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\n

The same rule can be added with the shorter command:

set fr any c=#wardriving m=bc d\n

Both forms use the same table. m=b means bridge only, m=c means crossover only, and get fr.N returns a short command for copying a numbered rule.

The unnumbered command adds the rule safely on existing devices whose saved tables predate this default. It works with either bridge packet source and both ESP-NOW formats. Nearby same-profile LoRa forwarding retains the separate four-hop default. Use mode=bridge for a bridge-only block, or mode=cross for a crossover-only block. See the mode matrix and examples.

"},{"location":"faq/","title":"Faq","text":"

View this page on MeshCore Docs.

"},{"location":"faq/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

For this fork's 1.17.1.5 USA Cascade release, start with feature switches by role and the USB web console. For nRF52 OTAFIX use the exact board/storage build from OTAFIX 2.4.6; older bootloader links below provide upstream/background context.

A list of frequently-asked questions and answers for MeshCore

"},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"

A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)

MeshCore is free and open source:

Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.

These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.

Anyone is able to build anything they like on top of MeshCore without paying anything.

"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"

A: Everything you need for MeshCore is available at:

You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).

"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"

MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.

For an up-to-date list of supported devices, please go to https://flasher.meshcore.io

To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.

"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"

MeshCore firmware is organized by role and, for companions, by host transport. The main roles are Companion, Repeater, Room Server, and Sensor. The tree also contains specialized terminal-chat and KISS-modem builds. Availability depends on the board; use the flasher or that board's PlatformIO environments as the current source of truth.

"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"

Companion radios connect MeshCore client software to LoRa. Depending on the board and build profile, the companion protocol can be exposed over BLE, USB serial, Wi-Fi, or Ethernet. Some boards also provide a Full Companion profile with additional local administration features. Check the exact build name and transport before flashing. Pending messages are held in a volatile shared queue; capacity depends on the platform and memory profile. See the Companion offline message queue for the exact defaults and limitations. See https://meshcore.io and the web client at https://app.meshcore.nz.

"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"

Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.

A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"

A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.

Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.

A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

When a client logs into a room server, the client will receive the previously 32 unseen messages.

Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.

The recommendation is to run repeater and room server on separate devices for the best experience.

"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"

A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.

If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.

If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.

After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:

set freq {frequency}

The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands

If you have more supported devices, you can use your additional devices with the room server firmware.

"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"

A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.

The native Android and iOS client uses the freemium model and is developed by Liam Cottle.

The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.

"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"

A: Supported frequencies depend on the radio hardware and the rules for the country in which it operates. Common MeshCore hardware covers portions of the 433, 868, and 915 MHz ISM bands.

Use the current regional preset offered by the client or flasher, confirm it with the local MeshCore community, and comply with local frequency, bandwidth, duty-cycle, and power limits. Do not copy an old frequency from this FAQ. Narrow presets commonly use BW62.5 with a lower spreading factor; their exact frequency and SF are region-specific and can change as deployments coordinate.

If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.

"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"

A: An advert announces a node and its public information to the network.

MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".

MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:

set flood.advert.interval {hours}

The separate set advert.interval {minutes} command controls the local zero-hop advert timer.

"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"

A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.

"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"

A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.

Repeater or room server can be administered with one of the options below:

https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"

A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:

set lat <GPS Lat>

set lon <GPS Lon>

You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.

"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"

A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:

password {new-password}

"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"

A: The default guest password to a room server is hello. Use the following command to change the guest password:

set guest.password {guest-password}

"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"

A: You can issue these commands to get or set a repeater's private key using a USB serial connection.

get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console

Reboot the repeater after set prv.key <hex> command for the new private key to take effect.

"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"

A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen

Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.

Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.

"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"

A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.

set agc.reset.interval <number>

The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.

The reset is skipped while a packet is pending, being received, or being transmitted. When safe, RadioLibWrapper::resetAGC() warm-sleeps the radio, returns the wrapper to idle so receive mode is re-armed, reapplies the cached RX boost setting, and starts a fresh noise-floor calibration.

"},{"location":"faq/#371-q-can-firmware-recover-an-external-radio-whose-busy-pin-is-stuck-high","title":"3.7.1. Q: Can firmware recover an external radio whose BUSY pin is stuck high?","text":"

A: Current firmware first attempts a non-destructive RX/AGC re-arm and then uses a physical radio reset when the board exposes one. A normal RadioLib reinitialization is not reported as a hard reset unless firmware can actually pulse NRST or cycle the radio's power rail.

Most supported external-radio boards expose NRST directly. RAK3401 pulses its dedicated NRST while leaving the shared peripheral rail and SPI controller intact, and LilyGo T-Beam 1W can cycle the radio regulator. The generic Tenstar C3 target describes a user-wired radio and has no reset connection by default. For unattended recovery on that target, wire NRST to a free GPIO and override P_LORA_RESET with that GPIO in the build configuration.

The SenseCAP T1000-E LR1110 path was hardware-qualified with 50 successful radio-only resets: 25 in continuous receive and 25 with RX power saving active. The saved radio tuple, TX power, RX gain, and RX power-saving configuration were restored after every reset, bidirectional LoRa status traffic still passed, and the nRF52840 uptime remained continuous.

An integrated STM32WL radio has no independently resettable RF peripheral, so it intentionally remains on the bounded soft-recovery path. If its radio core cannot recover, resetting only the radio is electrically impossible; the MCU must be restarted.

"},{"location":"faq/#372-q-does-firmware-automatically-update-lr1110-transceiver-firmware","title":"3.7.2. Q: Does firmware automatically update LR1110 transceiver firmware?","text":"

A: No. A transceiver update erases and rewrites the LR1110 itself, so it is not performed as a side effect of installing ordinary device firmware. Use the hardware vendor's physical-access maintenance procedure and verify the exact LR1110 and bootloader versions before changing them.

Full remediation of Semtech SEM-PSA-2026-001 requires LR1110 BL2 0x1001 as well as the applicable transceiver firmware. It is a separate two-stage maintenance procedure and is not performed automatically by MeshCore.

"},{"location":"faq/#373-q-how-does-firmware-handle-lr2021-lowhigh-band-changes","title":"3.7.3. Q: How does firmware handle LR2021 low/high-band changes?","text":"

A: Firmware first selects the board's matching RF path, retunes the radio, and then reapplies output power so the LR2021 selects the correct low- or high-frequency PA. It also uses the matching RF-switch mode when RX or TX is launched. LR2021 high-band output power is limited to 12 dBm; a lower board-specific limit still takes precedence.

On Meshnology W12, the two external FEM supplies are mutually exclusive: GPIO4 powers the sub-GHz GC1109 path and GPIO3 powers the 2.4 GHz RFX2402E path. Both supplies are disabled before the selected one is enabled. Temporary radio settings preserve the saved TX power and safely clamp only the active band; returning to the saved settings restores its applicable power. A permanent frequency change clamps the saved TX power to the new band's limit.

The external SX1262, SX1268, LLCC68, LR1110, and LR2021 drivers also bound the post-SetTx BUSY wait. A failed PA ramp therefore returns an error and can enter the physical-reset recovery path instead of hanging the node indefinitely.

"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"

A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard

"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"

A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.

However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.

Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.

"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"

Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.

"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"

The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.

"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"

As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.

Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.

"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command path.hash.mode do on a repeater?","text":"

This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.

Usage: set path.hash.mode {0|1|2}:

+----------------+-----------------------+\n| path.hash.mode | Advert path hash size |\n+----------------+-----------------------+\n| 0              | 1 byte (default)      |\n+----------------+-----------------------+\n| 1              | 2 bytes               |\n+----------------+-----------------------+\n| 2              | 3 bytes               |\n+----------------+-----------------------+\n

It is safe to set your 1.14+ repeaters to mode 1 or 2.

"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"

A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.

"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"

You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.

"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"

A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users

"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"

A:

  1. Device off
  2. Connect USB cable to device
  3. Hold down trackball (keep holding)
  4. Turn on device
  5. Hear USB connection sound
  6. Release trackball
  7. T-Deck in DFU mode now
  8. At this point you can begin flashing using https://flasher.meshcore.io
"},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"

A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.

GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.

Source

"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"

A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.

"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"

A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.

"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"

A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==

There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0

The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72

Source

"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"

A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):

Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script

There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script

"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"

Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.

"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"

A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"

A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}

See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19

Value Payload 0x00 Request 0x01 Response 0x02 Plain text message 0x03 Acknowledgment 0x04 Node advertisement 0x05 Group text 0x06 Group datagram 0x07 Anonymous request 0x08 Returned path 0x09 Trace 0x0A Multipart 0x0B Control/discovery data 0x0C OTA-over-LoRa data 0x0D-0x0E Reserved 0x0F Raw custom data

See Packet Format and Payload Format for the maintained descriptions.

Source

"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"

A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:

"},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"

A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")

"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"

A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.

"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"

A:

BW is bandwidth - width of frequency spectrum that is used for transmission

SF is spreading factor - how much should the communication spread in time

CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate

TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.

Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.

The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.

There are 4 code rates used in LoRaWAN:

4/5 4/6 5/7 4/8

For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.

Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.

Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you're far from each other, you have to talk slow (SF10), but if you're close, you can talk faster (SF7)

So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project

"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"

A: Companion clients do not repeat by default. Supported builds can opt into bounded client repeating on permitted frequencies, and the emergency-channel path has a bounded delayed relay behavior. Dedicated repeaters remain the normal and recommended way to extend network coverage. Room servers can repeat when explicitly configured, but separating the roles is generally preferable.

"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"

A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.

In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.

"},{"location":"faq/#54-q-how-does-a-node-discover-and-reuse-a-path-to-its-destination","title":"5.4. Q: How does a node discover and reuse a path to its destination?","text":"

Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.

Source

"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"

A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.

Source

"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"

A: The smartphone app key is in hex:

8b3387e9c5cdea6ac9e5edbaa115cd72

T-Deck uses the same key but in base64:

izOH6cXN6mrJ5e26oRXNcg==

The third character is the capital letter O, not zero 0.

Source

"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"

A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.

Firmware repo: https://github.com/meshcore-dev/MeshCore

"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"

A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.

Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase

Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut

"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"

A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769

Build instructions for MeshCore:

For Windows, first install WSL and Python+pip via: https://plainenglish.io/python/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80

(Linux, Windows+WSL) In the terminal/shell:

sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n

Mac: python3 should be already installed.

Then it should be the same for all platforms:

python3 -m venv meshcore-venv\ncd meshcore-venv && source bin/activate\npip install -U platformio\ngit clone https://github.com/meshcore-dev/MeshCore.git\ncd MeshCore\n

Choose an exact environment listed by pio project config or in the target board's variants/*/platformio.ini. For example:

pio run -e RAK_4631_repeater\n

The output is under .pio/build/RAK_4631_repeater/. Set the radio parameters to your current regional preset after flashing; if changing build defaults, keep that local change out of commits unless it is intended for every user.

"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"

A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.

Web client: https://github.com/liamcottle/meshcore-web JavaScript: https://github.com/meshcore-dev/meshcore.js

"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"

A: This repository does not ship or document an official ATAK integration. An external integration must account for LoRa airtime, moving endpoints, stale direct paths, and the collision cost of frequent flood fallback. Optional, bounded Companion repeating does not turn every mobile client into a dedicated repeater and does not remove those capacity constraints. Check current community projects before designing a deployment.

Source

"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"

A:

To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map

To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.

You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.

"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"

A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.

Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions

For ESP-based devices (e.g. Heltec V3) you need:

  1. Download the firmware file from https://flasher.meshcore.io.
    • Select the exact board and role. Artifact names and release URLs include a changing version and commit, so copy the current download URL instead of using an example URL from a guide.
    • A non-merged application image is for an already compatible bootloader and partition table. A merged image also contains low-level flash data. Back up settings before either operation; do not assume a merged flash preserves configuration, pairing data, or a previous partition layout.
    • To download a copied URL from the shell, use curl -L '<copied-url>' -o <firmware>.bin.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
    • Run ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null before and after connecting the board. ESP devices can appear under either name.
  3. Install esptool in a virtual environment.
    • python3 -m venv .venv && . .venv/bin/activate
    • python -m pip install --upgrade esptool
  4. Flash the firmware.
    • For non-merged bin:
      • python -m esptool --port /dev/ttyUSB0 --chip auto write-flash 0x10000 <non-merged-firmware>.bin
    • For merged bin:
      • python -m esptool --port /dev/ttyUSB0 --chip auto write-flash 0x00000 <merged-firmware>.bin

Instructions for nRF devices:

For nRF devices (e.g. RAK, Heltec T114) you need the following:

  1. Download the firmware file from https://flasher.meshcore.io.
    • Select the exact board and role, and download its current ZIP package. Do not rename a package from another board or reuse a static versioned URL.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
    • Run ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null; nRF bootloaders commonly appear as /dev/ttyACM0, but the number can change.
  3. In a virtual environment, install adafruit-nrfutil:
    • python3 -m venv .venv && . .venv/bin/activate
    • python -m pip install --upgrade adafruit-nrfutil
  4. Flash the nRF device.
    • adafruit-nrfutil --verbose dfu serial --package <firmware-package>.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200

To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:

To start managing your USB serial-connected device using picocom, use the following command:

From here, reference repeater and room server command line commands in the MeshCore docs here:

"},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"

A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.

As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.

"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"

A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore

Both the Windows and Mac versions of the client app are fully unlocked and are free to use.

"},{"location":"faq/#516-q-where-can-i-learn-more-about-meshcore","title":"5.16. Q: Where can I learn more about MeshCore?","text":"

A: Start with the MeshCore documentation and QuickStart playlist. For this fork, see feature switches by role.

"},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"

A:

You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.

"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"

A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.

"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"

A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.

"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"

A: The default Bluetooth pairing code is 123456 on builds without a physical display. Display-equipped Companion builds generate a six-digit PIN at boot and show it when a client requests pairing. The display stays awake on the PIN page until Bluetooth connects or the two-minute pairing window expires.

"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"

A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.

"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52/... device seems to be corrupted, how do I wipe it clean to start fresh?","text":"

A: If you're able to connect to the device from your MeshCore app 1. Navigate to Gear icon(Settings) on the upper right corner 2. Click on Export Settings button, choose Select All and Confirm. This will save your Node configuration 3. Now choose Factory Reset option 4. Confirm that you want to reset your device 5. Go to Bluetooth System settings and remove pairing of the device, so you can connect again 6. Connect your device in the app and enter the PIN 7. Navigate to Gear icon(Settings) on the upper right corner 8. Click on Import Settings and choose the file from 2., click on Select All and Confirm 9. Find Reboot button on of the Settings list

B: You're not able to connect to the App 1. Connect USB cable to your device 2. Go to https://flasher.meshcore.io 3. Search for your device in the list 4. Choose Companion Bluetooth 5. Press Enter DFU mode button, choose your USB device 6. Press Erase Flash button, choose the device again and wait until it completes 7. Press Flash! button and choose the USB device last time 8. The device is erased and newest firmware is installed 9. You might need to remove the pairing in Bluetooth System Settings in order to re-pair the app again.

An erase application or ordinary application ZIP/UF2 does not repair a damaged or partially overwritten SoftDevice. This can happen after flashing an nRF52 image for a different board or SoftDevice layout. A characteristic failure is that the application can boot and read its filesystem, but reboots or hangs when it writes or formats internal flash. Repeating Erase Flash is not a recovery for that condition.

Recover it with a combined bootloader + SoftDevice package for the exact board (including XIAO Sense versus non-Sense), installed through serial/BLE DFU or SWD. With adafruit-nrfutil, the serial form is:

adafruit-nrfutil dfu serial -p <port> \\\n  -pkg <exact-board-bootloader-with-softdevice>.zip -sb -t 1200\n

Then install the exact-board MeshCore application. Do not use an application- only UF2, filesystem formatter, or a similarly named package from another nRF52 board as a substitute. See the confirmed XIAO recovery in issue #3284 and obtain the matching package from the OTAFIX releases.

Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io

"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"

A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:

NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.

Allow user access on your USB port:

sudo setfacl -m u:$USER:rw /dev/ttyUSB0

"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"

A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:

  1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update
  2. On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
  3. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges
  4. Go to the Command Line tab, type start ota and hit enter.
  5. You should see OK to confirm the repeater device is now in OTA mode
  6. Run the DFU app, then tap Settings in the top-right corner
  7. Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.
  8. Select the firmware zip file you downloaded
  9. Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again
  10. If the device is not found, enable Force Scanning in the DFU app
  11. Tap Upload to begin OTA update
  12. If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app.
  13. Wait for the update to complete. It can take a few minutes.
  14. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.
  15. A companion must be running a build that exposes start ota; consult the current flasher release notes rather than relying on a version threshold in this FAQ.
  16. Please see the MeshCore Blog for additional information on OTA firmware flashing:
    • https://blog.meshcore.io/2026/04/06/otafix-bootloader
    • https://blog.meshcore.io/2026/04/02/nrf-ota-update
"},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"

A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX

After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.

"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"

A: For ESP32-based devices (e.g. Heltec V3):

  1. On https://flasher.meshcore.io, download the current non-merged application image for the exact ESP32 board and role (no merged in the filename).
  2. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.
  3. Go to the Command Line tab, type start ota and hit enter.
  4. The reply gives the OTA upload URL, network to use, and HTTP login details.
  5. If the ESP32 is not already joined to Wi-Fi, start ota starts the MeshCore-OTA hotspot at 192.168.4.1. It uses the node's admin password for WPA2 when that password is 8\u201315 printable ASCII characters. Other accepted admin passwords leave the hotspot open; the HTTP login still applies. If the node is already joined, the command uses its router-assigned address.
  6. From your phone or computer, join MeshCore-OTA when one was started. Use the node's admin password if the WiFi connection asks for a password.
  7. Open the exact URL reported by start ota. Sign in with HTTP Basic username admin and the node's current admin password, then upload the non-merged application bin from the flasher. The URL normally uses port 80, or port 8080 on Full Companion builds with browser OTA support.

start ota automatically stops WebConfig and reports the change. Stop the OTA uploader before running start webconfig again. Temporary partition-migration and recovery applications retain their documented access policy. See the WiFi guide for the shared admin-password policy.

"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"

A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.

Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.

Currently, the following boards are supported:

"},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"

A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo

"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"

A:

Channel: meshcore://channel/add?name=<name>&secret=<secret>

Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>

Where &type is:

"},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"

A: Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If it has no saved Wi-Fi configuration, it starts a MeshCore-Setup-XXXX access point. The hotspot uses the node's admin password for WPA2 when it is 8\u201315 printable ASCII characters; standard builds initially use password. Other accepted admin passwords leave the hotspot open and require an HTTP admin login. Join it and browse to http://192.168.4.1/. Protected first-time setup continues directly to the wizard, which configures WiFi, node identity, radio, and the admin password. MQTT companion builds add an MQTT step and tab, while non-MQTT companions remove those controls. After saving, the page and device screen show the station IP assigned by your router.

If a saved network remains unreachable for two minutes, the setup AP starts as a fallback. Sign in with the existing admin password before changing settings on that recovery page. If the original network returns, the companion closes the recovery portal and resumes normal LAN operation.

The setup AP always uses 192.168.4.1; the companion's station IP is assigned by DHCP and can be different. A real WIFI_SSID and WIFI_PWD can still be supplied at compile time as initial defaults, but they are no longer required.

On the LAN, sign in to the WebUI at the companion's station IP with its saved admin password. ESP32 WiFi Companions share that password between WebConfig, protected setup WiFi, and runtime browser OTA; other Companion builds retain their existing no-admin-password behavior. Change it with password <new-password> on USB or another available administrative connection. An ESP32 WiFi Companion's password is stored in NVS and survives a filesystem erase; get password reports its availability without revealing it. If it reports an unavailable management password, set a new one over USB to repair it.

The Companion protocol on TCP port 5000 retains its trusted-network policy; the browser login does not protect that port. Use a trusted WiFi network. Router WiFi passwords, MQTT credentials, and Bluetooth PINs remain separate. See the WiFi guide for password-change and reconnect behavior.

ESP32 repeater and room-server builds use the same page, but keep it off by default. Run set webui on from the admin CLI, open the reported LAN URL, and sign in with the admin password; get webui reports its current address. Their MQTT observer variants expose the MQTT controls, while ordinary builds do not. The two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt builds retain CLI-only configuration so their flash can keep two app slots for LoRa OTA.

"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"

A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx

[WARN] WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.

Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"faq/#78-q-how-do-i-use-ethernet-with-a-rak4631","title":"7.8. Q: How do I use Ethernet with a RAK4631?","text":"

A: MeshCore supports Ethernet on RAK4631 boards using the RAK13800 WisBlock Ethernet module (based on the W5100S chip).

Hardware required: - RAK4631 WisBlock Core - RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) - RAK13800 WisBlock Ethernet module - Ethernet cable connected to a network with a DHCP server

Firmware: Flash one of the Ethernet-enabled firmware variants: - RAK_4631_repeater_ethernet - Repeater with Ethernet CLI access - RAK_4631_room_server_ethernet - Room server with Ethernet CLI access - RAK_4631_companion_radio_ethernet - Companion radio over Ethernet (replaces BLE)

Connecting: - The device obtains an IP address via DHCP automatically on boot. - For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. nc <ip> 23 or PuTTY in raw mode). This gives you the same CLI available over serial/USB. - For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). - Use the eth.status CLI command to check connection status and see the assigned IP address.

"},{"location":"filter_tool/","title":"Filter tool","text":"

View this page on MeshCore Docs.

"},{"location":"filter_tool/#filter-policy-playground","title":"Filter policy playground","text":"

Build a forwarding policy, see its readable definition, and simulate how a repeater handles a packet. Rules match received packet facts, then apply actions such as dropping, scoping, rate-limiting, or retrying a flood.

Everything runs locally in this browser. Channel keys, packet facts, and policy drafts are not uploaded anywhere.

Policy design preview

The phases and core conditions model current FPF7 behavior, including its forward rows, scope rewrites, and shared blacklist. The readable policy language, JSON, and Base64 bundle are still a prototype: current firmware is configured with set flood.* or set fr commands and cannot install a bundle from this page.

"},{"location":"filter_tool/#build-a-policy","title":"Build a policy","text":"

Start with an example or build a rule, then test the draft against packet facts in the simulator below. The examples draw from Flood Filtering and Moderation.

To match every 1-byte path, select Path hash width \u2192 1 byte, type \u2192 ANY, all hops, no channel/input-scope restriction, and no prefix condition (or use the 1-byte example below). This also matches zero-hop packets. The firmware command is set fr any pb=1 d (or set flood.rule type=any hashbytes=1 drop); it adds a forwarding-drop rule, including for relayed login/admin floods. Direct routes and local delivery remain unaffected. Width-only rules require updated firmware. The simulator's policy definitions remain design-preview text, not installable commands.

Select 2+ bytes (2 or 3) to match both larger widths in one rule. The firmware command is set fr any pb=2+ d. A rate-limit action on this rule uses one allowance shared by both widths, rather than a separate limit for each. Four-byte paths remain unsupported.

Remote login and direct routes

This policy only decides whether a relay retransmits a flood. Direct packets and local delivery are unaffected. Rules matching the login/admin family can still reduce multi-hop remote-login reach, so the analyzer flags them.

Example policies Choose one to load its full rules into the builder and draft.

Read each summary as when all conditions match, do the actions. A match alone does not stop forwarding.

Payload selectors type= valueMatches grp_dataOnly the named payload type; see every exact type below anyEvery payload type class:groupGRP_TXT and GRP_DATA class:loginREQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH class:otherEvery remaining payload type that is not group or login Other conditions and actions FieldMeaning hops=Received hop count: all, 3+, 2-6, or 3 channel=* means no channel condition; a name or key authenticates one group channel; hash:XX is an unauthenticated one-byte fallback rx.scope=Original incoming transport scope path=Path prefix, blacklist, bucket, or loop match hashbytes=Encoded path-hash width: 1, 2, 3, or 2+ (2 and 3 bytes), including zero-hop packets tempradio=Temporary-radio state do dropDo not retransmit do scope=Set the outgoing transport scope do rate=Apply a per-minute rate and burst do timing=Select fast, normal, or slow scheduling

There is no class:txt. Use type=grp_txt for channel text or type=txt_msg for peer text.

Drop all floods with 1-byte path hashes (including zero hops) when type=any hops=all hashbytes=1 do drop Drop all floods with 2- or 3-byte path hashes (including zero hops) when type=any hops=all hashbytes=2+ do drop Set #BlackHole86 scope on all #rgdata group traffic when type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast Add #BlackHole86 scope to unscoped #rgdata data when type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast Replace #usa with #BlackHole86 on #rgdata data when type=grp_data hops=all channel=#rgdata rx.scope=scope:usa do scope=#BlackHole86 timing=fast Rate-limit packets whose path starts with 860C when type=any hops=all path=prefix:860C do rate=10/min burst=10 Drop selected flood types at their hop limits when type=control hops=1+ do dropwhen type=req hops=3+ do drop; same for type=grp_datawhen type=response hops=9+ do drop; same for type=anon_req and type=path Rate-limit Public messages from \u201cNoisy User\u201d when type=grp_txt hops=all channel=public sender=\"Noisy User\" do rate=5/min burst=5 Stop forwarding traffic from blacklisted internet gateways when type=any hops=all path=blacklist do drop Three defaults: OTA, distant #wardriving, and bridge/crossover #wardriving when type=ota hops=all tempradio=inactive do dropwhen type=any channel=#wardriving hops=5+ do dropwhen type=any channel=#wardriving hops=all via=bridge,cross do drop Set #BlackHole86 scope on login and bucket-matched other traffic when type=class:login hops=all do scope=#BlackHole86 timing=fastwhen type=class:other hops=all path=bucket:2 do scope=#BlackHole86 timing=slow

Rule builder

Reset form

The factory example contains all three repeater defaults. The playground's via= traffic-path matcher corresponds to firmware mode=radio|bridge|cross|bridge,cross; the advanced Active/Shadow/Disabled mode is a separate playground setting. See the firmware mode guide. This simulator models flood packets; firmware bridge/cross rules also cover direct routes.

Common match settings Payload type or class Any payload type Class: group text and data Class: login/admin family Class: every other payload REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel (optional) Original incoming scope Path hash width Any width 1 byte 2 bytes 2+ bytes (2 or 3) 3 bytes Path matcher No path condition Ordered 1/2/3-byte pbyte prefix Passive blacklist Path bucket 1 Path bucket 2 Path bucket 3 Path bucket 4 Path bucket 5 Path bucket 6 Own-ID loop: strict Own-ID loop: moderate Own-ID loop: minimal Ordered pbyte prefix Decrypted sender (optional) Traffic path (firmware mode) Normal radio forwarding Bridge entry/exit Radio profile crossover Bridge and crossover Temporary-radio state Either state Inactive Active Common actions

Choosing a phase-specific action automatically selects a compatible phase and ACL owner. Open the advanced section to inspect or override those choices.

Forwarding verdict No verdict change Drop / do not retransmit Region-gate decision Leave global gate unchanged Require original allowed region Bypass global gate for this packet Scope rewrite target Do not rewrite scope Regionless public scope Configured region Target name Token rate per minute (optional) Burst tokens Rewrite timing Inherit normal behavior Fast-track rewrite Normal queue timing Slow receive/transmit timing Queue priority Inherit High Normal Low Retry path bucket No retry action Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket 5 Bucket 6 Retry attempts Decision tag (optional) Advanced execution, ownership, and flood route

Direct packets already carry a supplied path and are intentionally outside this engine. The route condition below only distinguishes unscoped floods from transport-scoped floods.

Stable rule ID Processing phase 1 - Incoming scope gate 2 - Scope rewrite 3 - Forwarding decision 4 - Decrypted content 5 - Scheduling and retry Rule owner / capability ACL 4 - Region/scope manager ACL 5 - Filter manager Administrator Firmware-managed system rule Priority Mode Active Shadow - observe only Disabled Rule processing after a match Continue to later rules Skip later rules in this phase Skip all later policy rules Flood route Either flood route Unscoped flood only Transport-scoped flood only Readable policy definition

Add rule to policy Copy definition

Policy draft

Clear policy Approximate target budget Compact STM32 - 2 KB nRF52 - 8 KB Classic ESP32 - 16 KB Roomy ESP32 - 64 KB

Rules match the same immutable packet facts. Ordering is phase, then descending priority, then authenticated channel, raw hash, or wildcard specificity, then stable rule ID. Drop decisions are sticky.

Add a rule or load an example to start exploring.

Immutable packet simulator

Reset packet Read a raw MeshCore packet

Paste an on-air packet as hexadecimal. The decoder follows the MeshCore wire format, displays its header, route, pbyte path, and clear payload envelope, then loads only facts actually present on the wire into the simulator.

Runs locally Raw packet hex 014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de Decode and load packet Packet traffic path Normal radio forwarding Bridge entry/exit Radio profile crossover Received route Unscoped flood Transport-scoped flood Payload type REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel Encoded path hash width Infer from path (empty defaults to 1 byte) 1 byte 2 bytes 3 bytes Received pbyte path Original scope status Unscoped Allowed region Unknown or denied scope Original scope name Resolved region name Decrypted sender Passive blacklist result No match Matched Matching path buckets Own-ID loop result No loop match Strict only Moderate and strict Minimal, moderate, and strict Temporary radio is active Run policy simulation

Import and explain

Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.

Explain input Load into builder

Export

Readable policy Policy JSON Playground Base64

Proposed human-readable definition. Current firmware does not accept it yet.

Structured draft used by this page.

Browser-playground interchange only. It is not the final packed firmware codec.

Copy visible export Download JSON"},{"location":"filter_tool/#filter-builder-title","title":"Create a policy rule","text":""},{"location":"filter_tool/#filter-policy-title","title":"Rules in execution order","text":""},{"location":"filter_tool/#filter-simulator-title","title":"Explain an evaluation","text":""},{"location":"filter_tool/#filter-import-title","title":"Paste readable policy or a saved draft","text":""},{"location":"filter_tool/#filter-export-title","title":"Move or save this design","text":""},{"location":"filter_tool/#payload-type-reference","title":"Payload type reference","text":"

Use an exact type= value when a rule should match only one payload type. The class column shows which broader selector also matches it.

type= value Payload Class req Request class:login response Response class:login txt_msg Peer text message class:login ack Acknowledgment class:other advert Node advertisement class:other grp_txt Group-channel text class:group grp_data Group-channel datagram class:group anon_req Anonymous request class:login path Returned path class:login trace Path trace class:other multipart One frame in a multipart sequence class:other control Control or discovery data class:other ota OTA-over-LoRa data class:other 13 Reserved payload type 13 class:other 14 Reserved payload type 14 class:other raw_custom Application-defined raw data class:other"},{"location":"filter_tool/#current-fpf7-command-mapping","title":"Current FPF7 command mapping","text":"Firmware command FPF7 role fr / flood.rule / flood.filter Forward-phase match and action rows flood.channel.data Compatibility view over one visible type=grp_data forward drop row flood.channel.scope Scope-rewrite phase rows flood.filter.blacklist One shared unordered path-ID set referenced by path=blacklist rows

Generalized repeaters expose 63 forward rows and commit those sections together. The blacklist is useful for refusing to retransmit floods associated with internet gateways dumping bulk traffic, but a path ID is truncated and unauthenticated; it identifies a routing pattern, not a person.

The firmware accepts both full and short CLI forms for the same forward row:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\n

Use get fr.3 on the device to obtain a short setter ready to copy. This CLI syntax is separate from the playground's proposed policy/bundle format. See the shorthand reference.

"},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"

The simulator uses these rules:

  1. Only flood retransmission enters the policy. Direct routing and local packet delivery remain outside it.
  2. Every matcher reads the same immutable receive-time packet facts.
  3. Rules run by phase and descending numeric priority. At an equal priority, authenticated channel matches run before raw one-byte hash matches, which run before wildcard channel matches; stable rule ID breaks the remaining tie. An explicitly higher numeric priority overrides channel specificity.
  4. A drop decision is sticky and cannot be undone by a later rule.
  5. The first matching scope, timing, queue, and retry action in execution order wins.
  6. All matching token-bucket rate constraints remain attached to the decision.
  7. stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety.
  8. Shadow rules report what they would do without changing the decision or stopping other rules.
  9. Expensive facts such as channel-key matching, decryption, and path-table lookup are resolved once per packet and reused by every matching rule.

The byte-budget display is deliberately approximate until the packed firmware codec exists. It demonstrates why simple mappings should not reserve a maximum- sized structure for every possible condition and action.

"},{"location":"firmware_memory_budget/","title":"Firmware memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"firmware_memory_budget/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"firmware_picker/","title":"Firmware picker","text":"

View this page on MeshCore Docs.

"},{"location":"firmware_picker/#firmware-picker","title":"Firmware picker","text":"View files Hardware Loading hardware... Firmware role Loading roles... Hardware variant Choose hardware first Optional: chip family

Skip this if you know your board. Picking hardware selects its chip family automatically. Choose Any to clear this filter.

Chip family Any chip family - skip this filter Install operation Loading install choices... Logging / MQTT Loading logging choices... Firmware profile

Each button combines OTA support with the feature, sensor, and storage choices for that image. Reduced optional sensors means fewer environmental/ranging drivers; I2C and supported board peripherals remain available.

Loading firmware profiles... Connection / bridge mode Loading modes... Clear all choices Copy link to settings Short link to these settings

Current release set

Loading release information...

nRF52 bootloader Loading the current firmware catalog...

Exact firmware match

Recommended download

No exact firmware matched

Browse all firmware releases Picker help

Pick in any order. Use Any to clear one choice, or Clear all choices to start over. Use Copy link to settings to share your selection.

Choices stay limited to compatible builds. Picking hardware selects its chip family automatically. Filters disappear when they select the same file; runtime options appear under Restore your settings after flashing instead. Installation instructions and board-specific guidance follow your download.

The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, targets, and download links come from the published firmware assets.

Advanced: search current release filenames

Use this for uncommon board suffixes or expert recovery. A filename match is not a board-identity check.

Filename contains"},{"location":"firmware_picker/#what-the-choices-mean","title":"What the choices mean","text":"

New Heltec T114 images detect the optional screen automatically, so the same firmware works with or without it. Choose the board and firmware role; there is no screen option for an automatic-detection image. See the T114 update guide when moving from an older screenless target.

Pick the choices in any order. The controls stay limited to compatible firmware combinations that were actually built in the current release set. The result count stays visible while you scroll through the filters on a phone. Once the exact board and role are selected, the picker skips logging, connection, and profile choices that all select the same firmware. Logging settings move into Restore your settings after flashing beneath the files. Choices that select different images remain visible. For qualified nRF52 sensor pairs, Full is the normal recommendation and both Full and Reduced profiles are shown by default. Show Reduced sensor images under Advanced: sensor compatibility starts checked, including after clearing the picker. Uncheck it to show only the recommended Full profiles. Shared links retain their selected profile; showing Reduced does not switch a Full selection or change either image's OTA identity. The optional chip-family filter (ESP32, nRF52, RP2040, or STM32) can narrow the hardware list first. You can skip it: picking hardware fills it in automatically. Chip family and hardware are the only dropdowns. All remaining choices use buttons. Firmware profile combines OTA support, Full/standard, and sensor or storage differences when a choice between different images is needed.

Use Any to clear one choice, or Clear all choices to start over. The address bar updates with your choices; use Copy link to settings for a shorter link to the same selection.

Each result includes Restore your settings after flashing. Its commands follow the selected logging mode and the exact image's verified role/hardware controls. Click On, Off, or Check to view and copy the commands; Companion MQTT and GPS show their app/WebConfig steps instead. See feature switches by role for the full reference. The USB web console works with the default ASCII terminal on USB Companion and infrastructure roles.

The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, target names, and download links come directly from the published firmware assets.

Choice Use Companion A phone, computer, or host application controls the radio Repeater Standalone mesh relay Room Server Hosts room conversations and history Sensor / telemetry Publishes supported sensor data Terminal Chat Standalone serial-terminal interface USB logging / USB-connected MQTT Node remains attached to a computer over a data-capable USB cable Wi-Fi MQTT observer Firmware connects directly to MQTT over Wi-Fi; this is not USB logging USB logging + Wi-Fi MQTT Unified FULL image sends to both paths; avoid two publishers aimed at the same broker unless messages are deduplicated No logging Normal standalone operation with external logging output disabled Receives LoRa OTA Repeater, room server, or sensor image that can stage an exact matching update received over LoRa; any sensor/storage tradeoff appears on the same Firmware profile button Receives LoRa OTA - Reduced optional sensors Compact OTA image that omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals Reduced sensors + LoRa OTA Qualified nRF52 repeater, room server, or sensor build with the reduced optional sensor recipe and LoRa OTA retained Full supported sensors + LoRa OTA Qualified nRF52 build with all sensor drivers supported by that board's full recipe and LoRa OTA retained; does not add support for every possible attached sensor LoRa OTA source only Full Companion serving a host-supplied update to another node without self-installing it

Connection and bridge choices depend on the selected role. Companion firmware may offer Full, combined USB + Bluetooth, Bluetooth, USB, Wi-Fi, serial, or Ethernet transports. Normal repeater firmware includes runtime-controlled RS-232 support where the board has room; use the selected image's runtime directions to configure it. Choose Wi-Fi MQTT in the result's saved logging settings when it shares the Full image; separate MQTT images retain a Logging / MQTT filter. Full ESP32 LoRa repeaters include runtime ESP-NOW when capacity permits; dedicated ESP-NOW images remain for measured capacity exceptions. Ethernet still needs its exact hardware recipe.

For commands and option explanations, follow the ESP-NOW bridge setup guide.

An ESP-NOW bridge target keeps LoRa as its primary mesh radio. Expanded ESP32 Full LoRa repeaters include ESP-NOW, including boards without an MQTT recipe. Existing MQTT-backed Full repeater and room-server images combine both transports. Use set espnow.enabled on|off for ESP-NOW and, where supported, set mqtt.enabled on|off for MQTT. Newly combined ordinary repeaters start with ESP-NOW off until explicitly enabled, including the first upgrade from unmarked legacy preferences. Later reboots preserve that setting. On combined RS-232 boards without MQTT, bridge.enabled controls the UART. Use the selected download's board notes and runtime controls for images with independent UART, MQTT, and ESP-NOW services. MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must match the connected WiFi access point's fixed channel. ESP-NOW-only mode does not require WiFi credentials. Its runtime bridge.format setting chooses the peer protocol: wrapped (the backward-compatible bridge-to-bridge default using bridge.secret) or raw (direct MeshCore ESP-NOW LR frames for Generic_ESPNOW, SenseCapIndicator-ESPNow, and other primary-ESP-NOW nodes). This is one firmware choice, not two board images. Match bridge.channel to the primary nodes' espnow.channel before selecting set bridge.format raw.

"},{"location":"firmware_picker/#share-a-selection","title":"Share a selection","text":"

Select any combination of choices, then use Copy link to settings for a short URL under https://mikecarper.github.io/MeshCore/p/. Opening the link restores those choices after the release catalog loads. Partial selections work too, so you can share a board and role while leaving other choices open. Clear all choices removes the picker parameters from the URL. Changes update the current browser-history entry without reloading the page or adding a Back-button entry for every click.

For example, RAK3401 unified Full repeater preselects that board, role, OTA capability, and Full sensor policy. Supported internal or external storage is detected by the unified image.

Short links encode only the selected choices in a versioned s parameter. Hardware identifiers stay readable, and the link omits redundant hardware-family and automatically selected chip-family values. It keeps profile and install choices explicitly rather than relying on the current catalog's defaults. The address bar also uses the compact selection format; copying it works too.

Existing long links still work. Their query parameters are chipFamily, hardwareFamily, hardware, role, logging, ota, mode, feature, variant, and install. Values use the picker's internal identifiers rather than the displayed labels. One Firmware profile button sets ota, feature, and variant together. Existing links with only some of those choices still work and show their partial selection. The selection also records when chip-family filtering was chosen manually so automatic chip-family selection or an explicit Any choice behaves the same after reopening. Existing section anchors and unrelated query parameters are preserved.

Links use the current release catalog. If a linked choice is no longer available or conflicts with another choice, the picker identifies it and asks you to review the remaining selections. The downloadable HTML's Copy link to settings button creates a short public website link that other people can open.

"},{"location":"firmware_picker/#full-versus-standard","title":"FULL versus standard","text":"

Current ESP32 ordinary releases use one Full / complete image per exact board and role. Full keeps the supported feature set and CLI; runtime switches select USB, Bluetooth, Wi-Fi, logging, and other qualified features. Separate portable, minimal, and single-transport ESP32 images are installation/recovery recipes, not additional ordinary release choices. Board-specific capacity exceptions are shown with the selected download. Other chip families retain their qualified capacity and sensor-profile choices.

Moving an older ESP32 installation to an expanded Full layout requires a partition migration. Use the exact-board merged image over USB, or a supported exact board and role staged migration ZIP from the utility release. Read that ZIP's README before choosing the Wi-Fi or LoRa route. Do not send a loose app-only Full .bin directly to an older layout: the running application cannot move its own active and inactive partitions. Run get storage.layout on the installed firmware if available. It reports the live partition table; a version-based guess cannot confirm the installed layout. See the partition lookup guide when the old firmware lacks that command.

Current full-usb-wifi profiles use one binary for no external output, USB packet logging/USB-connected MQTT, direct WiFi MQTT, or both. The picker skips the logging filter when these modes use the same file; select the saved runtime mode with set logging.output off|usb|wifi|both. A FULL logging-fallback profile is listed only when no WiFi MQTT sibling exists; it supports both the no-output and USB settings because set usb.logging off|on is persistent. On a fresh unified FULL install with no saved SSID, the setup AP and WiFi radio remain available for 30 minutes per boot, then turn off automatically until the next reboot or power cycle. An explicit administrator start webconfig remains available as an override. A saved SSID switches to the normal indefinite reconnect behavior instead.

Fresh ESP32 Full infrastructure defaults to USB logging off. Existing saved logging and device power-saving settings survive an update. Automatic unconfigured boot setup also pauses network-bridge retries until an explicit bridge start, allowing its radio to turn off when the AP closes. Manual WebConfig/WiFi starts preserve an explicitly enabled ESP-NOW bridge, including when the master radio switch restores the selected services. Compact OTA images scan before raising a new AP and verify live driver readiness before reporting the update URL.

Full Companion profiles use one binary for USB, BLE, ordinary Wi-Fi on ESP32, source-only LoRa OTA, Terminal Chat, optional USB packet logging, and any board-qualified serial or Ethernet Companion transport. Bulk builds therefore omit separate attached-transport, Terminal Chat, and USB-logging artifacts whenever the exact Full recipe exists. RAK4631 repeater and room-server Ethernet images remain separate roles. Fresh installs default to logging off. Heltec V3 and base OLED V4 Full images also contain the former direct Wi-Fi MQTT Companion capability, configured at runtime through WebConfig, so their separate companion_radio_wifi_mqtt artifacts are omitted from canonical builds as well.

When Full Companion does not fit but a matching USB Companion does, that USB artifact also supplies Terminal Chat and replaces its standalone release image. Heltec E290 and T190 now publish one Full USB + BLE + WiFi Companion; their old combined and single-transport names are explicit-build compatibility aliases. SSD1306 Full Companion builds use set display.rotation 90|180|270; 0 restores the board default, so a separate rotated release image is not recommended.

New display-capable RAK4631 and RAK3401 images with OLED controller detection also offer saved display.controller choices: auto, ssd1306 or sh1106. This applies after restart and avoids a custom image when a panel needs an explicit controller. See controller selection.

New Heltec V4 R8 TFT images also support saved rotation in Companion, Repeater, Room Server and Sensor roles. The observer dashboard follows the selected portrait or landscape orientation. Once the canonical image proves this capability, the picker replaces separate portrait recommendations with runtime rotation controls. The stock R8 kit has no accelerometer, so automatic orientation is not offered. See display rotation.

Other new canonical images can replace redundant hardware or transport choices: the Heltec V4 OLED covers its matching OLED expansion kit, Pico W Companion combines USB/Bluetooth/WiFi, and RC32 detects its optional T108 display. The picker requires verified capabilities from the actual downloadable application before replacing an older recommendation. Existing downloads remain available through the advanced filename search; changing target names still requires the matching local migration rather than a cross-target LoRa update.

Muzi Base Companion also includes the optional SuperIO accessory for its exact Uno or Duo radio, and T-Echo Lite Companion detects its e-paper panel. A new verified canonical image can therefore replace the matching SuperIO or non-shell recommendation. These accessory choices do not change the radio hardware selection. See optional screens.

The variant consolidation validation records the qualified package hashes, controls checks and pending hardware tests.

Ordinary non-OTA roles also use one artifact for normal operation and USB logging. On ESP32 1.17.1.5, run set powersaving off before set usb.logging on. Select the saved mode with set usb.logging off|on; no -logging- artifact is emitted. KISS, BLE-only Companion, and constrained LoRa OTA repeater images retain their protocol/partition contracts and do not inherit plaintext USB logging.

nRF52 Full Companion keeps the multi-role primary interface on 00; it starts as an ASCII terminal and automatically hands a complete < frame to Binary Companion. set usb.logging on reboot adds its plaintext interface 02.

Every ESP32 Full Companion instead exposes one USB TTY. Logging is off by default, so the TTY serves the ASCII/Binary Companion switcher. On 1.17.1.5, run these two text commands to enable USB logging:

set powersaving off\nset usb.logging on\n

The second command turns that same TTY into an input-capable plaintext CLI/logging stream; framed Binary Companion is unavailable on USB while logging owns it. set usb.logging off stops the logs and leaves the TTY in the normal ASCII terminal, matching a fresh Full installation. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch from there to Binary Companion. BLE and Wi-Fi Companion remain usable while the USB TTY is logging. ESP32 Full builds use the repository's Arduino-ESP32 2.x base where the board supports it; RC32 and ESP32-C6 retain their board-required Arduino 3.x platform but still expose only one USB TTY. A second ESP32 CDC interface is not part of the release profile.

The picker includes the power-saving workaround when selecting USB or USB + WiFi logging on ESP32 1.17.1.5. WiFi/MQTT-only logging does not need it while the Repeater/Room Server bridge is running; check get mqtt.running. The workaround is not added to nRF52 directions. See logging by role for the saved settings and the original firmware's USB sleep issue.

"},{"location":"firmware_picker/#companion-usb-terminal-on-linux","title":"Companion USB terminal on Linux","text":"

Full, USB and USB + Bluetooth Companion results include Companion USB terminal (Linux / Bash) under Restore your settings after flashing, with copyable connection and recovery commands.

Updated 1.17.1.6 USB Companions default to ASCII at boot and after an observable USB session reset. picocom -b 115200 /dev/ttyACM0 is sufficient for a fresh session. A valid framed app command automatically selects Binary Companion. The command below also works with older firmware or a port left in Binary mode.

Run this in Bash on the Linux computer connected to your radio:

picocom -b 115200 --imap spchex \\\n  --initstring $'\\r+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

Replace /dev/ttyACM0 with your radio's port, such as /dev/ttyACM1 or /dev/ttyUSB0. Use the primary USB data interface (00 when multiple interfaces appear), and close other apps using it. The init string requests the ASCII CLI; its leading carriage return clears an unfinished input line. --imap spchex displays binary control bytes safely during the transition.

If the terminal display is scrambled, exit picocom with Ctrl+A, then Ctrl+X, run reset in your Linux shell, then reconnect with the command above. At the radio prompt, run version and board to identify the node. See the Companion USB mode guide.

"},{"location":"firmware_picker/#companion-tempradio2-transmit-settings","title":"Companion tempradio2 transmit settings","text":"

LoRa Companion results for 1.17.1.6 and newer include Companion tempradio2: transmit on both under Restore your settings after flashing. After configuring tempradio2 in rxtx mode, choose Transmit on both to copy:

set radio2.cross on\n

This copies new messages to both radio and tempradio2. The setting is saved, outlasts the temporary session and reboot, and also allows OTA traffic to cross profiles. Default isolation copies set radio2.cross off; Check shows the temporary profile, crossing policy and per-profile transmit counters. See the two-profile setup guide.

"},{"location":"firmware_picker/#installation-methods","title":"Installation methods","text":"File Use -merged.bin Erase/fresh install, recovery, role migration, or partition-profile change on ESP32 over USB Non-merged .bin Update an existing same-board, same-role, same-partition installation *-migration.zip Exact ESP32 board and role staged partition migration; follow its included README for Wi-Fi or LoRa steps Other .zip firmware shown by the picker Native nRF52 Serial DFU update package; it is not an extra archive .uf2 UF2 bootloader drag-and-drop install or update .hex Erase/recovery flash with a supported wired programmer

Never send a merged ESP32 image through browser OTA or LoRa OTA. Back up the node identity, configuration, keys, and radio settings, and verify every filename suffix before flashing. A staged migration writes the partition table; power loss during that write can require cable recovery. The package README states what each route preserves and which older layouts it accepts.

"},{"location":"firmware_picker/#lora-ota-and-otafix","title":"LoRa OTA and OTAFIX","text":"

A LoRa OTA capable build can receive and stage updates for its role. A later LoRa update still needs an exact target identity, compatible partition signature, matching radio settings, and the correct update package.

Selecting an nRF52 board shows its direct Bootloader UF2, Bootloader DFU ZIP, and Bootloader HEX (SWD) downloads, when an exact published profile is available. ESP32 and other chip families do not show this section. The web picker checks the latest stable OTAFIX release; the downloadable picker includes the bootloader release named in its saved catalog. If a board has no verified mapping or a file is missing, the picker explains that instead of offering another board's bootloader. OTAFIX 2.4.11 publishes a combined MeshTower V2 SD/internal image, mapped only to Heltec_tower_v2_sdcard. The historical Heltec_tower_v2 internal-only profile is explicitly retired: move to the normal combined image with a one-time local USB/BLE DFU or SWD installation, not a cross-profile LoRa bootloader update. The picker does not silently substitute its bootloader. RAK3401 and RAK4631 each use their board-specific adaptive storage bootloader. Ikoka Stick, Nano and Handheld plus SolarXiao use the XIAO bootloader; Wio Tracker L1 1W and E-Ink use the L1 bootloader. See the board coverage audit for the source checks and boards still awaiting a dedicated, tested release.

Board mappings and file names, URLs, sizes and SHA-256 hashes come from bootloader-manifest.json in the OTAFIX release. Application UF2 and DFU files do not install a bootloader. Follow the bootloader release's migration instructions if the installed version needs a recovery bridge.

nRF52 LoRa OTA requires an OTAFIX bootloader built for the exact board and storage layout. Select the hardware-matched HEX, Serial DFU ZIP, or bootloader-update UF2 for the application image being installed.

"},{"location":"firmware_picker/#hardware-and-variant-names","title":"Hardware and variant names","text":"

Hardware families with multiple released targets get Hardware variant buttons. They separate revisions, display type, expansion kit, radio/PA layout, pin map, and other physical differences without crowding the Hardware dropdown. Firmware profile buttons combine OTA support, Full/standard, and choices that need a different image or wiring, such as serial port or external storage. The picker skips this step when all available profile choices lead to the same firmware file. The 1.17.1.9 nRF52 repeater, room-server and sensor releases publish two separately qualified choices: Reduced sensors + LoRa OTA and Full supported sensors + LoRa OTA, with -reduced-ota and -full-ota artifact suffixes. Each keeps its exact board/role OTA identity and storage layout; the suffix is not a new radio or partition identity. Labels come from the release's qualified sensor.profile.reduced / sensor.profile.full metadata and verified LoRa receiver, not from the filename alone. Both profiles remain separate in local and published releases, including boards whose optional sensor set is small enough that the images have little or no size difference. Packaging requires both profiles with matching packaged OTA identity and flash geometry, even when a matrix is resumed or partial publication is requested. A failed build is reported, never presented as a supported download. Companion is unchanged.

Normal SolarXiao, XIAO nRF52, Ikoka Nano/Stick/Handheld, T-Echo, T-Echo Lite, ThinkNode M1/M6, Wio Tracker L1 and SenseCAP Solar Room Server builds use onboard QSPI storage through their exact *_room_server_lora_ota targets, with both sensor profiles retained. T-Echo Lite Repeater similarly uses its exact LilyGo_T-Echo-Lite_repeater_lora_ota target. Each requires the board-matched QSPI-capable OTAFIX bootloader; a shared board-family name is not enough to choose a bootloader. Internal-only Room recipes remain available by exact name for explicit compatibility/recovery builds, but are excluded from ordinary build menus and future releases. For newly converted boards, the picker keeps an existing release's internal Room choice until that same release provides its qualified, source-matched QSPI Full/Reduced pair. This preserves working Room links during rollout; SolarXiao's already-retired internal choices stay hidden. Existing downloads remain available through exact filename searches. The legacy and QSPI images have different OTA identities and layouts: a QSPI package is not a cross-target LoRa update for an internal-only installation. Use the matching bootloader and documented local migration path when changing layouts.

T-Echo, SenseCAP Solar, ThinkNode M1/M6 and Wio Tracker L1 already use QSPI in their ordinary Repeater targets. Their older *_repeater_lora_ota_no_external_sensors identities remain explicit compatibility builds, not extra normal Full/Reduced choices. The picker hides these aliases only when a source-matched, qualified ordinary QSPI pair exists. XIAO nRF52 Repeater now also includes its runtime RS232 bridge, disabled by default (set rs232.enabled on|off). The separate bridge recommendation is hidden only after both ordinary sensor profiles prove the linked driver and packaged runtime controls. Existing bridge OTA identities remain unchanged.

Muzi Base Uno/Duo, T-Echo Card and Nano G2 Ultra have explicit *_repeater_lora_ota and *_room_server_lora_ota qualification recipes. Their dedicated bootloader ports still require physical qualification and publication. These experimental recipes reserve raw QSPI for OTA (no shared QSPI filesystem), are excluded from ordinary menus and bulk builds, and carry hardware.qualification.pending in their capability manifests so release packaging refuses them. Their current normal builds and picker choices remain available; do not install another board's bootloader to bypass this gate.

The legacy no_external_sensors profile is labeled Receives LoRa OTA - Reduced optional sensors on the same button. On RAK3401 and RAK4631 this compact profile uses internal storage without an external storage board. The reduction does not disable generic I2C or unrelated board-integrated peripherals. Reduced RAK3401 and RAK4631 targets retain INA219/INA226/INA260/INA3221 as voltage/current entries in the optional sensor table. They are not the only I2C users: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals in compatible recipes. RAK12501/L76K GPS uses Serial1 instead. The explicit RAK4631 Serial1 bridge omits the combined GPS provider because its bridge owns the RAK12501 UART, so that legacy image does not expose RAK12500 either. The firmware-configured INA3221 and RAK12500 addresses are both 0x42; to install both, keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43. Companion power saving, controllable FEM receive gain, and radio-chip receive gain are saved settings rather than separate recommended firmware files. Do not substitute a similarly named physical target.

The header-wired RAK19007 W25Q16 LoRa-OTA recipes are also exact hardware variants, labeled External storage board (W25Q16) in the picker. W25Q16 is the flash-memory part on the added storage board; RAK13302 identifies the radio module, not the storage board. Choose this variant for RAK4631 or for RAK3401 + RAK13302 only when that core/radio combination, the EF4015 flash wiring, and its matching OTAFIX bootloader are all present. The common base-board wiring does not make the two firmware or bootloader identities interchangeable.

The picker recommends one Full Companion image instead of separate USB, BLE, ordinary WiFi, and USB-logging images. On ESP32, logging is off by default so the one USB TTY starts in ASCII and automatically changes to framed Companion when a complete < frame arrives. Enabling logging gives that TTY to the plaintext CLI/logger and disables framed USB Companion until logging is turned off and the normal ASCII-to-binary mode switch occurs. nRF52 Full Companion retains its optional dedicated interface 02. Exact filename search finds compatibility images included in the selected release family. For older releases, open their GitHub release pages.

The current public matrix keeps one ESP32 Full image per board and role. Some richer images, including Station G2 Full, retain an observer-named OTA identity. Exact older identities needed by a partition migration are packaged inside that board and role's migration ZIP instead of adding a second ordinary Full download. A deliberate target-ID override does not make a different partition layout or physical board compatible.

Some normal ESP32 roles begin in the legacy 1.25 MiB dual-OTA layout but have a Full image with larger slots. The utility release lists the exact board and role migration ZIPs available for that release. Each package checks its source layout and target identity, contains the bridge and Full application files for its supported routes, and documents what saved data it preserves. Use the package's own Full application after the bridge; do not substitute a loose Full image with a different target ID. If no exact package exists, use the matching merged USB image. Once migrated, routine app-only updates still need the same target and partition signature.

"},{"location":"firmware_picker/#maintaining-the-runtime-directions","title":"Maintaining the runtime directions","text":"

See optional screens for manufacturer-confirmed screen options, automatic detection support, and integral-screen exclusions.

The online picker and downloadable HTML use the same command renderer. Hardware-specific controls are enabled only when _data/firmware_controls.json matches the selected release family and exact target. If that metadata is missing or belongs to another release, the picker retains basic role/logging directions and links the complete guide without inventing hardware support. For 1.17.1.6, that JSON also records the verified LoRa OTA role of each image and USB logging modes where the packaged application was checked. Logging metadata is tied to the source hash of its firmware file. The normal iKoka 30 dBm repeater has USB logging and LoRa OTA; its compact no-external-sensors image has LoRa OTA without logging. Capacity directions come from each qualified profile's recorded reductions and are tied to the source hash of the selected download. The older 1.17.1.5 USB sleep workaround stays limited to that release.

After qualifying a new release, resolve its PlatformIO configuration with no other PlatformIO process running, then generate the controls from that source revision and its staged manifests:

pio project config --json-output > /tmp/meshcore-picker-pio-config.json\npython3 scripts/generate_picker_controls.py \\\n  --stage /path/to/staged-release \\\n  --pio-config /tmp/meshcore-picker-pio-config.json\n

Refresh the data-controls-url version query when publishing the generated metadata so browsers fetch the new release's controls.

The generated partitionMigrations lookup comes from BOARDS in scripts/package_esp32_partition_migration.py, the same exact board/role table used to build the migration packages. Expanded ESP32 Full results use this lookup (including canonical board/role recipes for Observer profiles), then require one matching published ZIP in the same release family's utility page. Hardware variants and roles remain distinct. Unlisted targets, missing or ambiguous assets, and stale metadata never produce a guessed download link.

For the downloadable version, save the release family's public GitHub release objects as a JSON array, then package the same picker UI and controls:

python3 scripts/package_firmware_picker.py \\\n  --releases-json /path/to/releases.json \\\n  --output /path/to/FIRMWARE-PICKER.html\n

This HTML embeds its catalog and directions, so selections work without an internet connection. Firmware downloads and the USB web console still need network access. When replacing the downloadable picker on release pages, update its entry in each page's SHA256SUMS.txt as well.

"},{"location":"firmware_picker/#memory-corrected-11715-downloads","title":"Memory-corrected 1.17.1.5 downloads","text":"

Corrected downloads retain the 1.17.1.5 release page and use source suffix aa20e927, with 1e4d1e16 for Wireless Paper Full's 350-contact follow-up. The picker accepts replacement source hashes within that exact release version. Its installation directions identify the nRF52 queue-sharing behavior, Wireless Paper Full's 350 contacts with a shared 256/128-slot queue, and the 150-contact limit on the other six affected ESP32 Full profiles. Those notices apply only to the corrected files. See the memory correction details and each replacement's .memory.json report before updating.

"},{"location":"fleet_management/","title":"Private fleet management","text":"

View this page on MeshCore Docs.

"},{"location":"fleet_management/#private-fleet-management","title":"Private fleet management","text":"

Fleet control lets one publisher send filtering, radio schedules, secondary-radio changes, and clock commands to all enrolled infrastructure nodes, regions, a geographic radius, one node, or a list of targets over a private LoRa channel. It is disabled until configured. It is supported on repeaters and room servers; fixed-size STM32 images omit it because of their flash limit. Both the publishing Companion and receivers need firmware built with fleet-control support; the existing app can use its local Command Line screen.

"},{"location":"fleet_management/#read-only-receivers","title":"Read-only receivers","text":"

Commands use two separate credentials:

Giving a repeater the channel key and publisher public key does not give it the publisher's signing key. An ordinary channel message, a copied display name, or possession of the channel key cannot authorize a fleet command. Each command signature also binds the channel, target, sequence, expiry, and exact command.

This makes management commands read-only for receivers. It does not turn ordinary MeshCore chat into a signed, read-only announcement channel: stock apps do not verify signatures on ordinary channel text. Fleet acknowledgements are convenient channel text, not authenticated inventory evidence. Use existing individually authenticated administration for sensitive confirmation.

"},{"location":"fleet_management/#enroll-nodes","title":"Enroll nodes","text":"

Use an existing authenticated admin connection or the local console. Generate a random private channel key; for example, openssl rand -hex 16. Do not use Public or a predictable hashtag key. Add that same key as a private channel on the publishing Companion, and note its channel index.

Get the publishing Companion's full public identity key from its device information or existing identity command. On every managed repeater or room server, run:

set fleet.controller <publisher-public-key-64-hex>\nset fleet.channel <private-channel-key-32-hex>\nget fleet.channel\nget fleet.controller\nget fleet.stats\n

get fleet.channel reports enrollment, storage health, a channel hash, and the last accepted sequence. It does not reveal the channel key. set fleet.channel off disables reception; set fleet.controller off revokes the publisher. Key changes use the existing individually authenticated admin path, never fleet commands.

If a settings save reports unconfirmed; restart required, the durable result is unknown. Fleet reception and further fleet settings writes are disabled, and get fleet.channel reports store=error. Enrollment shown in RAM is not active authority while held. Restart to load and validate the public file, then check enrollment before retrying. A not saved; retry later error occurred before publication or after a confirmed rollback, and does not impose this hold. On ESP32/RP2040, rename return values alone are not sufficient: the transaction checks both names before accepting the move or rollback. Fleet also checks the exact public settings image before acknowledging new authority. A Fleet hold does not stop the node's normal radio, USB, or individually authenticated admin services; it prevents uncertain credentials or replay state from granting remote Fleet control.

get fleet.stats includes parts, the number of fragments currently buffered across the two bounded, incomplete two-packet command slots.

Nodes need a clock set to current UTC. Commands expire after ten minutes and may be at most one minute ahead of a receiver's clock. A bad clock fails closed; correct it through the normal administration path.

"},{"location":"fleet_management/#send-commands-from-the-app","title":"Send commands from the app","text":"

On the publishing Companion, use the existing local Command Line screen. The Companion signs with its own identity. No publisher private key is copied to a repeater, and no app protocol change is required.

Examples use private channel index 3:

fleet send 3 get radio2\nfleet send 3 set radio2 mediamesh\nfleet send 3 set tempradio2 910.5,500,8,5,rxtx,120\nfleet send 3 set radio2.cross auto\nfleet send 3 set flood.rule.4 type=grp_txt hops=6+ drop\nfleet send 3 del flood.rule.4\n

Targets are optional: omitting the target sends to all enrolled nodes on the private fleet channel. Explicit all still works, for example fleet send 3 all get radio2.

To address particular nodes, insert 8 or 12 hexadecimal characters from the start of a node's public key, or its complete 64-character public key, before the command. Separate multiple targets with semicolons, without spaces. Lengths can be mixed:

fleet send 3 1257aee5 get radio2\nfleet send 3 1257aee5a754;9abc0123 get radio2\nfleet send 3 1257aee5;9abc01234567 set radio2.cross auto\n

These are example public-key prefixes; substitute your nodes' actual keys. A prefix addresses every enrolled node whose public key starts with it. Use all 64 characters when you need one exact identity, especially in a large fleet. Names and short LoRa path IDs are not accepted as target identities. all must appear alone. A node that matches several entries still executes the command only once, and the signature protects the entire list.

"},{"location":"fleet_management/#target-existing-regions","title":"Target existing regions","text":"

Fleet commands read each receiver's existing region map. Two selectors are available, and they can be mixed with public-key targets:

Selector Which receivers execute sea or region:sea Receivers with an exact sea entry in their configured region list home:sea Receivers whose home region is sea, or whose home has sea as a parent or ancestor
fleet send 3 sea get radio2\nfleet send 3 sea;pdx clock sync\nfleet send 3 region:sea set radio2.cross auto\nfleet send 3 home:sea get tempradio2\nfleet send 3 home:sea;home:pdx clock sync\nfleet send 3 sea;home:pdx;1257aee5 get radio2\n

The semicolon-separated list is an OR: a receiver that matches any entry executes once, even if it matches several entries. Names match exactly and are case sensitive; sea does not match seattle. One leading # is an alias, as in the existing region commands. $ remains significant for private region names. Names may contain up to 30 printable ASCII bytes using region-name characters. Use region: or home: explicitly for names with 8 or 12 characters, names made entirely of hexadecimal characters, or reserved command words such as get, set, del, time and clock. Bare all still means the entire fleet; region:all selects a region actually named all.

Inspect the existing configuration with region and region home. To choose a home from regions already defined on that receiver, use:

region home sea\nregion save\n

List matching includes a configured region even when forwarding for it is denied. Home matching follows the selected home's ancestry; a wildcard or unset home matches no named home target. The default transmit scope and the incoming packet's scope do not determine target membership. Region selection is checked against the live map when the complete command is received.

Targets select which nodes execute. Delivery still follows the Companion's channel scope and the intervening forwarding rules. Unknown names match no receivers and never become a whole-fleet request.

"},{"location":"fleet_management/#target-a-gps-center-and-radius","title":"Target a GPS center and radius","text":"

Use gps:latitude,longitude:radius-km to select receivers within a circle. For example, this selects nodes within 25 km of the given Seattle coordinates:

fleet send 3 gps:47.6062,-122.3321:25 get radio2\nfleet send 3 gps:47.6062,-122.3321:25 set radio2.cross auto\nfleet send 3 sea;home:pdx;gps:47.6062,-122.3321:25;1257aee5 clock sync\n

GPS, region, home and key selectors can share the same semicolon-separated list. They use the same OR rule: any matching entry selects the node, and the command executes once. The center, radius and complete target list are signed. The comma is only for the coordinate pair; ; is the only target-list separator.

Coordinates use ordinary decimal degrees, with latitude from -90 to 90 and longitude from -180 to 180, and at most six decimal places. Radius is in kilometers, from 0.001 to 20050, with at most three decimal places. For example, 0.5 means 500 meters. Scientific notation, excess decimal places, missing fields and out-of-range values are rejected without sending. Distance uses the great-circle calculation, including across the date line and near the poles; a node on the radius boundary is included.

Each receiver prefers its configured lat and lon. A stationary radio can have a fixed position even without GPS hardware; configure it through its normal administration connection:

set lat 47.6062\nset lon -122.3321\nget lat\nget lon\n

When both configured coordinates are zero, the receiver can use an existing valid GPS cache no older than 12 hours. Matching never wakes GPS, takes a UART or requests telemetry. A missing, expired, invalid or default (0,0) position matches no GPS selector; it can still match another entry in the list. Invalid nonzero configured coordinates are rejected rather than falling back to GPS. For a stationary radio that lets GPS sleep for longer than 12 hours, configure its fixed coordinates.

Private fleet matching uses this position even when location is hidden from public adverts. It reads the position when the complete command arrives, including after the second fragment of a two-packet command, so a moving node is selected using its position at completion.

On air, full keys use a 128-bit SHA-256 digest; prefixes use their original public-key bytes. Whole-fleet commands use the compact FMC2 format with no target records, whether the CLI target is omitted or explicitly all. This reduces the header from 29 to 14 bytes. A single complete key uses the smaller fixed-target FMC1 format. Region, home and GPS selectors use separate signed record types in FMC2. Separate fleets use separate channel keys.

Only local commands on the publishing Companion can originate fleet sends. A remote CLI command cannot turn that Companion into a multicast signing proxy. An OK means the Companion queued the command, not that every receiver applied it. Wait for acknowledgements and confirm the result on important nodes.

Use at most one new command per second. The publishing Companion remembers its last queued sequence for the current boot; its clock supplies the next sequence. Each receiving node reserves the Unix-second sequence in durable storage before dispatch. That receiver never executes the same sequence twice, including after reboot or a lost reply. Retry an uncertain command with a fresh sequence; the operation should be chosen to be idempotent.

"},{"location":"fleet_management/#supported-controls","title":"Supported controls","text":"

The Companion automatically uses one or two LoRa packets, depending on the signed command's size. Whole-fleet commands fit 87 ASCII bytes in one packet or 230 bytes across two. Targets consume some of that allowance:

Targets One packet: command bytes Two packets: command bytes Omitted or all 87 230 One 8-character public-key prefix 82 225 One 12-character public-key prefix 80 223 One complete 64-character public key 72 215 One three-character region or home name, such as sea 82 225 Two three-character region or home names, such as sea;pdx 77 220 One GPS center and radius 74 217 Two 8-character public-key prefixes 77 220 Two complete public keys 53 196

Each prefix in a list uses 5 or 7 bytes; each complete key in a list uses 17. Each region or home selector uses two bytes plus its canonical name length. Each GPS selector uses 13 bytes: its type, latitude and longitude in millionths of a degree, and radius in meters. A single complete key uses the smaller fixed-target format with a 29-byte header, so its limits come from packet space. Lists are capped at 17 entries and 86 bytes of encoded target records; for example, at most six GPS selectors fit. Requests exceeding their target-specific budget are rejected before signing or sending, with no partial target list.

The existing app/console input limit also applies. The app's normal 176-byte command frame fits 161 command bytes after fleet send 3; a correlated request uses three more bytes, and explicit targets reduce the space further. The shorter USB rescue console has an 80-byte input buffer. The 230-byte limit is the fleet protocol's capacity, rather than a promise that every console can enter that much text. Oversized or invalid lists are rejected as a whole, without sending or truncating any targets.

"},{"location":"fleet_management/#two-packet-delivery","title":"Two-packet delivery","text":"

Longer commands keep one 64-byte signature over the entire command, including its channel, targets, sequence and expiry. The signed envelope is split into two FMP1 fragments. Each fragment has an 11-byte collection header and up to 154 envelope bytes, within the existing 165-byte group-data limit. Together they can carry a 308-byte signed envelope. No third packet is used.

The Companion reserves both packets before sending. If either cannot be queued, it cancels the queued part and any radio copy, reports an error, and leaves the sequence available for a retry. Successful queueing still does not guarantee delivery over LoRa.

A receiver accepts either arrival order and identical duplicates. It executes and acknowledges only after both parts arrive and the complete signature, target, clock and replay checks pass. Missing or altered parts cannot execute a partial command. Two bounded assemblies are kept for up to five minutes from their respective first parts; duplicates do not extend those deadlines. Unverified fragment sequences cannot grant priority over another transfer. A third transfer replaces the oldest collection. A complete, valid signed command clears the partial collections, and durable replay checks remain authoritative. Reboot or changing enrollment discards the partial transfers.

Control Fleet commands Saved second profile get radio2, set radio2 <tuple|mediamesh|mediamesh-range|off> Temporary second profile get tempradio2, set tempradio2 <tuple|off> Scheduled primary profile get/set/del radioat, get/set/del tempradioat Scheduled second profile get/set/del radioat2, get/set/del tempradioat2 UTC clock clock, clock sync, time <epoch-seconds> Profile diagnostics get radio2.status, get radio2.timing Crossover get radio2.cross, set radio2.cross <auto|on|off> Repeater guest login get guest, set guest on|off Repeater guest sessions get guests [page] Repeater login attempts get logins [page] Filter rules get/set/del flood.filter[.n], flood.rule[.n] Path blacklist get/set/del flood.filter.blacklist[.n] Channel moderation get/set/del flood.moderation[.n] Scope policy get/set/del flood.channel.scope[.n], flood.channel.scope.require[.n] Group-data forwarding get/set flood.channel.data, flood.channel.data.hops Flood hop limits get/set flood.max, flood.max.unscoped, flood.max.advert

Each role still checks whether it supports the requested setting. Immediate primary radio changes, identity export, passwords, ACL edits, filesystem operations, firmware installation, reboot, and unrestricted CLI are outside fleet capability.

Repeater guest login defaults to off. set guest off saves that choice and clears existing guest sessions immediately. Enabling it uses the existing guest password; this switch does not change any password or assigned ACL role.

get logins [page] returns recent repeater login attempts, two per page, showing only 12-character public-key prefixes, latest UTC epoch times, results, and attempt counts. The 16 most recently attempted full keys are kept in RAM; repeated attempts update the existing key's record. Results distinguish accepted logins, wrong passwords, disabled guests, replay/state rejection, and unavailable ACL capacity. The history clears on reboot and survives set guest off. It contains no passwords or shared secrets and is not an online-user list. Anonymous owner, region, and clock requests and packets that fail authentication are excluded. Ordinary channel messages cannot read this list.

set radio2 mediamesh saves 911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, with automatic scan-aware preamble. mediamesh-range uses 125 kHz instead. These presets preserve the primary profile and crossing policy; new configurations default to radio2 off and radio2.cross off.

"},{"location":"fleet_management/#schedule-radio-changes-and-set-clocks","title":"Schedule radio changes and set clocks","text":"

Use the normal schedule syntax, with UTC epoch seconds or +N whole minutes. Primary schedules are supported by repeaters; a role without a primary scheduler replies that the command is unsupported. Second-profile schedules use rx or rxtx after the coding rate:

fleet send 3 clock\nfleet send 3 clock sync\nfleet send 3 set radioat 910.5,500,8,5,+5\nfleet send 3 set tempradioat 910.5,500,8,5,+5,+15\nfleet send 3 set radioat2 910.5,500,8,5,rxtx,+5\nfleet send 3 set tempradioat2 910.5,500,8,5,rxtx,+5,+15\nfleet send 3 1257aee5 get tempradioat2 1\nfleet send 3 1257aee5 del tempradioat2 all\n

Both relative endpoints use the receiver's clock at command acceptance. For a coordinated fleet switch, use the same absolute UTC start/end timestamps on all nodes. An optional final preamble value or auto uses the existing radio syntax. Schedule capacity, radio-range validation, and overlap checks remain unchanged; pending schedules still do not survive reboot.

clock sync advances a receiver to the publishing Companion's signed timestamp plus one second. time <epoch-seconds> sets an explicit UTC timestamp. Both use the existing forward-only clock rule. Initial setup or a clock far out of sync still requires local or individually authenticated clock correction: fleet clock commands retain the signature, expiry, replay, and maximum-clock-lead checks.

Radio mutations and schedule edits keep the old state until their exact acknowledgement packets drain, with at least one successful physical transmission. Queue failure, failed transmissions, or an acknowledgement timeout cancels the staged mutation. Temporary-radio leases and scheduled windows retain their original monotonic end time; waiting for an acknowledgement does not lengthen them.

Exact single-node acknowledgements use 0.5-1.5 seconds of random delay; prefix, multiple-target, region, GPS and whole-fleet replies use 0.5-10 seconds, including radio mutations. Queuing, airtime and forwarding can add delivery time. This reduces simultaneous responses but is not a reliable delivery protocol for an arbitrarily large fleet. Use individual requests for confirmation and divide very large fleets into separately keyed channels.

"},{"location":"fleet_management/#filtering-and-recovery","title":"Filtering and recovery","text":"

Local fleet reception happens before a node's forwarding decision, so that node can still process an authenticated command when a filter or repeat off blocks forwarding. Intermediate nodes can still block delivery to downstream nodes. Management traffic has no automatic exemption from forwarding filters. Preserve an appropriate group-data/channel forwarding rule and an existing direct-admin recovery path before changing network-wide filters. Primary-radio schedules can move nodes off the normal network; use a temporary window or a planned recovery path for experiments. A forwarding policy can also partition the fleet.

The receiver is allocated lazily when fleet settings are present or queried, with one bounded mailbox and two 308-byte fragment assemblies, plus bounded metadata. Cryptographic work and storage writes occur after the receive stack unwinds. Signature-validation attempts are bounded after private-channel MAC verification, complete framing, replay, target, time and command checks. Keyless matching-hash junk, already-used requests, other nodes' requests and incomplete fragments do not spend that signature budget. A corrupt or unreadable fleet store denies commands rather than discarding its replay state.

"},{"location":"fleet_management/#ideas-for-packet-abuse-containment","title":"Ideas for packet-abuse containment","text":"

A packet-count chart can show a burst, but cannot establish intent, an authenticated source, or the airtime consumed. A large Trace spike deserves investigation; MeshCore already rejects flood-form Trace and Control packets. Valid direct Trace requests need different treatment from flood forwarding rules.

Useful next additions, rather than features already implemented here:

Start with observation, then a narrow temporary rule, then verify neighboring nodes still communicate. Software filtering can reduce forwarding and response amplification; it cannot recover airtime already occupied by an RF transmitter.

See flood filtering, radio profiles, and ACL administration.

"},{"location":"flood_filtering/","title":"Flood filtering","text":"

View this page on MeshCore Docs.

"},{"location":"flood_filtering/#flood-filtering-and-moderation","title":"Flood Filtering and Moderation","text":"

This guide explains the Keymind forwarding filters. Repeaters expose the full set of channel, rule, blacklist, and moderation controls described here. FULL-profile ESP32 room servers expose the generalized flood.rule table (and its flood.filter alias) with 31 forward-rule slots, but not the repeater's scope-rewrite, passive-blacklist, or text-moderation phases. Standard room-server profiles do not compile the rule table. Filters decide whether the node retransmits a packet and can assign a transport scope before that decision. They do not stop local reception, packet logging, or MQTT observation.

Radio rules default to the two flood routes:

Generalized rules can opt into relayed direct routes (0x02 and 0x03) with route=direct, or both route classes with route=any. Scope rewriting, source-path matching, and the legacy moderation table remain flood-only. Local delivery and requests addressed to this node are not relay decisions. The route and payload values follow the upstream packet-format reference and payload layouts, with this fork's LoRa OTA assignment noted below.

"},{"location":"flood_filtering/#automatic-flood-advert-limits","title":"Automatic flood-advert limits","text":"

Repeaters, room servers (including non-FULL builds), and forwarding sensors automatically limit relayed ADVERT packets. No CLI rule is needed. Companions and the repeater's receive-only MQTT observer do not allocate this history. This is separate from discovery-response limits, self-advert timers, hop caps, and the per-rule rate=N/min setting below.

The ordinary quota is shared by the first 12 public-key hex characters (six bytes of the advertised origin key, not a path hash). Each prefix has a fixed 180-minute window starting with its first tracked valid flood advert.

Shortest received hops in that window Distinct adverts forwarded per 180 minutes 0\u20131 10 2 9 3 8 4 6 5 5 6 4 7 3 8+ 2

The minimum received hop count, before adding this relay, sets the quota; one-, two-, and three-byte path hashes have the same policy. A shorter valid duplicate may increase the quota without restarting the window or clearing its count. Longer routes never decrease the allowance during that window. The payload signature must verify before any history changes. Path metadata itself is not signed, so this is a received-distance heuristic, not proof of physical distance.

"},{"location":"flood_filtering/#continuing-abuse-and-recovery","title":"Continuing abuse and recovery","text":""},{"location":"flood_filtering/#inspect-the-current-limits","title":"Inspect the current limits","text":"

Use get flood.advert to list rate-limited origins, three per page. Continue with get flood.advert 2, etc. Rows show a 12-hex public-key prefix, the reason (quota, history, or bad), and the wait before another new advert is eligible under the automatic limiter. get flood.advert key 1 shows the full key and counters for row 1, including the remaining bad-list recovery time. A bad-listed key remains visible with wait=0s when its next single forward is due. Prefix collisions appear as separate full-key entries.

This is a live, read-only view: expiry can change row indices between queries, but reading never resets quotas or extends recovery. Sources rejected solely because the protected-history table is full are not retained, so they cannot be listed. See the CLI reference.

"},{"location":"flood_filtering/#clear-history","title":"Clear history","text":"

All timers use rollover-safe uptime, so setting the clock forward or backward cannot clear the restriction. This history is currently RAM-only: rebooting the relay clears it, and time while powered off is not tracked. A manual clear also resets both the ordinary quota and abuse history for its selected target:

clear flood.advert all\nclear flood.advert <64-hex-full-public-key>\n

These use the ordinary local/admin CLI authorization, including authenticated admin LoRa CLI. A prefix is never accepted for a targeted clear; missing or invalid selectors do not clear anything. Clearing an exact key preserves other full keys even when their 12-hex prefixes match. No preferences, contacts, neighbours, replay timestamps, or manually configured flood rules are erased.

"},{"location":"flood_filtering/#scope-and-resource-bounds","title":"Scope and resource bounds","text":"

Only forwarding is suppressed. Valid adverts remain available to local contact/neighbour handling and can still appear in raw RX logs or MQTT. This cannot prevent the origin or upstream repeaters from transmitting. Direct adverts, discovery control responses, messages, OTA packets, and this node's own scheduled adverts are not charged. Existing physical retry attempts of an admitted advert are not additional distinct adverts.

The limiter retains up to eleven 64-bit payload hashes per full key per window: ten possible ordinary admissions plus evidence of exceeding the largest quota. Paths and transport scopes do not change these hashes. Retained duplicate hashes cannot consume additional quota even if the general packet-seen cache has evicted them. Once the eleven distinct receive slots are full, additional unretained payloads are suppressed until the next window. Only packets passing all ordinary forwarding gates consume forwarding quota; valid received traffic still supplies abuse evidence when another gate blocks it.

The default is 128 full-key slots (18,432 bytes of entry storage plus small bookkeeping), 96 on nRF52 (13,824 bytes, preserving the mOTA runtime RAM reserve), or eight on RAM-constrained STM32 builds (1,152 bytes). FLOOD_ADVERT_SOURCE_SLOTS overrides the capacity at compile time. When full, the least-recently-heard normal key is removed first. Valid retained duplicates update last-heard too. Evicting a normal key discards its ordinary quota history, so capacity should still be sized for the deployment's active origins. First-strike evidence and bad-list entries are protected from eviction: if every slot protects abuse history, untracked origins are not forwarded until a slot expires or an administrator clears history. Table churn cannot erase an existing bad-list penalty. No heap allocation or per-packet filesystem writes are used.

"},{"location":"flood_filtering/#before-making-changes","title":"Before making changes","text":"

On a repeater, show the current forwarding controls:

get repeat\nget flood.max\nget flood.max.unscoped\nget flood.max.advert\nget flood.channel.data\nget flood.channel.data.hops\nget flood.channel.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.rule\nget flood.filter.blacklist\nget flood.moderation\n

flood.rule is an alias for flood.filter, not another table. Generalized repeaters have 63 forward-rule slots plus scope-rewrite and shared-blacklist sections in the same atomic policy file. FULL room servers have 31 forward slots and empty repeater-only sections. Compact target profiles retain their separate FPF6-era controls. flood.moderation has 16 slots. A new repeater FPF7 table starts with ota all suspend=tempradio in slot 1 and an authenticated #wardriving hops=5+ radio drop in slot 2, and an authenticated #wardriving hops=all mode=bridge,cross drop in slot 3. FULL room servers seed only the OTA row; their generalized engine also accepts transport-mode rules. flood.moderation starts empty. A row can opt into suspend=tempradio; temporary radio is not synonymous with OTA and can carry normal packet types too. A corrupt or truncated table fails open, so corrupt storage does not silently enable blocking.

On a FULL ESP32 room server, use get flood.rule (or get flood.filter) for the available table. Remote rule changes require room-server administrator access. flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= condition on a room server.

"},{"location":"flood_filtering/#force-floods-into-a-transport-scope","title":"Force floods into a transport scope","text":"

flood.channel.scope can add a scope to a received unscoped flood or replace the scope of a transport-scoped flood before this repeater forwards it:

set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nget flood.channel.scope\nget flood.channel.scope.<slot>\ndel flood.channel.scope.<slot>\ndel flood.channel.scope all\n

Use fs in place of flood.channel.scope for any of these commands. Its optional fields also accept p=bl for path=blacklist, p=b1 through p=b6 for path=bucket:1 through path=bucket:6, and tx=s/tx=f for slow/fast.

The channel may be public, #channel, or a 128/256-bit hex key. A bare target names an existing region with a usable transport key. Use scope=<name> instead to derive a regionless public hashtag scope exactly as flood.filter scope=<name> does. The direct name is normalized with a leading #, may contain up to 30 characters, and does not need a region-list entry. Keyed rules first check the one-byte channel hash carried in the packet, then validate the MAC by decrypting with the configured channel key. A hash collision alone cannot force a scope.

For example, this authenticates only #rgdata and rewrites it to #BlackHole86 without creating a region:

set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope.1\n

If that channel arrives scoped to #usa, the rule replaces #usa with #BlackHole86. It also handles unscoped packets and replaces any other incoming scope; the source scope is not a condition on the rule.

Add path=blacklist to make a channel-scope row eligible only when the received path matches the passive flood.filter.blacklist ID table. It does not require an enabled flood.filter drop row. With 3-byte paths, one exact listed ID qualifies. With 2-byte paths, two received path entries must match the first two bytes of listed IDs. A 1-byte path never qualifies.

Use path=bucket:<1-6> to match one of the existing flood.retry.bucket tables instead. Each bridge bucket holds up to 17 three-byte IDs and remains usable by channel scoping while flood.retry.bridge is off. Bucket matching uses the same thresholds as the blacklist: one exact hit for 3-byte paths, two qualifying entries for 2-byte paths, and no matches for 1-byte paths. Channel scoping reads the configured IDs directly; recent.repeater freshness and flood.retry.ignore do not change this match.

There are three independent wildcard classes:

login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable targets always take precedence over txt:*, even if that wildcard has a lower slot number. Within the exact class, matching path-qualified rows are tried before ordinary fallback rows. The same qualified-then-fallback order applies within each wildcard class. A missing or unusable target is skipped, so later rows remain eligible. The lowest usable slot wins within each priority tier.

For example, this uses bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, and assigns west to all other authenticated public packets:

set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n

More 3-byte IDs can be added to bucket 1 later. Any one of them qualifies the east row. Replacing or clearing that bucket changes which paths qualify but leaves both channel-scope rows intact. Bridge retry does not need to be enabled.

On a successful match, an unscoped route changes from ROUTE_TYPE_FLOOD to ROUTE_TYPE_TRANSPORT_FLOOD; an already-scoped route remains transport-flood but receives replacement codes. Transport code 0 is calculated with the target region or direct hashtag key over the payload type and payload, and code 1 becomes zero. The change happens before region enforcement, forwarding filters, and deduplication. Consequently flood.max.unscoped no longer applies to a packet converted from unscoped, while flood.max, flood.filter, loop detection, and moderation still apply to every rewritten packet. A region target must be flood-allowed. A regionless target is trusted for this matched receive pass, but it neither creates a region nor changes how unrelated packets with the same transport code pass the region gate. By default, if the selected scope differs and the rewritten packet passes those checks, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal outbound queue priority, and force the maximum txdelay factor of 2.0. The actual transmit delay is still randomized, from zero through ten packet airtimes. It does not preempt an active radio transmission or bypass CAD and airtime-budget limits. Selecting the scope already present is a no-op and does not grant special treatment.

Direct routes are never rewritten. Standard traceroute is direct-routed and therefore remains outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and the normal region/unknown-code gates still apply. Scope assignment does not override normal payload validation or make an otherwise non-forwardable packet type forwardable.

LoRa OTA (0x0C) falls under other:*. A matching row adds the selected transport code or replaces the existing one, but OTA still operates normally during the temporary-radio window because the OTA handler accepts both unscoped and transport-scoped flood routes. A region target must allow flooding; a direct target uses the regionless trust behavior above. A new repeater also seeds ota all suspend=tempradio in flood-filter slot 1. That visible rule blocks OTA forwarding at every received hop outside temporary-radio operation and is skipped while temporary radio is active. Independently, the OTA core refuses OTA receive, relay, and transmit outside an actually active temporary-radio window, even if the seeded row is deleted or replaced.

Capacity is selected at build time:

Classic ESP32 uses the 31-slot default even on boards with PSRAM because these tables live in internal static DRAM. The separate channel-requirement table defaults to the same capacity and uses another 34 bytes per slot. Together, the 31-slot defaults save 15,712 bytes of static RAM compared with 255 slots. Custom builds can override FLOOD_CHANNEL_SCOPE_SLOTS and FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS with build flags.

Each rule retains its 36-byte record. A separate table holds 32-byte normalized names for up to the smaller of the rule count or 32 distinct regionless targets, except that very-tight STM32WL builds retain one reusable direct target. Both configured regions and regionless targets can be reused by any number of rules.

On generalized builds these records are the shared rewrite phase, and the file stores only through the highest occupied slot. Compact FPF6 builds retain the standalone FCS5 file and the file sizes described by their build profile.

"},{"location":"flood_filtering/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"

flood.channel.scope.require changes region enforcement for received flood GRP_TXT and GRP_DATA packets from a global policy to a channel opt-in policy:

set flood.channel.scope.require <public|#channel|128/256-bit-key>\nset flood.channel.scope.require.<slot> <public|#channel|128/256-bit-key>\nget flood.channel.scope.require\nget flood.channel.scope.require.<slot>\ndel flood.channel.scope.require.<slot>\ndel flood.channel.scope.require all\n

fsr is the short name for flood.channel.scope.require, with the same arguments, slots, and permissions.

An empty table preserves the normal global region behavior. Once at least one row exists, a group-channel packet that authenticates against a listed key must arrive as ROUTE_TYPE_TRANSPORT_FLOOD with a transport code matching a locally flood-allowed region. An unscoped packet, an unknown transport code, or a code for a denied region is not retransmitted. The check uses the original incoming scope before flood.channel.scope or flood.filter scope= can rewrite it. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.

Group-channel packets that do not authenticate against any listed key bypass the region/unknown-code forwarding gate. They still pass through repeat, flood.max*, flood.filter, loop detection, payload validation, and moderation. Non-channel flood payload types retain the normal global region behavior. A one-byte channel-hash collision is only a prefilter; the packet must also pass MAC validation/decryption with the configured key.

Without .slot, setting an existing key updates its row and a new key uses the first empty row. Numbered set replaces that slot. Detail output displays only the first four derived hash bytes and key size, never the secret. The table has the same build-dependent slot count as flood.channel.scope; each row consumes 34 bytes of RAM and storage, plus a five-byte file header. ACL permission 4 can manage it.

For example, this requires an allowed incoming scope on #bot, while every other group channel bypasses region enforcement:

set flood.channel.scope.require #bot\nget flood.channel.scope.require\n
"},{"location":"flood_filtering/#interaction-with-duplicate-detection","title":"Interaction with duplicate detection","text":"

The seen-packet hash contains the payload type and exact payload bytes. It does not contain the route type, either transport code, or the ordinary flood path. For TRACE only, the encoded path_len byte is also included. Therefore an unscoped packet and the same packet after this repeater adds a transport code are the same duplicate. A later copy with a different scope is also the same duplicate; changing or adding scope cannot evade the seen table.

When equivalent non-TRACE flood copies overlap in rxdelay, the normal receive-quality timing still chooses the packet to process, but that winner takes a scope from the queued copies whose transport code matches an allowed region in this repeater. Unknown and denied scopes are ignored. If eligible copies have different scopes, the shortest received path supplies the scope. Equal path lengths prefer the deepest matching child region (the narrowest configured scope). A remaining tie keeps queue order. The winner keeps its own path, SNR, and delay schedule; only its route and transport codes can change, including replacement of a less-preferred scope it already carried.

Scope selection happens at dequeue so the original scopes remain available for comparison. It applies only while copies are queued and cannot alter a copy already processed into the seen table. Flood-form TRACE participates in the same arbitration. Direct traceroute never enters this flood queue.

A packet that already matches a configured fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses this inbound rxdelay queue entirely. A tx=slow row remains in the queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.

"},{"location":"flood_filtering/#runtime-flood-rules","title":"Runtime flood rules","text":"

On repeaters with the rule engine enabled and on FULL-profile ESP32 room servers, fr, flood.rule, and flood.filter name the same persistent table. The evaluator is fixed firmware, but every row is data, so an authenticated operator can add, replace, inspect, or delete a row without an OTA or reboot. Existing flood.filter commands remain compatible. FPF6, FPF7, and FPF8 files are accepted; FPF1-FPF5 files are rejected and filtering fails open. New sender/text, payload-length, route, probability, enabled-state, or extended-path fields are stored in FPF8. Tables using only older fields retain FPF7. Firmware without FPF8 support cannot load an FPF8 table; remove the new fields before downgrading and keep a copy of the rules. Older firmware cannot preserve an FPF7 table containing the packed retry action, one-byte hash:XX matcher, or mode=bridge/cross flags; remove those rows before downgrading. Old firmware rejects such a table and fails open, which disables its other filter rows too. New firmware loads older rows as mode=radio without changing their behavior.

The former flood.channel.block table is now represented by ordinary FPF7 rows. On a generalized repeater, an existing FCB2 file is imported once into free FPF7 slots and then removed. For example, an old #wardriving h=4 row becomes type=any channel=#wardriving hops=5+ drop. The 63-row table can hold all 31 former general filter rows, all 15 legacy channel-block rows, and the migrated global flood.channel.data gate. Compact STM32WL FPF6 builds cannot match authenticated channels and retain the older separate gate.

On generalized repeaters, flood.channel.data* is a compatibility view over one ordinary visible FPF7 type=grp_data ... drop row. Turning it off creates or updates that row; turning it on removes the row. Its hop setting maps to hops=all or hops=N+1+. There is no hidden GRP_DATA forwarding check ahead of FPF7. Normal ordering applies, so a matching higher-priority stop row can exempt selected traffic. The compact rule list marks the managed row with ~data.

FPF7 and FPF8 bind in=region:<name> and region=<name> to canonical region names, not numeric region IDs. Removing, reordering, or reusing a region ID cannot silently redirect a rule. If the saved name is missing, an input-region match does not match and a target-region rewrite is skipped. Re-adding the same name reactivates the rule.

The extended form is:

set flood.rule[.<slot>] type=<type> [mode=radio|bridge|cross|bridge,cross]\n    [hops=<range>] [channel=<channel>] [hashbytes=any|1|2|2+|3]\n    [prefix=<ID[,ID...]>|path=[^]ID>ID...[$]] [in=<input-scope>]\n    [sender=<pattern>] [text=<pattern>] [len=<range>] [route=flood|direct|any]\n    [prob=<1-100>] [enabled=on|off] <action> [rate=<N>/min]\n    [priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]\nget flood.rule\nget flood.rule.<slot>\ndel flood.rule.<slot>\ndel flood.rule all\n

The command must be entered on one line. Match fields in one row are ANDed. Radio rows are matched against the same immutable packet state captured on receive, before any rule rewrites its scope. Bridge/cross rows match at their transport boundary. Only rows for the current mode participate in ordering and stop processing. Matching rows are then processed by descending numeric priority. At an equal numeric priority, authenticated channel matches run before raw hash:XX matches, raw hashes run before channel=*, and lower slot number breaks the remaining tie. Priority defaults to 0; an explicitly higher numeric priority overrides specificity.

Numbered examples replace the selected slots, including factory defaults. Omit the slot number to add a rule without replacing an occupied slot.

The first matching stop row ends the FPF7 forward phase after that row. Higher-order matches and the stop row still apply; lower-order matches do not. A stop-only row is therefore an exception to lower-priority FPF7 rows. It cannot undo a higher-priority drop and it does not bypass hard forwarding gates or the scope-rewrite and moderation phases. Without a stop row, matching drop and rate rows remain independent and the highest-order matching scope or region rewrite wins.

Match fields:

Saved older prefix rules keep their start-anchored meaning. When retyping an older path=A1,B2 command, use prefix=A1,B2 or pa=^A1>B2 to retain it. - sender= (n=) and text= (x=) match decrypted group text. An explicit authenticated channel (public, #name, or a channel key) is required; a raw channel hash is insufficient. See content patterns. - len= (l=) matches payload bytes, using N, N+, or inclusive N-M. It excludes the route/path envelope; encrypted padding is part of the payload. - route= (rt=) selects flood, direct, or any; f, d, and a are short values. Omitted radio rules remain flood-only. Direct/any rows may drop, rate-limit, or stop; scope/region rewriting, retry, and TX timing require flood-only radio rows. - prob= (pr=) applies this rule to approximately that percentage of otherwise-matching packets. Values are 1-100; omitted means 100. A failed roll lets later rules run. This is sampling, not a delivery quota. The decision stays the same for the same packet and row during one boot, including repeated timing checks and scope rewrites. - enabled=off (e=off) retains a row without evaluating it. on/1 enables and off/0 disables. Change only this state with set fr.<slot> e=off and set fr.<slot> e=on; the rest of the row stays saved. - in=any|none|scoped|allowed|unknown|scope:<name>|region:<name> tests the original incoming route before any rewrite. none means an unscoped flood; scoped means any transport flood; scope:name compares the exact public hashtag-derived scope; and region:name compares an allowed configured region. allowed is the legacy require=region test and includes an unscoped packet when the wildcard region allows it. unknown means a scoped packet that does not resolve to an allowed local region.

Actions:

"},{"location":"flood_filtering/#content-patterns","title":"Content patterns","text":"

Sender/text patterns are case-sensitive byte matches. They search anywhere unless anchored with ^ and $. Supported forms include ., [A-Z], [^0-9], *, +, ?, grouping, and | alternatives. Escape punctuation with \\ to match it literally; use [0-9] for digits. Each pattern is at most 63 bytes. Quote values containing spaces.

For Alice: BEACON 123, n=^Alice$ matches the display name and x=^BEACON matches the body. They can be combined in one row. The parser uses the complete fields, including names longer than 31 bytes, and removes AES zero padding before matching. A message without : has no sender field; a text rule can still inspect its body. Wrong keys and non-text payloads do not match content rules. Pattern matching shares a 131,072-operation budget across the rules in one evaluation. Exhausting it rejects the packet rather than granting an unverified allow, rewrite, or retry action. Keep patterns short and specific.

# Block BEACON messages on this channel.\nset fr grp_txt c=#local x=\"^BEACON\" d\n\n# Exempt Alice from lower-order general rules, then block everyone else.\nset fr grp_txt c=#local n=\"^Alice$\" pri=200 a\nset fr grp_txt c=#local d\n\n# Temporarily disable an occupied row without deleting its settings.\nset fr.4 e=off\nset fr.4 e=on\n\n# Block floods whose most recent relay has this 3-byte ID.\nset fr any pa=A1B2C3$ d\n

The allow exception remains subject to separate moderation and normal forwarding gates. A sender name is freely chosen message text, not a verified public-key identity. Legacy flood.moderation keeps its existing exact, case-insensitive name matching.

Rows using the same channel key reuse the initial authentication verdict. Content patterns also decrypt into temporary scratch for matching. Neither the verdict cache nor plaintext survives the receive evaluation. A hash:XX row skips authentication and uses only the one visible byte.

The global retry controls remain hard gates. A retry row does not override a zero flood.retry.count, path/type attempt caps, disabled forwarding, flood.retry.advert, a drop verdict, or any other forwarding rejection. flood.retry.bridge independently chooses ordinary retry or bridge-bucket completion for an allowed packet. The selector applies to received floods; locally originated floods retain the normal global behavior.

The exact requested examples are:

# If #rgdata arrives unscoped with more than 3 received hops, add\n# the regionless #BlackHole86 scope.\nset flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\n\n# Rewrite the exact incoming #usa scope to #BlackHole86 for #rgdata.\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\n\n# Match a two-byte source-path prefix and cap forwarding at 10 per minute.\nset flood.rule.4 type=any prefix=860C rate=10/min\n\n# Keep authenticated #rgdata at two hops or less out of lower-priority FPF7\n# rules. Hard gates and separate tables still apply.\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\n\n# Retry received Public and #hamradio floods, using bridge completion.\n# Once these rows exist, other received floods are not retried.\nset flood.retry.bridge on\nset flood.rule.6 type=any channel=public retry\nset flood.rule.7 type=any channel=#hamradio retry\n\n# Fall back to an unauthenticated one-byte match if only the visible hash is known.\nset flood.rule.8 type=any channel=hash:A7 retry\n\nget flood.rule.2\nget flood.rule.3\nget flood.rule.4\n

Public's visible channel hash is 0x11, so a hash:11 drop row also matches Public unless an earlier exact-channel rule stops it. This standalone example preserves authenticated Public while dropping other packets that use the same visible byte:

set flood.rule.2 type=any channel=public retry stop\nset flood.rule.3 type=any channel=hash:11 drop\n

The Public rule's MAC/decrypt check must succeed before its stop applies. A colliding channel misses that rule and reaches the hash drop. With equal numeric priorities, authenticated-channel specificity automatically puts the Public rule first regardless of slot order. Without stop, both rows match Public and the sticky drop wins. Remove retry from the Public row if the exemption should not also opt Public into flood retry. An explicitly higher numeric priority on the hash rule remains an operator override.

The 240 KB STM32WL profiles keep MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the persistent compact FPF6 flood.filter and blacklist syntax below. They still perform filtering, but omit the generalized flood.rule parser and extended fields. No partition size is changed by this feature.

The compatible filter and blacklist commands are:

set flood.filter.blacklist <ID[,ID...]>\nset flood.filter.blacklist.<slot> <ID[,ID...]>\nget flood.filter.blacklist\nget flood.filter.blacklist.<slot>\ndel flood.filter.blacklist\ndel flood.filter.blacklist.<slot>\nset flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nset flood.filter.<slot> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nget flood.filter\nget flood.filter.<slot>\ndel flood.filter.<slot>\ndel flood.filter all\n

The blacklist holds up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds. Each is written as six hexadecimal digits. For example:

set flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter any all path=blacklist\n

This is intended for abuse containment, such as refusing to retransmit floods that repeatedly enter the mesh through known internet gateways dumping bulk traffic. The list is shared by every FPF7 row and scope-rewrite row that uses path=blacklist; it is not copied into each rule.

An unnumbered set replaces the list with up to 18 IDs, the largest command that fits every CLI transport. A numbered set writes a batch of up to 18 IDs beginning at an existing slot or the next consecutive slot. This is how an ESP32 list grows beyond 18. Deleting a numbered entry compacts the entries after it. Unnumbered get reports the total and prints the leading IDs that fit; numbered get retrieves one specific entry.

path=blacklist is an unordered precondition on that row. With 3-byte path hashes, one or more exact blacklist hits qualifies the packet. With 2-byte path hashes, two or more received path entries must match the first two bytes of listed IDs. Each received entry is counted at most once. A 1-byte path never qualifies. The IDs may occur anywhere in the received path; neither their list order nor their path order matters.

Without a slot number, set reuses an identical row or selects the first empty slot. Use a slot number to replace a row whose match or action is changing. Omitting the hop expression means all (0-63).

Numbered get flood.rule.N and get flood.filter.N normally use descriptive field names. On generalized builds, a reply that would be too long uses the complete set fr.N ... spelling below. get fr.N always requests that spelling. If even the short command exceeds one reply, it returns Err - compact command exceeds reply size instead of a partial command. Saving such a rule still succeeds and reports that its details exceed the reply size.

"},{"location":"flood_filtering/#shorthand-for-copying-rules","title":"Shorthand for copying rules","text":"

On generalized repeaters and FULL ESP32 room servers, fr is a shorter name for flood.rule. Both forms use the same parser, validation, permissions, and saved rows. Short and full field names can be mixed in either command. fr requires an explicit action, just like flood.rule.

These commands set exactly the same rule:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nset fr.3 any c=#wardriving m=bc d\n
Full form Short form Meaning flood.rule[.N] fr[.N] Same rule table for get, set, and del. type=any any or * First token: any payload type. Named/numeric types also work, with optional t=. hops=N, N+, N-M, or all h=N, h=N+, h=N-M, or h=* Hop condition; a bare range also works. Omitted means all hops. mode=radio, bridge, cross, bridge,cross m=r, m=b, m=c, m=bc Select the admission path. Omitted means radio; cb, b,c, and c,b also mean both transport paths. channel=... c=... Same authenticated name/key or explicit hash:XX matcher. prefix=... p=... Ordered path prefix. path=[^]ID>ID...[$] pa=... Ordered chain anywhere, or anchored at either end. sender=..., text=... n=..., x=... Case-sensitive patterns on an authenticated group-text channel. len=N, N+, or N-M l=... Payload-byte range. route=flood, direct, any rt=f, rt=d, rt=a Route class. prob=N, enabled=on/off pr=N, e=1/0 Sample matches or retain a disabled row. hashbytes=any, 1, 2, 2+, or 3 pb=*, 1, 2, 2+, or 3 Encoded path-hash width, even with zero hops; 2+ matches 2 and 3. path=blacklist p=bl Shared unordered blacklist; repeater only. in=any, none, scoped, allowed, unknown i=*, i=n, i=s, i=a, i=u Incoming scope condition. in=scope:name, in=region:name i=s:name, i=r:name Match a specific incoming scope or region. drop d Drop matching traffic. scope=name, region=name s=name, r=name Rewrite to a public scope or configured region. rate=N/min q=N Per-row forwarding allowance. priority=N pri=N Higher values run first. stop, retry s, r Stop later rules or allow the configured flood retry. allow, forward a Same action as stop; other forwarding gates still apply. tx=slow, suspend=tempradio, retry f=s, f=t, f=r Flags can combine, for example f=str.

The = distinguishes s=name (scope) from s (stop), and r=name (region) from r (retry). Short forms keep the same action restrictions; for example, m=bc r is rejected because transport rules cannot enable retries.

"},{"location":"flood_filtering/#filter-every-1-byte-flood-path","title":"Filter every 1-byte flood path","text":"
set flood.rule type=any hashbytes=1 drop\n

The equivalent short command is set fr any pb=1 d. This adds a rule in a free slot without replacing the default rules. It stops this node forwarding all 1-byte flood packets, including advertisements and relayed login/admin traffic; direct routes and local delivery are unchanged. Use a narrower type= or channel= if those floods must remain reachable. Use hashbytes=2 or 3 for an exact width, or set fr any pb=2+ d to drop both larger supported widths with one rule. Use get fr.<slot> to copy the saved command.

Width-only rules require updated firmware. Existing rules remain unchanged and the table uses no additional RAM or storage. Before downgrading to firmware without this selector, remove width-only rules: its older reader considers them invalid and can reset the rule table to defaults.

get fr.3\n

Returns a complete command ready to copy:

set fr.3 any m=bc c=#wardriving d\n

The compact reply omits default values and prints specific payload types as numbers. get fr lists the table. set fr ... without a slot reuses an identical rule or adds it to the first empty slot. del fr.3 removes slot 3; del fr all clears the forward rules.

Private keys are still hidden in replies. A displayed c=key:XXXXXXXX is a local reference to the exact key in an active rule on that device, so it can be pasted to edit or copy a rule there. On another device, or after deleting the last rule holding that key, supply the original key instead. An unknown or ambiguous reference is rejected; it never becomes an unauthenticated channel-hash match.

Other repeater families have short names too, with identical arguments, slot numbers, validation, persistence, and permissions:

Full family Short family Commands flood.moderation fm get, set, del flood.channel.scope fs get, set, del flood.channel.scope.require fsr get, set, del flood.filter.blacklist fb get, set, del flood.advert fa get, clear

For example, get fa key 1 inspects a full advert key and clear fa all clears the advert limiter history. Companions do not expose these repeater families. Compact FPF6 profiles retain their compiled legacy commands and their family aliases, but do not expose fr or the new generalized fields.

"},{"location":"flood_filtering/#scope-actions-and-legacy-rules","title":"Scope actions and legacy rules","text":"

A legacy row without scope= is the existing drop action. On extended builds, an explicit drop has the same result, while rate= by itself creates a rate-only row. A row with scope= adds transport scope to an unscoped packet or replaces the codes on an already-scoped packet. The scope name is normalized with a leading #, and the 128-bit transport key is derived directly from that hashtag. The name does not need to exist in the region list and is not added to it. Public names up to 30 characters are accepted; private $ scopes are not.

require=region is the legacy spelling of in=allowed. The repeater evaluates the packet's original route before any rewrite in that receive pass. An incoming transport scope must match a locally allowed region; an unscoped flood must be allowed by the wildcard region. If the check fails, that row is skipped, the filter does not grant a region bypass, and the unchanged packet is allowed to fail normal region enforcement. Other independently configured scope rows still apply in their normal order.

When multiple scope or region rows match, the highest numeric-priority row wins. Equal priorities use authenticated channel, raw hash, then wildcard specificity before the slot-number tie-break. Rewrite rows do not approve a packet: any matching drop row and every remaining forwarding gate can still reject it. A filter-assigned scope is trusted without local region-list validation, but repeat, flood.max, loop detection, and moderation still apply. By default, a changed scope bypasses inbound rxdelay, then is retransmitted with zero txdelay and the highest outbound queue priority. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal queue priority, and force the maximum txdelay factor of 2.0; the randomized transmit delay ranges from zero through ten packet airtimes. tx=fast explicitly restores the default. Selecting the scope already present does not grant special treatment. Active radio transmission, CAD, and airtime-budget limits are unchanged.

On generalized repeaters, forward rules, flood.channel.scope rewrite rows, the shared blacklist, and the flood.channel.data compatibility state are one atomic FPF7/FPF8 policy image. Existing /flood_ch_scope, /flood_filter_bl, FPF6, and FCB2 data is imported once; the old files are removed only after the new image verifies and commits. Compact FPF6 repeaters retain separate files. Deleting the blacklist leaves path=blacklist rows in place but dormant until IDs are configured again. Path hashes are truncated routing identifiers and are not authenticated proof that a particular repeater\u2014or a particular person\u2014handled a packet. FULL room servers reject blacklist commands.

On first initialization, flood-filter slot 1 is seeded with:

set flood.filter.1 0x0C all suspend=tempradio\n

This is a normal editable row. After the table has been saved, deleting it remains persistent across reboot; the firmware does not recreate it. Run the same command to restore the exact seeded row, or omit .1 to preserve existing slot assignments and use the first empty slot. Operators may add suspend=tempradio to any other row that should be skipped while either tempradio or tempradio2 is active. This includes scheduled temporary profiles. The row resumes once both temporary sessions have ended.

Suspension does not approve a packet or bypass the rest of the filter table. It skips that row, then evaluation continues with the next row and the remaining forwarding gates. An ordinary drop any row therefore still applies during the temporary-radio window unless an earlier matching stop row ends FPF7 processing. repeat, flood.max*, region handling, loop detection, and the OTA subsystem's own hop limit also remain in force.

Standard traceroute uses direct routing and bypasses mode=radio filters. Explicit bridge/cross filters can also match direct traffic at those transport boundaries; the built-in channel rule does not match TRACE packets. For a custom flood-form trace, type=any, explicit trace, scope, region, rate, and drop rows all behave normally. The stock core does not normally flood-forward TRACE packets.

"},{"location":"flood_filtering/#remote-administration-lockout-warning","title":"Remote-administration lockout warning","text":"

There are no hidden payload-type or short-hop exemptions in FPF7. Drop and rate rules can block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so.

A flooded login starts as ANON_REQ; its reply is commonly a PATH packet carrying an encrypted RESPONSE. Before a direct return path is established, administrative replies and CLI text can also be flooded. Transit repeaters do not have the session key and cannot distinguish those encrypted admin exchanges from ordinary peer packets with the same outer type. A rule therefore affects the complete outer packet class, not only packets that ultimately authenticate as administrators. Keep a serial or other recovery path and stage broad deny/rate rules carefully.

Hop expressions are based on the path count when this repeater receives the packet:

Examples:

# Stop forwarding group data once it arrives with four or more path entries.\nset flood.filter grp_data 4+\n\n# Stop long adverts, while still allowing shorter adverts.\nset flood.filter.2 advert 6+\n\n# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.\nset flood.filter.3 ota 2-4\n\n# Assign #local scope to group text without requiring #local in the region map.\nset flood.filter grp_txt all scope=local\n\n# Rewrite only packets whose incoming region was already acceptable.\nset flood.filter grp_data all scope=local require=region\n\n# Rewrite matching blacklisted paths without fast-tracking their retransmission.\nset flood.filter grp_data all path=blacklist scope=local tx=slow\n\n# Drop matching flood types after the unordered path blacklist qualifies.\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter any all path=blacklist\n\n# Apply a hard ceiling to flood payload types at 12 or more received hops.\nset flood.filter any 12+\n
"},{"location":"flood_filtering/#high-traffic-mesh-example","title":"High-traffic mesh example","text":"

This preset limits request and group-data propagation early while allowing the login-capable response, anonymous-request, and path types to travel farther:

set flood.filter req 3+\nset flood.filter response 9+\nset flood.filter 0x06 3+\nset flood.filter 0x07 9+\nset flood.filter path 9+\nset flood.filter control 1+\nget flood.filter\n
Rule Stops retransmission when received with req 3+ 3 or more path entries response 9+ 9 or more path entries 0x06 3+ (grp_data) 3 or more path entries 0x07 9+ (anon_req) 9 or more path entries path 9+ 9 or more path entries control 1+ 1 or more path entries

On a new generalized repeater table, the three factory rules occupy slots 1 through 3, so these unnumbered commands normally fill slots 4 through 9. FULL room servers and compact tables seed only OTA and use slots 2 through 7. Existing tables may choose different free slots. All six rules take effect at the thresholds shown; there are no hidden short-hop exceptions. The Control rule allows a flood received with path count 0 to be forwarded once, then stops it at the next repeater. Normal node-discovery Control packets are direct zero-hop packets and bypass these mode=radio rules. These rules affect only retransmission by the repeater; local reception and logging remain unchanged.

Accepted payload names are:

Value Short name Full name 0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA in this fork 0x0D 13 reserved 0x0E 14 reserved 0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM

Decimal values 0 through 15, hexadecimal values 0x00 through 0x0F, the full PAYLOAD_TYPE_* names, and any are also accepted. Upstream currently reserves 0x0C; this fork assigns it to LoRa OTA. Rows are suspended during temporary-radio operation only when explicitly configured that way.

"},{"location":"flood_filtering/#moderate-group-text-by-channel-and-username","title":"Moderate group text by channel and username","text":"

Use flood.moderation for flood GRP_TXT messages. The repeater validates and decrypts the selected channel, extracts the display name before the first :, then applies the rule:

set flood.moderation <channel> <sender> <action> [action...]\nset flood.moderation.<slot> <channel> <sender> <action> [action...]\nget flood.moderation\nget flood.moderation.<slot>\ndel flood.moderation.<slot>\ndel flood.moderation all\n

Channels can be specified as:

The key is stored locally so packets can be authenticated and decrypted. It is not included in get flood.moderation output.

Available actions are:

fm is the short family name. Its option aliases are d for drop, q=N for rate=N/min, h=N for hops=N, and p=... for path=.... q=N/min and q=N/m are also accepted; mixed duplicate rate/drop options are rejected just as in the full form.

At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. rate=0/min is equivalent to drop.

"},{"location":"flood_filtering/#per-user-per-channel-rate-limits","title":"Per-user, per-channel rate limits","text":"

Rate limits require an exact username; * is not accepted for a rate rule. Username comparison is ASCII case-insensitive, and names containing spaces must be quoted. A rule's counter is independent from rules for the same name on other channels, so this directly supports \"X messages per minute from user X on channel Y.\" Counters are local to this repeater and reset on reboot.

# At most five Public-channel messages per minute from this display name.\nset flood.moderation public \"Noisy User\" rate=5/min\n\n# A separate limit for the same name on #local.\nset flood.moderation #local \"Noisy User\" rate=10/min\n\n# Combine a rate limit with a maximum forwarding distance.\nset flood.moderation public alice rate=4/min hops=5\n\n# Same rule using short names.\nset fm public alice q=4 h=5\n

The wildcard sender * works with a hop restriction, including for names longer than 31 bytes. Exact configured names remain limited to 31 bytes. Moderation settings retain the FGM1 format and use staged writes, readback, and backup recovery. A failed save is reported as unconfirmed because a failure after publication can leave the new settings available on reboot.

"},{"location":"flood_filtering/#match-the-start-of-a-path","title":"Match the start of a path","text":"

Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule must use the same width, and matching always starts at the beginning of the received path:

set flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation public alice rate=3/min path=71\n

A path-qualified rule cannot match a zero-hop packet and does not match until the packet contains all path entries listed by the rule.

"},{"location":"flood_filtering/#how-the-forwarding-controls-combine","title":"How the forwarding controls combine","text":"

A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:

  1. flood.channel.scope.require evaluates a listed group channel against the original incoming scope; unlisted group channels bypass the later region gate while the table is active.
  2. flood.channel.scope tries a path-qualified channel row before that channel's ordinary fallback, then adds or replaces the scope from either a configured region or a direct scope=<name> target.
  3. All extended flood.rule match fields are evaluated against the same original incoming packet. Matches are ordered by descending numeric priority, then authenticated/raw-hash/wildcard channel specificity, then ascending slot. The first matching stop row removes every later FPF7 match. The highest-order remaining scope= or region= row may replace the channel-scope result; a direct scope does not require a region-list entry.
  4. repeat and flood.max* are checked.
  5. The FPF7 forward phase, including any row managed through flood.channel.data*, applies drop and rate decisions using that saved match result. No packet type or short-hop range is silently exempted.
  6. Region and loop-detection rules are checked; a regionless scope assigned by either table is already trusted when it has no region-list match, except that it cannot rescue a channel rejected by flood.channel.scope.require.
  7. flood.moderation checks decrypted group text, username, rate, hops, and path. If it accepts the packet, matching general-rule rate counters are charged immediately before retransmission is approved.

The first denial is enough to prevent retransmission. A packet that is denied can still appear in local logs or MQTT output, unless a separate mode=bridge rule blocks MQTT packet publication. Moderation runs last because its rate counters are charged only for packets that pass every other forwarding control and will actually be retransmitted.

"},{"location":"flood_filtering/#delegate-filter-management","title":"Delegate filter management","text":"

On repeaters, ACL permission 5 is the filter-manager role:

setperm <companion-public-key-hex> 5\n

A filter manager can read non-secret operational status and manage repeat, loop.detect, flood.max*, flood.channel.data*, flood.filter*, flood.rule*, fr[.N], and flood.moderation*, including the fm, fb, and fa aliases. Delegated get access uses an explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or unrelated administrator settings. Because flood.filter scope= derives a public hashtag key directly, a filter manager can configure that action without region-manager permission; it still cannot edit the region hierarchy.

FULL ESP32 room servers use their existing administrator check for remote fr, flood.rule, and flood.filter commands; they do not grant this table through permission 5.

ACL permission 4, the region/scope-manager role, can read, add, replace, and delete flood.channel.scope and flood.channel.scope.require rows and manage regions. This lets the same delegate create target regions, assign forced scopes from regions or direct hashtag names, and select the channels that require valid incoming scopes. The fs and fsr aliases retain this role.

Admin CLI sessions and these delegated roles use the same setters and validation as the serial console. On enrolled builds, the aliases also work inside signed fleet commands, for example:

fleet send 3 set fr.4 grp_txt c=public x=\"^BEACON\" d\nfleet send 3 set fr.4 e=off\n

Fleet enrollment, signature, expiry, replay, and command-size checks still apply. An ordinary channel message cannot authorize a filter change.

"},{"location":"flood_filtering/#security-limitations","title":"Security limitations","text":"

Public and hashtag channels use shared, well-known keys. A valid channel MAC proves that the sender knew the channel key; it does not identify a person. The <sender> value is an unverified display name and can be spoofed. Path hashes are truncated routing hints and can collide or be manipulated; they are not authenticated user identities.

Use username and path rules as traffic moderation, not as an authorization boundary. For a strict network boundary, combine these tools with region ACLs, private transport/channel keys, and controlled device access.

"},{"location":"flood_filtering/#filter-bridge-and-radio-crossover-traffic","title":"Filter bridge and radio crossover traffic","text":"

The mode= matcher selects a separate admission phase within the same flood.rule / flood.filter table:

Mode Where the rule applies radio Ordinary LoRa forwarding; flood-only by default, or opt into direct routes with route=. This is the default mode. bridge Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. cross Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. bridge,cross Both transport paths using one rule. cross,bridge is equivalent.

The third built-in repeater filter is:

set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\nget flood.rule.3\n

The equivalent short command is set fr.3 any c=#wardriving m=bc d. Use m=b for bridge only or m=c for crossover only.

It authenticates both GRP_TXT and GRP_DATA against the public #wardriving channel key. A different channel with the same one-byte hash is not blocked. It covers every hop count, including zero-hop/local messages, flood routes and direct routes. It remains active during temporary-radio sessions and with radio2.cross on: enabling crossover makes a copy eligible, but does not override the filter. The second built-in rule still permits ordinary #wardriving forwarding through four received hops; the third rule keeps those messages on their originating radio profile.

Bridge filtering runs in both directions before bridge deduplication, inbound queueing, serial writes, ESP-NOW queueing, or MQTT packet publication. It applies with either bridge.source rx or bridge.source tx and with wrapped or raw ESP-NOW framing. MQTT here is the observer packet output; this does not add MQTT-to-LoRa injection or filter MQTT status/telemetry. Local reception and local packet logs remain available. Switching one radio between profiles is covered by cross; the bridge's WiFi channel is a separate setting.

Transport modes support drop, rate=X/min, priority=, and stop, with the existing type, channel, hops, prefix, blacklist, incoming-scope and suspend=tempradio matchers. Scope/region rewrites, retry actions, and TX timing actions are rejected in these modes. Ordering and stop apply only to rules in the current mode: a radio exception cannot bypass a bridge/cross drop. A combined rule shares one rate counter across bridge and cross admission attempts; retries count too. A rejected queue insertion can consume an admission allowance. Queued packets already admitted are not recalled by editing a rule. Same-profile transmissions use the ordinary radio policy.

get flood.rule.N shows mode=; the overview marks each row with its mode. Modes are persistent, case-insensitive, and apply immediately to new admission attempts. radio cannot be combined with bridge or cross in a single row. For transport rules, hop and scope matchers see the packet at that boundary: a TX/crossover packet can already contain an appended hop or rewritten scope. The built-in rule uses all hops and any scope, so these changes cannot bypass it. On FULL room servers, add a transport rule explicitly; their factory table still has only the OTA row. Companions and compact FPF6 builds do not expose this generalized filter table.

Saved tables, occupied slots, and deleted defaults are preserved on upgrade. To add the new default to an existing table without replacing another rule, omit the slot number (repeating the identical command reuses its row):

set flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop\nget flood.rule\n

To allow only bridge traffic while retaining the crossover block, replace the third rule with mode=cross. Use mode=bridge to retain only the bridge block. Use del flood.rule.3 to remove both, after checking that slot 3 is still the built-in rule. Removing it does not change the separate LoRa hop-limit rule.

"},{"location":"flood_filtering/#restore-the-factory-seeded-rows","title":"Restore the factory-seeded rows","text":"

The repeater's factory-seeded forwarding rows can be restored through the CLI:

set flood.rule.1 type=ota hops=all drop suspend=tempradio\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop\n

Or use the equivalent short forms:

set fr.1 ota d f=t\nset fr.2 any 5+ c=#wardriving d\nset fr.3 any c=#wardriving m=bc d\n

These commands explicitly replace the three seeded generalized-repeater slots. Inspect them first if they may now contain other rules. Compact FPF6 builds use only the first command's flood.filter.1 0x0C all suspend=tempradio form.

"},{"location":"flood_filtering/#remove-the-custom-rules","title":"Remove the custom rules","text":"

To save both tables in an empty state:

del flood.filter all\ndel flood.moderation all\nget flood.filter\nget flood.moderation\n

This does not change the older flood.max*, loop-detection, or region settings; inspect or reset those separately when troubleshooting.

"},{"location":"four_fixed_tx_dwell_validation/","title":"Four fixed tx dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"four_fixed_tx_dwell_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"four_fixed_tx_single_pass_validation/","title":"Four fixed tx single pass validation","text":"

View this page on MeshCore Docs.

"},{"location":"four_fixed_tx_single_pass_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"full_companion_features/","title":"Full companion features","text":"

View this page on MeshCore Docs.

"},{"location":"full_companion_features/#full-companion-turn-features-on-and-off","title":"Full Companion: turn features on and off","text":"

For side-by-side Companion and infrastructure commands, see feature switches by role. MQTT and logging controls differ between these roles.

Use the exact companion_radio_full image for your board. Full includes its supported transports and MOTA sending in one firmware; ordinary settings do not require rebuilding it. USB remains the normal way to update the Companion itself. WiFi/Bluetooth updates are optional, and Full does not install LoRa MOTA packages on itself.

New Heltec T114 Full images automatically detect the optional screen and also run without it. Choose Heltec T114 in the firmware picker for either board configuration; see the T114 update guide.

The Wireless Paper Full memory-sharing update keeps 350 contacts and 256 queued messages normally. During an mOTA session, 128 message slots are available while the other half holds its workspace. If more than 128 unread messages are queued, sync them with an app before starting mOTA. Disconnecting the source or turning it off returns all 256 slots. Idle WiFi mOTA listening does not borrow the queue. See the 1.17.1.5 release guide for the corrected download; the earlier aa20e927 Wireless Paper image has 150 contacts.

"},{"location":"full_companion_features/#open-the-text-terminal","title":"Open the text terminal","text":"

Close the app or serial monitor using the USB port, then open a terminal at 115200 baud, or use the USB web console. A fresh Full Companion boot or observable USB session reset starts in ASCII text mode. If it is already in Binary Companion mode, send this line once and wait for the terminal banner:

+++MESHCORE-TERM-START\n

Run help, board, and version to inspect the device. On ESP32 you can alternatively open the node's LAN address and select CLI, or connect with nc DEVICE_IP 5002. Both use the same terminal commands as USB, including import meshcore://..., list, to, and send. Use disconnect to release a browser/TCP session. See the terminal guide. When finished, send +++MESHCORE-TERM-STOP, close the terminal, and reconnect your Companion app. These are local text commands; an app's custom-variable editor does not necessarily forward them.

"},{"location":"full_companion_features/#common-switches","title":"Common switches","text":"Feature Turn on Turn off When it takes effect Device power saving set powersaving on set powersaving off Immediately; saved LoRa receive power saving set radio.rxps on set radio.rxps off Saved; radio applies when available Radio chip RX boost set radio.rxgain on set radio.rxgain off Immediately; saved; supported radios only External FEM RX boost set radio.fem.rxgain on set radio.fem.rxgain off Immediately; saved; controllable FEM only External FEM TX gain set radio.fem.txgain on set radio.fem.txgain off Immediately; saved; controllable PA only ESP32 USB packet/debug logging For 1.17.1.5: set powersaving off, then set usb.logging on set usb.logging off Immediately; saved; logging owns the USB terminal nRF52 separate USB logging port set usb.logging on reboot set usb.logging off reboot Saves and reboots to add/remove the second USB port nRF52 one-port packet stream set usb.logging stream reboot set usb.logging off Saved; CLI and packets share the primary port for stock serial bridges ESP32 browser settings set webui on set webui off Saved; starts/stops WebConfig ESP32 temporary setup portal start webconfig ap stop webconfig This session; opens a setup network/QR where available ESP32 WiFi firmware uploader start ota or start ota ap stop ota This session; only usable with two application slots Temporary MOTA radio window tempradio 910.525,250,5,5,120 normalradio Bounded window; saved normal settings return afterward

Check a saved switch with the corresponding get command, for example get usb.logging, get radio.rxps, or get webui. Use get powersaving to inspect device power saving. Fresh installations enable device power saving and leave USB logging off. Existing saved preferences take precedence.

For an ESP32 1.17.1.5 USB logging session, use this sequence in the text terminal:

set powersaving off\nset usb.logging on\n

The power-saving step is the documented USB-session workaround for this release. It is saved separately; set usb.logging off does not restore power saving. nRF52 does not need the ESP32 workaround. WiFi-only MQTT logging does not need this USB step; see logging by role.

ESP32 logging and Binary Companion share one USB port. After turning logging off, return to Binary Companion with +++MESHCORE-TERM-STOP. nRF52 keeps its optional logging port separate from its primary USB port and BLE connection. See the USB switching guide.

"},{"location":"full_companion_features/#wifi-bluetooth-gps-and-mqtt","title":"WiFi, Bluetooth, GPS, and MQTT","text":"

On ESP32, configure WiFi through the setup portal or these commands:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nget wifi.status\n

Credentials are saved. A credential change schedules a reconnect; a TCP terminal will disconnect. The WiFi switch on supported device displays or their assigned WiFi button controls the Companion WiFi services. Stopping WebConfig alone stops its browser service, while the Companion's TCP and MOTA services can remain available.

WiFi modem sleep is independent of device power saving. Use set wifi.powersave min for normal BLE/WiFi coexistence; inspect the effective state with get wifi.powersave. max is available only where the firmware's radio coexistence policy permits it.

Most Full Companions provide BLE alongside their other transports. On Bluetooth-capable ESP32 and nRF52 Companions, get bluetooth reports the running state and set bluetooth on|off saves the service preference. Plain off requires another active management connection; set bluetooth off force explicitly permits disconnecting the only client. Reboot does not undo a saved-off preference. See Bluetooth controls for reply draining, connection checks, and unavailable boot modes.

SenseCAP Indicator selects one secondary wireless transport per boot:

set companion.transport wifi\nreboot\n

Use set companion.transport ble followed by reboot to select Bluetooth instead. USB stays available. The ESP-NOW layout keeps its primary ESP-NOW mesh active in either mode.

For GPS-equipped boards, use get gps, set gps on, and set gps off. The Companion app's gps=1 / gps=0 custom setting controls the same GPS. Only boards with a compiled GPS provider expose this setting. Sharing location with contacts is a separate setting.

Use set gps.sync.interval <hours> to save an automatic clock-sync interval of 1-336 hours (two weeks); get gps.sync.interval reports it. Larger inputs are capped at 336 and the reply shows the applied value. Existing GPS timings are unchanged until configured. This controls clock synchronization, not the position-update gps_interval or location-sharing policy.

When the exact Full image includes MQTT, use WebConfig's MQTT cards or the same CLI settings used by infrastructure, such as set mqtt1.preset custom and set mqtt1.server broker.example.com. set mqtt.enabled on|off controls MQTT without erasing the configured slots; check get mqtt.enabled, get mqtt.running, and get mqtt.status. The MQTT tab's Enable MQTT checkbox controls the same saved switch. set logging.output off|usb|wifi|both selects USB and MQTT outputs together. Status, packets, raw, receive, and transmit switches control their individual publications. Builds without MQTT omit these controls. There is no need for a separate logging or WiFi-MQTT Companion image when that feature is included in Full.

"},{"location":"full_companion_features/#send-mota-from-any-full-companion","title":"Send MOTA from any Full Companion","text":"

Place valid, destination-specific .mota files in a host directory. Set the source, destination, controller, and required relays to the same bounded temporary radio tuple. For USA Cascade on LoRa hardware:

tempradio 910.525,250,5,5,120\n

On the computer connected to the Full Companion, run:

motatool serve --serial /dev/ttyACM0 --dir ./motas -v\n

This works on both ESP32 and nRF52 Full. Close the text terminal first. motatool automatically sends ota folder on and owns USB while serving. Stop the host tool to detach the source; use normalradio afterward to return early, or let the window expire. A .bin, .uf2, or DFU .zip is not itself a .mota; use motatool to prepare the package for the exact destination firmware and hardware.

ESP32 also accepts a host over WiFi:

motatool serve --tcp DEVICE_IP:5001 --dir ./motas -v\n

nRF52 also accepts an authenticated Bluetooth host:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion --dir ./motas \\\n  --local 'tempradio 910.525,250,5,5,120'\n

Use one MOTA source transport at a time. See the complete MOTA instructions for pairing, remote destination commands, and automation. ESP-NOW hardware uses its ESP-NOW primary radio rather than transmitting LoRa.

"},{"location":"full_companion_features/#update-the-companion-itself","title":"Update the Companion itself","text":"

USB is supported for every Full Companion. Use the exact board's UF2/DFU package or ESP32 image and the normal flasher. Preserve the target's existing partition layout unless deliberately performing a documented USB migration.

For ESP32 with two application slots, start ota returns the WiFi uploader URL, normally http://DEVICE_IP:8080/update. start ota ap explicitly opens MeshCore-OTA; join it and use the returned URL. Upload the exact board's application .bin. The device reboots when the upload succeeds. Use stop ota to close an unused uploader. Starting OTA enables WiFi as needed, stops WebConfig automatically, and reports the network and URL to use. Use a trusted local network or a temporary setup network.

The 4 MB Full layouts and T-Beam 1W Full use a single application slot and require USB; the command reports that limitation. The artifact's .capabilities.json lists verified ota_update_methods when a wireless self-update path is usable.

nRF52 Full exposes Bluetooth DFU to paired clients. Use the matching application DFU .zip with a compatible Nordic/Adafruit Legacy DFU client and the board's matching BLE-DFU-capable bootloader/SoftDevice. USB remains the fallback when the installed bootloader does not support that procedure. Bluetooth DFU updates the Companion; the separate Bluetooth MOTA source service feeds packages to other mesh nodes.

For OTAFIX installations, use the exact board/storage profile from OTAFIX 2.4.6. It includes the Bluetooth reconnect fix from 2.4.5. Its release notes cover upgrades from older bootloaders, including the special UF2-drive precautions for version 2.4.3. The new 64 KiB retained-RAM staging feature applies to qualified internal-flash nRF52840 receivers; Full Companions continue to serve host-supplied packages without needing that receiver storage layout.

"},{"location":"full_companion_usb_switcher/","title":"Full companion usb switcher","text":"

View this page on MeshCore Docs.

"},{"location":"full_companion_usb_switcher/#companion-usb-cli-and-binary-switcher","title":"Companion USB CLI and binary switcher","text":"

USB Companion uses one primary USB serial interface for two incompatible wire formats:

The primary interface starts in the ASCII terminal after each boot and returns to ASCII after a USB session reset that the hardware can observe. A Binary Companion client does not need to send a special mode command: its first valid frame automatically hands the interface to the binary parser.

Current source keeps the default terminal quiet until the host sends ASCII input. Press Enter or type a command to see the banner. This prevents the ASCII > prompt from being mistaken for the beginning of an app's first binary response. See USB client compatibility validation for the tested firmware and release status.

These mode controls apply to Full and ordinary USB Companion builds. The explicit +++MESHCORE-TERM-START and +++MESHCORE-TERM-STOP controls remain available, including for older firmware. See the Terminal Chat CLI guide.

"},{"location":"full_companion_usb_switcher/#wire-formats","title":"Wire formats","text":"

Host-to-device Binary Companion frames use this layout:

'<'  length-low  length-high  payload[length]\n

Device-to-host frames use the same little-endian length with a different marker:

'>'  length-low  length-high  payload[length]\n

For example, a representative two-byte device query is:

3C 02 00 16 03\n

The ASCII terminal is line-oriented and accepts commands such as:

get radio.cad\nset display.rotation 90\nreboot\n

Changing the configured baud rate does not select a mode. Use 115200 for compatibility even though native USB CDC hardware does not use UART timing.

"},{"location":"full_companion_usb_switcher/#state-transitions","title":"State transitions","text":"
                         complete '<' frame\n                 +--------------------------------+\n                 |                                v\nboot ------> ASCII terminal                  Binary Companion\n                 ^                                |\n                 |                                | +++MESHCORE-TERM-START\n                 | incomplete '<' probe           |\n                 | (one-second timeout)            |\n                 +--------------------------------+\n\nASCII terminal -- +++MESHCORE-TERM-STOP ------> Binary Companion\nany mode ------- observable USB session reset ---> ASCII terminal\nESP32 logging terminal -- usb.logging off -----> ASCII terminal\nany mode ------- reboot ------------------------> ASCII terminal\n

Serial mOTA and single-TTY logging add exclusive ownership states described below. BLE, WiFi, Ethernet, and hardware-serial Companion transports are not switched; they remain binary.

"},{"location":"full_companion_usb_switcher/#how-automatic-detection-works","title":"How automatic detection works","text":"
  1. Companion initializes the normal USB Binary Companion interface, then gives its primary stream to the ASCII terminal before normal loop service begins.
  2. While the prompt has no buffered input, the terminal skips blank CR/LF delimiters and peeks at the next byte without removing it. A queued binary frame after a blank line is detected before any ASCII banner is emitted.
  3. If the byte is <, the terminal temporarily releases the stream and enables the existing ArduinoSerialInterface frame parser.
  4. The parser consumes the original <, the two-byte length, and the payload. There is no second parser and no copied or synthetic frame.
  5. A monotonically increasing completed-frame counter confirms that the parser received a complete frame. The interface then remains in Binary Companion mode.
  6. If no complete frame arrives within one second, the parser state is reset and the ASCII terminal prints a new banner and prompt.

The switcher checks framing, not client identity. A complete Binary Companion frame with 1-176 payload bytes confirms binary mode; it does not require the first command to be CMD_APP_START or CMD_DEVICE_QUERY. Normal command validation still occurs after the frame parser returns the payload.

Oversized frames are consumed and discarded, never truncated into executable commands or counted as proof of a Binary client. ASCII, framed Binary, and mOTA input handlers process at most the initial queued-byte count, so a continuously sending host cannot keep the radio loop trapped in one input call.

ASCII and mOTA reject an entire line containing an embedded NUL or exceeding its line buffer, discarding through the next CR/LF. A valid-looking suffix is not executed. Binary payloads may still contain NUL bytes. The immediate manual terminal-stop token remains an exception to ordinary line-oriented handling.

The empty-prompt requirement prevents a literal < in the middle of a command from silently changing modes. A literal < typed as the first character does start a probe, but the terminal returns after the one-second timeout if no binary header and payload follow.

"},{"location":"full_companion_usb_switcher/#manual-controls","title":"Manual controls","text":"

The original controls remain available.

From Binary Companion, send this exact unframed line while the parser is idle:

+++MESHCORE-TERM-START\n

Terminate it with CR or LF. The binary parser accepts control tokens only as complete delimiter-bounded lines; prefixes, suffixes, and partial tokens are ignored.

From the ASCII terminal, send this exact sequence to return to binary mode:

+++MESHCORE-TERM-STOP\n

The stop sequence takes effect as soon as its last byte arrives in ASCII mode. The start sequence is recognized only as a completed line while the binary parser is idle and is not examined inside a length-prefixed frame.

meshcli can normally connect directly after boot:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

An explicit terminal start token is still useful when the device is already in binary mode:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n
"},{"location":"full_companion_usb_switcher/#logging-and-mota-ownership","title":"Logging and mOTA ownership","text":"

The switcher never attempts to mix ASCII, framed Companion traffic, or binary mOTA traffic on the same stream.

Situation Primary USB behavior USB Companion after boot or observable session reset ASCII; a complete < frame switches to Binary Companion nRF52 logging enabled Primary interface still follows the switcher; logs use the optional second interface ESP32 logging enabled at boot Logging terminal owns the only USB TTY; automatic < detection is disabled nRF52 USB serial mOTA active mOTA owns primary USB; ASCII and Binary Companion are unavailable there BLE/WiFi/Ethernet/hardware serial Always Binary Companion and unaffected by the USB mode

On RAK4631 and other nRF52 Full Companions, ASCII and Binary Companion both use the primary interface (*-if00 on Linux). The optional *-if02 interface is for logging. OK - Binary mode acknowledges a mode change on the same primary port; it does not move the connection to the logging port.

On every ESP32 Full Companion, use set usb.logging off in its logging terminal before trying to use USB with an app. That command stops logging and returns to the ordinary ASCII terminal; it does not select Binary Companion. The app's first valid framed probe can perform the normal automatic switch, or a human can send +++MESHCORE-TERM-STOP first. The framed/binary Companion parser is disabled while logging owns that TTY. BLE and WiFi Companion transports remain available while USB is logging. ESP32 Full builds use the Arduino-ESP32 2.x base where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but follow the same one-TTY policy. No ESP32 Full build enumerates a second CDC interface.

usb.logging is the saved master for USB packet output. usb.debug separately saves verbose diagnostic intent and defaults off, including when older preferences do not contain the new field. RAW/RX/TX packet records need only the master; diagnostics need both switches and compiled debug support. Turning the master off preserves debug intent. get usb.debug reports that saved choice, not whether USB currently owns a plaintext logging stream.

Full Companion now emits complete received frames as uppercase ASCII RAW: hex, separately from decoded RX summaries and TX summaries. Leave debug off for USB-to-MQTT capture. These are bounded best-effort records; drops and host disconnects can separate a RAW/summary pair. The dedicated nRF52 log CDC cannot answer CLI/identity queries: programs expecting one bidirectional CLI/log port need separate control-port support, not just a change to the serial pathname. Companion supports get public.key in the primary terminal or framed CLI; the reply is > followed by 64 uppercase hex characters. This query does not make the dedicated log CDC input-capable or establish full bridge compatibility. Send get/set usb.debug to the primary ASCII terminal; changing verbosity needs no USB-interface reboot and does not relax Binary/mOTA ownership rules.

Current source adds get usb.watchdog and saved set usb.watchdog off|on|auto, defaulting to Auto until 14 continuous healthy logging-client days qualify it for On. Existing USB stats polls renew a 15-minute client lease; a cable or DTR alone does not count. A lost confirmed client can trigger USB-only recovery after lease expiry plus five minutes. MCU resets additionally require On and a full physical-fault interval, a verified tier save, and clear protocol/update ownership. Active Binary USB and mOTA sessions veto recovery. On dual CDC, only logging CDC1 accepts the bounded stats markers; primary CDC0 stats do not renew that lease. CDC1 still provides no CLI/statistics replies. See USB logging watchdog for status and limitations.

With logging enabled on a shared port, the stop token is rejected with ERROR: set usb.logging off before Binary mode. Turn logging off first; a successful binary-mode acknowledgement must not immediately revert to logging.

On nRF52, serial motatool is also text-first. Its exact initial ota folder on line is recognized in either startup ASCII or Binary Companion mode. From ASCII, the firmware leaves terminal mode and directly enters exclusive mOTA ownership; from binary, the idle frame parser recognizes the same control sequence. The following mOTA request/reply frames therefore cannot be consumed by the ASCII line editor. motatool serve --serial can be the first client after boot and does not require a terminal token or disconnect workaround.

Only the exact completed line selects mOTA from ASCII. Extra arguments, leading/trailing whitespace, or a partial line remain ordinary terminal input.

"},{"location":"full_companion_usb_switcher/#shortcomings-and-edge-cases","title":"Shortcomings and edge cases","text":"

This mechanism is deliberately small and deterministic, but it is not a full protocol negotiation layer.

"},{"location":"full_companion_usb_switcher/#session-detection-depends-on-the-usb-hardware","title":"Session detection depends on the USB hardware","text":"

After the first complete binary frame, the device stays in Binary Companion mode for that session. Native USB with DTR/session events restores ASCII when the host closes or resets the connection. Old input, queued output and mOTA ownership are cleared before selecting the default for the next session.

Some clients, including meshcore 2.3.10 used by stock MeshCLI, deliberately deassert DTR. The nRF52 primary interface also accepts actual incoming USB data as evidence of a client in the current session. Closing, changing line coding, or resetting USB invalidates that evidence. The dedicated logging interface continues to require its own connected reader.

ESP32 hardware USB Serial/JTAG reports bus reset and physical host loss, but cannot reliably report a terminal program closing its handle. A USB-to-UART bridge also usually cannot report that close. On those ports, use the terminal start token to return from Binary mode without rebooting. Idle time alone never changes the selected protocol.

An active network terminal temporarily delays restoring the USB ASCII prompt. If a fresh USB app sends a complete binary frame during that delay, its Binary mode remains selected when the network terminal closes.

"},{"location":"full_companion_usb_switcher/#detection-works-only-at-an-empty-prompt","title":"Detection works only at an empty prompt","text":"

If part of an ASCII command is already buffered, an incoming < is treated as ordinary terminal input. Clear or submit the line before starting a Binary Companion client. Only one process should have the serial port open.

"},{"location":"full_companion_usb_switcher/#the-first-frame-has-a-one-second-deadline","title":"The first frame has a one-second deadline","text":"

The complete marker, length, and payload must arrive within the probe window. This is generous for local USB but may reject a heavily buffered serial proxy, a debugger that pauses the MCU, or a tool that writes the header and body with a long delay. A timed-out client can retry after the ASCII banner appears.

"},{"location":"full_companion_usb_switcher/#framing-confirmation-is-not-authentication","title":"Framing confirmation is not authentication","text":"

Any complete length-prefixed frame selects binary mode, even if its command is unknown or malformed at the application layer. This is safe for stream separation but means a random complete frame can leave the device in binary mode until an observable USB session reset or a manual switch back.

"},{"location":"full_companion_usb_switcher/#the-default-terminal-waits-for-input","title":"The default terminal waits for input","text":"

The firmware enters ASCII mode during boot but suppresses the banner and unsolicited terminal events until ASCII input identifies a terminal client. Send Enter or a harmless get command to display the banner. A binary client instead gets a clean first response. Older builds that immediately printed > could consume the first binary response's marker as part of a bogus frame length; scanning past leading text alone did not reliably recover that reply.

"},{"location":"full_companion_usb_switcher/#text-and-binary-output-cannot-be-interleaved","title":"Text and binary output cannot be interleaved","text":"

Primary USB suppresses Binary Companion output while the terminal owns the stream. Packet/debug logging must use its dedicated CDC interface or the exclusive single-TTY logging mode. Writing diagnostic text directly to the primary binary stream will corrupt clients regardless of the switcher.

"},{"location":"full_companion_usb_switcher/#a-literal-leading-briefly-hides-the-prompt","title":"A literal leading < briefly hides the prompt","text":"

Typing < as the first terminal character begins a binary probe. With no complete frame, the prompt returns after one second and the banner is printed again. There is currently no escape syntax for entering a literal leading <; prefix it with another character if it is needed as command text.

"},{"location":"full_companion_usb_switcher/#troubleshooting","title":"Troubleshooting","text":"

If meshcli cannot connect:

  1. close every terminal or logging reader using the primary interface;
  2. on ESP32, confirm that saved USB logging is off;
  3. reboot and let meshcli be the first process to open the data interface;
  4. use the stable /dev/serial/by-id/*-if00 path on Linux when available;
  5. if a prompt appears after one second, the client's first frame was not completed inside the probe window.

If an ASCII terminal shows no banner, press Enter and issue a harmless query such as get radio.cad. If binary bytes appear, send the exact terminal start token or reboot.

The switch policy is implemented in UsbAsciiBinarySwitch.h, with stream ownership in main.cpp and framing in ArduinoSerialInterface.cpp.

"},{"location":"full_profiles/","title":"Full profiles","text":"

View this page on MeshCore Docs.

"},{"location":"full_profiles/#full-firmware-profiles","title":"Full firmware profiles","text":"

Full combines the features supported by an exact board and firmware role in one image. Use runtime settings to choose the services you need instead of flashing a separate image for each transport. Hardware, memory, and the installed flash layout still determine which combinations can run.

Full is not a runtime role switch. Choose Companion, Repeater, Room Server, or Sensor in the firmware picker. Changing roles requires the matching firmware; enabling a bridge does not turn a Companion into a repeater.

"},{"location":"full_profiles/#choose-the-board-role-and-profile","title":"Choose the board, role, and profile","text":"Choice What Full combines Important limits Full Companion The board's supported USB, Bluetooth, ESP32 WiFi, and qualified serial/Ethernet Companion transports, plus terminal chat and mOTA serving The primary radio and board interfaces remain those of the selected target. Full Companion serves updates to other nodes; it does not install LoRa mOTA on itself. ESP32 Full Repeater or Room Server Standalone operation, supported USB/WiFi services, and the qualified MQTT, ESP-NOW, or RS-232 combination Not every image contains all three bridges. Read its capabilities and check the compiled controls. ESP32 Full Sensor Sensor operation, supported CLI/logging, and qualified WiFi firmware-update and TCP 5001 seeding services Current Sensor profiles do not gain the repeater's MQTT/ESP-NOW/RS-232 bridges. nRF52 Full supported sensors or Reduced sensors Two separately qualified Repeater, Room Server, or Sensor recipes with LoRa OTA These are sensor-driver choices, not ESP32 network features. Full Companion remains a separate profile.

Select the exact hardware variant and storage choice. Read the selected image's capability manifest for update methods and requirements; a similar filename is not evidence that another board's image will work.

"},{"location":"full_profiles/#full-companion-choose-the-active-transports","title":"Full Companion: choose the active transports","text":"

Close any application using the USB port, then open the USB web console at 115200 baud. A fresh Full Companion starts in the text terminal. If an app already selected Binary Companion mode, send this line and wait for the terminal banner:

+++MESHCORE-TERM-START\n

Run help, board, and version to inspect the device. The same image can serve an app through its compiled transports or provide terminal chat. USB remains the normal installation and recovery path.

On a Bluetooth-capable Companion, get bluetooth shows the running state; set bluetooth on enables the saved Bluetooth preference. On an ESP32 profile with WiFi, start webconfig ap opens a temporary setup portal where you can save network credentials. MQTT-capable Companions also expose broker settings there; MQTT is not included on every Companion board.

Most Full Companions can use multiple transports together. SenseCAP Indicator selects Bluetooth or infrastructure WiFi per boot using set companion.transport ble or set companion.transport wifi, followed by reboot; USB remains available. Generic ESP-NOW and Indicator ESP-NOW profiles use ESP-NOW as their primary mesh radio. Full does not convert those profiles into LoRa radios.

USB logging shares the ESP32 Companion's single USB port. Use set usb.logging off, then +++MESHCORE-TERM-STOP and close the terminal before reconnecting a Binary Companion app. nRF52 Full Companion can instead add a separate logging port with set usb.logging on reboot; use its primary port for Companion commands and mOTA serving. A one-port host bridge can use set usb.logging stream reboot, which occupies the primary port until logging is disabled. See the USB mode guide.

"},{"location":"full_profiles/#esp32-infrastructure-enable-the-compiled-services","title":"ESP32 infrastructure: enable the compiled services","text":"

Full Repeater and Room Server images retain their normal role while their supported bridges run. A LoRa/ESP-NOW bridge keeps LoRa as its primary radio and exchanges packets with its ESP-NOW peers over 2.4 GHz. MQTT publishes observed traffic to brokers; it does not subscribe to broker messages or inject them into LoRa. RS-232 uses the board's configured UART and pins.

Check get bridge.type on bridge-capable images. Depending on the recipe, it can report combinations such as mqtt+espnow, rs232+espnow, or mqtt+rs232+espnow. Use only the controls compiled into your image:

Service Saved enable/disable control Check actual operation MQTT set mqtt.enabled on / off get mqtt.running, get mqtt.status ESP-NOW set espnow.enabled on / off get espnow.running RS-232 set rs232.enabled on / off get rs232.running USB packet logs set usb.logging on / off get usb.logging

An enabled preference is not proof that a service started. Missing credentials, a channel mismatch, or a UART conflict can prevent operation. bridge.enabled is a historical alias with different meanings between recipes; use the explicit transport controls on combined images.

For an MQTT-capable image, first follow its role's WiFi/MQTT setup and verify the actual connection with get wifi.status. Once connected, this example selects a custom broker:

set mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.status\n

Replace the sample broker value. Add the broker's username and password if needed. Use the WiFi/MQTT guide for TLS, publication settings, broker-slot limits, and reconnect behavior. Where MQTT and USB logging coexist, set logging.output off|usb|wifi|both selects those two outputs together; it does not change LoRa repeating.

For ESP-NOW, configure both peers with the same channel and compatible bridge.format. wrapped uses the shared bridge.secret for bridge peers; raw connects to primary-ESP-NOW MeshCore nodes. When MQTT WiFi and ESP-NOW run together, the ESP-NOW channel must match the access point's fixed WiFi channel. ESP-NOW alone needs no WiFi credentials. Follow the two-board setup for the complete commands.

For RS-232, stop the UART bridge before changing its configuration:

set rs232.enabled off\nget bridge.uart\nset bridge.baud 115200\nset rs232.enabled on\nget rs232.running\n

Use the UART and pin assignment supported by your board. GPS can reserve or share a UART; enabling the bridge may be rejected or pause a yieldable GPS provider. Check the board's recipe before changing bridge.uart.

"},{"location":"full_profiles/#capacity-and-role-exceptions","title":"Capacity and role exceptions","text":"

MKE S3 combines UART and ESP-NOW in its normal Full repeater. Full Heltec V3, WSL3, and RAK3112 MQTT repeaters also retain independent RS-232 alongside MQTT and ESP-NOW. T-LoRa V2.1 retains two Full repeater choices because of internal RAM limits: UART plus ESP-NOW, or MQTT plus ESP-NOW. Other measured capacity exceptions retain dedicated images. Non-ESP32 boards do not inherit these ESP32 combinations; Wio-E5, for example, keeps a separate RS-232 image.

"},{"location":"full_profiles/#nrf52-full-and-reduced-supported-sensors","title":"nRF52: Full and Reduced supported sensors","text":"

Qualified nRF52 Repeater, Room Server, and Sensor builds offer Full supported sensors + LoRa OTA and Reduced sensors + LoRa OTA. Full includes the drivers supported by that board's full recipe; it does not add support for every possible attached sensor. Reduced omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals. Reduced RAK3401/RAK4631 recipes also retain their supported INA219/INA226/INA260/INA3221 voltage/current drivers.

The picker recommends Full when both profiles are qualified from the same source. Reduced remains available through its Advanced sensor option and explicit shared links, with both downloads and their update identities retained. An absent sensor does not require a Reduced image; supported sensor discovery already handles missing peripherals.

Both profiles retain their exact board/role OTA identity and storage layout. Choose the matching storage variant and meet its staging/bootloader requirements. These profiles are LoRa OTA receivers; nRF52 Full Companion is an update source and does not acquire WiFi or on-device MQTT. An attached Pi can publish its USB packets using a separate host bridge. See sensor/storage choices.

"},{"location":"full_profiles/#install-or-update-the-matching-layout","title":"Install or update the matching layout","text":"

Expanded ESP32 Full images need application slots large enough for the image. Use get storage.layout on firmware that provides it to inspect the installed partition table. Firmware version alone does not establish the layout; use the partition lookup guide for older firmware.

For a compatible layout, use the exact board/role application-only image through a supported updater. To change an ESP32 layout, use the matching merged image over USB or an exact board/role staged migration ZIP whose README supports your installed source layout and update route. An app-only image cannot change the partition table, and a merged image is not a browser-OTA application. The picker links the matching migration package where available.

ESP32 Full Companion WiFi self-update requires two application slots; the single-slot profiles require USB. Infrastructure WiFi/LoRa self-update also depends on the exact image's declared capabilities and installed layout. nRF52 updates use the matching application UF2/DFU package or a qualified LoRa route; they do not use the ESP32 browser procedure. See the ESP32 WiFi update and partition guide for supported staged routes and the WiFi guide for role-specific network setup.

"},{"location":"full_usb_logging_validation/","title":"Full usb logging validation","text":"

View this page on MeshCore Docs.

"},{"location":"full_usb_logging_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"gps_tracking/","title":"Gps tracking","text":"

View this page on MeshCore Docs.

"},{"location":"gps_tracking/#gps-tracking","title":"GPS Tracking","text":"

This document describes how GPS telemetry works for companion/client nodes and sensor nodes.

"},{"location":"gps_tracking/#scope","title":"Scope","text":"

GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a new phone app protocol field.

The GPS telemetry value contains latitude, longitude, and altitude. Speed, heading, and freshness are not sent as separate telemetry fields.

If the firmware does not have a valid fresh GPS cache, it omits the GPS field from telemetry. This is intentional: stale or missing fixes are not reported as zero coordinates.

"},{"location":"gps_tracking/#freshness","title":"Freshness","text":"

GPS telemetry is cached separately from advert location.

The cache behavior is:

When a telemetry request asks for location, GPS is normally kept on for 2 hours after the latest location request. During that hold window, later location telemetry requests can use fresh GPS data as soon as valid fixes are available.

If GPS is manually enabled, it stays on and valid fixes continue to update the cache.

If no contact or ACL client can receive location telemetry, the scheduled 2-hour refresh does not run. A real location telemetry request can still turn GPS on, and manual GPS-on still keeps the cache updated.

"},{"location":"gps_tracking/#repeater-nodes","title":"Repeater Nodes","text":"

Repeaters use a shorter on-demand policy when GPS is manually off. An authorized location-telemetry request wakes the receiver only when its cached position is stale. The first valid fix is cached and GPS processing immediately stops; the next request can return that fix without another wake. gps off also cancels an acquisition already in progress. An acquisition that never obtains a fix retains the normal 15-minute safety timeout. Physical supply switching depends on the board and its shared peripheral policy.

When GPS and device power saving are both enabled, a repeater wakes GPS for up to 10 minutes to obtain a valid time and then sleeps it for seven days. A valid GPS time may correct the RTC either forward or backward. With device power saving off, an enabled GPS remains active and refreshes the RTC every 30 minutes.

"},{"location":"gps_tracking/#rak-wisblock-power-supply","title":"RAK WisBlock Power Supply","text":"

RAK4631 defaults to saved peripheral mode gps-only. With no protected power owner, stopping or sleeping GPS can turn off the shared 3V3_S rail. If other attached modules need that rail, explicitly select shared mode:

get peripherals.mode\nset peripherals.mode shared\nreboot\n

Use set peripherals.mode gps-only and reboot to return to the default. Changes are saved for the next boot, not applied live; the query reports the saved choice. Other peripheral requirements are not guessed from an I2C scan. Boot and discovery keep the rail on until the power policy is established.

This saved setting is available in Companion and Common CLI roles (repeater, room server, and sensor). Raw KISS and legacy Terminal Chat do not configure it and retain the powered rail. Shared mode controls the supply; it does not add Ethernet support to a non-Ethernet firmware target.

Ethernet/PoE builds and detected external NOR storage automatically retain power regardless of this setting. RAK3401 always retains the rail for its radio frontend and does not offer this override.

In shared mode, GPS-off does not mean the supply is off. In particular, the RAK12501/L76K module has no independent power switch: stopping MCU parsing leaves the receiver powered. Internal sleep support on other GNSS modules is receiver-dependent. This setting does not remove the UART1 bridge conflict or authorize another transmitter on the GPS wiring.

"},{"location":"gps_tracking/#companionclient-nodes","title":"Companion/Client Nodes","text":"

Companion/client telemetry uses the existing companion telemetry permission system:

Location telemetry is sent only when the requester's effective telemetry permissions include location. Those permissions are derived from the companion telemetry mode settings and contact flags.

No new phone app behavior is required. Existing clients see the existing GPS telemetry field when it is present.

The scheduled GPS cache refresh runs only when at least one stored contact has effective location telemetry access:

"},{"location":"gps_tracking/#sensor-nodes","title":"Sensor Nodes","text":"

Sensor telemetry access is controlled by:

get telemetry.access\nset telemetry.access all\nset telemetry.access acl\n

all is the default and matches the previous sensor telemetry behavior. A request that reaches the normal sensor telemetry request path can receive the requested telemetry fields, including GPS, subject to the request mask and GPS freshness rules.

acl gates telemetry through the sensor ACL. A requester with read-only or higher permissions receives the existing telemetry set, including GPS. Guest or unknown requesters receive no telemetry fields.

The scheduled GPS cache refresh follows the same access setting:

Use the existing ACL command to grant access:

setperm <pubkey> 1\n

Permission values:

"},{"location":"gps_tracking/#advert-location","title":"Advert Location","text":"

Advert location is separate from telemetry GPS. The advert policy is controlled with:

gps advert\ngps advert none\ngps advert share\ngps advert prefs\n

Policies:

prefs is the first-boot default for every repeater, room-server, and sensor build. Firmware updates retain an explicitly saved policy from the existing preferences filesystem.

Telemetry GPS can be fresh while advert location is fixed or disabled, depending on this policy.

"},{"location":"gps_tracking/#recommended-setup","title":"Recommended Setup","text":"

For private sensor tracking:

set telemetry.access acl\nsetperm <owner_pubkey> 3\nsetperm <trusted_pubkey> 1\n

For compatibility with the older sensor behavior:

set telemetry.access all\n

For GPS telemetry behavior, leave the phone app unchanged. The firmware omits GPS when it is stale or missing and sends the existing GPS telemetry field when fresh data is available.

"},{"location":"halo_keymind_settings/","title":"Halo keymind settings","text":"

View this page on MeshCore Docs.

"},{"location":"halo_keymind_settings/#halo-and-keymind-branch-settings","title":"Halo and Keymind Branch Settings","text":"

This file covers only CLI settings and helper commands added by the Halo or Keymind branches. Use CLI commands for the general infrastructure CLI. For MQTT, logging, and former variant controls, use feature switches by role; Full Companion uses different controls for some of these settings. See Repeater Flood Filtering and Moderation for a focused filter setup and troubleshooting guide.

"},{"location":"halo_keymind_settings/#quick-start","title":"Quick Start","text":"
set retry.preset rooftop\nset direct.retry.heard on\nset flood.retry.advert off\nset flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n

Then verify:

get retry.preset\nget direct.retry.heard\nget flood.retry.advert\nget flood.retry.prefixes\nget flood.retry.ignore\n

Use prefixes from the analyzer or neighbors list or get recent.repeater after the repeater has been online for a few hours.

"},{"location":"halo_keymind_settings/#common-examples","title":"Common Examples","text":"

Disable retrying advert packets:

set flood.retry.advert off\nget flood.retry.advert\n

Ignore a repeater as a successful flood retry echo: Use this if you have a car repeater and a house repeater; have the house ignore the car.

set flood.retry.ignore 71CE82,C7618C\nget flood.retry.ignore\n

Only accept specific downstream relays as flood retry success: You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.

set flood.retry.prefixes A58296,860CCA,425E5C\nget flood.retry.prefixes\n

Bridge two groups of repeaters:

set flood.retry.bridge on\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 BEEBB0,425E5C\nget flood.retry.bucket.1\nget flood.retry.bucket.2\n

Return to simple non-bridge flood retry:

set flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n
"},{"location":"halo_keymind_settings/#added-settings","title":"Added Settings","text":"Setting What it does How to use Example telemetry.temp, telemetry.volt, telemetry.volt.i2c, telemetry.gps Repeater and room builds record 30-minute MCU temperature and battery samples for seven days. Detected INA voltage channels retain four days at 0.02 V resolution through 655.34 V; all-zero channels are omitted as disconnected. GPS-capable builds retain GPS separately. Repeater telemetry.tx sends temperature, battery, and per-channel I2C RAW_CUSTOM packets over the shared data.tx path on a persistent 1-30 day schedule or immediately for testing; automatic transmission is off by default and its retained cadence starts at two days. data.tx defaults to zero-hop direct plus automatic region selection without enabling any producer. GPS is never included. History resets on reboot. get/set data.tx path|region, get telemetry.temp [page], get telemetry.volt [page], get telemetry.volt.i2c [channel [page]], get telemetry.gps [page], set telemetry.gps <1-30>, get telemetry.tx, set telemetry.tx schedule <off|1-30d>, send telemetry.tx now get data.tx battery.alert Sends opt-in, region-scoped low-battery warnings to #repeaters after 30 minutes of uptime. get battery.alert, get battery.alert.region, set battery.alert on [region], set battery.alert off set battery.alert on sea battery.alert.low Warning threshold percentage. Must be greater than battery.alert.critical. get battery.alert.low, set battery.alert.low <1-100> set battery.alert.low 20 battery.alert.critical Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. get battery.alert.critical, set battery.alert.critical <0-99> set battery.alert.critical 10 recent.repeater Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep. get recent.repeater [page], get recent.repeaters search <2|4|6 hex> [page], set recent.repeater <prefix> <snr_db>, clear recent.repeater get recent.repeaters search A1B2 flood.channel.data Compatibility view for a visible FPF7 type=grp_data drop row on generalized repeaters. off creates the row and on removes it; compact FPF6 builds retain the legacy gate. Use an authenticated channel= row when only selected channels should be blocked. get flood.channel.data, set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Sets the compatibility row's hop range: all maps to hops=all; 1-7 repeats through that hop count and maps to hops=N+1+. get flood.channel.data.hops, set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.scope FPF7 rewrite-phase rows that add a transport scope to received unscoped floods or replace the scope of already-scoped floods. A bare target uses an existing flood-allowed region; scope=<name> derives a public hashtag target directly without creating a region, exactly like flood.filter scope=. By default, a changed packet bypasses inbound rxdelay and is forwarded at the highest outbound queue priority with zero initial txdelay, so the selected scope can win at the next hop. tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), retains normal queue priority, and forces the maximum txdelay factor of 2.0; its actual randomized transmit delay ranges from zero through ten packet airtimes. path=blacklist and path=bucket:<1-6> make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat txt:*; path-qualified rows beat the ordinary channel fallback. login:* covers the remote-login family, and other:* covers every remaining flood type, including flood-form TRACE and OTA. Direct traceroute remains outside the flood table. Generalized builds commit these rows with the forward phase and blacklist; compact FPF6 builds retain separate storage. ACL permission 4 can manage the table. get flood.channel.scope[.n], set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow], del flood.channel.scope.<n>|all set flood.channel.scope #rgdata scope=BlackHole86 flood.channel.scope.require Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated GRP_TXT/GRP_DATA channels must arrive already scoped to a locally allowed region; unscoped, unknown, or denied incoming scopes are dropped before any rewrite can rescue them. Unlisted group channels bypass only the region gate and retain all other forwarding controls. An empty table preserves global region behavior; non-channel payloads are unchanged. ACL permission 4 can manage the table. get flood.channel.scope.require[.n], set flood.channel.scope.require[.n] <public|#channel|key>, del flood.channel.scope.require.<n>|all set flood.channel.scope.require #bot flood.filter Persistent flood-route rules selected by payload type and optional hop range. Generalized repeaters have 63 FPF7 forward slots; FULL ESP32 room servers have 31. Repeaters store their scope-rewrite phase, shared unordered blacklist, and channel-data compatibility state in the same atomic FPF7 image. path=blacklist is intended for forwarding abuse containment, including bulk internet-to-mesh dumping, but truncated path IDs are not authenticated identities. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and separate blacklist storage. New generalized repeater tables seed slot 1 with ota all suspend=tempradio slot 2 with an authenticated #wardriving hops=5+ radio drop, and slot 3 with #wardriving hops=all mode=bridge,cross drop. mode=radio is the default; bridge, cross, and bridge,cross select separate transport admission paths, including direct packets. Local receive/logging is unchanged. See the mode guide. get flood.filter[.n], set flood.filter[.n] <type> [N|N+|N-M|all] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio], del flood.filter.<n>|all; repeater only: get/set/del flood.filter.blacklist[.n] set flood.filter grp_txt all scope=local tx=slow flood.rule Live alias for extended flood.filter on rule-engine repeaters and FULL ESP32 room servers. A row can AND packet type, hop range, optional channel match, ordered 1/2/3-byte pbyte source prefix, and original scope/region conditions, then drop, rewrite to a direct scope or configured region, enforce a per-row rate, allow flood retry, and/or stop lower-priority FPF7 rules. channel=* means no channel condition; public, #name, and keys authenticate; hash:XX is an unauthenticated one-byte fallback with collision/spoof risk. With no retry rows, global retry behavior is unchanged; once any exists, matching retry rows allow-list received floods while the global retry gates and flood.retry.bridge algorithm still apply. scope=BlackHole86 directly derives a regionless sink scope; region=BlackHole86 would require a configured flood-allowed region. All rows match the original receive-time packet. Higher numeric priority runs first; equal values automatically order authenticated channels before raw hashes before channel=*, then use lower slot as the final tie-break. Repeated rows with one channel key share a per-packet authentication result. Persistent FPF7 stores canonical region names, so region ID reorder or reuse cannot retarget a rule. A missing saved region makes its region= rewrite and paired stop inert until the name returns, allowing lower safety rows to run. Fixed 240 KB STM32WL profiles keep FPF6 and do not expose this alias; partition sizes are unchanged. get flood.rule[.n], set flood.rule[.n] type=<type> [hops=...] [channel=...] [prefix=...] [in=...] <drop|scope=...|region=...|rate=N/min|retry|stop> [priority=0-255], del flood.rule.<n>|all set flood.rule.2 type=any channel=#hamradio retry flood.moderation Decrypts keyed GRP_TXT channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports public, #channel, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. get flood.moderation[.n], set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...], del flood.moderation.<n>|all set flood.moderation public \"Noisy User\" rate=5/min hops=4 clock.sync.mesh Defaults on for all repeater, sensor, and room-server builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result. clock.sync.mesh now queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its .table and .1 through .16 forms inspect collected samples. get clock.sync.mesh, set clock.sync.mesh <on|off>, clock.sync.mesh now, get clock.sync.status[.table|.1-.16] set clock.sync.mesh on clock.sync.mesh.edge Defaults on so edge infrastructure nodes can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so disabled forwarding and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples. get clock.sync.mesh.edge, set clock.sync.mesh.edge <on|off> set clock.sync.mesh.edge on clock.sync.internet Adds a read-only internet/NTP estimate at the same initial and seven-day checks on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. get clock.sync.internet, set clock.sync.internet <on|off> set clock.sync.internet on clock.sync.drift Absolute correction threshold in seconds; the default is 600 seconds (10 minutes). The clock is moved forward or backward only when the estimate differs by more than this value. get clock.sync.drift, set clock.sync.drift <30-86400> set clock.sync.drift 600 clock.sync.samples Minimum fresh evidence count before mesh time can be used: distinct receive paths in normal mode or distinct advert keys/Public-channel display names in edge mode. A strict majority of all fresh samples is also required. Range 3-16; default 9. get clock.sync.samples, set clock.sync.samples <3-16> set clock.sync.samples 9 outpath Overrides the primary direct route used for replies to the current remote client. get outpath, set outpath <hops>, set outpath direct, set outpath clear, set outpath flood set outpath A1B2C3,D4E5F6 altpath Adds a secondary direct route for repeater replies to the current remote client. get altpath, set altpath <hops>, set altpath direct, set altpath clear, set altpath flood set altpath 71CE82,BA09F0

The generalized filter table also accepts fr as a short alias for flood.rule: set fr.3 any c=#wardriving m=bc d is the full third built-in rule. Modes can be m=r (radio), m=b (bridge), m=c (crossover), or m=bc (both transport paths). get fr.N returns a short setter ready to copy; del fr.N deletes that row. Full names and short tokens can be mixed, with the same validation and permissions. See the shorthand reference.

"},{"location":"halo_keymind_settings/#other-keymind-commands","title":"Other Keymind Commands","text":"Command What it does How to use Example send text.flood Sends a #repeaters flood text message formatted as <node_name>: <message>, with : in the node name sent as ;. send text.flood <message> send text.flood checking ridge link"},{"location":"halo_keymind_settings/#battery-alerts","title":"Battery Alerts","text":"

Battery alerts are off by default. Enabling requires a named region. With no region argument, the repeater selects the single deepest (most narrow) region in the hierarchy; if multiple regions tie, the command asks for an explicit region. For example, after region def west pnw wa w-wa sea, set battery.alert on selects sea, while set battery.alert on w-wa overrides the default. Alerts are never sent as unscoped floods, and removing the selected region stops alerts until a valid scope is selected again.

The repeater suppresses alerts for its first 30 minutes of uptime. It then checks every 30 minutes and sends a flood text warning to #repeaters when voltage is above 1 V and the estimated battery percent is below battery.alert.low.

Warnings and critical alerts both use a 12-hour resend cooldown, beginning only after the radio reports that the alert transmission completed.

Defaults:

Setting Default battery.alert off battery.alert.region <unset> battery.alert.low 20 battery.alert.critical 10

Example:

set battery.alert.low 20\nset battery.alert.critical 10\nset battery.alert on\nget battery.alert\nget battery.alert.region\n

CPU power saving remains compatible with the check. The battery timer never requests a wake earlier than its 30-minute deadline; when the normal loop is already awake after that deadline, no additional wake is needed. Sleeping time counts toward the startup delay, and an outbound warning prevents another sleep until the queued packet has been handled. This is separate from RX duty-cycle power saving, which only cycles the LoRa receiver and does not stop the main loop's battery timer.

"},{"location":"halo_keymind_settings/#recent-repeater-table","title":"Recent Repeater Table","text":"

Direct retry uses the recent repeater table when direct.retry.heard is on. Bridge buckets also use this table: a configured bucket prefix is active only when it was heard within the last hour.

Show learned rows:

get recent.repeater\nget recent.repeater 2\nget recent.repeaters 2\nget recent.repeater page 3\nget recent.repeaters search A1\nget recent.repeaters search A1B2 page 2\n

Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row shows its stored SNR and a whole s, m, or h age measured from the most recent recording. Search pages contain up to six rows.

Seed or correct a prefix:

set recent.repeater A1B2C3 8.5\n

Clear learned and manually seeded rows:

clear recent.repeater\n

Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by 0.25 dB. Unfiltered paged replies contain up to 10 rows; an unpaged local serial query prints the full table.

"},{"location":"halo_keymind_settings/#direct-path-overrides","title":"Direct Path Overrides","text":"

outpath and altpath apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.

Room Server also supports these commands for readers and writers, restricted to the caller's own routes. See room return routes for session persistence, alternate chat delivery and client requirements.

Set paths with comma-separated hop hashes. Each hop must be 2, 4, or 6 hex characters, and all hops in one path must use the same width. Hex input is case-insensitive. Replies use uppercase hex and retain the commas, so the value can be copied directly into another set outpath or set altpath command.

get outpath\nget outpath path\nset outpath A1B2C3,D4E5F6\nset outpath path\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath clear\n

For example, both set altpath 600000,0d2784,f8dada and Set altpath 600000,0d2784,f8dada store the same path and reply with:

> 600000,0D2784,F8DADA\n

The first CLI word is case-insensitive (set, Set, and SET are the same, as are get, Get, and the other command verbs). Argument case is preserved.

get outpath path reports the reciprocal path received after the latest flood login without changing outpath. That packet arrives asynchronously, so retry shortly if the first query reports > path pending; the observation window expires after one minute. The login path is not automatically selected. set outpath path copies the observed route into outpath and saves it; it leaves the current route unchanged when no observed route is available.

set outpath direct sets a zero-hop direct route for a client reachable without repeaters. set outpath clear forgets the override, replies > outpath cleared, and lets normal path discovery fill it again. set outpath flood forces replies to use flood packets until the client logs in again.

When outpath is a valid direct path and altpath is also a valid, different direct path, repeater DM replies send two packets: one on outpath and one on altpath. The secondary altpath copy does not create its own direct-retry state, so retry tracking stays attached to the primary outpath packet. altpath clear disables the secondary direct reply. altpath flood is accepted for command symmetry, but it does not create a second flood reply; only a valid direct altpath sends the second packet.

"},{"location":"halo_keymind_settings/#direct-retry-settings","title":"Direct Retry Settings","text":"

Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. Repeaters expose the settings below; non-repeater firmware uses the same packet-type timing rules with fixed shared base/step timing.

Setting What it does How to use Example retry.preset Applies shared direct and flood retry defaults. Values: infra, rooftop, mobile or 0, 1, 2. get retry.preset, set retry.preset <value> set retry.preset rooftop direct.retry.heard Uses the recent repeater table as the direct retry eligibility gate. get direct.retry.heard, set direct.retry.heard on/off set direct.retry.heard on direct.retry.margin SNR margin in dB above the SF-specific receive floor. get direct.retry.margin, set direct.retry.margin <0-40> set direct.retry.margin 5 direct.retry.count Maximum direct retry attempts after initial TX. Direct-routed type 2 text packets always use 21 attempts regardless of this setting or the short-path cap. get direct.retry.count, set direct.retry.count <1-15> set direct.retry.count 15 direct.retry.base Base wait in milliseconds before retry; packet-length add-on is 3x for TRACE and ANON_REQ/type 7, 7x for TXT_MSG/type 2, and 6x for other direct retry packets. get direct.retry.base, set direct.retry.base <10-5000> set direct.retry.base 175 direct.retry.step Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter. get direct.retry.step, set direct.retry.step <0-5000> set direct.retry.step 100 direct.retry.cr Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use CR4, CR5, CR7, or CR8, then escalate by attempt: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters start at the current radio CR and follow the same escalation pattern, clamped at CR8. get direct.retry.cr, set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>, set direct.retry.cr off set direct.retry.cr 10.0,7.5,2.5,0

The default adaptive coding-rate profile is 10.0,7.5,2.5,2.5. SNR 10.0 dB and up uses CR4, 7.5 dB and up uses CR5, 2.5 dB and down uses CR8, and the middle band uses CR7. If no recent repeater table entry is available, retry packets use CR5. Use set direct.retry.cr off to disable adaptive coding-rate overrides. Repeater attempts escalate from the adaptive starting CR: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters use the current radio CR as the first retry CR and follow the same pattern up to CR8.

Preset details:

Preset Base Count Step SNR gate infra 275 ms 4 150 ms SF floor + 15 dB rooftop 175 ms 15 100 ms SF floor + 5 dB mobile 175 ms 15 50 ms SF floor

Example for a quiet fixed repeater:

set retry.preset rooftop\nset direct.retry.heard on\nset direct.retry.margin 5\n

Example for a moving or weak-link node:

set retry.preset mobile\nset direct.retry.margin 0\n
"},{"location":"halo_keymind_settings/#flood-and-advert-settings","title":"Flood And Advert Settings","text":"

Flood retry applies to flood-routed packets. A queued retry is canceled when the same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses the bucket rules below instead.

Setting What it does How to use Example flood.retry.count Base flood retry attempts after initial TX. Role path scaling happens first, then all builds apply payload caps: REQ is 0; GRP_TXT keeps up to 15; remote-login RESPONSE, TXT_MSG, ANON_REQ, and PATH keep up to 15 at path count 0 and cap at 2 in transit; every other flood type caps at 1. A lower calculated value is preserved, and 0 disables flood retry. get flood.retry.count, set flood.retry.count <0-15> set flood.retry.count 7 flood.retry.path Maximum path hash count eligible for flood retry, or off to disable the gate. get flood.retry.path, set flood.retry.path <0-63/off> set flood.retry.path 1 flood.retry.group.path Additional path gate for group data (type=6) flood retries. The stricter of this and flood.retry.path applies; off disables only this additional gate. Setting the general path gate to 0 forces this setting to off; a named preset restores the default of 1. get flood.retry.group.path, set flood.retry.group.path <0-63/off> set flood.retry.group.path 1 flood.retry.advert Allows or blocks retry for node advert packets (type=4). Default is off. get flood.retry.advert, set flood.retry.advert on/off set flood.retry.advert off flood.retry.prefixes Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry. get flood.retry.prefixes, set flood.retry.prefixes <prefixes/none/off> set flood.retry.prefixes BEEBB0,425E5C flood.retry.ignore Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. get flood.retry.ignore, set flood.retry.ignore <prefixes/none/off> set flood.retry.ignore 71CE82,C7618C flood.retry.bridge Enables bucket-based bridge retry logic. get flood.retry.bridge, set flood.retry.bridge on/off set flood.retry.bridge on flood.retry.bucket.<n> Shows one bridge bucket. Buckets are numbered 1-6. get flood.retry.bucket.<n> get flood.retry.bucket.1 flood.retry.bucket Sets bridge bucket prefixes. set flood.retry.bucket <1-6> <prefixes/none/off> set flood.retry.bucket 1 71CE82,C7618C

Extended flood.rule rows can select which received floods may use these settings. For example, enable bridge completion only for Public and #hamradio with set flood.rule type=any channel=public retry and a second row using channel=#hamradio. Adding the first active retry row changes received retry selection into an allow-list; deleting the last restores the legacy global selection. channel=hash:XX is available when only the visible one-byte hash is known, but it is not authenticated and is collision-prone. For example, hash:11 includes Public and any other packet using 0x11; channel=public alone performs the Public-key MAC/decrypt check.

Bridge buckets may also be reused as passive flood.channel.scope path selectors with path=bucket:<1-6>. This does not require flood.retry.bridge to be enabled and does not depend on recent-repeater freshness or the retry ignore list.

Forwarded neighbor adverts also use an automatic echo guard in every build. If this node hears a downstream, longer-path echo after transmitting an advert whose signed timestamp is less than six hours old, it will not forward that exact advert again during the six-hour age window. This guard works even when flood.retry.advert is off and does not affect self-originated adverts.

Self-originated advert retries are deliberately slow: the first retry waits at least one extra minute beyond the normal airtime-aware delay. Queueing a newer self advert retires queued and future retry attempts for older self adverts without disturbing other flood retry sequences. Companion firmware allows the one slow retry for its own adverts but not for neighbor adverts it relays.

The shared retry preset sets these flood defaults:

Preset Retry count Path gate Group-data path gate infra 1 1 1 rooftop 3 2 1 mobile 15 1 1

Hop-zero flood retries use the direct-retry CR ladder starting at the active radio CR. With CR5, infra allows CR5, CR7; rooftop allows CR5, CR7, CR7, CR8, CR8, CR8; mobile allows CR5, CR7, CR7, then CR8 through retry 15. These lists exclude the initial send and remain subject to payload caps and echo cancellation. Floods with recorded hops keep the active CR. flood.retry.count 0 still disables every automatic flood retry, including hop zero, without changing direct-retry settings or their CR schedule. See hop-zero flood retry coding rate for every starting CR and the payload limits.

Example for path-gated retry:

set retry.preset rooftop\nset flood.retry.path 1\nset flood.retry.group.path 1\nset flood.retry.advert off\nset flood.retry.ignore 71CE82,C7618C\n
"},{"location":"halo_keymind_settings/#north-south-buckets","title":"North South Buckets","text":"

Buckets describe groups of repeaters on different sides of this relay. Bucket numbers do not have built-in meanings; this example uses bucket 1 for North and bucket 2 for South.

              North bucket 1\n        +-----------------------+\n        | A1B2C3       D4E5F6   |\n        | North A      North B  |\n        +-----------+-----------+\n                    |\n                    v\n              +-----------+\n              | This node |\n              +-----------+\n                    ^\n                    |\n        +-----------+-----------+\n        | 71CE82       C7618C   |\n        | South A      South B  |\n        +-----------------------+\n              South bucket 2\n

Configure the buckets:

set flood.retry.bridge on\nset flood.retry.bucket 1 A1B2C3,D4E5F6\nset flood.retry.bucket 2 71CE82,C7618C\nset flood.retry.ignore none\n

Packet heard from the North:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | North bucket | -----> South bucket\n  | bucket 1     | -----> Other fresh/unbucketed relays\n  +--------------+\n

Packet heard from the South:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | South bucket | -----> North bucket\n  | bucket 2     | -----> Other fresh/unbucketed relays\n  +--------------+\n

Packet heard from an unbucketed or pathless source:

     heard source\n         |\n         v\n  +--------------+        retry targets\n  | Other bucket | -----> North bucket\n  | implicit     | -----> South bucket\n  +--------------+\n

Bridge retry stays eligible until every target bucket has been heard or flood.retry.count is exhausted. A configured bucket is a target only when at least one of its prefixes is fresh in recent.repeater. Prefixes in flood.retry.ignore never count as bucket hits.

Configuration reports a warning when prefixes in different buckets, including bucket 7 (flood.retry.prefixes), share the same first byte. A 1-byte path cannot distinguish those buckets. Bridge mode therefore excludes every matching bucket when that short prefix is the source, and credits every matching target bucket when it is heard as an echo. This prevents an ambiguous short prefix from keeping an impossible target outstanding through every retry.

Each flood retry wait retains the fixed maximum-frame plus 20 packet-airtime delay, then adds random jitter from zero to 200 percent of one additional packet airtime. This keeps nearby repeaters from repeating a collision on fixed timing while capping the added wait at two frames.

Only one enhanced retry sequence can be active for the same logical flood packet. Identical floods still receive their normal transmission, but do not multiply the extra attempts. Evicted queued retries release their bridge state, and the final echo wait does not reserve a packet-pool entry.

Earlier path hops from a successful bridge echo refresh a separate per-bucket reachability cache without an SNR value. Only the final hop, which actually sent the received RF frame, updates recent.repeater and its SNR. Indirect path hops therefore cannot change direct-retry SNR gating or coding-rate selection.

"},{"location":"halo_keymind_settings/#troubleshooting","title":"Troubleshooting","text":"

If advert packets are still retrying:

get flood.retry.advert\nset flood.retry.advert off\n

If ignored prefixes still appear in flood retry good logs:

get flood.retry.ignore\nset flood.retry.ignore <prefix>\n

The ignored prefix must match the last hop shown as heard=<prefix>. For example, this log needs C7618C in the ignore list:

flood retry good (... path=7773D0>C7618C, heard=C7618C ...)\n

If retries are too aggressive:

set flood.retry.count 1\nset flood.retry.path 1\nset direct.retry.count 4\n

If retries are too sparse:

set flood.retry.count 7\nset flood.retry.path 2\n
"},{"location":"hardware_validation_bluetooth_stealth_2026-09-07/","title":"Hardware validation bluetooth stealth 2026 09 07","text":"

View this page on MeshCore Docs.

"},{"location":"hardware_validation_bluetooth_stealth_2026-09-07/#report-moved","title":"Report moved","text":"

This report is now in Test Results.

"},{"location":"hardware_validation_checklist/","title":"Hardware validation checklist","text":"

View this page on MeshCore Docs.

"},{"location":"hardware_validation_checklist/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"heltec_t114/","title":"Heltec t114","text":"

View this page on MeshCore Docs.

"},{"location":"heltec_t114/#heltec-t114-screen-detection","title":"Heltec T114 screen detection","text":"

Choose Heltec T114 and your firmware role in the firmware picker. New T114 images with automatic screen detection work on boards with or without the optional stock 1.14-inch screen. There is no separate screen selection. For Companion, use the Full image for USB and Bluetooth support together. The KISS modem role remains headless.

The picker currently supplies 1.18.0.1 preview1 for T114. Automatic screen detection is the default for future T114 builds. The preview includes Full Companion and Full/Reduced optional-sensor profiles for Repeater, Room Server and Sensor, plus the headless KISS utility.

At startup, the firmware checks whether the screen responds before starting the display. The backlight stays off during the check. If no screen responds, the display stays powered off and the node continues without a screen. The selected role's supported connections and radio functions continue to work normally.

A detected screen uses the existing display settings and button wake behavior. Detection does not reset saved screen timeouts, power-saving preferences, identity, or radio settings. To add or remove the screen, power the board off first, then restart it after changing the hardware.

"},{"location":"heltec_t114/#updating-an-older-t114","title":"Updating an older T114","text":"

Use USB when moving from an older Heltec_t114_without_display firmware target to the canonical Heltec_t114 target. The old and new target names have different LoRa update identities; a package built for one is not an update for the other. Follow the normal update procedure and preserve your settings; screen detection does not require a factory reset or a new bootloader.

Previously published firmware remains unchanged. The picker identifies automatic detection from the selected release's verified firmware metadata; an old display-enabled image does not gain detection from its filename.

See the implementation and validation record for the startup checks and hardware-testing boundary.

"},{"location":"heltec_v4_wifi_partition_migration/","title":"Heltec v4 wifi partition migration","text":"

View this page on MeshCore Docs.

"},{"location":"heltec_v4_wifi_partition_migration/#heltec-v4-legacy-partition-migration-over-wi-fi","title":"Heltec V4 legacy-partition migration over Wi-Fi","text":"

Heltec V4/V4.3 OLED repeaters with their normal 16 MiB flash use the shared ESP32 partition migration bridge. It changes Arduino's old default.csv layout, with two 1.25 MiB OTA slots, to default_16MB.csv, with two 6.25 MiB OTA slots.

Build the complete V4 package with the repeatable ESP32 recipe in WSL/Linux:

python3 -B scripts/build_esp32_partition_migration.py \\\n  --board heltec-v4 --version v1.17.1.7-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

The ZIP contains the Wi-Fi bridge, LoRa bridge, and exact-target Full image. Upload its wifi-bridge.bin through the legacy browser Wi-Fi updater. Rejoin MeshCore-Migrate using the node's saved admin password as the Wi-Fi password (see the shared credential and fallback rules); when it reports Expanded layout ready, upload the ZIP's full-application.bin at /update.

Do not use an -merged.bin in either browser uploader. Merged images contain bootloader and table offsets for cable flashing. The bridge preserves the private identity and supported saved configuration, including node name, radio profiles, ACL and login replay state. Files outside the supported preservation set may need to be recreated after the Full image starts.

For an existing heltec_v4_repeater that already supports LoRa mOTA, use the ZIP's lora-bridge.mota with the identity-only LoRa handoff described in the shared guide. It preserves the old repeater across the partition change so the Full image can also be installed over LoRa without reaching the board's Wi-Fi network. That route does not restore saved settings; its post-expansion Wi-Fi recovery AP normally uses the published fallback. The optional Partition Expander route instead restores supported settings and retains the saved admin password.

"},{"location":"home_text_spacing/","title":"Home text spacing","text":"

View this page on MeshCore Docs.

"},{"location":"home_text_spacing/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"host_cli_service/","title":"Host cli service","text":"

View this page on MeshCore Docs.

"},{"location":"host_cli_service/#lora-cli-host-service","title":"LoRa CLI Host Service","text":"

The repeater host <text> command lets an authenticated LoRa administrator request a small, explicitly allowed operation from a USB-connected computer. The included Raspberry Pi endpoint supports these exact requests:

cmd host help\ncmd host cpu-temp\ncmd host hostname\ncmd host uptime\ncmd host load\ncmd host memory\ncmd host disk-free\ncmd host clock status\ncmd host clock sync\ncmd host clock set <unix_epoch>\ncmd host network restart\ncmd host reboot\ncmd host action status <operation_id>\ncmd host run <alias> [arguments]\n

The first seven actions after help are read-only. Clock changes, network restart, and reboot are opt-in actions and are disabled unless explicitly enabled. Text such as reboot now, cpu-temp; reboot, and embedded newlines is not a command: the endpoint accepts only an exact allowlist match.

The bridge is included in normal repeater firmware except on Wio-E5. Its specialized RS232 bridge image omits the host service because the combined application exceeds the fixed 240 KiB partition; use the normal Wio-E5 repeater image when the USB/MQTT host service is needed.

"},{"location":"host_cli_service/#run-alongside-meshcoretomqtt","title":"Run alongside meshcoretomqtt","text":"

meshcoretomqtt must remain the only process that opens the repeater USB serial port. The endpoint communicates with it through the same MQTT broker, which can be local to the Raspberry Pi:

LoRa -> repeater -> USB -> meshcoretomqtt -> MQTT -> host endpoint\nLoRa <- repeater <- USB <- meshcoretomqtt <- MQTT <- host endpoint\n

Clock-recovery deployments must run the loopback broker, meshcoretomqtt, and endpoint on the same Pi. A wrong Pi clock can prevent TLS validation against a remote broker, while split host clocks can reject the signed live claim before the correction arrives. Remote brokers remain suitable for ordinary commands when the connection and both clocks are already healthy.

Launch meshcoretomqtt with its existing arguments plus --debug; its current debug-topic parser needs that flag to publish the repeater request record. Do not open the serial TTY from the host endpoint too.

This feature does not compare the repeater wall clock with the Pi clock. Minutes of drift, an unset repeater clock, and later clock corrections are safe: request freshness comes from a live one-time challenge. The normal meshcoretomqtt sync_time setting may remain enabled for its other uses, but host-command authorization does not depend on it.

Generate a dedicated service key using the Python environment installed by meshcoretomqtt:

sudo /opt/mctomqtt/venv/bin/python3 host_cli_service.py \\\n  --generate-key /etc/mctomqtt/host-cli-key.json\nsudo chown mctomqtt:mctomqtt /etc/mctomqtt/host-cli-key.json\nsudo chmod 600 /etc/mctomqtt/host-cli-key.json\n

Add the printed public key to the existing meshcoretomqtt configuration:

[remote_serial]\nenabled = true\nallowed_companions = [\n  \"SERVICE_PUBLIC_KEY_PRINTED_ABOVE\"\n]\nnonce_ttl = 120\ncommand_timeout = 10\n

Restart meshcoretomqtt, then run the endpoint. Replace USA with the exact three-character IATA namespace configured in the broker and supply the repeater's complete 64-character public key:

sudo -u mctomqtt /opt/mctomqtt/venv/bin/python3 host_cli_service.py \\\n  --broker 127.0.0.1 \\\n  --iata USA \\\n  --repeater-key REPEATER_PUBLIC_KEY \\\n  --service-key /etc/mctomqtt/host-cli-key.json\n

Add --username NAME --password-file FILE for broker authentication. Add --tls, and optionally --ca-cert FILE, for TLS. Custom installations can use --request-topic and --command-topic to match their broker namespace. The endpoint source and its systemd/configuration details are in examples/host_cli_service.

"},{"location":"host_cli_service/#allowlisted-programs-and-arguments","title":"Allowlisted programs and arguments","text":"

Add --programs-file FILE to expose locally selected programs as host run <alias> [arguments]. The example JSON file fixes each executable, fixed leading arguments, maximum 1-5 second runtime, and the exact remote argument schema. For example:

cmd host run fan on 15\n

The example maps that request to the fixed process argument vector:

/usr/local/bin/mesh-fan-control --source lora on 15\n

The supported argument rules are an explicit non-option choice, a bounded nonnegative integer, or a short token using a restricted ASCII character set. Alias lookup and every argument validation happen before process creation. Execution uses an absolute configured path, no stdin, shell=False, a minimal environment, and / as its working directory. Extra arguments, leading-option injection, shell syntax, control characters, and invalid quoting are rejected. The allowlist file and executable must not be group- or world-writable.

Copy and edit programs.example.json, then give the service account only the operating-system permissions needed by those trusted programs.

"},{"location":"host_cli_service/#opt-in-network-and-reboot-recovery","title":"Opt-in network and reboot recovery","text":"

These actions use their own socket-activated root broker, independently of the clock-control broker. Installing or enabling clock control does not grant host recovery actions. Install the following as root-owned files:

sudo install -o root -g root -m 0755 meshcore_host_actions.py \\\n  /usr/local/sbin/meshcore-host-actions\nsudo install -o root -g root -m 0644 meshcore-host-actions.socket \\\n  /etc/systemd/system/meshcore-host-actions.socket\nsudo install -o root -g root -m 0644 meshcore-host-actions.service \\\n  /etc/systemd/system/meshcore-host-actions.service\nsudo install -o root -g root -m 0644 meshcore-networkmanager-restart.service \\\n  /etc/systemd/system/meshcore-networkmanager-restart.service\nsudo install -o root -g root -m 0644 meshcore-host-reboot.timer \\\n  /etc/systemd/system/meshcore-host-reboot.timer\nsudo install -o root -g root -m 0644 meshcore-host-reboot.service \\\n  /etc/systemd/system/meshcore-host-reboot.service\n

The root-owned broker policy enables nothing by default. Create a systemd drop-in and select network-restart, reboot, or both as an exact comma-separated list:

sudo systemctl edit meshcore-host-actions.service\n
[Service]\nEnvironment=MESHCORE_HOST_ACTIONS=network-restart,reboot\n

Reload systemd and enable only the broker socket:

sudo systemctl daemon-reload\nsudo systemctl enable --now meshcore-host-actions.socket\n

After changing the policy on an already active installation, restart meshcore-host-actions.service so the broker reads the new value.

Finally add --allow-network-restart, --allow-reboot, or both to the unprivileged endpoint. An action must pass both gates: the endpoint flag and the root-owned broker policy. The endpoint never invokes sudo; remove legacy wifi-restart program aliases and host-action sudoers entries because sudo aliases are not a fallback for this hardened service. Use the exact host network restart action instead.

Systemd creates /run/meshcore-host-actions.sock as root:mctomqtt mode 0660. The root broker authenticates both the endpoint UID and primary GID with SO_PEERCRED, while the endpoint authenticates the root-created listener. The protocol is one bounded ASCII line. Neither the caller nor request text can choose an executable, unit, path, or argument. The broker can start only the fixed static NetworkManager restart service or the fixed reboot timer; the broker and action units have empty capability sets and systemd sandboxing. The configured mctomqtt UID and its effective primary GID are the delegated local trust boundary; supplementary group membership alone is rejected.

Side effects follow a fail-closed two-phase sequence. The endpoint reserves a canonical 128-bit operation ID with PREPARE, publishes its reply with MQTT QoS 1, and requires wait_for_publish plus is_published confirmation before sending one COMMIT. It does not block the Paho network callback while waiting. No confirmation means no commit. Reboot uses a fixed approximately 10-second timer that starts only after commit. The MQTT confirmation proves acceptance by the local broker, not delivery over USB and LoRa; there is no correlated serial delivery acknowledgement, so the physical reply is best effort.

network restart deliberately drops Wi-Fi and Tailscale management while NetworkManager restarts. Use a loopback MQTT broker and keep the broker, meshcoretomqtt, host endpoint, and USB device services independent of NetworkManager: they must not have Requires=, BindsTo=, or ordering dependencies on it. This preserves the local reply/commit path while remote management temporarily disappears.

The operation ID is derived from the authenticated repeater key, request ID, and nonce. The reply displays it, and its state can be queried later:

cmd host action status 0123456789ABCDEF0123456789ABCDEF\n

The root broker retains at most 64 records for the current boot. It persists prepared before replying and in-progress before scheduling, schedules a committed operation at most once, and never automatically retries an ambiguous outcome. A broker restart converts recovered in-progress to ambiguous. Only an old uncommitted prepared record may be evicted; otherwise a full store rejects new work. Reusing an operation ID for a different action is rejected. scheduled means PID 1 accepted the fixed unit job; it does not claim that the subsequent network restart or reboot completed successfully.

A distinct reboot is rejected while another reboot is committed or ambiguous, so a second request cannot falsely promise a fresh 10-second timer while the first timer is already running. A new reboot reservation may supersede only an older, uncommitted reboot reservation; the superseded operation can no longer be committed.

"},{"location":"host_cli_service/#opt-in-clock-recovery","title":"Opt-in clock recovery","text":"

clock status reports the Pi epoch and NTP synchronization state without root access. To enable the exact clock sync and clock set <unix_epoch> actions, install meshcore_clock_control.py as /usr/local/sbin/meshcore-clock-control, owned by root and mode 0755, then install the accompanying meshcore-clock-control.socket and meshcore-clock-control.service and static meshcore-chrony-step.service files in /etc/systemd/system as root-owned mode-0644 files:

sudo install -o root -g root -m 0755 meshcore_clock_control.py \\\n  /usr/local/sbin/meshcore-clock-control\nsudo install -o root -g root -m 0644 meshcore-clock-control.socket \\\n  /etc/systemd/system/meshcore-clock-control.socket\nsudo install -o root -g root -m 0644 meshcore-clock-control.service \\\n  /etc/systemd/system/meshcore-clock-control.service\nsudo install -o root -g root -m 0644 meshcore-chrony-step.service \\\n  /etc/systemd/system/meshcore-chrony-step.service\nsudo systemctl daemon-reload\nsudo systemctl enable --now meshcore-clock-control.socket\n

The socket is fixed at /run/meshcore-clock-control.sock; systemd creates it as root:mctomqtt with mode 0660. Run the endpoint as user and primary group mctomqtt, then add --allow-clock-control. The endpoint validates the socket metadata and authenticates the connected server as root with Linux SO_PEERCRED. The root service independently requires the peer's UID and primary GID to match mctomqtt:mctomqtt before it reads or executes a request. No clock-control sudo rule is used; remove old clock-helper sudoers lines when upgrading.

Both layers require one canonical unsigned decimal argument with no sign, leading zero, whitespace, or trailing text. The accepted epoch range is 2020 through 2099. The private protocol accepts only the complete bounded ASCII lines sync and set <epoch>. Every child process uses a fixed absolute argv, shell=False, and a timeout no longer than 1.5 seconds.

clock set changes CLOCK_REALTIME without disabling NTP, then requests an NTP step. If the NTP request fails, the response honestly reports that the clock changed and which synchronization step remains incomplete. If clock sync enables NTP but the explicit step/restart fails, that partial outcome is also reported. When chrony is available, the root broker starts only the fixed static meshcore-chrony-step.service. That hardened one-shot runs /usr/bin/chronyc -a makestep as _chrony:_chrony, with no capabilities, AF_UNIX only, and a one-second start timeout; it can therefore reach chrony's private runtime socket without broadening the root broker. The _chrony account comes from the Debian/Raspberry Pi chrony package. Without chrony, the broker restarts systemd-timesyncd. Clock drift cannot authorize a captured command: the signed request still needs the repeater's live one-time claim, whose deadline is monotonic rather than wall-clock based.

"},{"location":"host_cli_service/#trust-and-injection-controls","title":"Trust and injection controls","text":"

The MQTT broker transports records but does not establish their authenticity. The endpoint checks the configured repeater identity and verifies its Ed25519 signature over the complete request ID, random nonce, and Base64URL request. It also validates framing, UTF-8, and byte limits. It does not execute that first record.

Instead, the endpoint stores the request in memory, creates a random 64-bit challenge, and sends @claim=<random> through meshcoretomqtt's signed serial channel. The repeater accepts it only from physical USB while the exact ID and nonce are pending, then signs a CLAIMED proof containing the challenge. Only a matching live proof lets the endpoint perform allowlist matching and execute the action. It consumes the proof before execution, so MQTT redelivery cannot execute an action twice.

A captured request only causes a new challenge that an idle repeater refuses. A captured proof does not match a new challenge, and restarting the endpoint forgets pending challenges. Repeater/Pi clock drift therefore cannot turn an old reboot or run record into a valid action. The reply and claim commands still use short-lived JWTs; meshcoretomqtt checks their signer allowlist, target, expiration, signature, and separate replay nonce. Wall-clock expiry and its in-memory nonce cache are not sufficient across a backward clock jump plus restart; the firmware's live one-time claim is what protects these host actions. Keep recovery-mode allowed_companions limited to the dedicated service key, with the endpoint and meshcoretomqtt on the same Pi.

Newlines and other reply control characters are converted to spaces and are also independently rejected by the firmware parser.

Protect the service private key and use broker ACLs that allow it to subscribe only to the selected repeater debug topic and publish only to that repeater serial-command topic. A private key listed in allowed_companions is trusted to sign serial requests.

"},{"location":"host_cli_service/#limits","title":"Limits","text":"

Use cmd get host to report the bridge state and limits.

"},{"location":"kiss_modem_protocol/","title":"Kiss modem protocol","text":"

View this page on MeshCore Docs.

"},{"location":"kiss_modem_protocol/#meshcore-kiss-modem-protocol","title":"MeshCore KISS Modem Protocol","text":"

Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.

KISS protocol version 2 adds an opt-in logical port 1 for MeshCore's second time-shared LoRa profile. It is still one physical transceiver: the firmware fast-switches between radio and radio2; it does not create a second radio.

"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"

115200 baud, 8N1, no flow control.

"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"

Standard KISS framing per the KA9Q/K3MC specification.

Byte Name Description 0xC0 FEND Frame delimiter 0xDB FESC Escape character 0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0) 0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
+------+-----------+--------------+------+\n| FEND | Type Byte | Data (escaped)| FEND |\n| 0xC0 |  1 byte   | 0-510 bytes  | 0xC0 |\n+------+-----------+--------------+------+\n
"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"

The type byte is split into two nibbles:

Bits Field Description 7-4 Port 0 primary radio; 1 secondary radio2 for Data only in protocol v2; controls stay on port 0 3-0 Command Command number

Maximum unescaped frame size: 512 bytes.

"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data 0x00 Raw packet Queue on primary radio; use type 0x10 to queue on radio2 in v2 (one pending packet total) TXDELAY 0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence 0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime 0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail 0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex 0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware 0x06 Sub-command + data MeshCore extensions (see below) Return 0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data 0x00 Raw packet Received packet from primary radio Data 0x10 Raw packet Received packet from secondary radio2 (v2)

Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. A v2 port-1 Data packet reaches RF only while radio2 is configured rxtx; with off or rx, the modem returns TxDone(0) and never falls back to primary.

Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).

"},{"location":"kiss_modem_protocol/#fast-dual-profile-receive-switching","title":"Fast dual-profile receive switching","text":"

After OpenHop (or another KISS client) configures radio2 or tempradio2, the board's radio_driver.loop() performs the RF hops locally. KISS does not send a command for every hop: that would be slower than the serial link alone. The shared scanner uses a 600 \u00b5s nominal retune allowance per hop, with the profile timing/preamble calculation accounting for it. It is a measured target rather than a hard maximum; packet handling and a slow board loop can extend an individual hop. The KISS parser services at most 32 incoming bytes per outer loop so a busy OpenHop serial peer cannot starve the scanner.

Thus \u201caround 0.6 ms\u201d refers to the on-board RF setting transition, not a full host-to-board request/response round trip. The returned Data port (0x00 or 0x10) identifies which profile actually received each packet.

SetRadio2 and SetTempRadio2 acknowledge that their tuple was accepted and scheduled; they do not acknowledge that a receiver has already entered that tuple. The next local scanner service performs the retune. KISS clients should configure the profiles once and let the board perform automatic time-sharing; a 115200-baud serial round trip cannot be a 0.6 ms receive-hop control plane. Clients that need temporary-lease state can poll GetTempRadio2.

"},{"location":"kiss_modem_protocol/#host-output-backpressure","title":"Host Output Backpressure","text":"

Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; loop() never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.

"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"

The TNC implements p-persistent CSMA for half-duplex operation:

  1. When a packet is queued, monitor carrier detect
  2. When the channel clears, generate a random value 0-255
  3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
  4. Otherwise, wait SlotTime and repeat from step 1

In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.

"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"

MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.

"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
+------+------+-------------+--------------+------+\n| FEND | 0x06 | Sub-command | Data (escaped)| FEND |\n| 0xC0 |      |   1 byte    |   variable   | 0xC0 |\n+------+------+-------------+--------------+------+\n
"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity 0x01 - GetRandom 0x02 Length (1 byte, 1-64) VerifySignature 0x03 PubKey (32) + Signature (64) + Data SignData 0x04 Data to sign EncryptData 0x05 Key (32) + Plaintext DecryptData 0x06 Key (32) + MAC (2) + Ciphertext KeyExchange 0x07 Remote PubKey (32) Hash 0x08 Data to hash SetRadio 0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower 0x0A Power dBm (1) GetRadio 0x0B - GetTxPower 0x0C - GetCurrentRssi 0x0D - IsChannelBusy 0x0E - GetAirtime 0x0F Packet length (1) GetNoiseFloor 0x10 - GetVersion 0x11 - GetStats 0x12 - GetBattery 0x13 - GetMCUTemp 0x14 - GetSensors 0x15 Permissions (1) GetDeviceName 0x16 - Ping 0x17 - Reboot 0x18 - SetSignalReport 0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport 0x1A - SetRadio2 0x1B Radio2 parameters (13; see below) GetRadio2 0x1C - SetTempRadio2 0x1D Radio2 parameters + duration (15) GetTempRadio2 0x1E - SetRadio2Ext 0x1F Radio2 parameters + protocol (14) GetRadio2Ext 0x20 - GetRadioSlot 0x22 - (Station G3 only) SetRadioSlot 0x23 Mode (1): 0=auto, 1=slot 1, 2=slot 2 (Station G3 only)"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"

Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.

Sub-command Value Data Identity 0x81 PubKey (32) Random 0x82 Random bytes (1-64) Verify 0x83 Result (1): 0x00=invalid, 0x01=valid Signature 0x84 Signature (64) Encrypted 0x85 MAC (2) + Ciphertext Decrypted 0x86 Plaintext SharedSecret 0x87 Shared secret (32) Hash 0x88 SHA-256 hash (32) Radio 0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower 0x8C Power dBm (1) CurrentRssi 0x8D RSSI dBm (1, signed) ChannelBusy 0x8E Result (1): 0x00=clear, 0x01=busy Airtime 0x8F Milliseconds (4) NoiseFloor 0x90 dBm (2, signed) Version 0x91 Version (1) + Reserved (1) Stats 0x92 RX (4) + TX (4) + Errors (4) Battery 0x93 Millivolts (2) MCUTemp 0x94 Temperature (2, signed) Sensors 0x95 CayenneLPP payload DeviceName 0x96 Name (variable, UTF-8) Pong 0x97 - SignalReport 0x9A Status (1): 0x00=disabled, 0x01=enabled Radio2 0x9C Saved radio2 parameters (13) TempRadio2 0x9E Active temporary radio2 + remaining minutes (15) Radio2Ext 0xA0 Saved radio2 parameters + protocol (14) RadioSlot 0xA2 Saved mode, active slot, detected mask, reboot needed (4 bytes) OK 0xF0 - Error 0xF1 Error code (1) TxDone 0xF8 Result (1): 0x00=failed, 0x01=success RxMeta 0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength 0x01 Request data too short InvalidParam 0x02 Invalid parameter value NoCallback 0x03 Feature not available MacFailed 0x04 MAC verification failed UnknownCmd 0x05 Unknown sub-command EncryptFailed 0x06 Encryption failed TxBusy 0x07 Radio TX busy, or host output queue full SaveFailed 0x08 Setting could not be saved; running slot unchanged"},{"location":"kiss_modem_protocol/#station-g3-physical-radio-slot","title":"Station G3 physical radio slot","text":"

GetRadioSlot takes no data and replies with four bytes: saved mode (0 auto, 1 or 2 forced socket), active socket (0 unavailable, 1 or 2), detected socket mask (bit 0 = socket 1, bit 1 = socket 2), and reboot-needed flag (0 or 1). This selects the physical SX1262 daughterboard socket, not the logical radio2 PHY profile. Other boards return UnknownCmd.

SetRadioSlot requires exactly one mode byte (0\u20132). An OK means the mode was saved; the running socket stays unchanged until a separate Reboot command or power cycle. Radio settings and identity are untouched. A failed save returns SaveFailed, with no change to the running socket.

If no usable socket is found during boot, the normal KISS modem has not started. Use the separate USB recovery console: !g3 get radio.slot, !g3 set radio.slot auto, then !g3 reboot. Recovery accepts only these prefixed management commands (and !g3 help), never KISS packet payloads.

"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"

The TNC sends these SetHardware frames without a preceding request:

TxDone (0xF8): Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.

RxMeta (0xF9): Sent after each Data frame (type 0x00 or v2 type 0x10) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). It remains exactly two bytes; the Data port identifies the receiving profile. Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.

"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"

All values little-endian.

Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#secondary-radio-parameters-setradio2-radio2-response","title":"Secondary Radio Parameters (SetRadio2 / Radio2 response)","text":"

SetRadio2 is the binary KISS equivalent of radio2. It changes only RAM state for this KISS session; it does not write the Mesh CLI profile file. All values are little-endian. The mode values are 0=off, 1=rx, and 2=rxtx. An off tuple may contain zero radio parameters, which are ignored.

Field Size Description Frequency 4 bytes Hz Bandwidth 4 bytes Hz SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8) Mode 1 byte off, rx, or rxtx as above Preamble 2 bytes Symbols; 0 chooses automatic

GetRadio2 returns this exact 13-byte tuple. The modem preserves the exact frequency and bandwidth values sent by the host even though its radio driver uses floating-point MHz/kHz internally.

"},{"location":"kiss_modem_protocol/#optional-extended-secondary-radio-setradio2ext-radio2ext-response","title":"Optional Extended Secondary Radio (SetRadio2Ext / Radio2Ext response)","text":"

These additive commands retain protocol version 2 and all existing OpenHop commands unchanged. SetRadio2Ext takes the exact 13-byte secondary tuple above followed by one protocol byte: 0 = MeshCore (LoRa sync word 0x12), or 2 = raw Ethernet (sync word 0x16). Other values, unavailable wire support, invalid modem tuples and lengths other than 14 bytes are rejected without changing the saved or active profile. GetRadio2Ext takes no data and returns the saved tuple plus that protocol byte, exactly 14 bytes.

Both modes use explicit-header, CRC-on, normal-IQ LoRa. Ethernet frames remain raw Data on KISS port 1 (0x10); the modem adds no IP stack, ARP implementation or WiFi server. Hosts provide Ethernet/IPv4/TCP and must respect the one-pending- TX limit by waiting for TxDone before submitting another Data frame. The standard KISS TXDELAY default is 500 ms; hosts may explicitly change it for a low-delay link. Signal reports retain their existing two-byte format.

The extended setting is RAM-only and has the same saved/temporary semantics as SetRadio2: an active SetTempRadio2 lease is not interrupted, and its expiry or cancellation restores the saved tuple including its protocol. The legacy 13-byte SetRadio2 and 15-byte SetTempRadio2 always select MeshCore; the legacy GetRadio2 remains exactly 13 bytes and does not report the protocol. Hosts that need Ethernet must first probe GetRadio2Ext; older firmware replies UnknownCmd. Do not silently fall back to MeshCore when this probe fails.

Builders can opt a compatible raw KISS modem into Ethernet wire support with MESH_ENABLE_ETHERNET_WIRE=1, leaving MESH_ENABLE_ETHERNET_TUNNEL=0. The wire flag defaults to the tunnel flag, so existing Companion builds are unchanged. The radio chip must independently support the requested profile; for example, SX1276 cannot use SF5 even when Ethernet wire support is compiled in.

Bench images compiled with MESH_HIL_TX_FREQ_MHZ additionally support the read-only SetHardware GetLabTxGuard (0x21, no request data). Its response 0xA1 is the compiled allowed frequency as u32 little-endian Hz; the standard 909.5 MHz bench lock returns 909500000. It proves the same frequency boundary used by the radio TX guard, not merely a requested setting. Production images without this explicit build flag return UnknownCmd; no setter exists.

"},{"location":"kiss_modem_protocol/#temporary-secondary-radio-settempradio2-tempradio2-response","title":"Temporary Secondary Radio (SetTempRadio2 / TempRadio2 response)","text":"

SetTempRadio2 is the binary KISS equivalent of tempradio2: the 13-byte secondary tuple followed by duration_minutes:u16, from 1 to 10080. It is RAM-only and automatically restores the saved radio2 tuple on expiry or reboot. A mode off with duration zero cancels the temporary lease and immediately restores saved radio2; all other off/zero-duration combinations are invalid. GetTempRadio2 returns the active 13-byte tuple plus remaining minutes, rounded up. It returns a zero/off tuple with zero duration when no temporary lease is active.

Changing saved radio2 during an active temporary lease does not interrupt the active tuple; the replacement becomes active when the lease ends. Pending KISS Data is bound to a profile generation at queue time. If that profile is replaced before RF transmission begins, the modem sends normal TxDone(0) and never falls back to the other profile.

"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"

All values little-endian.

Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"

All values little-endian.

Field Size Description Noise floor 2 bytes int16_t, dBm (signed)

The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.

"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"

All values little-endian.

Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"

All values little-endian.

Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"

All values little-endian.

Field Size Description Temperature 2 bytes int16_t, tenths of degC (e.g., 253 = 25.3degC)

Returns NoCallback error if the board does not support temperature readings.

"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"

Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.

"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0 0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure)

Use 0x07 for all permissions.

"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"

Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.

"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":""},{"location":"lora_ota_automation/","title":"Lora ota automation","text":"

View this page on MeshCore Docs.

"},{"location":"lora_ota_automation/#scripted-lora-ota-from-start-to-finish","title":"Scripted LoRa OTA from start to finish","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

The dedicated RAK3401 chain report records the physical failures of withdrawn migrations and the physically qualified compact nine-step replacement. Its runner blocks the withdrawn chains and pins every accepted bridge image by SHA-256.

tools/lora_ota/lora_ota.sh and tools/lora_ota/lora_ota.ps1 automate a MeshCore LoRa firmware update from a release .zip or ready .mota. They identify the destination, validate the hardware and running firmware, prepare the right container, move the participating nodes to a temporary radio channel, serve and monitor the download, request installation, restore the controller, and check the rebooted node.

The script cannot install the destination's first OTA-capable firmware. It can stage signed v3 bootloader containers with --no-install, but never sends the privileged bootloader install command. Invalid v2 containers carrying the bootloader flag remain rejected. Do the first nRF52 bootloader installation over USB. An already-capable, explicitly supported internal-flash, XIAO QSPI, or exact MeshTower V2 microSD repeater may later use the separate manual ota bootloader install workflow.

"},{"location":"lora_ota_automation/#required-topology","title":"Required topology","text":"

The reliable serial topology uses two local radios:

                                      authenticated admin commands\ncomputer -- MeshCore binary API --> controller Companion -------------------+\n   |                                                                         |\n   +-- raw text CLI + mOTA seeder --> OTA source ---- LoRa OTA blocks ----> target\n                                                        |                    ^\n                                                        +-- relay(s) --------+\n

One serial port cannot serve both controller roles: meshcli must keep reopening the controller while motatool owns the source port. The script rejects an attempt to use the same port for both.

The USB ASCII switch (+++MESHCORE-TERM-START) is the local control path, not the mOTA data framing. On an nRF52 dual-CDC Full Companion, the script uses that mode briefly for ota status and TempRadio commands. It then closes the CLI and starts motatool; the exact ota folder on preamble switches the same USB interface 00 into exclusive mOTA mode from either startup ASCII or Binary Companion mode. If USB logging was enabled and the Full Companion rebooted, its separate interface 02 continues to provide plaintext logging and is not a controller or source port. With default logging off, interface 02 is not enumerated. BLE remains available.

ESP32 Full Companion normally uses its dedicated TCP seeder on port 5001. ESP32 builds that also retain serial folder support use the same exclusive USB mOTA ownership state as nRF52. The exact ota folder on line transfers the USB port from startup ASCII or idle Binary Companion mode before binary mOTA frames begin; neither console mode shares that port until the folder detaches.

"},{"location":"lora_ota_automation/#bootloader-packages-and-station-identifiers","title":"Bootloader packages and station identifiers","text":"

Both wrappers use the same package-aware runner. It identifies application versus bootloader updates from container metadata, not filenames. A ZIP mixing application inputs and bootloader mOTAs requires --zip-member; their versions and target IDs are independent and must not be ranked together.

Signed format-3 nRF52 bootloader packages can be staged with --no-install. Use the same controller/source/TempRadio arguments as an application transfer, but provide the bootloader .mota (or its ZIP member) and --no-install. Without that flag, even --yes stops before opening a radio. Bootloader staging requires Python cryptography in the interpreter running the wrapper and a current motatool supporting format 3. The runner reuses tools/mota/motalib.py for strict geometry, embedded identity/CRC, capability, hash and signature validation, and still runs motatool verify including any --public-key pin.

If Python cannot load cryptography, an interactive run offers:

Install cryptography privately and retry signature verification? [y/N]\n

After approval, it uses the exact running Python interpreter to install cryptography==50.0.1 and its dependencies from PyPI into a separate per-user, per-interpreter cache. Binary wheels only are accepted; the repair does not compile native code, overwrite system/virtual-environment packages, change PATH, use sudo, or bypass externally-managed Python protections. It checks Ed25519 against a known valid signature and a tampered message, then retries the original full package verification. A later run can reuse a working cache without another download. This works for direct .mota files and bootloader members inside ZIPs; ordinary application updates and raw --tcp-cli commands do not need this library.

The interpreter must have working pip. If pip is missing, the script gives an exact-interpreter ensurepip command and operating-system pip/venv guidance; it does not silently install system tools. No compatible binary wheel, an old pip, network/download failures, an unwritable cache, or a failed signature check stop the run before radio access. Pip output is shown and installation is bounded at ten minutes. The radio admin-password environment variable is not passed to pip. Partial installation files may remain in the displayed private directory. The cache is under meshcore-lora-ota/python/<interpreter-id>/cryptography==50.0.1 in the same per-user cache base described below for motatool.

Only y or yes approves this repair; --yes is not installation consent. Noninteractive runs print a copyable private-install command and stop. After running that command manually, rerun the original OTA command; no Python path changes are needed. Declining never skips signature verification.

It probes actual motatool inspect support before radio access: older and newer builds may advertise the same version. If the tool is missing or incompatible, an interactive run offers an automatic repair:

Install this bootloader-capable motatool and continue after verification? [y/N]\n

Only y or yes approves installation. --yes does not approve host software installation. Noninteractive runs stop and print a copyable manual installation command plus the --motatool argument to use afterward.

The motatool repair builds the mikecarper/motatool fork at pinned revision 8c38369e7d35ad50cf74261869676d52dd24adf7, with Cargo's --locked dependencies, in a separate per-user cache directory. It needs matching stable Cargo and rustc 1.86.0 or newer and a native linker already installed; otherwise it points to Rust installation and stops without installing anything. The pinned dependency set needs more than the older rust-version in motatool's own manifest: zeroize 1.9.0 needs edition 2024, and ICU/idna dependencies need Rust 1.86. Cargo 1.75 is a version problem, not a root-permission problem; no nightly toolchain is necessary.

Before asking to build or downloading crates, the runner checks both executable versions. It keeps a compatible default pair; otherwise it looks for versioned executables on PATH (such as Ubuntu's cargo-1.91 and rustc-1.91) and already installed stable rustup toolchains. It displays the selected paths and pins both for the build process, so a newer Cargo cannot silently invoke an old rustc. Rustup proxies are resolved to their actual toolchain binaries before changing to the build directory. No default toolchain, symlink, system PATH, or package installation is changed, and probes disable rustup's automatic toolchain install. Tool version probes have ten-second timeouts. A working cached motatool does not need Cargo or Rust checks at all.

To choose a pair explicitly, append:

--cargo cargo-1.91 --rustc rustc-1.91\n

These options also accept executable paths, not shell aliases or a command with embedded arguments. Explicit selections and RUSTC/CARGO_BUILD_RUSTC compiler overrides are honored; if incompatible, they fail instead of silently selecting another compiler. --rustc takes precedence over those environment variables. The manual build command includes the chosen compiler as well as Cargo. Native linker, SDK, and system-library availability are still checked by the real build; a passed version check is not a guarantee that every host prerequisite is present. Source/dependency downloads, build time and disk use are disclosed before confirmation. Cargo progress is displayed, with a one-hour build timeout. The radio admin-password environment variable is not passed to the build.

The installer does not overwrite the selected/system motatool or alter PATH. It may replace only a previous copy in its displayed private install directory. After installation it repeats the bootloader-package capability check and uses the new binary for this run only if that check passes. A later run using the default motatool selection can reuse the checked cache without downloading again; switching from an explicit --motatool path still asks permission. Refusal, a failed build, or a failed recheck stops before opening any radio. Failed build files may remain in the displayed cache directory for diagnostics.

The install root is meshcore-lora-ota/motatool/<revision> under %LOCALAPPDATA% on Windows, ~/Library/Caches on macOS, or $XDG_CACHE_HOME (default ~/.cache) on Linux. Cargo uses an explicit --root, --rev, and --locked instead of replacing a global installation or following a moving branch.

Before transfer it queries the destination's ota bootloader status, checks its bootloader-specific target/hardware identity, installed ABI/codecs and matching internal/QSPI/SD storage profile. It does not compare an application version or application target ID to a bootloader package. After transfer it checks the destination's exact staged MID and image-hash confirmation, then prints the manual command; it never sends a bootloader install command:

ota bootloader status\nota bootloader install <MID8> <HASH16>\n

Review and send those commands on the destination, not the source. The device independently enforces signer authorization, safe live storage, continuity and upgrade-only policy at installation. --base, --allow-non-upgrade, and --prepare-only remain application-only. See the bootloader prerequisites and recovery limits.

TARGET_NODE, --relay, and --source-contact accept a contact name, full public key, or unique hexadecimal key prefix. The runner reads the controller's existing contact table and binds each selection to a full key before remote commands, so emoji names need not be typed and later name changes cannot redirect the run. Duplicate names, ambiguous prefixes, or the same radio under different participant aliases stop with an actionable error. Missing destination and relay contacts must first be imported or discovered. A missing connected source can be imported with the confirmation prompt below; a key alone does not create a contact.

For a separate source with a managed USB/TCP console, automatic source selection reads the physical source's full public key and matches it in the controller's contacts. Repeaters use get public.key. Full Companion does not implement that repeater command: on both USB and TCP, its key is read from a fresh terminal welcome banner, with a supported ver reply on the same connection proving the terminal is live. USB briefly uses the existing STOP/START/STOP terminal wrapper to obtain that banner, even for an ASCII-first Full Companion; TCP reads the greeting from a new connection. Identity rechecks repeat this exchange rather than trusting a key cached from an earlier connection.

ota status identifies the Full Companion seeder role before this probe. For unrecognized firmware, only an explicit unsupported get public.key response can trigger the Companion fallback. Timeouts, permission errors, malformed keys, missing/ambiguous banners, and failed ver replies still stop the run. Firmware that exposes neither identity mechanism needs a supported terminal firmware; the runner does not guess a key from the node name or skip the identity check.

Source selection does not require the source's current name to match an old saved advert, and duplicate/emoji names cannot redirect this lookup. An explicit --source-contact must identify that same physical source. If its key is missing, an interactive run offers:

Add the connected OTA source to this controller's contacts? [y/N]\n

This works through both the shell and PowerShell launchers. The runner reads the source's existing card command over USB/TCP, checks the full key in its contact card against the connected source, and shows its name/key before asking. On approval it rechecks the source/controller identities and live contact table, then uses MeshCore's existing import_contact API. Signature validation remains with the controller's normal advert importer. For manual-add controllers it can approve only that exact source through add_pending; it does not enable general auto-add. A fresh reload_contacts must confirm the full source key before OTA continues. No LoRa advertisement or firmware update is required for this import.

The default answer is No; --yes does not approve adding a contact. A noninteractive run stops with instructions to rerun in a terminal. Unsupported card firmware, malformed/mismatched cards, and unverified imports stop with manual-import/discovery guidance. To protect existing contacts, the controller's overwrite-oldest policy must be verifiably disabled; otherwise the runner stops and shows how to disable that bit without changing other auto-add flags. It does not change this policy itself. A full table or rejected import cannot be worked around by deleting another contact; an uncertain import is not automatically retried. An approved contact remains on the controller for subsequent runs.

The normal/TempRadio on-air ACK checks still run; adding a contact does not prove radio reachability or skip those checks. When the controller and source are the same TCP Full Companion, use the verified --source-shares-controller topology; it does not need a contact for itself.

"},{"location":"lora_ota_automation/#destination-requirements","title":"Destination requirements","text":"Destination Package installed One-time prerequisite Raw ZIP handling ESP32 Full application image OTA-enabled image with an A/B partition table Builds a full mOTA from the matching non-merged application .bin nRF52, internal flash In-place delta Exact-board OTAFIX bootloader with mOTA apply support Requires --base with the exact image currently running nRF52 repeater, external QSPI Full image or in-place delta QSPI-aware exact-board OTAFIX bootloader and matched repeater build Builds a full mOTA; adding --base requests a delta MeshTower V2 nRF52, microSD Full image or in-place delta SD-aware exact-board OTAFIX bootloader and compatible card Builds a full mOTA; adding --base requests a delta

MeshTower V2 microSD application and bootloader containers require an Ed25519 signature from a key in the device allowlist. Supply --sign-key when the runner must build a container, and install the BLM2-capable SD-aware bootloader locally before attempting application or bootloader OTA. Preview.12 requires USB/BLE DFU or SWD; see MeshTower V2 microSD LoRa OTA.

The firmware inside a raw ZIP must have a valid MeshCore EndF trailer. An ESP32 merged/factory image is not an application image and is rejected. A generic vendor DFU ZIP may also be unusable if it does not contain the raw EndF-bearing .hex or .bin.

For an internal-staging nRF52, the exact base image is irreducible information. The node reports its eight-byte body hash, but that hash cannot reconstruct the firmware bytes needed to create a delta. Keep the .pio/build/ENV/firmware.hex that was actually flashed. A matching filename or version alone is not enough.

"},{"location":"lora_ota_automation/#1-install-the-host-tools","title":"1. Install the host tools","text":"

Install Python 3.10 or newer, Rust, Git, the official meshcore-cli 1.6.0 or newer, and the official motatool.

For signed bootloader staging, see the automatic cryptography and bootloader-capable motatool repairs above. Pip is needed only when installing the missing Python dependency; Rust/Cargo and a native linker are needed only when building motatool, not when using an already compatible binary. The runner checks the selected meshcli version before opening radios. Keep its serial/BLE dependencies in meshcli's own environment (pipx handles these); installing cryptography in meshcli's pipx environment does not make it available to the separate Python interpreter running this wrapper. A ready bootloader .mota does not need the Python detools or intelhex build dependencies.

On Bash:

python3 -m pip install --user pipx\npython3 -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n\nmeshcli -v\nmotatool --version\n

On PowerShell:

py -m pip install --user pipx\npy -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path .\\motatool\n\nmeshcli -v\nmotatool --version\n

Restart the shell if pipx or Cargo reports that it changed PATH.

"},{"location":"lora_ota_automation/#2-identify-and-test-both-local-links","title":"2. Identify and test both local links","text":"

List serial devices:

meshcli -l\n

The examples below assume /dev/ttyACM0 is the controller and /dev/ttyACM1 is the OTA source. On Windows they might be COM7 and COM8. Close picocom, a serial monitor, the phone app, and any other program holding either link.

Test the controller's binary API:

meshcli -s /dev/ttyACM0 -b 115200 ver\n

For an ordinary raw-text source, test its OTA support:

meshcli -r -s /dev/ttyACM1 -b 115200 \"ota status\"\n

The command must print an OTA | ... target:XXXXXXXX status.

For an nRF52 full Companion, open the source port with terminal mode selected automatically:

picocom -b 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM1\n

Run ota status. It must report OTA seeder, install:disabled, and target 00000000; send +++MESHCORE-TERM-STOP before closing the terminal. The automation detects and performs this token-wrapped preflight itself, so no extra command-line option is needed.

For an ESP32 full Companion, test its separate WiFi control console instead:

./tools/lora_ota/lora_ota.sh --tcp-cli 192.168.1.50 \"ota status\"\n

It must report OTA seeder, install:disabled, and target 00000000.

This standalone fallback uses Python's raw TCP socket directly. It works with meshcli versions whose -r mode rejects -t; it does not need meshcli, motatool, nc, or a firmware package. It defaults to text port 5002; port 5000 speaks the binary Companion protocol and port 5001 serves mOTA data. The firmware must expose the text console; this option does not enable one.

Specify a port or adjust the total connection/command timeout (10 seconds by default):

./tools/lora_ota/lora_ota.sh --tcp-cli 192.168.1.50:5002 --timeout 15 \"ver\"\n

PowerShell uses the same option:

.\\tools\\lora_ota\\lora_ota.ps1 --tcp-cli 192.168.1.50 \"ota status\"\n

Place --tcp-cli first. Each invocation sends one command and waits for the complete reply and terminal prompt. Connection failures, timeouts, missing replies, and rejected commands exit nonzero. If USB owns the terminal, close the active USB terminal and try again. Commands are never automatically resent in this standalone mode: a timeout after sending may mean the command executed but its reply was lost. Inspect the device state before repeating a change.

For an actual OTA run, continue using --source-tcp HOST:5001 with --source-cli-tcp HOST:5002; that existing control path also bypasses meshcli raw mode. The standalone fallback does not transfer a firmware image itself.

Changing a terminal to 57600 baud does not select ASCII mode. USB Companion builds and the normal raw management CLI use 115200 unless a particular build was explicitly configured otherwise.

"},{"location":"lora_ota_automation/#3-check-the-destination-once","title":"3. Check the destination once","text":"

The destination must be in the controller's contacts and remotely reachable on the normal channel. The script runs these authenticated checks itself:

ota status\nget bootloader.ver\nota self\nota stats\n

The script uses get bootloader.ver as an optional platform/version hint, not as an install gate. It makes one attempt without an interactive retry loop. unknown, an unsupported/missing command, an unrecognized reply, or a lost reply produces a warning and does not by itself block the run. In particular, older MeshCore firmware can report unknown for the installed signed OTAFIX 2.4.6 MeshTower V2 SD image because it omits the legacy UF2 version string. Both the shell and PowerShell launchers use this same behavior; no application or Companion firmware update is required for this host-side workaround. The runner also accepts the new OTAFIX2.4.6 and explicitly labelled 0.11.0 (base) formats.

The runner still requires ota self to report bootloader: apply OK, bootloader: QSPI apply OK, or bootloader: SD apply OK and checks the reported bootloader ABI and codec mask against the selected package. If the version command is unavailable on older firmware, the script warns and falls back to the legacy ota self platform marker. If an nRF52 bootloader lacks the required capabilities, install the exact-board OTAFIX bootloader first. Positive nRF52 capability markers take precedence over a generic unsupported getter response, so it cannot silently skip nRF52 safety checks. Bootloader packages still require the independently checked ota bootloader status board, target, ABI, codec and storage identity; missing version text never bypasses package signature checks or the explicit bootloader-install workflow. The separate hardware-qualified rak3401_mota_chain.py retains its exact deployed version restriction; it is not a general OTAFIX 2.4.6 update runner. Current firmware also reports maxblk:2048 near the front of both OTA replies. The runner treats a missing marker as the deployed 1 KiB receive limit, rejects a ready 2 KiB package for such a target before transfer, and passes the live limit to motatool --block-size when it builds from raw firmware. Thus updating an older target from a release ZIP produces a compatible 1 KiB package while a current target retains the 2 KiB application default. ota stats is only an optional EndF version probe. It uses one bounded retry cycle; unsupported firmware or a lost reply falls back directly to the required ver command instead of entering an operator continuation loop.

The default TempRadio tuple is:

909.5,500,5,5,120\n

The lab test default is 909.5 MHz, 500 kHz bandwidth, SF5, and CR5. The frequency is only a North American example: choose a legal frequency supported by every participating radio and appropriate to your location. Older radios that do not support SF5 require a complete replacement tuple passed with --temp-radio. The fifth value is the TempRadio window in minutes. To change only that value, pass --temp-radio-minutes 90; this works with --relay and keeps the chosen frequency, bandwidth, SF, and CR. The hard minimum is 20 minutes. The 120-minute default and the runner's worst-case timeout estimate are guidance. A window below the estimate prints a warning and continues; the radio lease can expire before a slow OTA finishes. For an interactive run configured at 250 kHz, the runner offers a 125-kHz path rehearsal at SF5 before the final update confirmation. If a 250-kHz rehearsal cannot prove the temporary path but every short lease expires and the exact normal path returns without active recovery, it offers the same 125-kHz SF5 start. If the 125-kHz SF5 path fails in that same cleanly returned way, the operator can approve a fresh SF6 rehearsal, then SF7 if SF6 fails. Each change prints the revised plan and requires confirmation before its rehearsal; the long transfer uses only the setting that passes. The selected long-window duration is unchanged, and each attempt repeats the full short rehearsal. --yes and non-interactive runs never change bandwidth or spreading factor automatically. To start directly at 125 kHz SF5 without a prompt, pass --temp-radio 909.5,125,5,5,150 (choose a legal frequency and an appropriate window for the actual route). The separate three-minute path rehearsal and its identity checks still run.

Before that long window is allowed, the live runner performs a mandatory independent three-minute rehearsal. Its exact normal-channel ota status and ota self identity proof gets one shared four-minute, read-only budget before any schedule, radio override, seeder, or transfer is armed; this tolerates a marginal link without consuming or extending a live lease. It advances a stale managed controller with exact readback and treats a source terminal's whole-minute display as an uncertainty window, never as an exact epoch. A source minute overlapping the host is preserved; a completely stale minute is pinned to a guarded future value and read back once. Neither clock is moved backward, and the runner stops if a managed clock cannot be proven within the ten-minute limit. Each remote participant must expose an empty tempradioat schedule. The runner projects one fixed start/end interval into each participant's independently sampled RTC, sends each mutation once, proves every exact identity on the temporary tuple, waits for natural expiry, and proves the complete normal path again. A lost schedule reply is resolved by those on-air proofs; it is never blindly replayed with a fresh duration.

The published RAK3401 v1.16.7-c1caa5ad LoRa-OTA image includes get/set/del tempradioat and can take this safe first step. A truly older or reduced build without fixed TempRadio scheduling is rejected before mutation; bootstrap it locally or with another explicitly controlled maintenance path. The automation does not substitute an immediate tempradio command whose first delivery could remain queued after cleanup.

"},{"location":"lora_ota_automation/#rxps-handling-during-tempradio","title":"RXPS handling during TempRadio","text":"

RXPS improves receive performance per unit of radio-on time, so the runner keeps the destination's saved RXPS preference enabled whenever the selected fast tuple has a qualified timing window. Before changing any radio, it reads ver from the destination, controller, source, and every named relay. It treats v1.17.1.5 as the first forward contract in which every SF5-SF8 transmission, including a retry, uses the same tuple-selected physical preamble: normally 32 symbols, 64 only when 32 cannot enable RXPS, and 128 only when neither 32 nor 64 can. A saved RXPS level is also safely retuned after a radio change. An older or unparseable destination version fails closed: the runner temporarily sends set radio.rxps off instead of assuming an ad-hoc build contains the timing fixes. Automation deliberately treats the version as a wire-format contract.

For the default SF5/BW250 tuple, the runner keeps destination RXPS on only when the destination and every possible sender are v1.17.1.5 or newer. It does not overwrite an existing level-based preference. Current firmware retunes that preference against the tuple-selected 64-symbol wire preamble; the qualified boundary is effective level 8, preamble 64 (1252 / 6424 us). A manually configured node may save set radio.rxps level 8 preamble 32 before entering TempRadio: when 32 symbols cannot cover the TCXO transition, firmware safely selects the real 64-symbol preamble. If the saved setting uses fixed manual timings, or even one participant is older or unknown, RXPS is temporarily off. This avoids the receive gap that a 32-symbol sender or an unretuned manual window would create.

The complete qualified SX1262+TCXO policy is:

TempRadio SF/BW Saved reference setting Qualified boundary Automation SF7/BW500 level 7 preamble 32 7 / 32 RXPS on for a current destination SF6/BW250 level 7 preamble 32 7 / 32 RXPS on for a current destination SF5/BW125 level 7 preamble 32 7 / 32 RXPS on for a current destination SF5/BW250 level 8 preamble 32 8 / 64 RXPS on only when every participant follows the 64-symbol contract SF6/BW500 level 8 preamble 32 8 / 64 RXPS on only when every participant follows the adaptive-preamble contract SF5/BW500 level 8 preamble 32 8 / 128 RXPS on only when every participant follows the adaptive-preamble contract SF5/BW62.5 level 10 preamble 16 10 / 16 RXPS on for a current destination Unqualified tuple none continuous RX RXPS temporarily off

If RXPS was already off, the runner leaves it off. Otherwise it writes the original level, preamble assumption, receive/sleep periods, and temporary decision to protected target-rxps-settings.json in the run's working directory, verifies the temporary state, and restores the exact original setting after the target is back on its normal radio. Current firmware exposes this complete state through get radio.rxps.config; a legacy reply has only on/off and periods, so the runner can restore those periods but cannot reconstruct an unreported saved level. A radio change later recalculates from a preserved saved minimum, so moving back to a slower tuple returns to the operator's saved level.

The OTA source has a stricter policy than the destination. For every source with a managed serial or TCP CLI, the runner reads and retains its exact RXPS preference, using the legacy fixed-period query only when the detailed query is unavailable. It builds and verifies the package, completes the read-only target checks, and obtains confirmation while that preference remains unchanged. Immediately before the first radio mutation, it reads the source RXPS state again, disables RXPS, and verifies the readback. Source RXPS stays off through catalog serving, download, installation, and post-install identity verification. Cleanup first proves that the source has returned to its normal radio, then restores and verifies the exact saved level/preamble or fixed-period state once. A source whose RXPS state cannot be read, disabled, or restored safely fails closed. If current firmware explicitly rejects an RXPS disable or restore with radio busy; retry, the runner retries that idempotent mutation at staggered 210\u2013378 ms intervals. All 32 delays are distinct and contribute about 9.4 seconds of waits; source-CLI command round-trip time is additional. This avoids repeatedly sampling one radio phase while retaining a strict attempt cap. Other rejections are not replayed.

"},{"location":"lora_ota_automation/#4-run-an-esp32-update","title":"4. Run an ESP32 update","text":"

The ZIP can contain a compatible ready .mota or the exact board-and-role non-merged application .bin:

export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof ESP32\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n

The script shows the detected target, hardware, running hash, chosen package, version, manifest ID, and action before asking for confirmation. For an unattended job, add --yes:

./tools/lora_ota/lora_ota.sh ./release.mota \"Roof ESP32\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1 \\\n  --yes\n

PowerShell equivalents:

$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof ESP32' `\n  --controller-serial COM7 `\n  --source-serial COM8\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Roof ESP32' `\n  --controller-serial COM7 `\n  --source-serial COM8 `\n  --yes\n

Prefer the environment variable or the interactive password prompt. Passing --password works, but the wrapper's own command line may be visible to other local processes. The runner keeps the password out of child meshcli command lines and removes its protected temporary command file after each call.

"},{"location":"lora_ota_automation/#5-run-an-nrf52-update","title":"5. Run an nRF52 update","text":"

If the input ZIP already contains a compatible in-place delta .mota, no base argument is needed: its embedded base hash is compared with the live node. If the ZIP contains raw new firmware, supply the exact running image:

./tools/lora_ota/lora_ota.sh ./nrf52-new-release.zip \"Hill nRF52\" \\\n  --base ./firmware-that-is-running.hex \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n
& .\\tools\\lora_ota\\lora_ota.ps1 '.\\nrf52-new-release.zip' 'Hill nRF52' `\n  --base '.\\firmware-that-is-running.hex' `\n  --controller-serial COM7 `\n  --source-serial COM8\n

Before building a delta, the runner proves that the base's target ID, hardware identity, firmware version when available, and EndF body hash match the live destination. It then asks motatool for codec 2, the nRF52 in-place format. The normal workspace is 0x98000.

For a QSPI-backed repeater or the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without --base. Supplying an exact base requests a smaller in-place delta and automatically selects the conservative external workspace 0xC6000, which is safe for S140 v6 and v7 application layouts. An explicit --inplace-memory overrides the automatic value.

The live runner detects QSPI from ota self (QSPI apply OK) or ota status (bl:QSPI) and refuses an explicit QSPI store:ERR 0K report even when the bootloader itself advertises QSPI apply support. Offline --prepare-only runs must supply --nrf-qspi; do not use that switch for a board that only exposes QSPI pins or uses the chip as a Companion filesystem. The application and bootloader must both be from the matched repeater list in the nRF52 QSPI guide.

"},{"location":"lora_ota_automation/#6-add-intermediate-relays","title":"6. Add intermediate relays","text":"

The runner automatically checks ota config on the destination, managed source, and each OTA-enabled relay before confirmation. The declared --relay count is the minimum required OTA reach: one relay requires at least one hop, not the direct-only hops=0. After the radio rehearsal passes, it rechecks the original policies and temporarily raises only limits below that minimum. Higher existing limits remain unchanged. Every change needs exact readback; a lost setter reply is reconciled by reading the policy, not blindly repeating the write.

The original limits are restored after the download, before installation can reboot the destination, and cleanup also attempts restoration after an error or Ctrl-C. A serial seeder is stopped before the source text CLI is used. The private, password-free ota-hop-settings.json recovery file is flushed before any hop write. Hop limits are persisted by firmware: if the host loses power or a radio becomes unreachable, use that file's original values once the correct radio channel is reachable. Unexpected external policy changes are not overwritten.

For a route containing additional, undeclared repeaters, add --ota-hops N (0..8, at least the declared relay count). This is a minimum, not a command to reduce larger limits. It does not discover or switch undeclared repeaters; all participating radios still need overlapping TempRadio windows. Direct runs without relays or a positive explicit minimum leave hop policies untouched. Explicitly non-OTA relays remain opaque forwarders; unsupported/ambiguous policy replies, authentication failures and timeouts on OTA participants stop the run. No signer, install-security or forwarding-filter checks are bypassed.

List relays from farthest to nearest so each command is sent before its route moves to TempRadio. A bare relay name uses the destination password; use NAME=PASSWORD when it differs:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1 \\\n  --relay \"Far Relay=far-password\" \\\n  --relay \"Near Relay=near-password\"\n

PowerShell uses the same arguments:

& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Remote Target' `\n  --controller-serial COM7 `\n  --source-serial COM8 `\n  --relay 'Far Relay=far-password' `\n  --relay 'Near Relay=near-password'\n
"},{"location":"lora_ota_automation/#other-connection-choices","title":"Other connection choices","text":"

The controller can use any one of:

--controller-serial PORT\n--controller-tcp HOST[:PORT]       # default port 5000\n--controller-ble ADDRESS_OR_NAME\n

An ESP32 FULL/repeater source can serve over its dedicated WiFi seeder port while its raw USB CLI is used to start TempRadio:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-serial /dev/ttyACM1\n

An ESP32 companion_radio_full uses WiFi for both dedicated source links:

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.50:5001 \\\n  --source-cli-tcp 192.168.1.50:5002\n

Port 5002 defaults automatically when it is omitted from --source-cli-tcp. Current Full Companion firmware exposes its complete role-specific text terminal there; the runner uses only ota, tempradio, and normalradio, and remains compatible with older bounded port-5002 consoles. The source-only Companion never stages or installs the image itself; it streams the host folder to other nodes over LoRa. See the full Companion guide for manual operation and interface details.

An nRF52 companion_radio_full uses one USB source port sequentially. The runner automatically wraps local control commands in the terminal tokens, and unmodified motatool switches that port into mOTA mode when seeding starts:

The wrapper sends STOP, then START, before each command. This makes fallback independent of whether a prior raw probe left an unobservable USB-UART connection in ASCII or Binary mode. Seeder startup is reported only after the verbose device log contains its COUNT acknowledgement; an immediate device ERR or a missing acknowledgement fails during startup instead of surfacing later as a catalog timeout.

Newer motatool builds prepare raw-DEFLATE blocks with Zopfli before opening the source link. This host-only work has a separate 30-minute preparation limit (--seeder-prepare-wait SECONDS) and reports progress while waiting. The five-second device acknowledgement timer (--seeder-start-wait SECONDS) starts after the final compressed N/N blocks message, not when compression starts. Neither compression progress nor a host \"serving\" message proves device readiness: the runner still requires the device's COUNT reply. Older motatool builds without a preparation phase keep the original device startup timeout. Process exits and device errors still fail immediately. Preparation happens after TempRadio is armed, so leave enough lease time for both compression and transfer, especially on a slow host. The suggested TempRadio budget includes the preparation limit; it remains a warning, not a hard requirement to select that entire worst-case budget.

./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-serial /dev/ttyACM1\n

If the source is already on the exact TempRadio tuple through a scheduled or manual operation, --source-already-temp lets a TCP source run without a raw CLI link. The script cannot verify, extend, or shorten that source window, so leave a comfortable time margin. It also cannot inspect or change that unmanaged source's RXPS state; disable source RXPS yourself before starting the run and restore it only after returning the source to its normal radio.

Use --controller-baud or --source-baud only for a build whose corresponding interface is genuinely configured to another speed.

"},{"location":"lora_ota_automation/#package-selection-and-safety-gates","title":"Package selection and safety gates","text":"

For a ZIP, the runner first examines every .mota without extracting paths. It keeps only packages matching the live target, hardware, base, platform, codec, and bootloader capabilities. It chooses the newest compatible version and prefers a delta over a full image at the same version. If equally suitable files differ, select one explicitly:

--zip-member path/inside/archive/update.mota\n

If no ready mOTA is usable, it searches .bin and .hex members for a valid, matching EndF, then builds the platform-appropriate container. Every result is structurally checked by the runner and independently passed through motatool verify before any radio changes. Direct firmware and mOTA inputs, as well as individual ZIP members, are rejected above 64 MiB before being loaded.

Useful controls:

For offline package preparation only:

./tools/lora_ota/lora_ota.sh ./release.zip offline \\\n  --prepare-only \\\n  --platform nrf52 \\\n  --target-id 1234ABCD \\\n  --target-base-hash 0011223344556677 \\\n  --target-hw Heltec_T114 \\\n  --base ./firmware-that-is-running.hex\n

Live operation is safer because the script obtains these values directly from the destination.

"},{"location":"lora_ota_automation/#what-happens-during-a-run","title":"What happens during a run","text":"
  1. Validate the input paths and host tools, then prove the source is either an OTA-enabled raw CLI or a source-only full Companion control interface. Read and durably save a managed source's exact RXPS preference without changing it.
  2. Before sending any remote packet, prove the managed source has no active, pending, or fixed TempRadio work, then gate the source and controller clocks. Advance a stale exact clock to host time; for a minute-resolution source, preserve an overlapping minute or use one guarded forward value with readback. Preserve a small future lead and fail closed above the ten-minute drift limit. Then authenticate to the target, query its target ID, hardware, running body hash, firmware version, bootloader version, and nRF52 bootloader capabilities, and save the controller's normal radio tuple.
  3. Select or build one compatible v2 application mOTA and verify all block hashes, Merkle root, full-image hash where applicable, identity fields, signature, codec, base, and the live firmware's reported maximum block size (1 KiB for legacy replies without maxblk, currently 2 KiB otherwise). Version-3 bootloader packages are refused before any target state changes.
  4. Read every participant's version, save the destination's RXPS state, select the qualified destination policy, and show the confirmation prompt.
  5. Run the fixed-window three-minute rehearsal described above. This phase does not disable RXPS, start a seeder, request a pull, or install anything; the long transfer remains completely unarmed until temporary and normal reachability both pass.
  6. Fresh-read, disable, and verify source RXPS; apply and verify the destination RXPS policy; then start TempRadio on the target, far-to-near relays, and source. A separate controller is moved and read back through Binary; a shared Full Companion instead schedules its bounded local override while Binary remains the transport. The runner rejects TempRadio windows under 20 minutes and warns when the selected window does not cover worst-case setup, seeder preparation and startup, discovery, the transfer timeout, final polling, and install checks.
  7. Start motatool serve, discover the exact eight-hex manifest ID, request ota pull <id> flash, and poll until that same ID reports ready. A seeder process exit stops the run immediately. For --no-install, schedule all script-controlled nodes back to their normal radios before restoring the controller, unless --leave-controller-radio was requested.
  8. Recheck that exact ID, give the target a short final TempRadio safety window, and request ota install. Then shorten each relay's TempRadio window so the normal multi-hop route returns, stop the seeder, shorten the source window, restore the controller, and probe ota self every 10 seconds through the configured readiness window (five minutes by default). The exact new body hash is the readiness signal; only then does the runner require the exact package version. A relayed run continues the 10-second probes through the mandatory relay-return window. A source supplied with --source-already-temp is never modified. --leave-controller-radio moves the controller back to TempRadio only after this normal-channel verification. Restore the destination's exact original RXPS setting after normal-channel identity is proven. A managed source stays RXPS-off through that verification and its exact setting is restored only after its own TempRadio state is proven inactive.

Remote replies are matched only after queued messages have been drained and only when they come from the intended contact and fit the command. A ready status for another manifest ID is an error, never permission to install it.

"},{"location":"lora_ota_automation/#transmission-loss-and-retries","title":"Transmission loss and retries","text":""},{"location":"lora_ota_automation/#legacy-relay-manifest-workaround","title":"Legacy relay manifest workaround","text":"

The deployed 2d03e098 firmware uses the ordinary 160-entry, non-expiring packet hash FIFO for OTA forwarding. Identical manifest requests and replies can therefore be suppressed after packet loss, even while CLI commands work. The firmware fix replaces that forwarding decision with a small timed OTA cache, suppresses downstream echoes and already-queued copies, and keeps TempRadio, hop limits and forwarding filters authoritative. It applies to discovery, manifest and block traffic, including opaque OTA relays.

Until the relay is updated, add --legacy-relay-dedup-workaround to the same runner command. This is opt-in because it adds airtime. It requires --source-shares-controller, exactly one --relay, a saved direct (zero-hop) route to that relay, and no extra --ota-hops beyond one. After an exact download: failed (manifest timeout) 0/0 for the requested MID, it sends up to 192 unique 13-byte direct, non-forwarded OTA no-operation/unknown-opcode frames on the verified TempRadio channel. These replace legacy FIFO entries; 192 gives some loss headroom over 160 entries, not proof of reception. Then it rechecks the destination and requests the same signed package once. A lost pull reply is reconciled using status, not blindly replayed.

There are at most two recovery rounds within the original transfer deadline. Each round adds 2,496 origin Mesh bytes plus LoRa overhead, normally roughly a minute at 125 kHz/SF7 before command/status latency. The companion's local queue acceptance is not an RF acknowledgement. Recovery stops on insufficient verified lease time, wrong radio settings, command errors, or changed session. It never reboots a relay, cancels a partial image, changes package bytes, or bypasses signature/install checks. It does not fix later block-retry stalls on old firmware or relays beyond the controller's direct radio reach; the firmware update is the permanent fix.

For a short path, --legacy-relay-reboot-recovery offers a lower-airtime alternative after that same exact 0/0 timeout. It requires one managed relay whose saved normal-channel and active temporary-channel routes are each zero or one hop, plus --source-shares-controller. Before any reboot, the runner checks the relay and destination identities, the exact failed manifest, and at least eight minutes of remaining transfer and target TempRadio time. It asks for a separate confirmation at the failure (or requires both this flag and --yes in unattended use). The relay's reboot CLI intentionally sends no reply: the runner returns the shared source/controller to normal radio, authenticates the relay's exact key there, re-arms only the remaining portion of the target's TempRadio lease, then proves the relay, destination and same manifest on the temporary channel before one pull retry. If the normal path cannot be proved after reboot, it stops instead of assuming the reboot worked; the other temporary leases still expire. A reboot also clears the relay's temporary radio setting, so manually rebooting it during a transfer without this re-arming sequence will break the temporary path. When both legacy recovery options are enabled, the reboot choice is offered first; declining it leaves the direct-cache workaround available on a zero-hop route.

Read-only and replay-safe transmissions retry up to three times. Three retries or 90 seconds, whichever comes first, opens a 10-second stop-or-continue prompt when stdin is an interactive terminal. Continue remains the default on timeout or Enter, so an operator can persist through a temporary outage; enter s or stop to end the run, and Ctrl-C remains immediate. With non-interactive stdin, the runner stops after that finite automatic retry cycle instead of silently starting another cycle forever. Optional participant-version probes always use a bounded cycle and degrade to unknown; optional destination ota stats falls back directly to ver. The mandatory normal-channel destination baseline is the narrow exception: it may make nine total read-only attempts but is stopped by one hard four-minute deadline before any TempRadio lease or OTA mutation exists. Proofs inside the live rehearsal retain the smaller four-attempt limit.

Commands that change OTA state are reconciled before replay:

Retries and operator-selected continuation can outlast the original TempRadio budget. If a bounded window expires, rerun the same package after the nodes return to their normal channel; the manifest-ID check resumes its partial download without replacing it.

The working directory is created before a managed source can be changed, then retained and printed at exit. It contains the exact served mOTA, motatool-serve.log, extracted build inputs when needed, and controller-radio.txt, plus ota-hop-settings.json when OTA reach needed a temporary increase. A managed source also gets a protected source-rxps-settings.json containing its exact original preference and idempotent restore command. Its contents and directory entry are flushed before RXPS is disabled. When the destination started with RXPS enabled, protected target-rxps-settings.json records its manual recovery state. The RAK3401 chain points every nested step at one chain-root source record, so a rerun after host power loss does not adopt the temporary RXPS-off state as the original. A retained record is accepted only for the same managed CLI endpoint. These files contain no saved admin password. After exact source restoration, a standalone run atomically retires its record; the chain retains its shared record between steps and retires it only after the verified endpoint restoration completes.

"},{"location":"lora_ota_automation/#interruption-and-recovery","title":"Interruption and recovery","text":"

Ctrl-C stops the seeder, detaches its serial folder, makes a bounded attempt to shorten a source TempRadio window started by the script, and attempts to restore the controller. For a managed source it leaves RXPS off until the source is proven back on its normal radio, then restores and verifies the saved source preference. A transient success-path restore failure remains armed for one more idempotent cleanup attempt. The target and relays remain on TempRadio only until their bounded windows end; rebooting also restores their saved radio settings. A normal cleanup restores the destination's exact RXPS periods. If that remote restore cannot be confirmed, use target-rxps-settings.json after the target returns to its normal channel. A partial download remains safe. Once the target is reachable again (after its TempRadio window ends, or after putting the controller back on that tuple), rerunning the same package recognizes its manifest ID and resumes the existing session instead of clearing it.

The destination and each controlled relay also receive separate protected target-tempradio-cleanup.json or relay-tempradio-cleanup-<full-key>.json records. Each record is flushed before its single one-minute shortening attempt. A lost acknowledgement remains unconfirmed; a reply on the temporary channel does not establish that the shortening arrived. Restoring the controller or reaching the destination through another path does not establish that every relay returned either.

Default cleanup succeeds only after the controller is back on its normal radio and every owned participant independently returns its bound full public key and an inactive get tempradio result. Shared Full Companions must also report their live TempRadio override inactive; Binary radio settings alone show the saved preference. Unresolved participants retain their own record and the run reports failure. A deliberately staged --leave-controller-radio run preserves its windows and records. After an install, that option proves all participants during the normal-radio verification interval before returning the controller to the requested temporary tuple.

After a process interruption, retain these records and use a new work directory. Existing records cannot be overwritten to reset a shortening attempt. Never repeat a relative tempradio command whose attempt is recorded. On the normal route, use the recorded full key and a separately supplied admin password to read get public.key and get tempradio. Both must confirm that same participant and an inactive override; a timeout, changed key, active profile, or malformed reply leaves its obligation pending. Wait for its original finite lease and repeat only these read-only checks. Keep the original record and a separate private restoration receipt binding its file hash and successful proofs. These records contain no admin passwords and do not authorize reboot or installation.

When a chained run has already proved the newly running body hash, its retained previous package can briefly report verifying staged blocks after TempRadio reactivates the OTA manager. The runner waits only through the configured discovery timeout, keeps checking source liveness, and accepts only the same manifest becoming ready to install or the manager becoming idle. A changed ID, failed or incomplete state, or timeout stops the chain. It then proves the exact installed body again. If that same session is still attached and ready, the runner detaches it with ota cancel; if the manager has become idle, it sends no cancel. An ordinary-channel no download status proves only that the manager is idle, not that persistent staging was erased. The runner therefore does not issue or describe an IDLE cancel as durable cleanup. The next chain transition explicitly re-adopts and proves the expected previous MID before detaching it; after the final install OTAFIX has consumed the approval word, so any retained container is inert and is replaced by the next valid pull.

A hard process kill or host power loss cannot run cleanup. Recover a serial controller using the tuple saved in the printed work directory:

radio=$(tr -d '\\r\\n' < ./meshcore-lora-ota-20260807-123456-1234/controller-radio.txt)\nmeshcli -s /dev/ttyACM0 set radio \"$radio\"\n
$radio = (Get-Content '.\\meshcore-lora-ota-...\\controller-radio.txt' -Raw).Trim()\nmeshcli -s COM7 set radio $radio\n

For a managed source, first return it to its ordinary radio, then inspect source-rxps-settings.json and issue its exact restore_command through the same serial or TCP-console endpoint recorded in that file. The command is idempotent; confirm the full setting with get radio.rxps.config before resuming an update.

If you stop during final confirmation, reconnect on the node's normal channel and run ota self and ver. A completed run returns success only when ota self reports a valid new body hash and ver exactly matches the package; an unverified install returns status 2. Do not immediately replace a staged image: the default active-download guard preserves it until you explicitly use --replace-active-download or run ota cancel.

Exit status is 0 for success, 2 for a validation or operational error, and 130 for Ctrl-C.

"},{"location":"management_decoder/","title":"Management decoder","text":"

View this page on MeshCore Docs.

"},{"location":"management_decoder/#management-report-decoder","title":"Management report decoder","text":"

Paste MGR1 or MGR2 management-report payloads or complete GroupData packets from a packet analyzer to read a radio's public management information. Everything, including password authentication and ACL decryption, happens locally in this browser. The report, password, and candidate public key are never uploaded.

The public portion of a report is deliberately plaintext, but it is not authenticated until a management password is supplied. A password-authenticated page proves that its public fields and encrypted ACL bytes have not been altered by someone who does not know that password.

"},{"location":"management_decoder/#decode-a-management-report","title":"Decode a management reportEncrypted ACL entries","text":"MGR1/MGR2 payload or complete GroupData packet hex

Spaces, line breaks, colons, dashes, 0x, and MQTT JSON fields named raw or data are accepted. Paste all pages from one report together to view a complete multi-page ACL. Press Ctrl/Command+Enter to decode.

Management password (optional for public fields; required for ACLs)

The password remains in this page only. The decoder derives the management AES-SIV key in your browser and does not send it anywhere.

Candidate administrator public key (optional)

ACL encryption reveals per-radio 12-byte fingerprints, not recoverable public keys. Supplying a complete candidate key checks whether its fingerprint appears in this report.

Decode management report Clear local data Load authenticated example Decode notes"},{"location":"management_decoder/#what-the-decoder-accepts","title":"What the decoder accepts","text":"

Use the same password configured by set mgmt.password. A decoded ACL lists the report-specific fingerprints and the administrator and/or OTA-signer flags. It cannot turn a fingerprint back into a full key. Use the optional candidate field to test a specific full public key.

MGR2 also shows USB logging, host/reader connection, logger-client activity, stalled/recovering/deferred state, recovery stage, saved backoff, retry interval/inactive seconds, Auto qualification seconds, and persistence readiness. Unsupported observation is shown explicitly; legacy MGR1 has no USB block and shows it as unavailable. These values are the report-start snapshot, not proof that a Pi or host application is healthy. Older decoder copies require an update for MGR2.

MGR2 also shows the latest USB watchdog action, reason mask and names, advisory node-clock timestamp, recorded-boot uptime seconds, event sequence, and whether the record was durably saved. Software recovery and re-enumeration mean attempted actions, not successful repairs. A reboot request is not proof that a reboot completed; a later cancellation can replace it. The node clock can be wrong, even when it resembles a valid date. Event uptime and sequence remain useful for ordering. MGR1 does not carry this event field and shows it as unavailable. All 36 ACL entries still fit, using up to nine MGR2 pages.

The earlier USB-only MGR2 development layout was never released. This decoder accepts the finalized MGR2 format with its 111-byte public header and latest watchdog event.

Reports contain a frozen latest-event snapshot, not immediate watchdog alerts or a complete event history. See USB logging watchdog for local review and persistence limitations.

Host/reader connection only describes the USB link or open reader. Logger-client activity means recent USB stats polling within a 15-minute lease, renewed by the existing five-minute polls without a new registration or heartbeat command. The flag also requires logging enabled and a connected, unstalled USB link. It does not identify the client or prove MQTT delivery, disk writes, or Pi health.

For the report schedule, public-field layout, cryptographic design, and the offline Python capture tool, see Management reports.

"},{"location":"management_reports/","title":"Management reports","text":"

View this page on MeshCore Docs.

"},{"location":"management_reports/#management-reports-mgr1mgr2","title":"Management reports (MGR1/MGR2)","text":"

Available on repeater (including observer), room-server and sensor firmware. Exception: the constrained wio-e5-mini_repeater omits management reporting and shared data.tx routing to fit its existing application region. Normal authenticated administration, statistics, live USB/MQTT logging and the host-service bridge remain available. Companions/terminal-chat nodes and KISS modems do not originate these reports. Off by default. A management password and explicit enable are both required. No radio settings, existing preferences layout, or OTA authorization policy is changed by enabling reporting. This protocol does not authorize updates.

Current firmware emits MGR2, including a public USB logging/recovery snapshot and its latest watchdog event. Updated decoders still accept released MGR1; older decoder copies need an update to read MGR2. The earlier USB-only MGR2 development layout was never released and was replaced before release. Reporting USB status does not enable logging or change retry policy.

"},{"location":"management_reports/#cli","title":"CLI","text":"

Run through the existing local CLI or an authenticated administrator session:

get data.tx\nset data.tx path 1:12ab77\nset data.tx region auto\nset mgmt.password <12-to-96-byte password>\nset mgmt.direct 5\nset mgmt.flood 21\nset mgmt.enabled on\nget mgmt\nset mgmt.enabled off\n

Use a long randomly generated password. There is no password getter. Firmware stores the derived 32-byte key, not the plaintext password. That stored key is equivalent authority to decrypt reports and must also be protected. Passwords entered into terminal programs may still be recorded by those programs. The recognized firmware password command wipes its value from the input buffer on success and error, including unavailable management or allocation failure.

data.tx is the single shared route for management reports, telemetry history, and future scheduled data producers. Its fresh-install defaults are path=direct (zero hops) and region=auto; configuring it never enables a producer. Paths use 1:, 2: or 3: followed by complete hop hashes without separators, or the comma-separated form accepted by set outpath. Empty hops, including a trailing comma, are rejected. none removes the path. get/set mgmt.path remain compatibility aliases for get/set data.tx path. Alias expansion that exceeds the command buffer is rejected, not truncated.

region=auto uses the radio's configured default region when it is usable; otherwise it resolves the unique deepest flood-enabled entry in the region hierarchy. If equally deep candidates make that choice ambiguous, it resolves to nothing and no fallback is transmitted. default requires and follows region default; a region name pins that named scope. none disables scoped fallback. The resolved transport key is looked up when a report starts, so edits to the region definition take effect without rewriting /data_tx.

The direct path leads to the receiver/uplink's vicinity; this broadcast-radio datagram has no private destination identity. The radio ID in the payload identifies the reporter.

Direct and flood schedules are independently configurable and may each be turned off. Fresh settings are direct=5d and flood=21d; global reporting is still off until mgmt.enabled on. Direct accepts 5-90 days and requires the shared path. Flood accepts 21-90 days and requires a resolvable shared region. At least one route must remain active while reporting is enabled. The legacy set mgmt.interval N shorthand enables both, setting direct to N and flood to max(21,N). No transmission is sent merely by configuring a password. Initial reporting waits for the configured schedules. Invalid settings leave saved configuration, countdowns, and any report pages already in progress unchanged. Day counts must be unsigned decimal integers; signs and trailing text are rejected. Reports have deterministic per-radio/per-sequence jitter of up to an hour; pages are spaced at least a minute apart. Watchdog events appear in the next scheduled snapshot, not as immediate radio alerts. Multiple events between snapshots can replace the latest-event record; this is not a complete off-node event history.

The radio cannot know that an observer uploaded a packet to MQTT, so the flood schedule is deliberately independent of direct transmission. A region-scoped TRANSPORT_FLOOD is sent at its independently configured interval. An unresolved or ambiguous data region blocks that transmission rather than sending an unscoped flood. If direct and flood become due together, the flood is sent and replaces the redundant direct copy. Ordinary reports are never retried in a tight loop; a partial report gives up after an hour. Existing relay filters, hop limits and duty constraints still apply.

Schedule state is atomically reserved before transmission, and checkpointed hourly. Timers use elapsed powered-on time rather than the RTC: clock corrections cannot create floods, and reboot does not clear the budget. Downtime is not credited; each reboot can delay a report by up to an additional hour. This is a deliberately conservative tradeoff for nodes with unreliable clocks. Off/on and password changes do not reset the flood limit. Corrupt/unreadable state or failed writes stop reporting; get mgmt shows FAULT(no TX) until storage is repaired and the radio restarted. /management and the shared /data_tx are versioned, CRC-protected, and replaced transactionally.

On upgrade, an existing saved data.tx route takes precedence over the legacy management-only path. Otherwise that path must be saved successfully to /data_tx before reporting resumes. A failed migration leaves the old /management file intact for retry after storage is repaired and the radio restarted.

Weekly history is collected once a minute while enabled. Hour-bucket extrema cover 7 days to 7 days + 1 hour (conservative boundary bucket). The first report after enable/reboot is marked partial where appropriate. Since-report extrema reset after all pages have been queued, retaining readings taken since the snapshot for the next report. These statistics are not durable; reboot loses the history, not the flood countdown. Temperature is MCU temperature, not ambient. Reporting does not wake GPS, start Wi-Fi, or initialize external OTA media. Unknown capabilities/readiness are explicitly distinguishable from false. History/snapshot working memory is allocated only when reporting is enabled and is bounded to 1.5 KiB, plus a small configuration object and temporary stack use.

"},{"location":"management_reports/#routing-and-mqtt","title":"Routing and MQTT","text":"

Both direct/path and flood reports use PAYLOAD_TYPE_GRP_DATA (0x06). The route bits independently select direct or flood. MGR1/MGR2 are application extensions with literally plaintext public fields, not a call to the ordinary encrypted createGroupDatagram() builder. A fixed public marker is not an owner or password-derived channel ID. There is no outer channel encryption.

The body is padded with zeroes to a group-compatible length 3 + 16*n (maximum 179 bytes). Existing repeaters in the checked upstream implementation route group data without requiring a successful channel decryption. This fork recognizes the management envelope before ordinary channel processing. Reception of a management packet never exempts it from forwarding policy. Some third-party firmware may apply additional channel/layout policies; interoperability with every fork is not guaranteed. Packet logging/uplinks can capture it without the password.

RAW_CUSTOM (0x0F) is not used for reports because stock upstream does not flood-route it.

The observer's existing MQTT PACKET JSON supplies the complete frame in raw. No broker configuration or password changes are necessary to capture a report. The decoder understands all four route forms, 1-3-byte hashes, scope transport codes, and duplicate copies heard by several uplinks:

python -m pip install -r tools/management/requirements.txt\npython tools/management/report.py --mqtt capture.jsonl\n

The password is prompted, not supplied as a process argument. Input may be JSONL or a JSON array of MQTT messages. Alternatively omit --mqtt for a JSON array of canonical payload hex strings. --match-admin FULL_PUBLIC_KEY matches a known administrator against the encrypted fingerprints in either input mode. MQTT captures match each radio's fingerprints separately. Invalid files or keys are rejected before the password prompt; an unavailable password input gives a command-line error instead of a traceback. This is an offline capture decoder, not a broker subscriber or downlink service.

For a browser-local decoder that also decrypts and authenticates ACL entries, use the Management report decoder. It does not upload the captured packet or password.

"},{"location":"management_reports/#canonical-mgr2-payload-little-endian","title":"Canonical MGR2 payload (little endian)","text":"Offset Bytes Field 0 4 MGR2 4 16 First 16 bytes of reporter public key 20 4 Persisted report sequence (never wraps; exhaustion stops TX) 24 4 RTC Unix timestamp; advisory, may be wrong 28 4 Firmware major/minor/patch/pre packed as mOTA version 32 4 Bootloader packed version, or unknown 36 4 EndF target ID 40 8 Complete mOTA delta-base body hash 48 4 EndF image length 52 4 Staging capacity; planning still checks actual package geometry 56 4 OTA capability bits 60 2 Uptime hours, saturated at 65535 62 4 Weekly minimum mV, minimum C, maximum C 66 4 Since-report extrema in the same format 70 1 History coverage hours, capped at 168 71 1 Interval days for this report's direct or flood schedule 72 1 Role: 1 repeater, 2 room server, 3 sensor 73 1 Compiled/detected capability bits 74 1 Active status bits 75 1 Mask of status bits whose state is known 76 2 Validity/partial-history flags 78 1 Zero-based page index 79 1 Total pages (1-9) 80 1 Unique ACL count (0-36) 81 1 First ACL index in this page 82 1 ACL entries on this page (0-4) 83 2 USB status flags 85 1 Packed USB recovery stage and retry backoff step 86 4 USB full-reboot retry interval seconds 90 4 USB inactive seconds 94 4 USB continuous Auto qualification seconds 98 1 Latest watchdog reason/action/durability code 99 4 Latest watchdog event RTC Unix epoch; advisory, zero if unavailable 103 4 Uptime seconds at that event's boot 107 4 Latest watchdog event sequence, saturated at 4294967295 111 13 * count AES-SIV encrypted ACL entries after ACL 16 Full AES-SIV authentication tag after tag 0-15 Zero padding to group-compatible length; not part of canonical payload

MGR2 retains all 36 ACL entries, using four per page and up to nine pages. The largest canonical page is 179 bytes and already group-compatible; the radio payload budget is unchanged. The USB snapshot is frozen with the other public fields at report start and repeated on each page. It is not a live host-health monitor.

USB flag bits 0-10 are, in order: supported, logging enabled, watchdog enabled, host connected, reader connected, stalled, recovering, recovery deferred, persistence ready, watchdog Auto, and logger active. Bits 11-15 must be zero. Byte 85 bits 0-1 hold stage: 0 idle, 1 soft-recovery wait, 2 re-enumeration wait, and 3 reboot pending. Bits 2-5 hold the saved backoff step; bits 6-7 must be zero. Backoff is not a count of successful recoveries. The three timers are unsigned seconds; local recovery/reboot counters are not sent. The retry field is the saved backoff interval, not a countdown to an action; inactive age is reported separately. USB-only cleanup can precede that interval. Unsupported USB observation/recovery is explicitly represented by a false supported bit, not omitted or mistaken for a connected healthy reader. USB status describes the radio's observed transport and policy, not proof that the host process, MQTT uplink, SSH service, or Pi is healthy. Host/reader connection alone does not mean a logging client is running. The logger-active bit means recent USB stats polling within a 15-minute lease while logging is enabled and the observed USB host/reader link is not stalled, separate from USB attachment or an open reader. The existing five-minute stats polls renew this lease; no new client-registration or heartbeat command is required. This does not identify the client, prove logs reached MQTT or disk, or show that the Pi remains otherwise healthy.

Missing watchdog state on new or upgraded installations defaults to Auto. Auto promotes to saved On after 14 continuous healthy logging-USB days with renewed stats polling; its qualification age resets on reboot, detach, stall, stats-lease expiry, or USB logging off. Explicit On and Off remain available. Nonlogging/no-reader nodes remain Auto without watchdog board reboots. The Auto flag and age report this qualification separately from an enabled On policy. Use the node's get usb.watchdog and set usb.watchdog off|on|auto CLI to inspect or change policy; receiving a management snapshot changes nothing.

The 13-byte latest-event block is all zero when no event is available. Otherwise byte 98 bits 0-3 are the reason mask: host absent, reader absent, TX stalled, and confirmed stats client inactive. Bits 4-6 hold the action: 1 software recovery attempted, 2 re-enumeration attempted, 3 reboot requested, or 4 reboot cancelled after a final health/ownership check. Bit 7 means the event was saved and verified on durable storage. Sequence is nonzero for a present event. Physical faults may combine reason bits; stats-only expiry cannot authorize a board reboot. Epoch zero is allowed when the clock source is unavailable.

Event time is the node's advisory clock, not verified UTC. Default clocks can start at a plausible fixed date. Clock corrections do not rewrite old events; recorded uptime and sequence help review them independently. A saved reboot request is intent, not proof of a completed reset. The latest event survives normal reboot and mode changes, but is not a log of hardware hangs or sudden power failures. See USB logging watchdog for storage failure, cancellation, and review limitations.

Legacy MGR1 retains the same offsets through byte 82, followed immediately by encrypted ACL entries at byte 83 and the authentication tag. It carries six entries per page and up to six pages, with no USB block. Its absent USB status is decoded as unavailable, not as all switches off.

Each private entry is a per-radio 12-byte keyed fingerprint followed by flags: bit 0 administrator, bit 1 trusted OTA signer. The firmware combines permissions for the same fingerprint before paging, so every transmitted entry is unique. Decoders reject authenticated duplicate fingerprints within or across pages; repeated identical packet copies from multiple uplinks are still deduplicated. An oversized ACL fails closed rather than silently truncating. The reported allowlist includes all current full administrators and all four possible trusted OTA signing keys; region/filter managers and ordinary clients are excluded.

Feature bits: 0 Wi-Fi, 1 GPS, 2 NTP, 3 USB data, 4 LoRa OTA. Wi-Fi active means connected, GPS means receiver enabled (not necessarily a fix), NTP means an actual accepted NTP response this boot, USB means observable native USB data connection (not power or an unobservable external UART bridge). OTA capability means compiled support; active means an established usable apply/store path.

Validity bits: 0 firmware version, 1 bootloader version, 2 EndF/base identity, 3 staging capacity, 4 partial week, 5 partial since-report period, 6 MCU temperature. Voltage is unsigned millivolts, zero missing. Temperatures: zero missing, 1-251 represent -50..200 C, 252 below range, 253 above range, 254-255 reserved. OTA bits: 0 protocol compiled, 1 transfer DEFLATE, 2 2-KiB app transfer blocks; bits 8-23 are apply codec bits (full/sequential/in-place). Transfer DEFLATE is not compressed bootloader apply. There is no manifest ID. Exact old binaries are still needed on the computer to generate a differential update; a hash alone cannot reconstruct them. Unknown metadata must not be treated as OTA readiness.

"},{"location":"management_reports/#cryptography","title":"Cryptography","text":"

Password root: SHA256(\"#\" || literal UTF-8 password) (hashtag-style derivation). Subkeys: HMAC-SHA256(root, ASCII_domain || radio_id16). Domains: MeshCore-MGR1-SIV, MeshCore-MGR1-ACL. Fingerprint: HMAC-SHA256(ACL_subkey, radio_id16 || full_admin_key32)[0:12]. The same owner has different fingerprints on different radios. A collector needs a candidate administrator's full key to identify a fingerprint.

Encryption is RFC 5297 AES-SIV-CMAC-256, using the existing rweather AES primitive. The single associated-data string is the complete clear header: 111 bytes for MGR2, including USB status and its event, or 83 bytes for MGR1. Both retain the existing derivation domains; the authenticated magic and layout distinguish their wire versions. Ciphertext is exactly the ACL byte length. Full 16-byte tag, no truncation. This deterministic misuse-resistant mode avoids reliance on the firmware's noncryptographic general-purpose RNG or RTC nonces. An identical restored snapshot may repeat ciphertext, but does not expose XORs of different ACL plaintexts as nonce-reused stream encryption would.

All public fields are readable without a password but are authenticated only to password holders. A shared password authenticates knowledge of that password, not a unique individual radio identity; another holder can forge reports. There is no per-device signature. Collectors must enforce their own persisted sequence/replay policy and explicitly handle factory resets/restored backups. The decoder verifies snapshot consistency but does not maintain a database. This fast password derivation permits offline guessing, so twelve characters is only a minimum length, not a guarantee of password strength. Packet timing, public metadata and entry/page counts remain visible.

"},{"location":"mbedtls-tls-footprint/","title":"Mbedtls tls footprint","text":"

View this page on MeshCore Docs.

"},{"location":"mbedtls-tls-footprint/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"mesh_pocket_gui/","title":"Mesh pocket gui","text":"

View this page on MeshCore Docs.

"},{"location":"mesh_pocket_gui/#meshpocket-full-companion-display","title":"MeshPocket Full Companion display","text":"

Full Companion on MeshPocket includes a channel message board, a worldwide coastline map and display settings. Other boards keep their existing home UI. USB, Bluetooth and notifications remain available.

Use the Pocket's user button:

Page One click Two clicks Hold Normal pages Next page Previous page Page action Message board, browsing Older message Newer message Finish browsing Map, zooming Zoom in Zoom out Save and finish Settings, editing Change value Next setting Save and finish

Hold on the message board, map or settings page to enter its mode. Three or four clicks on the board select the next or previous configured channel. The inbox still opens individual messages, including direct messages.

The board shares the existing 32-message history, showing the selected channel with sender names and local timestamps. Successfully sent app messages appear there too. History is held in RAM; it is not a permanent message archive.

Settings cover channel, text size, sender name length, screen orientation, light/dark theme, return-to-board delay, full refresh interval and map zoom. They use a separate checked, atomically replaced record in primary storage. Existing node, channel and contact formats are unchanged. Timezone comes from the existing node preferences.

Changing pages or map zoom uses a full clean refresh to clear the previous image. Updates within the same page keep the faster partial refresh, with a clean refresh after at most ten partial updates. Settings allow shorter intervals of one, three or five updates.

The idle screen-off timer defaults to 15 minutes on battery and USB. Existing saved timeouts are retained; set display.timeout and display.usb.timeout to 900 to use 15 minutes. The return-to-board delay is a separate setting.

The map centres on this node's location, or the first contact with a location. With no locations it shows a world overview, keeping the selected zoom saved for when a location becomes available. Zoom controls require a location. Auto zoom uses a 200 km view when a location is available. Contacts appear as dots, with nearby names at closer zooms. The view spans 20,000 km down to 5 km vertically; coastline detail stays coarse at close zooms. It contains no streets, terrain, regional downloads or ocean tiles. See the map data notes for resolution, storage and reproducible generation.

The UI is adapted from MeshCore-Mesh-Pocket-MessageBoard. Include PocketGui-NOTICE.txt alongside distributed firmware for the UI and font notices. The application DFU ZIP keeps its required manifest, binary and metadata entries; the notice is a sidecar.

Build with Mesh_pocket_companion_radio_full. Defining UI_POCKET_GUI=0 retains the standard Pocket display.

"},{"location":"mixed_scan_validation/","title":"Mixed scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"mixed_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"mixed_sf_bw_validation/","title":"Mixed sf bw validation","text":"

View this page on MeshCore Docs.

"},{"location":"mixed_sf_bw_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nimble_companion_trial/","title":"Nimble companion trial","text":"

View this page on MeshCore Docs.

"},{"location":"nimble_companion_trial/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nimble_companion_trial_results/","title":"Nimble companion trial results","text":"

View this page on MeshCore Docs.

"},{"location":"nimble_companion_trial_results/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"notifications/","title":"Notifications","text":"

View this page on MeshCore Docs.

"},{"location":"notifications/#companion-notification-builder","title":"Companion notification builder","text":"

Make a rule, preview it here, then copy the CLI commands or apply and test it over USB. The device needs firmware containing the new notification commands. The programmable alert engine is enabled on ESP32, nRF52 and RP2040 builds. Compact STM32WL Companions retain their existing alerts because the engine does not fit their 224 KiB application partition. Browser previews show one cycle; device tests use the configured repeat and stop policy. USB testing uses Binary Companion mode at 115200 baud.

Find my node Food order ready VIP chat Only channel 9 Who should trigger the alert? Messages fromAll contacts and channelsOne contactOne room serverOne channel Contact/room key or channel slot Companion client statusConnected or disconnectedConnectedDisconnected Silence other message alerts (channel exception)

This saves a silent default plus the selected channel's rule. It also silences ordinary DMs and room-message alerts. Existing specific/state rules and authorized !notify strings can still alert; review the channel 9 example and caveats. Copy commands and USB Save include both rules; preview only plays the exception.

Connected means an active phone/app, USB, WiFi, Ethernet or serial companion client. It selects a message rule; connecting or disconnecting alone does not create an alert. Contact matching uses the full public key. Channel slots resolve to the channel's key when the rule is saved.

What should the device do? Vibration patternMillisecond on/off durations, or off / inherit LED patternStarts on, alternates off/on, finishes off Sound melodyRTTTL: note letters a-g, # for sharp, p for rest; off / inherit also work ScreenUse normal screen policyOn during alertOff during alert GPIO outputoff / inherit, or an approved pin and pattern: 22:50,300,50

50,300,40,20,500 means on for 50 ms, off for 300 ms, on for 40 ms, off for 20 ms, then on for 500 ms. A rule controls only outputs physically supported by its firmware. The USB connection reports the supported outputs and approved GPIO pins before applying anything.

Repeating and stopping Repeat countA number, or forever Gap between cycles (ms) Stop alertButtonNode connectedNever automatically dismiss

Node connected stops an active alert when a companion client connects. Never disables button/connection dismissal; a finite repeat count still finishes. notify.stop always stops playback. A new matching message replaces the current alert.

Allow notification strings from this contact or room PermissionLeave permission unchangedAllow this contact or roomRevoke this contact or room

Only applies to a contact or room server's full public key, without a connection condition. An allowed contact can DM the notification string below, or it can be posted in an allowed room. Trusting a room allows its posters to send alerts through that server. GPIO, stop-policy and permission changes are forbidden in DMs or room posts. The device stops playback after at most 15 seconds, including infinite repeats. Incoming strings do not save settings and obey the recipient's master switches.

Preview one cycle VibrationLEDGPIOScreen

Ready

Ready to play the melody

Preview with sound Stop preview

The melody plays on this device after a one-second audio warmup, then the timer and output indicators start together. No contact or room key is needed to preview an example. Sound set to off or inherit plays no melody here.

Timing here is a visual approximation. Firmware schedules outputs using millisecond timestamps without blocking radio work; loop and hardware latency can delay an edge. A haptic motor also takes time to start/stop.

Device commands
Copy commands Notification DM or room post

The recipient must first grant your contact or room permission. Send the string as an ordinary DM or room post; it contains no GPIO action. Long patterns may need shortening to fit 159 characters for a DM, or 150 for a room post.

Try it on your device

No device connected

Connect USB Disconnect USB Save rule Save and test Test saved rule Stop device alert Delete selected rule

Save writes the generated commands in order and stops at the first device error; earlier successful settings remain saved. A test with a connection suffix selects that profile even over USB. Stop-on-connect still watches real subsequent connections. The silence-other-alerts option saves both the silent default and the selected channel rule. Deleting the selected rule does not delete that default. USB testing requires Chrome or Edge.

"},{"location":"notifications/#cli-reference","title":"CLI reference","text":"

Settings apply immediately and survive reboot and filesystem-preserving updates. They live in a separate checked file, /notify_prefs, with an atomic replacement and backup. There are 12 rule slots. Existing alerts stay in use until a rule or master control enables the new system. Unmatched messages keep their existing alert behavior, subject to the master switches.

"},{"location":"notifications/#master-controls","title":"Master controls","text":"
get notify\nset notify.enabled on\nset notify.sound on\nset notify.vibration on\nset notify.led on\nset notify.screen on\nset notify.gpio off\nget notify.gpio.pins\n

Each output has get notify.<output> and set notify.<output> on|off. Master off cannot be overridden by a contact, channel or incoming notification DM. set notify.enabled off disables custom rules and DM actions and returns to the existing alert behavior. Supported-output bits in get notify are vibration=1, sound=2, LED=4, screen=8 and GPIO=16. GPIO uses the same board pin approval as repeater GPIO control and is inactive by default.

"},{"location":"notifications/#rules","title":"Rules","text":"

Targets are all, contact:<64 hexadecimal public-key digits>, room:<64 hexadecimal room server public-key digits>, and channel:<slot>. A channel's 32-digit key also works. Add @connected or @disconnected to select the client state; no suffix matches either state.

set notify.vibration channel:3 50,300,40,20,500\nset notify.sound channel:3 order:d=8,o=5,b=180:c,e,g,4c6\nset notify.led channel:3 100,100,100,500\nset notify.screen channel:3 on\nset notify.gpio channel:3 22:50,300,50\nset notify.repeat channel:3 forever\nset notify.gap channel:3 2000\nset notify.stop channel:3 button\nget notify.vibration channel:3\nnotify.test channel:3\nnotify.stop\nnotify.delete channel:3\nget notify.rules\nget notify.rules 0\n

The GPIO example works only if pin 22 appears in get notify.gpio.pins. Pulse lists have up to 12 positive durations of 1-60000 ms each. GPIO output is active-high, inactive-low. Sound is one note at a time, not chords. RTTTL requires name:d=<1|2|4|8|16|32>,o=<4..7>,b=<25..900>:notes; names use up to 12 ASCII letters/digits/underscores. Melodies are at most 63 characters and 60 seconds. Notes use a through g, p for rest, optional duration/octave, # for sharps on a/c/d/f/g, and one . for a dotted duration. For example 16c#6 and 4g.5 are valid. Invalid melodies are rejected before the buzzer library sees them. This ASCII format matches the installed NonBlockingRTTTL driver's pitch table; musical staff symbols and polyphony are not supported.

For any rule output, off explicitly disables it and inherit removes that field's override. Rules combine per field: shared all, shared state rule, contact/channel rule, then contact/channel state rule. A matching rule replaces the old message sound/vibration; unspecified outputs are off unless inherited from a shared rule. An unspecified screen keeps its normal display policy. screen on|off temporarily overrides that policy and restores it when the alert ends. A selected LED pattern temporarily takes ownership from the usual status heartbeat; the master LED switch also controls that heartbeat.

Finite repeats are 1..65535; forever loops until dismissed. Gap is 1..60000 ms after the longest output sequence. Stop modes are button, connected, and never. Triple-press sound/vibration mute controls also update their notification master switch where those buttons are supported. Continuous alerts prevent MCU sleep so their timers can run.

"},{"location":"notifications/#contact-and-room-permission-for-notification-strings","title":"Contact and room permission for notification strings","text":"

On the recipient, replace PUBLIC_KEY with the sender's complete public key:

set notify.remote contact:PUBLIC_KEY on\nget notify.remote contact:PUBLIC_KEY\nset notify.remote contact:PUBLIC_KEY off\nset notify.remote room:ROOM_PUBLIC_KEY on\n

Permission defaults to off, is saved per contact or room key, and cannot be granted to all, channels or connection-state variants. The identity comes from the authenticated received DM, not a key written inside its text. The permission alone does not replace the contact's normal message alerts. A trusted room's forwarded !notify posts use the room server's complete key for permission, not the post author's four-byte prefix. Trusting a room allows its posters to send notifications; trusting a chat contact alone does not authorize their posts forwarded by an untrusted room. Both plain DMs and signed room posts pass through the same validation and 15-second playback cap.

The allowed sender can send:

!notify vibration=50,300,40,20,500 led=100,100,100,500 sound=order:d=8,o=5,b=180:c,e,g screen=on repeat=forever gap=2000\n

Allowed fields are vibration, sound, led, screen, repeat and gap. The whole notification string is validated before any output changes. Unknown/duplicate fields, GPIO, stop modes, permission changes and inherit are rejected. The command cannot change saved rules. Button dismissal is always allowed, notify.stop works, and playback stops at 15000 ms even when the requested pattern or repeat count is longer. Unapproved or invalid notification strings remain ordinary message text. Repeated delivery of the same recently received post or DM does not restart its playback deadline. Channel messages cannot execute notification strings. Receiver master switches apply even while connected.

"},{"location":"notifications/#examples","title":"Examples","text":""},{"location":"notifications/#only-channel-9-alerts-all-other-channels-silent","title":"Only channel 9 alerts; all other channels silent","text":"

This example makes message alerts silent by default, then plays one short melody for channel 9. It works both with and without a connected companion client and requires a device with a buzzer. Messages still arrive normally; this changes the node's alerts, not the phone app's notification settings. Use the Only channel 9 button above to load this setup into the builder. You can change the channel slot or alert pattern before copying or saving. Uncheck Silence other message alerts to generate only the selected rule; this does not remove a silent default already saved on the node.

channel:9 means the firmware's zero-based slot 9 (the tenth slot), not a channel named ch9. Configure that slot before running these commands. The rule follows its saved channel key; recreate it if you replace the channel in that slot.

Start with no conflicting custom rules. Use get notify.rules, then get notify.rules <rule-slot> to inspect existing selectors. Remove only unwanted rules with notify.delete <selector>: existing state-specific and contact/channel rules can override the silent default below.

set notify.enabled on\nset notify.vibration all off\nset notify.sound all off\nset notify.led all off\nset notify.screen all off\nset notify.gpio all off\nset notify.repeat all 1\nset notify.gap all 500\nset notify.stop all button\nset notify.sound on\nset notify.sound channel:9 ch9:d=8,o=5,b=180:c,e,g\n

Keep the sound master on: set notify.sound all off silences the default rule, while set notify.sound off would also block the channel 9 exception. The other outputs remain silent, including screen wake-ups for messages. To add vibration for channel 9 on a device with a vibration motor:

set notify.vibration on\nset notify.vibration channel:9 100,100,300\n

Test the saved exception with notify.test channel:9; stop it with notify.stop. Send a message on another configured channel to verify that it stays quiet.

Scope: there is no channels-only wildcard: all also silences ordinary DM and room-message alerts unless a more specific rule enables them. Previously authorized contact/room !notify strings can still request alerts. If those should stay quiet too, revoke their permission with set notify.remote contact:PUBLIC_KEY off or set notify.remote room:ROOM_PUBLIC_KEY off, using the full sender/server key.

"},{"location":"nrf52-usb-ready-fix/","title":"Nrf52 usb ready fix","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52-usb-ready-fix/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nrf52_bluetooth_debug/","title":"Nrf52 bluetooth debug","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_bluetooth_debug/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"nrf52_companion_storage_recovery/","title":"Nrf52 companion storage recovery","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_companion_storage_recovery/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"nrf52_power_management/","title":"Nrf52 power management","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_power_management/#nrf52-power-management","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"

The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.

"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":""},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":""},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":""},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"

Shutdown reason codes (stored in GPREGRET2):

Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board family Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (xiao_nrf52) Yes Yes Yes SenseCAP Solar Yes Yes Yes RAK4631 / RAK3401 Yes Yes Yes GAT562 30S / EVB Pro / Tracker Pro / Watch13 Yes Yes Yes Heltec T096 / T114 / T1 / Tower V2 Yes Yes Yes Muzi Works R1 Neo Partial Inactive Inactive Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite / T-Impulse Plus No No No WIO Tracker L1 / L1 E-Ink / WM1110 No No No Mesh Pocket / Meshtiny / Nano G2 Ultra No No No ThinkNode M1 / M3 / M6 No No No T1000-E No No No Ikoka Nano / Stick / Handheld (nRF) No No No Keepteen LT1 / Minewsemi ME25LS01 No No No

Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - R1 Neo has the integration compiled, but its board configuration deliberately sets PWRMGT_VOLTAGE_BOOTLOCK to 0; automatic protective shutdown and its voltage/VBUS recovery wake are therefore inactive. - User power-off does not enable LPCOMP wake; voltage recovery is armed only for boot protection and automated low-voltage shutdown. - VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.

"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#rak-peripheral-shutdown","title":"RAK peripheral shutdown","text":"

Explicit power-off and timed deep sleep on RAK4631 and RAK3401 stop peripheral owners before switching off the shared 3V3_S supply. On RAK4631, Ethernet must quiesce first; if its worker cannot stop within the bounded shutdown interval, keep its rail and bus intact instead of removing power during an SPI operation. Detected external-storage holds also retain the supply, including after an unresolved flash-operation timeout. Before storage discovery and configuration finish, rail removal is refused. Shutdown does not probe again, cancel a staged update, or float a storage owner's select pin. This does not change ordinary interrupt-idle sleep, boot/low-voltage protection, or the configured WisBlock GPS/shared-rail policy during normal operation.

The RAK19007 power schematic and RAK13800 connector schematic show that IO2 switches 3V3_S, not the separate upstream PoE VCC_IN supply. Merely compiling Ethernet support therefore does not require this rail to stay on after terminal shutdown. The RAK4631 radio-enable GPIO controls the RF-switch supply, not the SX1262 core supply.

RAK3401's external RAK13302 radio and RAK13800 Ethernet use the same IO slot, SPI signals, select pin and IO3 control. Their simultaneous operation is not supported; Ethernet owner registration on RAK3401 is rejected rather than allowing those electrically conflicting controls to run together.

Software shutdown cannot remove every hardware leakage path. The baseboard has I2C pull-ups to unswitched VDD. RAK13302 VD's boost enable follows IO2, but the legacy VC default resistor configuration connects it directly to VBAT; that boost cannot be switched off by firmware alone.

"},{"location":"nrf52_power_management/#xiao-battery-divider-safety","title":"XIAO battery-divider safety","text":"

XIAO and XIAO-based Ikoka startup preload READ_BAT_ENABLE LOW before enabling its output. XIAO protective and user shutdown also keep it LOW. Seeed warns that HIGH can overvoltage the ADC input during charging, so this is not a safe pin to switch HIGH as a power optimization. The schematic's 1 MOhm + 510 kOhm divider draws about 3 microamps at 4.2 V, not 3 milliamps. Charging current settings are unchanged. See Seeed's battery guidance.

"},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"

The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).

"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"

A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before: - SystemInit() (priority 102) - which clears RESETREAS - Static C++ constructors (default priority 65535)

This ensures we capture the true reset reason before any initialisation code runs.

"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"

To enable power management on a board variant:

  1. Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT

  2. Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)

  3. Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };

void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);

 if (enable_lpcomp) {\n   configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n

} #endif

void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...

#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```

For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).

  1. Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif
"},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"

The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered

VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).

LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):

REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V

Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold ~ (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).

"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"

The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)

This ensures compatibility regardless of BLE stack state.

"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"

Power management status can be queried via the CLI:

Command Description get pwrmgt.support Returns \"supported\" or \"unsupported\" get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power) get pwrmgt.bootreason Returns reset and shutdown reason strings get pwrmgt.bootmv Returns boot voltage in millivolts

Boards with nRF52 power management and a text CLI also support saved pwrmgt.bootlock, pwrmgt.cutoff, battery.profile, battery.empty, battery.full, and relative adc.multiplier settings. See the CLI reference for their ranges, board-specific defaults, and wake behavior. KISS builds honor a saved policy if one exists, but have no text CLI for changing it.

On boards without power management enabled, get pwrmgt.source and get pwrmgt.bootmv return:

ERROR: Power management not supported\n

get pwrmgt.support returns unsupported. get pwrmgt.bootreason is not compiled out at all and answers on every board: getResetReason() and getShutdownReason() are virtuals on the base board class, so a board that does not override them reports Not available rather than an error. ESP32 boards override the reset half with esp_reset_reason(), so they return a real reset reason and Not available for the shutdown reason.

"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"

When MESH_DEBUG=1 is enabled, the power management module outputs:

DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n
"},{"location":"nrf52_power_management/#further-power-management-work-planned","title":"Further power-management work (Planned)","text":"

For the opt-in Sensor check-in workflow using System ON idle sleep and bounded GPS acquisition, see Sensor dog tracker. It is separate from the battery-protection SYSTEMOFF shutdown described here.

"},{"location":"nrf52_power_management/#references","title":"References","text":""},{"location":"nrf52_sleep_event_validation/","title":"Nrf52 sleep event validation","text":"

View this page on MeshCore Docs.

"},{"location":"nrf52_sleep_event_validation/#nrf52-idle-sleep-regression","title":"nRF52 idle-sleep regression","text":""},{"location":"nrf52_sleep_event_validation/#cause","title":"Cause","text":"

The MCU could repeatedly enter the board's sleep function without actually sleeping. Three operations could leave the Cortex-M event register set:

  1. The pinned Adafruit core yields after every Arduino loop(). Its FreeRTOS yield issues SEV.
  2. The old board sleep function queried SoftDevice state immediately before waiting. In the measured SoftDevice-off case this SVC recreated the event. Moving only the scheduler-event clear was therefore insufficient.
  3. Adafruit CDC available() yields when its receive FIFO is empty. A USB client that never asserted DTR but established an RX session could therefore recreate an event on every idle console poll.

With SoftDevice enabled there is an additional, separate application-interrupt history. The core's PendSV yield updates it. Clearing the ARM event register does not acknowledge that history, so the next sd_app_evt_wait() still returns immediately. This follows from the public SDK contract and the pinned S140 6.1.1/7.3.0 interrupt-forwarder/wait code, not just the ARM event model. The failed SoftDevice-on captures below had another blocker as well.

There was also a disabled but pending CryptoCell IRQ42 after BLE startup. SoftDevice's documented disabled-pending-IRQ rule prevents sleep in this state, regardless of the event-register/history fixes. The pinned CC310 library polls rather than enabling that interrupt; the pinned BSP already clears its pending bit in its own idle wait. The application-owned wait had missed that cleanup.

This is distinct from the existing sleep-policy guards. A large eligible time or a high number of wait calls does not establish CPU sleep residency.

"},{"location":"nrf52_sleep_event_validation/#fix-and-ordering","title":"Fix and ordering","text":"

All four nRF52 role loops now begin with prepareSleepCycle(). It queries the actual SDK SoftDevice state and USB power status. When SoftDevice is active, preparation forces a real application PendSV with taskYIELD(), checks that it was serviced in an unmasked application thread, and acknowledges its SDK history with one early sd_app_evt_wait(). A bare SEV cannot guarantee that this SDK call returns: the SDK can internally wait again without an application interrupt. The real PendSV is required. This ordering depends on the pinned core: the other SDK wait caller is the lower-priority idle task, which cannot consume that acknowledgement while the sketch task is ready. Preparation rejects exception context, masked interrupts, a suspended/stopped scheduler, a still-pending PendSV, or an invalidated cycle instead of trying that early SDK acknowledgement without proof.

Preparation then clears the stale ARM event with SEV; WFE, before servicing work or checking sleep predicates. A current loop's USB-power snapshot also avoids another state-query SVC during UI work.

On chips with CryptoCell, preparation also clears that specific pending IRQ only when its interrupt is disabled and the SDK is not in a critical region. It never sweeps other pending bits or clears an enabled CryptoCell owner's interrupt. Clearing NVIC pending does not discard the polling crypto result.

The snapshot is valid for one loop and at most one sleep attempt. Both application-task Bluefruit.begin() call sites invalidate it before startup, including partial or failed startup. Invalid, missing or failed preparation declines sleep. The existing SDK/direct-register USB access policy is retained. SoftDevice startup remains owned by the application task; the cache is not permission to add an asynchronous SDK enable/disable path racing a wait.

Primary USB RX polling now uses the native CDC0 FIFO query, with the existing session checks both before and after that query. It does not change DTR policy, CDC1 logging ownership, or the facade's blocking-read/write protections.

The late wait remains a single sd_app_evt_wait() when SoftDevice is enabled, or the existing WFE; SEV; WFE sequence when disabled. There is no late SEV; WFE; WFE double-wait: that could discard the only radio IRQ arriving after the caller checked for work. No blanket IRQ clearing or SEVONPEND change was added.

"},{"location":"nrf52_sleep_event_validation/#controlled-hardware-evidence","title":"Controlled hardware evidence","text":"

The test device was a RAK3401/nRF52840 with an external RAK13302 SX1262 on Mercerwood. Tests used 908.5 MHz, BW250, SF9, CR5, power saving on, manual RXPS 30000/10000 us, AGC 0, TX power 2, no forwarding/adverts and USB watchdog off. Runtime switches in private benchmark binaries allowed same-binary paired controls without reflashing. Benchmark controls are not production features.

Control Capture Time-weighted sleeping observations Original behavior 30 s 0% Early clear only 30 s 0% Non-yielding USB poll only 30 s 0% Early clear plus USB poll, still with late state SVC 30 s 0% No late SVC, non-yielding USB poll, without early clear 30 s 0% Early clear, no late SVC, non-yielding USB poll 90 s 76.7% Never-DTR client held open, old empty USB poll 30 s 0% Same client held open, non-yielding USB poll 30 s 82.9%

The first production candidate restored sleeping observations with SoftDevice off (6522/8331 samples, about 78% over 90 s), but not with SoftDevice on (0/8344 samples with a never-DTR client over 90 s; 0/2776 with the client closed over 30 s). This negative result exposed the separate SDK history and is why the final preparation also acknowledges a real PendSV before servicing work. The second candidate added that acknowledgement but still did not sleep with SoftDevice on. A breakpoint then confirmed the SDK branch, the actual S140 6.1.1 vector, and disabled IRQ42 pending (ISER1=0x81, ISPR1=0x400). Clearing only that bit, with the same second binary and no configuration change, restored 1911/2775 sleeping observations over 30 s. That control justified the specific disabled-CryptoCell cleanup, rather than a speculative IRQ change.

The final, normal Full repeater artifact was then tested without benchmark controls or a DTR bypass. Its application readback matched the packaged image byte-for-byte (SHA-256 8f6a1654f95d0179dad358d1fc68887eaacc41f4c60281a23099482e1f44af94).

Final production test Capture Sleeping samples Weighted estimate SoftDevice off, USB terminal closed 90 s 6526/8310 78.3% SoftDevice on after OTA start/stop, never-DTR client held open 90 s 6077/8341 73.0% SoftDevice on, client closed 30 s 1943/2776 70.0%

The client received valid CLI replies both before and after the capture. The final frozen SDK-on diagnostics had 281272 completed late waits, only 12 short returns, and zero vetoes/errors. Their accounting window was longer than the capture; those counters are not its sleep-residency measurement. All final samples had the same valid debugger/clock state described below.

In the controlled old/new USB-poll pair, the never-DTR client successfully issued get powersaving and held the same file descriptor open across both captures. Early clearing and removal of the late state SVC were held constant in that pair, isolating the USB poll change. A separate unsafe late-double-wait control also slept, but was rejected because of the lost-work race above.

Samples read DHCSR S_SLEEP at jittered 7-13 ms intervals. All causal captures had C_DEBUGEN set, HFCLKSTAT 0x10001 and DCDC baseline 1, without sampled halt, step, mask, lockup or reset flags. All timestamps were monotonic with no gap over 20 ms. Percentages weight each sampled state until the next timestamp; they are point-observation estimates, not exact sleep-residency accounting. Zero sleeping observations does not exclude extremely short sleeps. Different calibration phases can contribute to variation between otherwise fixed captures.

"},{"location":"nrf52_sleep_event_validation/#limits","title":"Limits","text":"

There was no current meter. These results establish the CPU-sleep regression, not a measured mA reduction or a complete explanation of the T096/Wio L1 screenshots. SWD and USB alter the power/clock environment. Radio RXPS, FEM/LNA, GPS, screen, regulator and USB clock loads require matched electrical tests. In particular the T096 FEM/LNA default differs from EasySkyMesh; this RAK cannot measure that board-specific load. No RF delivery or long battery-life claim follows from these captures.

"},{"location":"nrf52_sleep_event_validation/#sensor-tracker-wake-timer","title":"Sensor tracker wake timer","text":"

A separate tracker-mode issue posted xTimerChangePeriod() on every eligible loop. Waking the higher-priority timer daemon recreates a scheduler event just before the late wait. The helper now reuses an active timer whose remaining time is nonzero and no later than the requested wake. Its active/expiry/tick snapshot is protected by a short FreeRTOS critical section. Dormant, expired or too-late timers are rearmed outside that section; allocation/queue failure still keeps the node polling. No late event clearing was added.

Host tests execute the actual helper, including the original repeated-rearm negative control, deadline changes, one-shot expiration, queue failures, tick wrap and late IRQs. T1000e and RAK3401 Sensor builds pass. This timer follow-up has not been physically qualified in GPS tracker mode; the RAK captures above are repeater tests.

See power-saving diagnostics for the permanent on-demand CLI counters. Counters remain off by default. Wait-call counters cover the late eligible wait, not the guaranteed-nonblocking early SDK acknowledgement. Preparation failures are reported through the board error or veto path instead.

"},{"location":"number_allocations/","title":"Number allocations","text":"

View this page on MeshCore Docs.

"},{"location":"number_allocations/#number-allocations","title":"Number Allocations","text":"

This document lists unique numbers/identifiers used in various MeshCore protocol payloads.

"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"

The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.

To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.

NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.

Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.

Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com \u2014 https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com \u2014 https://buymeacoffee.com/ripplebiz 0120 MCO Advanced most.original.address@gmail.com \u2014 https://hdden.ru/MCOa/ 0130 - 013F StreamSensor william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion 0140 Nelos support@nelos.app \u2014 https://nelos.app FF00 - FFFF -reserved for testing/dev-

(add rows, inside the range 0100 - FEFF for custom apps)

"},{"location":"optional_screens/","title":"Optional screens","text":"

View this page on MeshCore Docs.

"},{"location":"optional_screens/#optional-screens","title":"Optional screens","text":"

Use the normal display-capable firmware for your exact board and role. When automatic detection is included, that image starts the supported screen if it responds, or continues headless if it is absent. There is no screen-presence setting. Power the board off before attaching or removing an optional screen.

The firmware picker describes automatic screen detection only when the selected application has been verified to contain it. Previously published firmware retains its original behavior. These additions become available as new firmware is built and qualified; changing the picker does not add detection to an older download.

"},{"location":"optional_screens/#manufacturer-confirmed-optional-screens","title":"Manufacturer-confirmed optional screens","text":"Hardware Manufacturer evidence Firmware behavior Heltec T114 Heltec offers the optional stock screen. Automatic ST7789 detection; see the T114 guide. Heltec RC32 with T108 The T108 is a separate pin-header display module. The manufacturer example documents its pins and power controls. New display-capable images detect the NV3001B panel. Legacy without_display targets remain headless compatibility builds. Heltec Wireless Tracker V1/V1.1 The manufacturer listing explicitly offers Screen Yes/No. New display-capable images detect the ST7735 panel. Its absence releases only the display's power claim; GPS retains its own claim on the shared supply. Heltec V4 R8 Expansion Kit The manufacturer kit offers front-panel configurations; its V2 expansion schematic connects the display's separate return line. New R8 TFT images detect the panel through the documented MISO pin. Existing touch reset and backlight-only sleep/wake behavior are retained. Heltec V4/V4 R8 OLED The manufacturer listing offers OLED and No Display configurations. Existing SSD1306 images already check whether the OLED responds. RAK WisBlock The RAK4631 kit guide labels the RAK1921 OLED optional. Display-capable RAK4631 and RAK3401 images check presence and detect SSD1306 or SH1106 controllers. New images also provide a saved controller choice; see below. iKoka Stick and Handheld The designer documents an optional Stick OLED and optional Handheld OLED. Existing display-capable images already check presence. Handheld Companion supports the screen; currently headless infrastructure recipes remain headless. Original LilyGo T-Beam LilyGo offers soldered/non-soldered OLED configurations. Existing display-capable SSD1306 images already check presence. XIAO ESP32-S3 with Wio-SX1262 Seeed lists the expansion board as an accessory for adding screens and sensors. Existing display-capable Companion images already check presence. Muzi Base Uno/Duo with SuperIO SuperIO adds the OLED, GPS, buttons and buzzer to the matching Base radio. New Companion images probe the SH1107 display, leave its 12 V supply off when absent, and discover GPS independently. Uno and Duo retain their different radio drivers. LilyGo T-Echo Lite core/display assembly LilyGo documents the split core/base design. New Companion images check SSD1681 status readback before e-paper initialization and continue headless when the panel does not respond. The old non-shell target names remain compatibility recipes.

The older V4 TFT expansion configuration is also optional, but its current recipe has no verified panel readback connection. Detection is not enabled for that wiring. A touchscreen acknowledgement alone does not establish that the LCD responds. The V4 R8 TFT and older V4 TFT pin mappings are distinct.

RAK4631 and RAK3401 images with OLED controller detection support set display.controller auto, ssd1306 or sh1106 in all display-enabled roles. Keep auto for normal use; choose the panel's controller explicitly if detection gives shifted or wrapped pixels. The choice is saved and applies after restarting the node. get display.controller shows the configured and active choices and any pending restart. With no saved choice, automatic detection or an existing build-time override supplies the default; saved auto takes precedence over that override. Normal controller selection does not require custom firmware. See the controller commands.

New R8 TFT images support saved display.rotation values of 0, 90, 180 and 270, including portrait/landscape dashboard changes without reflashing. The stock R8 expansion board has no accelerometer for automatic orientation. See the rotation commands.

The variant consolidation validation records the ten qualified packages, software checks and remaining physical tests for R8 rotation, Muzi SuperIO and T-Echo Lite detection.

Full Wireless Tracker repeaters retain the board's 50-neighbor capacity, as the V1.1 and V2 Full profiles already do. This preserves RAM for the screen, GPS, MQTT and update services; the RAM admission limits remain unchanged.

The R8 readback route is documented in Heltec's Expansion Kit V2.03 schematic. Tracker V1 wiring is documented in the V1.1 schematic.

Screen presence does not establish controller type, dimensions, physical orientation, or board revision. Continue choosing the exact hardware profile. Use USB when changing an old firmware target name; a different target's LoRa update package is not interchangeable. Saved identity and settings do not need to be erased for screen detection.

"},{"location":"optional_screens/#integral-screens","title":"Integral screens","text":"

Manufacturer documentation identifies built-in screens on the LilyGo T-Echo Card, Heltec T190, T1, T096, Wireless Tracker V2, ThinkNode M9 and T-Deck. No new optional-screen detection is enabled for these boards. In particular, the T190's optional module is its LoRa radio, not its screen.

"},{"location":"optional_screens/#validation","title":"Validation","text":"

The new probes run before display SPI initialization and framebuffer allocation, with the backlight off. Bounded repeated reads under opposite pull directions reject floating, stuck and unstable return lines. An absent screen stays disabled until reboot; later drawing or wake requests cannot power it back up. Shared supplies retain other peripherals' ownership.

Native tests cover controller replies, timing, pin turnaround, power ownership, headless behavior, rendering, sleep/wake, and exclusions for integral screens. Release checks require the detection marker in the uploadable application, the exact resolved board/driver flags and the matching source commit before the picker can advertise detection.

Check Result Native detection, rendering, power and headless regressions PASS, 31 test methods, including 12 new detection methods with sanitizers and negative controls Recipe, release-proof, controls and offline-picker regressions PASS, 42 test methods Resolved PlatformIO configuration PASS, 31 optional-screen environments across the three hardware families; integral-screen recipes excluded Picker JavaScript regressions PASS, 4 suites Complete build-profile selection suite PASS Hosted documentation links PASS, 7 test methods Packaged ESP32 firmware smoke builds PASS, RC32 Full Companion, Tracker V1 Full Repeater/MQTT, Tracker V1.1 Full Repeater/MQTT, and V4 R8 TFT Full Companion

The qualified smoke packages use firmware source 8268befbd. All four uploadable applications contain their board-specific detection marker and matching firmware/source version. Their bound package hashes and generated picker controls pass verification. RAM headroom after the complete modeled allocation and transient margin is 68,672 bytes for RC32, 1,942 for Tracker V1, 4,054 for Tracker V1.1, and 66,528 for V4 R8 TFT. These are build-time admission results, not measurements of physical runtime load.

Physical verification on RC32, Tracker V1 and V4 R8 with and without their manufacturer-supported screen has not been performed. Hardware qualification requires cold USB/battery boots, screen sleep/wake, headless input, normal radio transmit/receive, and GPS or touch operation where fitted.

"},{"location":"ota_easy/","title":"Ota easy","text":"

View this page on MeshCore Docs.

"},{"location":"ota_easy/#easy-firmware-updates-over-lora","title":"Easy firmware updates over LoRa","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

This guide shows the shortest manual path for sending firmware from a computer to a MeshCore node over LoRa. Choose the package type for the destination node:

For an end-to-end controller that accepts a release ZIP or ready mOTA, see Scripted LoRa OTA from start to finish.

Destination Update type Files needed to build the .mota Installer ESP32 Full firmware New non-merged application .bin ESP32 A/B firmware slots nRF52, internal staging In-place delta Exact running firmware.hex and new firmware.hex Exact-board OTAFIX bootloader Supported nRF52 QSPI repeater Full firmware or in-place delta New firmware.hex; a delta also needs the exact running firmware.hex Matching QSPI-aware OTAFIX bootloader MeshTower V2 SD target Full firmware or in-place delta New firmware.hex; a delta also needs the exact running firmware.hex Matching SD-aware OTAFIX bootloader

An internal-staging nRF52 target accepts only an in-place delta built against its exact running firmware. Matched QSPI repeater targets and the MeshTower V2 microSD target can also install a full image because the complete container stays off-chip. See nRF52 repeater OTA with external QSPI and MeshTower V2 microSD LoRa OTA.

MeshTower V2 microSD application and bootloader containers must be Ed25519-signed by a key in the device allowlist; pass --sign to motatool build (or --sign-key to the end-to-end runner) when building one. Its BLM2-capable SD-aware bootloader is also required before ordinary application or bootloader OTA can use the reset-retained authorization record. Upgrade preview.12 locally over USB/BLE DFU or SWD first; there is no raw-card compatibility handoff.

"},{"location":"ota_easy/#temporary-ota-channel-used-in-this-guide","title":"Temporary OTA channel used in this guide","text":"Setting Value Center frequency 910.525 MHz Bandwidth 250 kHz Spreading factor SF5 Coding rate used in this guide CR5 Example window 120 minutes

The copy/paste command is:

tempradio 910.525,250,5,5,120\n

The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates do not need to match.

tempradio is not saved and the node returns to its normal radio settings when the window ends or the node reboots. Current full-parser firmware also accepts normalradio, which cancels the temporary window and restores the saved tuple after replying on the current channel. This frequency is intended for North American configurations. Confirm that it is permitted in your location and change it when necessary.

At SF5/BW250, an SX1262+TCXO destination can keep RXPS enabled only when it and every sender that can reach it (controller, source, and relays) follow the v1.17.1.5-or-newer adaptive-preamble contract. The qualified destination boundary is effective level 8, preamble 64 and 1252 / 6424 us. For manual setup, save set radio.rxps level 8 preamble 32 before entering TempRadio; the 32 is a conservative saved timing assumption and current firmware selects the real 64-symbol wire preamble when it retunes the temporary radio. For an older, unknown, or mixed-version path, use set radio.rxps off for the maintenance window. First record get radio.rxps.config; after normalradio, restore a reported level with set radio.rxps level N preamble P (omit preamble P when P is 0). A legacy node returns only on/off and periods, which can be restored with set radio.rxps RX_US SLEEP_US. The scripted runner performs the version checks, temporary change, verification, and restoration automatically while preserving automatic retuning. SF5/BW500 is too fast for RXPS even with a 64-symbol preamble, so current firmware selects 128 and uses the qualified level-8 626 / 6398 us timing when every participant supports the adaptive contract.

"},{"location":"ota_easy/#before-you-start","title":"Before you start","text":"

Both paths require:

LoRa OTA packets are generated, consumed, and relayed only while tempradio is active. Intermediate repeaters apply their normal forwarding filters, duplicate checks, and flood limits; they do not interpret the OTA payload. If any required window closes, the transfer stops making progress and can resume during a later overlapping window.

build.sh provides a *_repeater_lora_ota_no_external_sensors or *_room_server_lora_ota_no_external_sensors build for standalone infrastructure targets that need a smaller internal update workspace. nRF52 sensors also have *_sensor_lora_ota_no_external_sensors builds. ESP32 room servers and sensors use their expanded Full image for LoRa OTA. These compact siblings omit selected optional environmental and ranging drivers while retaining board-native features such as displays, buttons, battery monitoring, and GPS where the target uses the GPS-preserving lean profile. RAK3401 and RAK4631 reduced builds also retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors; together these drivers cost less than 5 KiB in the measured profile. Those four monitor drivers are not the only retained I2C support: the board's SSD1306 OLED, supported autodiscovered RTCs, and the RAK12500 GPS remain separate I2C peripherals in GPS-compatible RAK recipes. RAK12501/L76K GPS instead uses UART Serial1. The RAK3401 OTA repeater supports either GPS module in sensor slot A; slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The RAK4631 OTA repeater defaults its runtime RS-232 bridge to Serial2 so the UART RAK12501 can retain Serial1. The runtime guard always reserves Serial1 because a bounded silent probe cannot prove a cold RAK12501 is absent; the shared WB_IO2/3V3_S rail keeps a fitted module powered and driving the UART. Use Serial2. Serial1 requires an explicit no-GPS/dedicated image. The merged image remains fail-closed even when it detects an I2C RAK12500. The explicit legacy Serial1 bridge omits the combined GPS provider, including the otherwise non-UART RAK12500 path.

The firmware-configured INA3221 address and the RAK12500 address are both 0x42; they cannot coexist on one bus at those addresses. Keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a build with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

Selected nRF52 boards with matched external QSPI application and bootloader support can instead make the normal full-sensor repeater or room-server install-capable; those targets do not need to reserve internal flash for the downloaded container. SolarXiao 30S and 33S use this matched external-QSPI path: repeaters retain their existing OTA target, and room servers use the explicit *_room_server_lora_ota target with the matching OTAFIX QSPI bootloader. A RAK19007 can use the same separately wired W25Q16 with either RAK4631 or RAK3401 + RAK13302: the flash consumes no WisBlock slot and GPS remains supported in slot A. This is not the RAK15001 arrangement; RAK15001 and RAK13302 share a chip-select and cannot be used together. Other normal repeaters can still serve as intermediate relays but cannot necessarily install an update themselves. ESP32 -ota- siblings also retain the lightweight browser WiFi uploader (start ota), the complete CLI, and up to 254 neighbors; constrained targets use 50 as recorded in their capability manifests. RP2040 and STM32 repeaters do not currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely during TempRadio.

For one explicitly selected internal-flash nRF52 repeater or room server, the default --build-profile auto run emits two install-capable choices. The *-full-ota-* artifact retains external sensors, while the *_lora_ota_no_external_sensors-reduced-ota-* artifact trades only those optional drivers not explicitly retained by the board profile for more delta-staging space. They retain the same OTA target identity so update tooling does not confuse the optimization choice with a different board. QSPI- and SD-backed repeater and room-server roles emit only the complete choice unless a measured size failure requires a fallback.

nRF52 -ota- siblings are compiled with size optimization instead of the Adafruit platform's default speed optimization. This prevents the retained software Ed25519 fallback from expanding beyond the fixed in-place workspace; CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features remain enabled.

ESP32 *-full-usb-wifi-ota-* artifacts retain all compiled features and enable LoRa OTA for every FULL role, including room servers, sensors, observers, and bridges. A FULL image requires its expanded partition table: install the matching merged image over USB once before installing later non-merged FULL updates over LoRa. ESP32 MQTT observers and ESP-NOW bridges are always emitted as FULL artifacts; compact-CLI variants are no longer built. The *-full-usb-wifi-ota-* profile compiles USB packet logging and direct WiFi MQTT into one image. Use set logging.output off|usb|wifi|both to persist the desired path. A *-full-logging-ota-* artifact is now only a fallback for a hardware/role combination without a WiFi MQTT target and can produce substantial serial output.

"},{"location":"ota_easy/#choose-the-source-radio","title":"Choose the source radio","text":"

Use an OTA-enabled MeshCore node as the source. It receives the update folder from the computer, then advertises it over LoRa. ESP32 USB/WiFi companions and FULL ESP32 roles include the required transport. A *_companion_radio_full target keeps only the source half of LoRa OTA: it serves host images but cannot stage or install one for itself. ESP32 full combines USB, BLE, and WiFi; nRF52 full combines USB and BLE because nRF52840 has no WiFi. A small set of high-capacity, non-PSRAM classic ESP32 companions keep their normal image and provide a separate -full-logging-ota- fallback with 100 contacts, 8 group channels, a 16-frame offline queue, and persistent USB output selection. Install that variant's merged image over USB once before using it. Connect the source by USB serial or, when supported, by WiFi. For an ordinary raw-text USB source, confirm that its USB CLI accepts:

ota folder on\n

If an older build reports that OTA_FOLDER_SERIAL is not compiled in, install a current -ota-, -full-usb-wifi-ota-, or applicable -full-logging-ota- build first. Do not use a KISS modem: KISS firmware is a TNC/KISS frame interface and does not provide the MeshCore CLI or the OTA-folder transport that motatool serve requires.

An nRF52 companion_radio_full starts in its USB ASCII terminal and automatically changes to Binary Companion when it receives a complete < frame. It also recognizes motatool's exact initial ota folder on line and enters exclusive mOTA mode directly, so motatool serve --serial can be the first client after boot. No terminal token or preliminary disconnect is required. Stopping the tool or disconnecting resets USB to Binary. BLE remains available throughout. See the Full Companion USB switcher guide for the complete ownership transitions. Protocol v14 can instead take the catalog from a paired phone or Linux host over a separate encrypted BLE mOTA service while Binary Companion remains active. USB and BLE catalog sources are mutually exclusive. See the Full Companion Bluetooth source guide.

For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:

motatool serve --dir ./motas --tcp <source-host>:5001 -v\n

Port 5001 is separate from the companion application port (5000) and the HTTP configuration/browser-OTA port (80, depending on the role). An ESP32 companion_radio_full also has its complete role-specific text terminal on port 5002; OTA automation uses its ota and TempRadio subset. See the full Companion guide. On a FULL repeater or room server, start webconfig can bring up the saved WiFi connection. Other FULL roles with browser OTA support can raise MeshCore-OTA with start ota and use 192.168.4.1:5001. The TCP seeder auto-attaches; do not also run ota folder on for USB serial.

"},{"location":"ota_easy/#install-motatool","title":"Install motatool","text":"

Install Rust if necessary, then install the standalone packaging and serving tool:

git clone https://github.com/vk496/motatool\ncargo install --path ./motatool\n
"},{"location":"ota_easy/#esp32-package-a-full-firmware-image","title":"ESP32: package a full firmware image","text":"

The destination must be an OTA-capable ESP32 with an A/B partition table. Download or build the new non-merged .bin application for the destination's exact board and role. Do not use an ESP32 -merged.bin factory image.

Put the application firmware in a working directory, then build a full .mota container. For example:

mkdir -p ./motas\nmotatool build --fw ./Heltec_v3_repeater-ota-v1.16.05.bin --out-dir ./motas\nmotatool verify ./motas/*.mota\n

Replace the example filename with the firmware for the destination's exact target. With no --base argument, motatool build creates a full-image update. The firmware must contain its MeshCore EndF identity trailer so motatool and the destination can verify the target, hardware, and version.

Do not continue if motatool verify reports a failure.

"},{"location":"ota_easy/#nrf52-package-a-full-image-or-in-place-delta","title":"nRF52: package a full image or in-place delta","text":""},{"location":"ota_easy/#1-install-and-check-the-otafix-bootloader","title":"1. Install and check the OTAFIX bootloader","text":"

This is a one-time prerequisite. Install the OTAFIX bootloader built for the destination's exact board from the OTAFIX nRF52 bootloader releases. Internal-staging delta updates require OTAFIX 2.4 or newer. External QSPI and SD staging require OTAFIX 2.4.1 or newer and release notes that explicitly list the exact board and storage mode. Follow the release's board-specific installation and erase instructions. If it does not contain the destination's exact board, this LoRa install path is not yet available for that board; never substitute a similar board's bootloader.

Before preparing or downloading a LoRa update, run this on the destination:

get bootloader.ver\nota self\n

The first command identifies the installed nRF52 bootloader. Continue only if the ota self reply includes the apply mode required by this target:

bootloader: apply OK\nbootloader: QSPI apply OK\nbootloader: SD apply OK\n

The reply also contains the running firmware's base_hash. Save it for the package check below. A stock, legacy, or older OTAFIX bootloader without .mota in-place-apply support will report that apply support is missing, and ota install will refuse to reboot into it.

"},{"location":"ota_easy/#2-choose-full-or-delta-packaging","title":"2. Choose full or delta packaging","text":"

For a QSPI- or SD-backed target, a full update needs only the new raw firmware.hex:

mkdir -p ./motas\nmotatool build --fw ./LilyGo_T-Echo_repeater-new.hex --out-dir ./motas\nmotatool verify ./motas/*.mota\n

Use a full package when an exact base image is unavailable. Use the delta path below when reducing airtime is worth retaining the exact running image.

"},{"location":"ota_easy/#3-keep-the-exact-current-and-new-application-images-for-a-delta","title":"3. Keep the exact current and new application images for a delta","text":"

You need the raw .pio/build/<environment>/firmware.hex from the build that is actually running, plus the corresponding firmware.hex from the new build. Save the current file before building the new version, because PlatformIO reuses that path. For example:

# Save this immediately after building/flashing the version now running on the node.\ncp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \\\n  ./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex\n\n# After checking out and building the new version, save its image separately.\ncp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \\\n  ./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex\n

Replace Heltec_t114_repeater_lora_ota_no_external_sensors with the destination's exact PlatformIO environment. The two images must be for the same board and role, and both must contain their EndF trailers. Do not pass a release .uf2 or BLE-DFU .zip to motatool; those are installation containers rather than raw application images.

Keeping a file with the same version label is not enough: the base must be byte-for-byte identical to the running application. The hash check in the next step proves that it is the right file.

On RAK4631 repeaters, use the RAK_4631_repeater_lora_ota_no_external_sensors environment. It retains built-in battery monitoring but omits selected optional environmental/ranging drivers so the delta fits the safe in-place workspace. INA219, INA226, INA260, and INA3221 I2C voltage/current monitors remain supported despite the legacy target name. That trim does not remove the SSD1306 OLED, autodiscovered I2C RTCs, or GPS-compatible RAK12500 I2C path. The 0x42 RAK12500/INA3221 addressing rule above still applies. If the device has a RAK15001 installed in sensor slot C and the matching RAK15001 OTAFIX bootloader, use RAK_4631_repeater_rak15001_slot_c_lora_ota instead. That target retains the full sensor/GPS set and can install either a full image or a delta from the external 2 MiB store.

With the separately wired Winbond breakout, use RAK_4631_repeater_w25q16_lora_ota for RAK4631 or RAK_3401_repeater_rak13302_w25q16_lora_ota for RAK3401 + RAK13302. Both require an EF4015 2 MiB device. As a one-time prerequisite, install wiscore_rak4631_w25q16 for RAK4631 or wiscore_rak3401_rak13302_w25q16 for RAK3401 + RAK13302. Wire CLK/MISO/MOSI to the RAK19007 IO connector pins 26/27/28, CS to J11 AIN1, and VCC/GND to J12 VDD/GND. Fit an approximately 10 kOhm CS-to-VDD pull-up; never use VBAT. The SPI pins require underside taps or an interposer, but no sensor slot is occupied. See the nRF52 QSPI guide before installing either image.

For a build that uses only the 2.54 mm headers, use the unified RAK_4631_repeater_unified_lora_ota or RAK_3401_repeater_unified_lora_ota image with its matching compatible OTAFIX 2.4.10 bootloader. Wire CLK to J10 TX1, DO to J10 RX1, DI to J11 IO1, and CS to J11 AIN1; keep the CS pull-up and J12 power wiring. This gives up UART1 GPS/PPS but leaves I2C sensors and the RAK3401 radio available. See the header-only wiring table.

"},{"location":"ota_easy/#4-build-and-check-the-in-place-delta","title":"4. Build and check the in-place delta","text":"
mkdir -p ./motas\nmotatool build \\\n  --base ./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex \\\n  --fw ./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex \\\n  --patch-type in-place \\\n  --out-dir ./motas\nmotatool verify ./motas/*.mota\n

motatool prints the generated filename. Inspect that file:

motatool inspect ./motas/GENERATED_FILENAME.mota\n

Check all three of these before serving it:

Current layout-aware motatool derives the safe workspace from the new firmware's authenticated EndF layout record, so the normal recipe deliberately omits --inplace-memory. If older tooling requires an explicit override, use 0xC6000 only for matched external SD/QSPI staging and 0x98000 for an internal-staging nRF52 such as the RAK4631 lean OTA build. Do not substitute the external value on an internal target, and do not override either value unless you have verified the exact app base, bootloader, and package geometry. Do not continue if verification or any identity check fails.

"},{"location":"ota_easy/#transfer-and-install-either-package","title":"Transfer and install either package","text":""},{"location":"ota_easy/#1-start-the-temporary-ota-channel","title":"1. Start the temporary OTA channel","text":"

First save the current ota config output. For a direct source-to-destination link, set both OTA-enabled endpoints to direct-only mode before starting the transfer:

ota config hops 0\n

Do not use hops 1 as extra margin on a direct link. It tells a receiving repeater to retransmit each direct DATA/PROOF packet; that unnecessary half-duplex transmission can make it miss the source's next fragment, especially with older single-block receivers. Use hops 1 only when one real intermediate relay is required, hops 2 for two, and so on. This setting is saved, so restore the original value after the maintenance window.

On the source node, destination node, and every intermediate repeater, then run:

tempradio 910.525,250,5,5,120\n

All participating nodes must use the same frequency, bandwidth, and spreading factor. Their time windows must overlap. Start with the farthest hop (the destination) and work back toward the source when using tempradio.

If you administer the destination over LoRa, the controller used to send later ota commands must also be able to communicate on this temporary channel. For unattended nodes, use synchronized tempradioat entries instead of manually starting the windows. Ensure the nodes' clocks are set before using tempradioat.

"},{"location":"ota_easy/#2-serve-the-update-from-the-computer","title":"2. Serve the update from the computer","text":"

Close any serial terminal using the source node's USB port, find its device name, and start the server:

motatool serve --dir ./motas --serial /dev/ttyACM0 -v\n

Replace /dev/ttyACM0 with the USB serial device of the source companion selected above. motatool attaches the folder to the source, which advertises the update over LoRa while its temporary-radio window is active. KISS modem serial ports cannot be used here.

For an nRF52 Full Companion and a Bluetooth-capable host, the equivalent cable-free source is:

python3 tools/ble_mota/ble_mota_seeder.py \\\n  --device MeshCore-MyCompanion \\\n  --dir ./motas \\\n  --local 'tempradio 910.525,250,5,5,120'\n

Leave this command running until the destination finishes downloading.

"},{"location":"ota_easy/#3-find-and-download-the-update","title":"3. Find and download the update","text":"

On the destination node, check its state and ask for nearby updates:

ota status\nota ls\n

Discovery is asynchronous. ota ls says refreshing; wait a few seconds and run it again even if it first shows an older row. Select [same target]: it can say full for ESP32 and external SD/QSPI nRF52 targets, or delta for any supported nRF52 target. Do not select [unsupported] (for example, a source's self-served full image on an internal-staging nRF52). Rows labelled bootloader are also outside this ordinary application flow: they are never automatic and ota install refuses them. Only an already-capable, allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2 microSD nRF52 target can use the separate explicit bootloader workflow. Use the row's stable eight-hex manifest ID rather than its changing list position:

ota pull 838B8169 flash\n

If a legacy internal-flash nRF52 reports no EndF, only a row marked [rescue] is eligible. Current rescue-capable firmware requires ota pull <mid8> flash rescue, followed after completion by ota rescue install <base_hash16>. Older running firmware without those commands must be recovered over USB. Shared-internal bootloader-update builds are stricter: without a valid live EndF, every internal pull is refused before erase because the normal application may extend through 0xED000. Recover those builds over USB/BLE DFU or SWD instead of relying on the older 608 KiB rescue estimate.

Monitor the transfer:

ota status\n

To adjust LoRa OTA pacing, use get ota.speed and set ota.speed <0.05..3> on the source, destination, and OTA relays. 1 preserves the existing timing; lower values slow OTA down, and higher values shorten adjustable delays. This saved setting covers both profiles and OTA adverts as well as the transfer. It does not change radio modulation or the configured airtime budget. See LoRa OTA speed for details.

For a powered bench update where restart-resume is not needed, ota config checkpoint 0 on the destination removes periodic progress writes. It is a smaller optimization than selecting the correct hop count and it trades away persisted mid-download resume; restore the previous checkpoint cadence afterward.

Current repeater firmware automatically uses the full transmit budget during a bounded TempRadio window. Older receivers do not: if get af reports a nonzero value on such a node, record it, use set af 0 for the maintenance window, and restore it afterward. This affects how promptly the legacy receiver can send its next block/proof request; it does not increase LoRa transmit power. Use a full duty budget only where the selected frequency and local rules permit it.

The update is ready when the status says ready to install. Discovery is background traffic, while an active OTA download is primary mesh traffic. At the temporary-radio settings in this guide, allow roughly one hour for a typical ESP32 full image over a quiet, direct link. That is a planning estimate, not an upper bound: repeaters, retries, weak links, and normal mesh traffic can extend it well past an hour. The 120-minute example window is intentional. If necessary, start another overlapping tempradio window; the download resumes rather than starting over.

"},{"location":"ota_easy/#4-verify-and-install","title":"4. Verify and install","text":"

Once the destination reports ready to install, run:

ota install\n

The destination verifies the complete package again before approving it. ESP32 installs the full image into its inactive A/B slot. nRF52 checks the bootloader and storage capabilities and, for a delta, the base hash, then reboots into OTAFIX. The bootloader independently rechecks the package, installs the external full image or applies the delta in place, and verifies the resulting image. Pre-install failures leave the running firmware unchanged and report the reason. If power is lost after an nRF52 in-place apply has begun, OTAFIX will not boot a partial image; it enters recovery DFU so a known-good application can be restored.

After the node returns, reconnect on its normal radio channel and confirm:

ota status\n
"},{"location":"ota_easy/#quick-troubleshooting","title":"Quick troubleshooting","text":"

For additional commands and safety details, see the full OTA user guide. For protocol and container internals, see the OTA protocol specification.

"},{"location":"ota_meshtower_v2_sdcard/","title":"Ota meshtower v2 sdcard","text":"

View this page on MeshCore Docs.

"},{"location":"ota_meshtower_v2_sdcard/#meshtower-v2-microsd-lora-ota","title":"MeshTower V2 microSD LoRa OTA","text":"

Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors is the one canonical MeshTower V2 repeater target. Its SD OTA target ID 0A9DBBF0, hardware ID Heltec_tower_v2, application layout, and artifact names remain unchanged. The required Full and Reduced sensor choices use that same target. Existing SD users need no configuration change.

The build aliases Heltec_tower_v2_repeater and Heltec_tower_v2_repeater_lora_ota_no_external_sensors resolve to this primary target. An installed internal-only image has a different OTA identity and bootloader/layout contract: migrate it once with the matching combined OTAFIX profile and application over local USB/BLE DFU or SWD. An alias does not make that migration a same-target LoRa update.

"},{"location":"ota_meshtower_v2_sdcard/#automatic-storage-selection","title":"Automatic storage selection","text":"

The onboard microSD socket remains primary. A mountable card accepts full application images, in-place application deltas, and explicitly selected signed v3 bootloader packages. After verification, the SD-aware OTAFIX bootloader reads the staged file and programs the application or bootloader.

With a missing or unmountable card, the same application can select internal flash for signed application in-place deltas only. The signer must be trusted by the node, as on the SD application path. This fallback requires the published combined MeshTower SD/internal profile in OTAFIX 2.4.11: the privileged storage profile must remain exactly 09 (SD|BOOT_UPDATE), with a separate positive MOTASTOR optional-application storage record of 02 (STAGE_CEILING). A missing record or an internal-only bootloader does not authorize fallback. Full images, bootloader packages, SD archiving, and automatic own-image offering remain unavailable on internal storage.

Selection is deferred until OTA use so card probing does not delay USB setup. The selected backend stays pinned through the transfer, including a paused or failed transfer. Removing a card produces a storage failure; it does not move the partial download into internal flash. Use ota cancel before selecting storage again after changing the card. The fallback uses internal flash only; it does not reserve or use the 64 KiB APPHYBRID retained-RAM arena.

The pin assignment follows the Heltec MeshTower V2 partial reference circuit:

Signal nRF52840 pin Arduino pin number SD CS P1.00 32 SD MOSI P1.01 33 SD SCK P0.06 6 SD MISO P0.26 26

The SD socket uses its own SPI peripheral, so card traffic does not change the LoRa radio pinout.

"},{"location":"ota_meshtower_v2_sdcard/#card-requirements","title":"Card requirements","text":"

Use a FAT16, FAT32, or exFAT card accepted by the bundled SdFat version. The normal update path imposes no additional MBR/sector-1 layout requirement; filesystem layouts SdFat cannot mount are still rejected. MeshCore creates /meshcore-ota.mota as a contiguous file and passes its exact sector range to OTAFIX in reset-retained MCU RAM.

MeshCore never reads or writes raw sector 1 and does not infer ownership from blank card sectors. Both application and bootloader OTA require a locally provisioned BLM2-capable bootloader that understands the retained-RAM record. Preview.12 must first be upgraded over USB/BLE DFU or SWD.

"},{"location":"ota_meshtower_v2_sdcard/#capacity-and-update-types","title":"Capacity and update types","text":"

When SD is selected, the card removes the internal-flash staging limit. The .mota container may be much larger than the old internal staging gap, and either a full image or an in-place delta may be downloaded. The installed firmware itself must still fit the nRF52840 application region below InternalFS (ending at 0xED000); SD storage does not increase the MCU's executable flash.

For this S140 v6 target, the maximum application image including its EndF trailer is 0xC7000 bytes (815,104 bytes). To package a full self-update:

motatool build --fw ./Heltec_tower_v2_sdcard-new.hex \\\n  --sign ./trusted-signer.key --out-dir ./motas\nmotatool verify ./motas/*.mota\n

A delta uses the exact installed image as its base. The normal 0x98000 workspace remains compatible. If either image is larger than that legacy limit, build the patch with the SD target's larger workspace:

motatool build \\\n  --base ./Heltec_tower_v2_sdcard-running.hex \\\n  --fw ./Heltec_tower_v2_sdcard-new.hex \\\n  --patch-type in-place \\\n  --inplace-memory 0xC7000 \\\n  --sign ./trusted-signer.key \\\n  --out-dir ./motas\nmotatool verify ./motas/*.mota\n

The internal fallback has a smaller flash staging gap and in-place workspace. A delta generated for the larger SD workspace can be rejected even though it updates the same application and target. Keep a usable card inserted for that package, or generate an exact-base delta that fits the fallback's available workspace and staging capacity. The firmware refuses an oversized package; it does not switch backends during its download.

An SD download is resumable because the partial .mota stays on the card. Once it reaches ready, ota install performs the final verification, publishes a one-reset authorization record, and reboots. SD application installation requires a valid signature from a key in the node's allowlist. Keep the card inserted through the reboot and installation.

The BLM2 retained-auth SD-aware bootloader is mandatory for application OTA. ota install refuses to reboot if its continuity metadata does not match the running S140 family/FWID/application layout or if its capability marker does not advertise SD staging and the selected codec. Earlier internal-only MeshTower firmware retains its old identity until the deliberate local migration described above.

"},{"location":"ota_meshtower_v2_sdcard/#signed-bootloader-update","title":"Signed bootloader update","text":"

Only the canonical SD target with SD selected exposes a usable privileged LoRa bootloader-update path. Internal application fallback does not stage bootloader packages. It requires an already installed exact-board ABI-3 OTAFIX bootloader whose one unambiguous capability marker is exactly 0x09 (SD|BOOT_UPDATE) and whose codec mask is 0x0005 (FULL|INPLACE). Requiring both application codecs prevents a bootloader self-update from disabling either normal SD application path. A stock bootloader still requires USB/BLE DFU or SWD. The sole remote bootstrap is intentionally absent: preview.12 and any other legacy-v1 image must be upgraded locally before either SD application or bootloader OTA.

The signed v3 container is exactly 41,330 bytes and carries a 40 KiB candidate for the installed 239A0071 / TOWER_V2_OTA identity (boot target 1150F50E). It uses the same contiguous /meshcore-ota.mota file as an application update. GPREGRET 0x6B plus the distinct SD source marker 0x53 selects the bootloader path; LoRa transport remains payload type 0x0C.

The application linker still ends at 0xED000, but OTAFIX needs 0xE0000..0xEA000 as temporary scratch while replacing itself. Before a boot package can be downloaded or approved, MeshCore requires a hash-valid live EndF proving the complete running image ends by 0xE0000; OTAFIX repeats that no-overlap check before its first erase. An application extending above that boundary can still receive normal FULL or delta application updates from SD, but its bootloader must be updated locally.

MeshCore also checks the live boot settings before touching that scratch page. Erased settings and a valid-app record with CRC disabled are allowed. When a nonzero bank CRC is active, its recorded bank_0_size must cover the complete EndF-inclusive running image and must end by 0xE0000; an undersized or oversized record refuses the operation and requires local DFU/SWD.

Because the card is removable, approval is bound to the exact bytes that were authenticated. For both fmt2 application and fmt3 bootloader packages, MeshCore authenticates one exact signed manifest, requires every streamed manifest byte to match it, and verifies the leaves, payload, and image. During that same pass it hashes the entire container with only the mutable four-byte APRV field normalized to zero. After writing and syncing APRV, MeshCore publishes a 72-byte MOTASDA2 record at reset-retained RAM address 0x20006008. The record binds package purpose/format, exact LBA range, card size, container length, and that normalized SHA-256. OTAFIX consumes and clears the record before reading the card; changing the card or file can only fail. A power loss erases the authorization and also fails closed.

For fmt3, MeshCore additionally writes and readback-verifies the temporary 64-byte MOTASDBL token at 0xE0000, binding the exact total and signed manifest image_hash. OTAFIX requires the parsed manifest, streamed payload, and final scratch image to match it. The token page becomes scratch during a successful boot update and is not permanently reserved.

Every new candidate also carries a backward-compatible BLM2/SOFT continuity extension next to its legacy embedded manifest. The complete 76-byte envelope is fixed at final raw-image offset 0x9FB4; a relocated copy is not a valid candidate. Its embedded bootloader version must equal the outer package version, match the runtime SoftDevice family/FWID, application base, and layout ABI, and be strictly newer than an installed BLM2 version. Preview values use low bytes 1..254 and a stable release uses 0xFF; zero-preview and all-ones versions are invalid. Remote bootloader rollback is not supported; use local DFU/SWD when rollback or migration is intentional.

Bootloader packages are never autofetched or autoinstalled. Select and confirm one exact package manually:

ota ls\nota pull <MID8> flash\n# wait for ota status to report the bootloader download ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy the MID and first 16 image-hash hex digits from ota bootloader. Ordinary ota install rejects this package, and the bootloader command rejects an application package. Keep the SD card inserted through the reboot. A later ota status value of blup:C8 reports a successful bootloader replacement.

After reboot, ota bootloader displays the installed board identity, CRC and capabilities. Its mid and hash fields describe a currently staged download, not the installed bootloader, so staged:none mid=- hash=- is not an install failure. blrc:00 means no retained apply result is available; it does not by itself prove either success or failure.

Older MeshCore application firmware can report get bootloader.ver as unknown for OTAFIX 2.4.6 because this bootloader image omits the legacy UF2 Bootloader text. The updated getter also reads the CRC-validated BLM2 version. For the official signed MeshTower V2 SD 2.4.6 .mota, the installed identity is TOWER_V2_OTA, CRC 5DACDB3D, ABI 3, and capabilities 09. Compare the exact board/storage package when interpreting these values; the non-SD image differs.

"},{"location":"ota_meshtower_v2_sdcard/#sd-card-cli","title":"SD card CLI","text":"

The SD-backed target provides these CLI commands:

set sdcard format [--force]\nset sdcard erase [--force]\nget sdcard\nget sdcard *\nget sdcard format\nget sdcard erase\nget sdcard free\nget sdcard ls\nget sdcard ls 2\nget sdcard dir 3\n

format creates a new FAT16, FAT32, or exFAT filesystem according to card size. erase first uses the card's raw media erase command and then formats it, so a successful erase finishes with a usable filesystem. Both operations destroy all data on the card and cancel a staged SD OTA download. If the internal fallback slot is occupied, card maintenance is refused until an explicit ota cancel; it does not abandon an internal download to format SD.

The firmware records successful format and erase completion times in RAM. Repeating the same operation within five minutes is rejected unless --force is present. Format and erase have independent cooldowns. Because erase also formats the card, a successful erase updates both timestamps. The timestamps reset when the device reboots. The get sdcard age queries report how long ago each operation completed. get sdcard free reports used and free filesystem space in human-readable binary units.

get sdcard ls and get sdcard dir recursively list files on the card, two files per page. A bare command shows page 1; append a positive page number to move through the remaining results. Each row includes the path and a compact file size. The header reports the selected page, total pages, and total files.

"},{"location":"ota_meshtower_v2_sdcard/#persistent-ota-archive-and-seeder","title":"Persistent OTA archive and seeder","text":"

With SD selected, automatic OTA archiving is on by default. While the temporary OTA radio is active, the node requests full catalogs from seeders and saves every complete mOTA it discovers, including firmware for other hardware targets and codecs that this node cannot install. Archive downloads use the same per-block Merkle proof checks as an install download, but archived images are never selected for local installation.

Completed containers are stored as /mota/<manifest-id>.mota. An interrupted download remains /mota/<manifest-id>.part and resumes when that mOTA is seen again. Completed files survive reboot, are enumerated on the first archive access or TempRadio window, and are advertised and served directly from SD. The SD target supports the protocol maximum served set: its own running firmware plus up to 254 archived mOTAs.

Automatic capture preserves an 8 MiB free-space reserve for manual /meshcore-ota.mota installation staging. It stops starting new archive files when the next file would cross that reserve. Archive allocation first tries the fast contiguous path and then falls back to an ordinary fragmented FAT file; only the bootloader staging file requires contiguous sectors.

"},{"location":"ota_meshtower_v2_sdcard/#preload-many-motas-from-a-computer","title":"Preload many mOTAs from a computer","text":"

You can populate the archive much faster on a computer than over LoRa. Use only complete, verified .mota containers. Do not copy firmware .bin, .hex, .zip, or .part files into the archive.

Install the standalone motatool first if it is not already available:

git clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n

The on-card filename is part of the archive index and has a strict format:

/mota/<merkle_root>.mota\n

<merkle_root> is the eight-hex-digit value printed by motatool inspect. It is also the mOTA's four-byte manifest ID. The extension must be lowercase, the file must be directly inside /mota, and descriptive release filenames are not indexed. For example, if inspection reports:

merkle_root    : ABCD1234\n

copy that container to:

/mota/abcd1234.mota\n

If two containers have the same Merkle root, they have the same protocol ID and cannot both be present. Keep only the intended one. The current SD seeder can index up to 254 archived files. A served file must use a logical block size of at most 1024 bytes and contain at most 2048 blocks; check block_size and block_count in motatool inspect when importing unusually large images. Files outside that serve geometry are not counted or advertised. If a malformed /mota/<id>.mota conflicts with a newly discovered valid image, the repeater preserves the malformed file as <id>.bad through <id>.bad9 and downloads a clean replacement instead of treating path existence as a valid cache hit.

To prepare and load the card:

  1. Format it as described under Card requirements. The easiest route is to insert it in the repeater, run set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents.
  2. Mount the card on the computer and create a directory named mota at the filesystem root. Do not use a nested directory such as /firmware/mota.
  3. Run motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL.
  4. Run motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card.
  5. Flush pending writes, safely eject the card, power the repeater off, insert the card, and boot Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors.

On Linux or macOS, this Bash example verifies and imports every .mota from ./motas. Replace the example mount path before running it:

card_mount=/media/YOU/MESHCORE\nmkdir -p \"$card_mount/mota\"\n\nshopt -s nullglob\nfor image in ./motas/*.mota; do\n  motatool verify \"$image\" || exit 1\n  mid=$(motatool inspect \"$image\" |\n    awk '$1 == \"merkle_root\" { print tolower($3) }')\n  if [[ ! $mid =~ ^[0-9a-f]{8}$ ]]; then\n    echo \"Could not read the Merkle root from: $image\" >&2\n    exit 1\n  fi\n\n  destination=\"$card_mount/mota/$mid.mota\"\n  if [[ -e $destination ]]; then\n    cmp -s \"$image\" \"$destination\" || {\n      echo \"Different containers have the same ID: $mid\" >&2\n      exit 1\n    }\n  else\n    cp \"$image\" \"$destination\"\n  fi\ndone\nsync\n

On Windows, create E:\\mota, inspect each source with motatool inspect, and rename it to the reported lowercase root in the same way. Safely eject the drive after all copies finish.

After boot, scan the archive and confirm the card contents from the repeater console:

get sdcard ls\nget sdcard ls 2\nota cache\nota folder\n

ota cache should report the imported count. ota folder reports the current served count; once OTA serving starts in TempRadio, that set also includes the repeater's running firmware. If the files appear in get sdcard ls but not in the served count, check their exact names, run motatool verify again, and inspect their block geometry. It is fine to run ota cache off for a curated, read-mostly archive: that disables capture of new mOTAs but continues serving every valid file already on the card.

"},{"location":"ota_meshtower_v2_sdcard/#serve-the-preloaded-archive-over-tempradio","title":"Serve the preloaded archive over TempRadio","text":"

LoRa OTA traffic exists only during an active temporary-radio window. The SD repeater, each receiving node, and every intermediate repeater in the path need overlapping windows on the same temporary channel. Use a frequency permitted for the node's configured region. This North American example uses the recommended fast OTA settings and a 120-minute window:

tempradio 909.950,250,5,5,120\n

Start the farthest receiving node first, then intermediate repeaters, and the SD source last so their windows overlap for as long as possible. Before or during the source window, ota cache makes the source scan and attach the SD archive. Entry into TempRadio automatically triggers an OTA advertisement burst; ota announce can send another advertisement immediately.

On a receiving OTA-capable node, allow a few seconds for catalog exchange, then run:

ota ls\nota ls 2\nota get <mid8> flash\nota status\n

Use ota ls 2, ota ls 3, and so on when the source advertises more than the two rows that fit in one remote CLI reply. Each row includes a stable eight-hex-digit manifest ID. Use that ID instead of a list number, because asynchronous catalog refreshes can reorder rows between the list and pull commands. A receiver retains the complete protocol catalog and verifies every transferred block. It still applies its normal target, hardware, codec, signature, and installation checks. The SD repeater may advertise images for many hardware families; it never installs those archive files merely because it serves them.

When a temporary window expires or a node reboots, it returns to its saved radio settings and OTA transfer stops. The archive remains on the card. Start another set of overlapping tempradio windows to resume an interrupted download, or use synchronized tempradioat entries for a scheduled window.

Archive capture is lower priority than operator work. An explicit ota pull to install storage or a host folder immediately takes the single receive slot; the archive partial is checkpointed and resumes later. Existing cached files can still answer peer requests while another archive file is downloading.

Use these commands to inspect or control automatic capture:

ota cache\nota cache on\nota cache off\nota config cache on\nota config cache off\n

The on/off choice is saved on the SD card. Turning capture off stops new downloads but keeps serving files already cached. Formatting or erasing the card removes both the archive and its off marker, so a newly formatted card returns to the default-on setting. ota config sdseed on|off is also accepted as an alias.

"},{"location":"ota_nrf52_bootloader_update/","title":"Ota nrf52 bootloader update","text":"

View this page on MeshCore Docs.

"},{"location":"ota_nrf52_bootloader_update/#nrf52-bootloader-updates-over-lora","title":"nRF52 bootloader updates over LoRa","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Selected nRF52840 repeater LoRa-OTA builds can replace their matching OTAFIX bootloader without replacing the running application. This is a privileged maintenance path, not a normal firmware update. A stock bootloader must first be replaced with the exact ABI-3 self-update-capable OTAFIX build over USB/BLE DFU or SWD. MeshTower SD also requires the BLM2 retained-auth version to be provisioned locally before either application or bootloader OTA.

The released adaptive RAK3401 and RAK4631 OTAFIX 2.4.8/2.4.9 profiles support application full and delta updates, but not bootloader self-update over LoRa. USB UF2 and local Bluetooth/serial Legacy DFU bootloader updates still work. Their running bootloader protects 0xF4000..0x100000 with the nRF52840 ACL before starting MeshCore. This includes the MBR parameter page at 0xFE000, so an application cannot initiate COPY_BL even after verifying a signed bootloader package. Hardware testing on a RAK3401 confirmed the MBR SVC enters a bus fault and leaves the installed bootloader unchanged. For new RAK installations use the normal OTAFIX 2.4.10 compatible *_auto bootloader. Existing standard 3401_DFU / 4631_DFU devices can receive its signed bootloader .mota over LoRa. Devices already running 3401_AUTO_DFU / 4631_AUTO_DFU need the matching 2.4.10 recovery bridge installed locally before switching to the normal image.

"},{"location":"ota_nrf52_bootloader_update/#storage-layouts","title":"Storage layouts","text":"Layout Application limit Staged boot package Work area Handoff / capability XIAO-module raw QSPI below 0xE0000 external QSPI offset 0 dedicated internal 0xE0000..0xEA000 scratch source 0x51, flags 0x0E Qualified internal-flash target normal 0xED000 flash limit; top 64 KiB SRAM reserved for application-delta staging shared internal slot, exact start 0xE2000 the same eleven-page slot; no second flash reservation source 0xED, flags 0x0A MeshTower V2 microSD normal 0xED000 limit contiguous /meshcore-ota.mota dynamic internal 0xE0000..0xEA000 scratch; live image must end by 0xE0000 source 0x53, flags 0x09

The exact SD target is Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors. Its normal application FULL and delta updates continue to use the SD file without the bootloader-update scratch restriction. For a bootloader package only, both MeshCore and OTAFIX require a hash-valid live EndF proving the complete running image ends at or below 0xE0000. If boot settings carry a nonzero app bank CRC, the recorded bank size must also cover that full EndF-inclusive image and stop by 0xE0000; erased or explicitly CRC-disabled settings remain valid. OTAFIX then copies the verified 40 KiB payload from SD into 0xE0000..0xEA000 and uses the MBR copy operation to replace 0xF4000..0xFE000. The application linker remains at 0xED000; a future application extending above 0xE0000 can still use application mOTA but must update its bootloader through local DFU/SWD.

The removable SD authorization is fail-closed for both application and bootloader packages. MeshCore directly authenticates one exact signed manifest, requires the streamed copy to remain byte-identical, verifies all leaves/payload/image bytes, and computes a normalized full-container SHA-256 in that same pass (APRV bytes are treated as zero). After syncing APRV, it publishes a 72-byte MOTASDA2 record in reset-retained RAM at 0x20006008. That record binds purpose/format, first LBA, sector count, total length, card sector count, and the normalized digest. OTAFIX copies, validates, and clears the record before SD access. A card swap/mutation or a power cycle therefore fails closed; there is no general sector-1 ownership inference.

For fmt3, MeshCore also writes and readback-verifies a 64-byte MOTASDBL token at 0xE0000. That token contains the exact container length and the exact authenticated signed manifest image_hash. OTAFIX requires the same hash in the parsed manifest, streamed payload, and final scratch image. The page is consumed as scratch during success and is not permanently reserved.

The first BLM2-capable MeshTower bootloader cannot be delivered to preview.12 through the retained-RAM protocol that preview.12 predates. MeshCore does not write a raw-sector compatibility record because a blank sector is not proof of ownership. Upgrade preview.12 through USB/BLE DFU or SWD. Both fmt2 application and fmt3 bootloader OTA then require the installed BLM2 metadata to match the live SoftDevice/application layout; neither MeshCore nor OTAFIX uses sector 1.

The internal path does not change the application's 0xED000 flash limit or permanently set aside separate app-OTA, boot-package, and flash-scratch regions. Qualified applications do use a dedicated linker that reserves the top 64 KiB of SRAM for hybrid application-delta staging and a retained authorization record. That SRAM is never part of a bootloader-update package. The ordinary bottom-aligned internal flash store holds one container prefix at a time: either an application delta or the complete bootloader package.

The exact bootloader container is 41,330 bytes: 365 bytes of signed mOTA metadata, a 40 KiB payload, and the five-byte trailer. Below the normal 0xED000 store ceiling it bottom-aligns at 0xE2000. Admission requires a hash-valid live EndF proving the current application, including its trailer, ends at or before 0xE2000. OTAFIX then reads each source window before erasing and compacts the payload forward in place to the page-aligned raw range 0xE2000..0xEC000; it verifies every page and the whole image before asking the MBR to copy that image over 0xF4000..0xFE000.

An ordinary application delta can be smaller or larger than this eleven-page shape. On the qualified profile, a delta larger than one page uses the minimum page-aligned flash prefix ending at 0xED000 and keeps up to 64 KiB of its logical tail in reset-retained SRAM; its detools workspace must stop at the actual flash-prefix start. A one-page-or-smaller delta stays wholly in flash. In either case the reconstructed application must stop below 0xED000. The two package kinds are mutually exclusive because they use the same flash store, and a bootloader package never uses the volatile SRAM suffix.

For internal-self-update builds, an absent or corrupt live EndF disables all internal staging before the first erase. The older 608 KiB rescue estimate is unsafe when a normally linked application may extend to 0xED000. Builds without this feature retain the legacy rescue behavior.

"},{"location":"ota_nrf52_bootloader_update/#internal-flash-target-inventory","title":"Internal-flash target inventory","text":"

The release builder consumes the allowlist in tools/mota/nrf52_internal_bootloader_targets.txt. Ten names are also literal PlatformIO environments and are enabled automatically when built directly. The other eleven are release aliases assembled from a base environment plus the lean OTA overlay; build them through build.sh, which passes the same allowlist decision to the common pre-build guard. These lean repeater/bridge targets have no OTA-owned SD/QSPI store and have an exact curated OTAFIX manifest identity:

Build target(s) Installed OTAFIX identity Boot target ID Heltec_tower_v2_repeater_lora_ota_no_external_sensors 239A0071 / TOWER_V2_OTA 1150F50E T096 lean repeater and RS232 bridge 239A0071 / T096_DFU 42354C85 Heltec_t1_repeater_lora_ota_no_external_sensors 239A0071 / T1_DFU FC556FFC T114 display and without-display lean repeaters 239A0071 / T114_DFU 0C3F2902 Mesh_pocket_repeater_lora_ota_no_external_sensors 239A0071 / MESH_POCKET_OTA 059277F4 KeepteenLT1_repeater_lora_ota_no_external_sensors 239A00B3 / KeepteenLT1_OTA DB2E7B51 Minewsemi_me25ls01_repeater_lora_ota_no_external_sensors 239A0029 / MX25_DFU 026AA982 ProMicro_repeater_lora_ota_no_external_sensors 239A00B3 / PROM_DFU AF79E8CC t1000e_repeater_lora_ota_no_external_sensors 28860057 / T1KE_DFU E6F5F03F ThinkNode_M3_repeater_lora_ota_no_external_sensors 239A00DA / TNM3_DFU 0CA41DB2 RAK_3401_repeater_lora_ota_no_external_sensors 239A0029 / 3401_DFU 23818A80 RAK4631 lean repeater and both lean RS232 bridges 239A0029 / 4631_DFU 2D0DF000 GAT562 30S/Tracker Pro/EVB Pro lean targets 239A0029 / GAT562_DFU D50D2D44 R1Neo_repeater_lora_ota_no_external_sensors 239A0029 / 4631_DFU 2D0DF000 RAK_WisMesh_Tag_repeater_lora_ota_no_external_sensors 239A0029 / RTAG_DFU C72E9C9C

Board IDs are not globally unique. For generic targets, the signed hardware ID is the exact NUL-padded 32-byte value NRF_BL_<BOARD_ID>_<DEVICE_NAME>. The wire target is the little-endian first four SHA-256 bytes of all 32 padded bytes. The installed and candidate embedded manifest pairs must match exactly. XIAO retains its deployed XIAO_BL_28860044 / XIAO_BL_28860045 identity and raw board-ID target.

The Python reference builder and release tooling audit these boot targets for duplicates and collisions with application target IDs. Generic image parsing can inspect a future canonical identity, but signing/building a package fails until that exact identity is in the qualified inventory.

Earlier GAT562 installations may report the RAK-compatible 4631_DFU identity. The installed and candidate identity pair must match exactly, so a remote bootloader update cannot migrate one of those devices to GAT562_DFU. Provision the GAT562-specific OTAFIX bootloader once through USB/BLE DFU or SWD; subsequent signed bootloader mOTA updates use target D50D2D44.

"},{"location":"ota_nrf52_bootloader_update/#embedded-continuity-and-version-policy","title":"Embedded continuity and version policy","text":"

Every remotely supplied successor retains the legacy 44-byte CRC-valid BLMF v1 record for identity continuity and diagnostics. Immediately after that record is a 32-byte BLM2/SOFT extension. The complete 76-byte envelope is fixed at the final raw-image offset 0x9FB4; relocated candidates are refused, while installed legacy-v1 discovery remains a generic diagnostic scan. The extension carries the actual packed bootloader version, SoftDevice family and FWID, application base, layout ABI, and zero compatibility/reserved fields; the legacy whole-image CRC covers the extension too. Scanners first count CRC-valid 44-byte base manifests, then interpret continuity only after one base identity remains. A malformed claimed extension therefore cannot make a duplicate identity disappear, and a sole half-present extension is rejected rather than treated as legacy.

MeshCore requires the candidate extension, exact equality between its embedded version and the signed outer mOTA version, and exact agreement with the running SoftDevice family/FWID/application layout. Qualified internal/QSPI paths may bootstrap a CRC-valid installed legacy-v1 image once; the SD path never does and requires local BLM2 provisioning. Once the installed bootloader has BLM2 metadata, the candidate version must be strictly greater. Preview low bytes are 1..254, a stable release uses 0xFF, and low-byte zero or all-ones values are rejected. There is no remote rollback/migration override; intentional rollback uses USB/BLE DFU or SWD.

The legacy no_external_sensors profiles trim selected optional environmental and ranging drivers to preserve flash headroom; they do not generally disable the I2C bus. Reduced RAK3401 and RAK4631 recipes retain INA219, INA226, INA260, and INA3221 voltage/current monitors. Those are entries in the optional sensor table, not the complete set of I2C consumers: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals where the exact board recipe enables them. RAK3401 retains its compatible RAK12500 I2C and RAK12501/L76K UART GPS paths in sensor slot A. The plain RAK4631 repeater and Serial2 bridge retain GPS. Its RAK12501 path uses Serial1; the explicitly compiled Serial1 RS232 bridge therefore omits the combined GPS provider, including RAK12500, even though RAK12500 itself is I2C rather than UART.

The firmware-configured INA3221 and RAK12500 addresses are both 0x42. They cannot coexist on the same bus at those addresses. Leave RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use firmware built with -DTELEM_INA3221_ADDRESS=0x43 when both modules are installed.

Boards with onboard external flash are not silently redirected to internal staging. Mesh Solar, Nano G2 Ultra, T-Impulse Plus, ThinkNode M8, T-Echo Lite/Card, MeshTracker X1, and Wio WM1110 have board-specific QSPI hardware and need a separately matched QSPI path where available. Full Companions, other SD/QSPI/ExtraFS roles, Ethernet roles, source-only roles, and unqualified full-sensor roles are excluded from the internal-flash inventory. The exact MeshTower V2 SD role above is separately qualified for its SD path.

This internal layout is limited to nRF52840 devices with 1 MiB internal flash, the exact S140 v6/v7 map, and the 40 KiB boot region at 0xF4000..0xFE000. nRF52833 and smaller nRF52 parts cannot provide that map plus a non-overlapping 41,330-byte live staging slot, so configuration fails closed instead of selecting smaller or overlapping geometry.

"},{"location":"ota_nrf52_bootloader_update/#explicit-install-workflow","title":"Explicit install workflow","text":"

The tools/lora_ota/lora_ota.sh and .ps1 runners can perform the discovery, transfer, verification and radio cleanup for a bootloader mOTA with --no-install. They detect format 3 automatically and check the destination's bootloader-specific identity/capabilities. They do not install it: the final MID/hash confirmation below remains an explicit operator action. A mixed application/bootloader ZIP requires --zip-member.

Check the installed identity and capability marker:

ota bootloader\n

The response must show a CRC-valid exact identity, ABI 3 or newer, both FULL and INPLACE application codecs (codecs=0x5), and exact flags 0x09 for MeshTower V2 SD, 0x0A for internal shared storage, or 0x0E for XIAO QSPI. A bootloader row is visible in ota ls, but it is never autofetched or autoinstalled. Fetch its exact MID, then explicitly arm it:

ota pull <MID8> flash\n# wait for ota status to report the download ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy both confirmation values from the second ota bootloader response. Ordinary ota install rejects a bootloader package; the bootloader command rejects an application package. The FULL-codec exception exists only for that manual bootloader MID. Ordinary application FULL remains disabled on an internal single-slot node, bootloader autofetch remains off, and a partial bootloader package is not automatically resumed after an application reboot.

Before writing APRV, the application authenticates and authorizes the package: exact v3 geometry, trusted Ed25519 signer, signed/embedded identity, one unambiguous capability marker, embedded CRC, sane vectors, complete Merkle/payload/image hashes, storage-specific safe live placement, and the typed MID/hash confirmation. The manifest root and target inside the received manifest must also equal the MID/target that opened the fetch before any store is allocated. On SD, APRV, the retained geometry/container authorization, and the internal signed-image-hash token are published in that order before the reset trigger. OTAFIX consumes that application-written authorization and then independently rechecks the safety/integrity subset: strict v3 structure, canonical identity/capabilities, vectors, full payload SHA, embedded manifest CRC, the applicable live EndF/bank-settings no-overlap geometry, the SD token binding where applicable, scratch readback, and final copy hash. It does not re-run Ed25519, the signer allowlist, Merkle leaves/root, or the typed operator confirmation. Success is reported as blup:C8.

"},{"location":"ota_nrf52_bootloader_update/#failure-behavior","title":"Failure behavior","text":"

The feature fails closed when the MCU/map is wrong, required valid EndF headroom is insufficient, an unsupported external/ExtraFS role owns the target, the exact installed capability marker is absent or ambiguous, identity cannot be derived unambiguously, or any package check fails. Before the storage-specific scratch/copy step, the application and bootloader are unchanged. OTAFIX consumes the trigger and authorization before its first destructive action, so an interrupted operation cannot automatically retry a partly consumed package. On SD, the retained-RAM authorization is zero-consumed before media access; APRV and the fmt3 flash token may persist but are inert without a new explicit authenticated re-arm and reset trigger. No SD raw-sector handoff is created or consumed. Use USB/BLE DFU or SWD for initial provisioning and local recovery.

For XIAO and ordinary external-QSPI details, see nRF52 repeater LoRa OTA with external QSPI.

"},{"location":"ota_nrf52_qspi/","title":"Ota nrf52 qspi","text":"

View this page on MeshCore Docs.

"},{"location":"ota_nrf52_qspi/#nrf52-repeater-lora-ota-with-external-qspi","title":"nRF52 repeater LoRa OTA with external QSPI","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Selected nRF52840 repeater builds use their dedicated external QSPI NOR flash as a raw LoRa OTA staging device. This removes the internal-flash staging conflict: the complete .mota stays off-chip, and the bootloader can use the entire internal application region while it installs either a full image or an in-place delta.

This is a matched application-and-bootloader feature. A board merely having an nRF52840, free RAM, or pins named QSPI is not enough. Both halves must use the exact flash wiring, and ota self must confirm the store and bootloader before an update is downloaded.

"},{"location":"ota_nrf52_qspi/#supported-repeater-families","title":"Supported repeater families","text":"

QSPI staging is enabled for explicitly matched repeater-role environments (and RS232 bridge variants that directly inherit one of those repeater targets) on these currently matched families:

Heltec T114 is intentionally not in this list. Its public V1, V2.0, and V2.1 schematics show U9 (MX25R1635F) as an optional QSPI footprint, so standard T114 application and bootloader targets do not assume that external NOR is populated.

The XIAO-module derivatives use the matching XIAO or XIAO Sense OTAFIX bootloader shown by the module's INFO_UF2.TXT. Do not substitute a similar bootloader for a board with different QSPI pins. For example, the T-Echo Card and T-Echo Lite have different flash wiring and are not enabled by the original T-Echo target.

Companion builds are intentionally excluded. Some companion targets use the same external QSPI as a LittleFS message/data store, where raw OTA staging would corrupt the filesystem; other companions simply do not assign that chip to OTA. Dedicated *_room_server_lora_ota environments also enable raw QSPI staging with their own exact role identity and matched bootloader layout. For Ikoka, these cover the Handheld E22-30 and Nano/Stick 22, 30, and 33 dBm variants. The legacy *_room_server environments retain their internal-flash layout and a different OTA target identity; use the matching wired installation and bootloader migration procedure before switching to a QSPI Room image. Sensor and KISS roles do not gain QSPI staging from these recipes.

"},{"location":"ota_nrf52_qspi/#rak15001-placement-and-module-conflicts","title":"RAK15001 placement and module conflicts","text":"

RAK15001 is a 2 MiB GD25Q16C standard SPI module, not a quad-I/O flash. The dedicated RAK4631 target uses the nRF52840 QSPI peripheral in its single-data-line FAST_READ/page-program modes at 8 MHz and accepts only the module's exact C8 40 15 JEDEC ID. ota self reports QSPI store:2048K only when the expected module responds. An empty slot or a different SPI device reports QSPI store:ERR 0K, and install is refused.

The module is electrically usable in sensor slot A-D because those slots share the SPI signals and RAK15001 has onboard 10 kOhm pull-ups on WP# and HOLD#. The supported MeshCore/OTAFIX combination nevertheless requires slot C. It is the placement that remains safe when a RAK12501 GNSS is fitted in either of its supported slots, A or D: slot C avoids the GNSS PPS/reset nets on IO1/IO2 and IO5/IO6. In particular, GPS in A plus flash in B would share IO1/IO2 through the modules' auxiliary pins.

Only one device that uses the shared WisBlock SPI chip-select may be fitted. Do not combine this target with RAK13800 Ethernet, RAK15002 SD, or another SPI module. RAK13800 and RAK15001 cannot coexist because they use the same SPI chip-select. Update a RAK13800 Ethernet build locally over USB using the release's Manual UF2 or Serial DFU (.zip) download; it cannot use this RAK15001 LoRa-OTA staging target.

RAK3401 with RAK13302 remains incompatible with RAK15001. The 1 W radio uses the same WisBlock SPI clock/data pins and the same chip-select as RAK15001. Firmware cannot independently select or detect those two chips, so a stock RAK3401 + RAK15001 assembly cannot provide reliable OTA staging without a hardware chip-select rework. This restriction does not apply to the separate-CS W25Q16 wiring below.

"},{"location":"ota_nrf52_qspi/#one-w25q16-wiring-for-rak4631-or-rak3401-rak13302","title":"One W25Q16 wiring for RAK4631 or RAK3401 + RAK13302","text":"

An external Winbond W25Q16JV breakout can remain attached to a RAK19007 while the core is changed between RAK4631 and RAK3401 + RAK13302. It shares only the WisBlock SPI clock/data nets and has its own chip-select on AIN1:

W25Q16 breakout RAK19007 connection nRF52840 / Arduino pin CLK IO connector pin 26, SPI_CLK P0.03 / 3 DO / MISO IO connector pin 27, SPI_MISO P0.29 / 29 DI / MOSI IO connector pin 28, SPI_MOSI P0.30 / 30 CS J11 AIN1 P0.31 / 31 VCC J12 VDD regulated 3.3 V GND J12 GND ground

Fit a physical approximately 10 kOhm pull-up from CS to J12 VDD. That keeps the flash deselected during reset, bootloader entry, and a core-module swap. Power the breakout only from J12 VDD; never use J11 VBAT, which can exceed the flash's supply rating. The six-pin breakout does not expose WP#/IO2 or HOLD#/IO3; do not add nRF IO2/IO3 wiring. A breakout with an onboard power LED works, but that LED draws continuously and is undesirable for low-power or solar operation.

This arrangement consumes no WisBlock sensor slot. However, SPI_CLK, SPI_MISO, and SPI_MOSI are not available on the easy 2.54 mm J10/J11/J12 headers. Those three signals require a short underside-pad tap or a suitable IO-connector interposer at pins 26-28. J11 and J12 alone are not sufficient.

GPS remains supported in sensor slot A. RAK12500 uses I2C, while RAK12501/L76K uses UART plus its auxiliary control signals; neither uses the shared SPI clock/data bus. On RAK4631 the built-in SX1262 has a separate private SPI bus. On RAK3401 the RAK13302 and flash share clock/data, but have independent chip-selects: RAK13302 NSS is P0.26 and flash CS is P0.31. The matched firmware holds the radio NSS high while it temporarily hands the bus to the flash, then restores the radio SPI interface.

The breakout target accepts only a 2 MiB W25Q16 with JEDEC ID EF 40 15 and runs it at 8 MHz in standard single-data-line SPI mode. A substituted W25Q32, W25Q64, or W25Q128 fails closed instead of being treated as the OTA store. Use the complete application/bootloader identity row that matches the fitted core:

Core and radio PlatformIO environment MOTA hardware identity OTAFIX board / DFU device name RAK4631 built-in SX1262 RAK_4631_repeater_w25q16_lora_ota RAK4631_W25Q16 wiscore_rak4631_w25q16 / 4631_W25Q16_DFU RAK3401 + RAK13302 1 W RAK_3401_repeater_rak13302_w25q16_lora_ota RAK3401_RAK13302_W25Q16 wiscore_rak3401_rak13302_w25q16 / 3401_W25Q16_DFU

The physical flash wiring is identical, but the two application and bootloader pairs are not interchangeable.

"},{"location":"ota_nrf52_qspi/#header-only-w25q16-wiring-for-the-unified-rak-images","title":"Header-only W25Q16 wiring for the unified RAK images","text":"

The unified RAK repeater images and matching OTAFIX 2.4.10 compatible bootloaders also support a W25Q16 through the RAK19007 2.54 mm headers. This arrangement does not need an IO-connector interposer or underside SPI solder points:

W25Q16 breakout RAK19007 header nRF52840 / Arduino pin CLK J10 TX1 P0.16 / 16 DO / MISO J10 RX1 P0.15 / 15 DI / MOSI J11 IO1 P0.17 / 17 CS J11 AIN1 P0.31 / 31 VCC J12 VDD regulated 3.3 V GND J12 GND ground

Keep the approximately 10 kOhm CS-to-VDD pull-up. This mapping gives up the UART1 GPS and PPS pins while the flash is fitted. The unified firmware detects the exact EF4015 device before GPS discovery and leaves I2C sensor pins SDA and SCL available. RAK4631 can still use its UART2 bridge; its UART1 bridge is unavailable with header flash. RAK3401 retains its 1 W radio and shared sensor/radio power rail. Do not substitute SCL for flash clock or use SWCLK, which is the debug clock. The older dedicated W25Q16 images above expect the WisBlock SPI connector wiring and do not use this header mapping.

"},{"location":"ota_nrf52_qspi/#one-time-prerequisite","title":"One-time prerequisite","text":"

Install a QSPI-capable OTAFIX 2.4.1 preview.9 or newer bootloader for the exact board from the OTAFIX releases before using LoRa OTA. The release notes must explicitly list that board's QSPI mode. Also install the SoftDevice version expected by that target. The application refuses the install handoff when the bootloader does not advertise QSPI support.

For the external W25Q16 option, install the corresponding bootloader once before installing its MeshCore application: wiscore_rak4631_w25q16 (DEVICE_NAME=4631_W25Q16_DFU) for RAK4631, or wiscore_rak3401_rak13302_w25q16 (DEVICE_NAME=3401_W25Q16_DFU) for RAK3401 plus RAK13302. Use only a release that explicitly names that exact pairing. Do not substitute the RAK15001 bootloader or swap the two W25Q16 bootloaders merely because the base-board wiring is the same.

For the first migration from the ordinary wiscore_rak4631_board bootloader to wiscore_rak4631_board_rak15001_slot_c, use Nordic serial DFU or a compatible BLE DFU client with the exact slot-C OTAFIX combined bootloader + SoftDevice DFU package (or use SWD), then reinstall the slot-C MeshCore application. The release filename has this form:

wiscore_rak4631_board_rak15001_slot_c_bootloader-<OTAFIX-version>_s140_6.1.1.zip\n

Do not copy the slot-C bootloader-update UF2 onto the stock UF2 drive. The stock loader is bound to DEVICE_NAME=4631_DFU, while the slot-C image is bound to 4631_15001C_DFU, so that UF2 is intentionally rejected. After the one-time DFU/SWD migration, later canonical slot-C bootloader UF2 files work normally. The combined OTAFIX package above is not the MeshCore application's Serial DFU .zip; an application package does not migrate the bootloader.

After installing the repeater application, check:

get bootloader.ver\nota self\nota status\nota qspi\n

A ready RAK15001 target reports all of the following:

QSPI store:2048K\nbootloader: QSPI apply OK\nbl:QSPI\nQSPI jedec=C84015 size=2048K sr1=00 stage=jedec\n

A ready external W25Q16 target instead identifies the Winbond part:

QSPI store:2048K\nbootloader: QSPI apply OK\nbl:QSPI\nQSPI jedec=EF4015 size=2048K sr1=00 stage=jedec\n

Other supported boards can report a capacity different from 2048K; the RAK15001 and W25Q16 targets must report exactly 2048K and their respective exact JEDEC IDs. QSPI store:ERR 0K, NO QSPI, or bl:NO-QSPI means the flash wiring, flash power, chip-select pull-up, application, or bootloader does not match. Do not start an install in that state.

ota qspi is a read-only diagnostic probe available on QSPI OTA builds. It reports the exact JEDEC ID, status-register byte, last store stage, and the first latched storage error. Run it after an immediate storage error before starting another pull; later capacity probes preserve that failure detail.

"},{"location":"ota_nrf52_qspi/#capacity-and-package-types","title":"Capacity and package types","text":"

The store reads the JEDEC capacity at runtime and accepts supported 1 MiB through 16 MiB devices using 24-bit addressing. QSPI capacity is not the final firmware limit. The reconstructed application, including its 56-byte EndF trailer, must fit its build's linked application region:

SoftDevice layout Application region Maximum image S140 v7, boot-update-capable XIAO module 0x27000..0xE0000 0xB9000 (757,760 bytes) S140 v7, app base 0x27000 0x27000..0xED000 0xC6000 (811,008 bytes) S140 v6, app base 0x26000 0x26000..0xED000 0xC7000 (815,104 bytes)

The XIAO limit is deliberately 52 KiB smaller. Its linker and post-link record cap every ordinary application at 0xE0000; OTAFIX reserves 0xE0000..0xEA000 as a 40 KiB self-update scratch bank, with the remaining gap left unused before InternalFS. Full and delta application packages for these targets derive their effective application end from that 0xE0000 record and are rejected if their reconstructed image or detools geometry crosses it.

A full package needs only the new raw firmware.hex or non-merged application image. An in-place delta still needs the exact image currently running. The automation uses a conservative 0xC6000 detools workspace for external nRF52 staging so one package setting is safe for both layouts.

For online automation, QSPI is detected from ota self or ota status. For offline preparation, identify it explicitly:

./tools/lora_ota/lora_ota.sh ./release.zip target-name \\\n  --prepare-only \\\n  --platform nrf52 \\\n  --nrf-qspi \\\n  --target-id 12345678 \\\n  --target-hw Xiao_nrf52\n

Use the real target ID and hardware identity from the destination. A ready full .mota normally uses that target ID for discovery and routing. An operator can deliberately override the routing target for a role change, so target_id is not an apply-time safety assertion; the destination still enforces the package's hardware identity before approval.

"},{"location":"ota_nrf52_qspi/#explicit-xiao-bootloader-updates-over-lora","title":"Explicit XIAO bootloader updates over LoRa","text":"

This section documents the deployed XIAO raw-QSPI layout. Curated nRF52840 targets without external staging use a separate internal-flash layout with the same explicit operator safety model; see nRF52 bootloader updates over LoRa.

Bootloader delivery is available only when all of these are already true:

This is not part of normal automation. Bootloader catalog rows are labelled bootloader; even a capable node never autofetches or autoinstalls them, and ordinary ota install rejects them in every application backend. The tools/lora_ota deployment runner also refuses v3 packages. Fetch and arm one only with the explicit flow:

ota ls\nota pull <MID8> flash\n# wait for ota status to report the same bootloader download as ready\nota bootloader\n# copy the exact mid= and hash= values printed above\nota bootloader install <MID8> <HASH16>\n

The pull command is only transport intent; it does not authorize an install. The final command must reproduce both the complete staged manifest ID and the first eight bytes of its signed image hash. Before approval the application again checks package/root/payload/image hashes, Ed25519 signature and trusted allowlist, exact signed XIAO_BL_... ID, installed and incoming embedded manifest/CRC/name/board identity, vector table, ABI-3 QSPI+boot-update marker, and the adjacent CRC-covered BLM2/SOFT continuity extension at the exact final-image offset 0x9FB4. The embedded boot version must equal the signed outer package version, the SoftDevice family/FWID/application base/layout ABI must match the running platform, and a successor to installed BLM2 must be strictly newer. Low-byte-zero and all-ones versions are invalid. Remote rollback is refused; use local DFU/SWD.

After the reply drains, GPREGRET 0x6B and GPREGRET2 0x51 enter the special OTAFIX path. APRV carries the app's signature/allowlist and explicit operator authorization decision. OTAFIX independently rechecks the strict structure, identity/capabilities, vectors, payload SHA, embedded CRC, and copy hashes before using the reserved scratch bank to replace its own 0xF4000..0xFE000 region. The running application remains preserved; a rejected candidate returns to it unchanged. Post-reboot ota status reports bootloader-update diagnostics as blup:C0 to blup:CF (blup:C8 is success), separately from ordinary application blrc.

This feature cannot update a stock or old bootloader that lacks the capability marker: perform the first exact-board combined bootloader+SoftDevice migration over USB/BLE DFU or SWD. Do not attempt to bootstrap it with a v3 package.

"},{"location":"ota_nrf52_qspi/#storage-ownership-and-recovery","title":"Storage ownership and recovery","text":"

The QSPI store is raw, not a file inside LittleFS. It owns the flash from offset zero, erases 4 KiB sectors as blocks arrive, writes data before progress metadata, and verifies every programmed page. A previously interrupted download is reopened only when its header and trailer are valid; every claimed block is re-hashed before it is trusted. Between a probe, transfer operation, or checkpoint, firmware puts the NOR into deep power-down, deactivates the nRF QSPI peripheral, and turns off a board-provided flash power-enable pin. The next operation powers the chip, shifts the NOR's 0xAB wake command over GPIO, then activates the nRF QSPI peripheral and identifies the flash. Waking it before peripheral activation is required because a flash in deep power-down ignores the activation traffic itself. Merely running ota self therefore does not leave QSPI drawing active-mode current or prevent the following operation from reactivating it.

Installing a QSPI repeater build over a former companion build therefore repurposes the external flash and destroys companion filesystem data as OTA sectors are written. Back up anything important first. Returning to a companion build may require formatting its external data store.

Before changing internal application flash, the application verifies package integrity, hardware identity, signature policy, and bootloader capabilities. The target ID selects discovery/fetch routing and can be deliberately overridden; it is not a second hardware gate. The bootloader then verifies a full payload before its first application erase. For a delta, the application rejects invalid detools geometry before approval and the bootloader independently repeats the base and geometry checks before applying it. It clears the one-shot approval marker before invalidating the running image. If power is lost after application writes begin, the bank remains invalid and OTAFIX enters USB/BLE recovery rather than booting a partial image.

See Easy firmware updates over LoRa for the transfer commands and the OTA protocol for the container and handoff details.

"},{"location":"ota_nrf52_qspi/#experimental-rak15002-sd-archive-source-only","title":"Experimental RAK15002 SD archive (source-only)","text":"

RAK_4631_repeater_rak15002_sd_seeder_only is an optional RAK4631 repeater with a RAK15002 in the base board's IO slot. It keeps the reduced sensor/GPS, OLED and BLE recovery features. It automatically caches up to 16 complete peer .mota files on SD and serves them after reboot; the card also retains interrupted captures for verified resume. It is not an ordinary OTA receiver and is excluded from automatic Full/Reduced receiver releases. This profile has host regression/build checks but has not been physically tested with a RAK15002.

The official RAK15002 datasheet specifies connector pins 25/26/27/28 for CS/SCK/MISO/MOSI, pin 38 for active-low card detection (IO6), and pin 5 for unswitched 3V3. On the RAK4631 these SPI signals map to Arduino GPIO 26/3/29/30. A separate SPIM2 object at 4 MHz accesses SD; global SPI/SPIM3 remains dedicated to the radio. This exact SD module does not need the GPS-only switched sensor rail held on. The card's 3V3 supply is not software-switchable, so its idle current remains a hardware consideration. Card detection is not driven by this profile; absent or failed media reports an archive error and is retried without blocking USB startup.

Do not combine this profile with RAK13800 Ethernet, RAK15001, or another external SPI/QSPI module. Compile-time guards reject the known conflicting firmware combinations; software cannot identify every physically connected module. Use a FAT/exFAT card supported by the pinned SdFat library. Existing files outside /mota are left alone; automatic capture reserves 8 MiB free space for manual staging/work rather than filling the card.

Use ota cache for status, ota cache off to stop new captures (existing files still seed), ota cache on to resume, and ota announce to advertise. Source-only means ota install, bootloader installation, and ota dev apply are disabled even if the installed bootloader supports SD. No RAK15002 SD bootloader handoff is claimed. Update this trial firmware over USB/BLE; normal RAK4631 receiver targets and MeshTower SD self-update are unchanged.

"},{"location":"ota_nrf52_qspi/#optional-rak15002-storage-in-canonical-rak4631-images","title":"Optional RAK15002 storage in canonical RAK4631 images","text":"

This is a separate STORE path, not an expansion of the older RAK_4631_repeater_rak15002_sd_seeder_only CACHE-only experiment above. It is limited to the canonical RAK4631 Full/Reduced LoRa-OTA receiver roles (repeater, room server, Ethernet room server, and sensor) and to RAK_4631_companion_radio_full. Existing exact-NOR identities, non-OTA roles, and the CACHE-only seeder retain their previous storage contracts.

The receiver and Full Companion use the same optional card hardware, but not the same update authority:

Image type RAK15002 role Local firmware install from that card Canonical RAK4631 receiver Adaptive application staging, plus the retained mOTA archive/source Only a trusted, signed application package, after bootloader capability checks RAK_4631_companion_radio_full Cache and serve peer .mota files Never; it remains a source-only Companion RAK_4631_repeater_rak15002_sd_seeder_only Historical strict cache/source-only profile Never

The receiver chooses the card only during real OTA/cache work, not at boot or while merely reporting status. With no card it retains its existing qualified internal/NOR selection. With a card fitted, the firmware deliberately does not probe NOR as a fallback, even if the card, filesystem, or installed loader is unsuitable: both devices use CS26, so issuing a NOR probe could clock the card. RAK15002 card-detect is active-low WisBlock IO6 (Arduino pin 10), not Arduino pin 6; the SD bus is SPIM2 at 4 MHz on 29/3/30 with CS26.

The new STORE path accepts FAT16/FAT32 only. It deliberately uses the smaller FAT-only SdFat types to fit the RAK4631 flash budget and reduce its RAM use; an exFAT card is not a supported STORE volume. This restriction does not change the historical CACHE-only RAK15002 seeder or MeshTower SD profiles, which retain their existing SdFs/FsFile (FAT/exFAT) behavior.

"},{"location":"ota_nrf52_qspi/#future-receiver-loader-prerequisite-and-update-flow","title":"Future receiver loader prerequisite and update flow","text":"

The matching wiscore_rak4631_auto all-in-one OTAFIX bootloader is only a local candidate today: it is not released and has not been hardware-qualified. Do not provision it as an SD-apply upgrade. Until a released, qualified loader explicitly advertises the supplemental authenticated RAK15002-SD application capability, the receiver fails SD application apply closed and continues to use its qualified internal/NOR path when no card is selected.

Once such a loader is released, it must preserve the deployed internal/NOR capabilities while adding the separate SD application handoff record. A stock, older, or otherwise ordinary RAK4631 OTAFIX bootloader must refuse the SD handoff. Its first migration will need to be performed locally over USB/BLE DFU or SWD; it must not be bootstrapped from the SD card or with a LoRa bootloader package.

The supplemental capability is application-only: an SD card never authorizes a bootloader update. Existing qualified internal bootloader-update behavior is separate and still applies only to its exact package and loader identity. The Full Companion's archive/source feature never installs firmware locally, so it does not need this SD-apply bootloader capability.

Only after that future matching loader is released, installed locally, and qualified may a receiver use the normal explicit application flow:

ota status\nota ls\nota pull <MID8> flash\n# wait for `ota status` to show the same fetch as ready\nota install\n

ota cache manages the card-backed archive/source (ota cache off stops new captures; retained files can still seed). ota storage and ota self report the selected backend and loader state, but they intentionally do not force a removable-media probe at startup. After the future loader is present, a card-backed receiver reports SD store and authenticated SD apply OK; before then, a missing supplemental capability, unsigned application, incompatible media, or a busy store fails before reboot/approval.

"},{"location":"ota_nrf52_qspi/#rak13800-ethernet-ownership","title":"RAK13800 Ethernet ownership","text":"

The SD-capable Ethernet-room receiver RAK_4631_room_server_ethernet_lora_ota_no_external_sensors is also an explicit one-at-a-time profile. Its RAK13800 Ethernet starts off on every reboot and is not a saved setting. With the card removed and no active/cache, staged, serving, or NOR work, enable it from local USB or an existing Telnet session with either command spelling:

eth on\n# or: set eth on\neth.status\neth off\n

eth on/set eth on refuses a fitted card, retained NOR owner, or busy OTA state. eth off/set eth off is cooperative and likewise refuses to release the bus while OTA work is active. Do not use these commands as a way to switch media during a transfer.

"},{"location":"ota_nrf52_qspi/#full-companion","title":"Full Companion","text":"

The Full Companion is deliberately different: its optional RAK15002 holds peer archives for caching and serving, never its own install image. It may also compile RAK13800 Ethernet, but Ethernet and the card electrically share CS26 and cannot operate together. In this combined profile physical Ethernet starts off and set eth on is only a this-boot choice.

To use Ethernet, stop/cancel any active OTA capture or serve work, detach any host folder source, physically remove the SD card, then run:

get eth\nset eth on\n

The command refuses unsafe ownership states rather than forcing the bus. To return to the card, run set eth off, wait for it to report off, then insert the card and use get sdcard [free] or ota cache [on|off]. If a card is inserted while Ethernet is active, Ethernet fails closed; use set eth off before accessing the card. This is not a supported hot-swap procedure.

The combined RAK15002 receiver/Companion/RAK13800 path has host-side checks, but has not yet received hardware qualification. Do not treat these rules as proof that a particular card, Ethernet module, loader migration, or live card insertion has been field-tested.

"},{"location":"ota_protocol/","title":"Ota protocol","text":"

View this page on MeshCore Docs.

"},{"location":"ota_protocol/#meshcore-ota-mota-container-lora-protocol","title":"MeshCore OTA - .mota container & LoRa protocol","text":"

This is the single source of truth for MeshCore's over-the-air firmware update system (\"mOTA\"). It is written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool) in another codebase or project. Everything below is implemented in this repository and covered by host, simulation, build, or hardware tests as noted in the relevant section. Hardware qualification is target- and chain-specific; do not infer it from implementation alone. Where a section names a source file, that file is the authoritative reference for byte-level details.

Just want to update your node? See the plain-language OTA user guide - this document is the technical/wire specification.

Design goals

Source map (all under src/helpers/ota/ unless noted)

Concern File Constants, enums, flags OtaFormat.h Container/manifest parse MotaContainer.{h,cpp} Merkle tree + proofs MerkleTree.{h,cpp} EndF self-identity FirmwareInfo.{h,cpp} Wire message codec OtaProtocol.{h,cpp} Negotiated direct KISS framing / build policy OtaDirectWire.h, OtaDirectConfig.h Session engine (serve+fetch+discovery) OtaManager.{h,cpp} Multi-mota / folder relay OtaSource.h, MotaSourceSerial.{h,cpp}, MotaSeederProto.h Staging stores OtaStore.h, OtaStoreFlashNrf52.*, OtaStoreFlashEsp32.* Apply OtaApply.*, bootloader Adafruit_nRF52_Bootloader_OTAFIX Device glue (CLI/context) OtaCli.cpp, OtaContext.h Host tooling motatool (standalone Rust CLI: build/verify/inspect/serve); tools/mota/ (Python reference lib motalib.py + build/test glue)"},{"location":"ota_protocol/#1-conventions","title":"1. Conventions","text":"

Reference constants (OtaFormat.h):

Name Value ASCII / note Container MAGIC 6D 4F 54 41 mOTA Container TRAILER 76 6B 34 39 36 vk496 EndF marker 45 6E 64 46 EndF hash_algo (sha2-256) 0x12 multihash code application format_ver 0x02 ordinary full/delta application package bootloader format_ver 0x03 privileged exact-identity nRF52 bootloader package only approval = not approved FF FF FF FF erased NOR word approval = approved 41 50 52 56 APRV MFLAG_FULL 0x01 flags bit0 MFLAG_SIGNED 0x02 flags bit1 MFLAG_BOOTLOADER 0x04 flags bit2; valid only in the exact v3 bootloader profile CODEC_FULL / _SEQUENTIAL / _INPLACE 0 / 1 / 2 Section 5 PAYLOAD_TYPE_OTA 0x0C MeshCore packet type (src/Packet.h) MAX_PACKET_PAYLOAD 184 ordinary routed packet payload (src/MeshCore.h); direct KISS exception: Section 8.5.1 Default application block size 2048 block_size_log2 = 0x0B; deployed 1 KiB application packages remain accepted Bootloader-package block size 1024 fixed block_size_log2 = 0x0A for format 3 OTA discovery TX priority 250 background (OTA_TX_PRIORITY, src/Mesh.h) OTA active-transfer TX priority 0 primary (OTA_TRANSFER_TX_PRIORITY, src/Mesh.h)

Current application firmware publishes its receive/reassembly ceiling as the compact maxblk:2048 field near the front of both ota status and ota self. Keeping the field ahead of variable diagnostics ensures it survives the 160-byte remote-admin reply limit. Host updaters must treat an absent field as the deployed legacy capability (1024), reject a ready package whose manifest block size exceeds that capability, and use the reported size when turning a raw firmware image into a new application package. This capability does not change the format-3 bootloader package's fixed 1 KiB geometry.

"},{"location":"ota_protocol/#2-firmware-image-the-endf-trailer","title":"2. Firmware image & the EndF trailer","text":"

Every OTA-capable build appends a fixed 56-byte EndF trailer to its flashed image so a running node can discover its own size and self-describing identity on any MCU (no linker symbols needed). Every field is always present at a constant offset. Implemented by FirmwareInfo.cpp; appended at build time by tools/mota/pio_endf.py (post-build hook).

flashed image = BODY (image bytes) || EndF trailer\nEndF trailer (fixed 56 bytes):\n  off 0   4   \"EndF\"        45 6E 64 46\n  off 4   4   body_len      uint32 LE - length of BODY (excludes the whole trailer)\n  off 8   8   body_hash     sha2-256:8 of BODY\n  off 16  4   fw_version    uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre  (0 = unknown)\n  off 20  4   target_id     uint32 LE - sha2-256:4(pio_env): hardware + role + partition (fetch routing)\n  off 24  32  hw_id         NUL-padded ASCII hardware tag (brick-safety), e.g. \"RAK4631\" (\"\" = unknown)\n

The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).

"},{"location":"ota_protocol/#esp32-application-slot-profiles","title":"ESP32 application-slot profiles","text":"

ESP32 Companion firmware is exempt from the portable-slot limit. When an exact Full recipe exists, one expanded-partition image supplies USB, BLE, WiFi, source-only LoRa OTA, and persistent USB logging instead of separate transport artifacts. A small set of high-capacity, non-PSRAM classic ESP32 companions use 100 contacts, 8 group channels, and a 16-frame offline queue in that combined image to preserve internal-DRAM headroom. MQTT observers and ESP-NOW bridges always use FULL builds because fitting them into the legacy slot would require removing CLI and role features. Except for those FULL roles and the ESP32-C6 case below, non-companion ESP32 artifacts, including room, sensor, and repeater roles, must fit the legacy slot from 0x10000 up to 0x150000 (0x140000, 1,310,720 bytes), including the 56-byte EndF trailer. The build checks both that limit and the target's actual app partition. The ESP32-C6 no_external_sensors OTA siblings are the narrow exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app partition. ESP32 room servers and sensors use their expanded Full image for LoRa OTA. For standalone repeater and room-server roles, and for nRF52 sensors that need a lean staging profile, build.sh exposes an explicit *_lora_ota_no_external_sensors artifact: the ordinary role remains sensor-enabled, while that sibling trims selected optional environmental/ranging drivers for LoRa distribution. SolarXiao 30S and 33S use matched external QSPI staging. Their repeaters keep the existing OTA target; their room servers use an explicit *_room_server_lora_ota image with the matching OTAFIX QSPI bootloader. Integrated GPS and other board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The legacy suffix describes a driver trim, not removal of the generic I2C bus. Reduced RAK3401 and RAK4631 profiles retain INA219, INA226, INA260, and INA3221 voltage/current monitors. These are retained optional-sensor-table drivers, not the only I2C consumers: SSD1306 display, supported autodiscovered RTCs, and RAK12500 GPS remain separate I2C peripherals where selected by the board recipe. The RAK3401 OTA repeater also retains RAK12500 I2C and RAK12501/L76K UART GPS support; install either GPS module in sensor slot A because slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The plain RAK4631 OTA repeater and its Serial2 bridge retain GPS. RAK12501 uses Serial1. Its explicitly compiled Serial1 bridge therefore omits the combined GPS provider, including RAK12500, even though RAK12500 itself does not use the UART.

The firmware-configured INA3221 address and RAK12500 address are both 0x42, so those devices cannot share one bus at those addresses. Keep RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use firmware built with -DTELEM_INA3221_ADDRESS=0x43 when both are installed.

ESP32 siblings retain the compact browser WiFi updater and use up to 254 neighbors. Internal-DRAM-constrained targets use 50 as recorded in the artifact capability manifest; see ESP32 memory budget. RP2040 and STM32 targets are not offered because those platforms do not yet have a safe bootloader/apply path.

nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default -Ofast. This keeps the runtime software Ed25519 fallback from being expanded into tens of kilobytes of repeated curve arithmetic while retaining CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features.

WiFi-heavy non-companion roles are not reduced to fit the legacy application slot. build.sh automatically promotes every ESP32 MQTT observer and ESP-NOW bridge to the expanded FULL partition profile. These artifacts retain the complete role CLI, WebConfig where supported, display and optional sensor support, full timezone and TLS behavior, and the board's normal power-management implementation. The compact CLI is not compiled into any build. Ordinary repeater and room-server builds remain sensor-enabled; only explicitly named *_lora_ota_no_external_sensors siblings trim selected optional environmental/ranging drivers for LoRa distribution, and those siblings retain the complete CLI and target-declared I2C peripherals.

MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset during a settings save restores the last committed common preference image or publishes the completed new image; it does not leave a partially written /com_prefs file to fail on the next boot. A truncated legacy image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.

Option 3 in build.sh emits one *-full-usb-wifi-ota-* ESP32 artifact for each FULL-capable non-companion hardware/role that has a matching MQTT environment. It compiles USB packet logging and direct WiFi MQTT together. A *-full-logging-ota-* fallback is emitted only when there is no MQTT sibling; ordinary non-OTA roles compile runtime USB logging into their canonical image, so separate standard-logging artifacts are not emitted. Canonical RAK3401/RAK4631 repeater, room and sensor OTA pairs, and Tower SD repeater/room pairs, retain runtime USB packet logging in both sensor variants too. Verbose debug is off by default; packaged logging.usb.packets and logging.usb.control capability checks prevent a stale no-logger image from satisfying a resumed build. Specialized storage/transport aliases and other constrained OTA targets keep their existing logging policy. Non-MQTT FULL twins are also skipped for covered ESP32 roles. MQTT observers and ESP-NOW bridges are emitted only with expanded FULL partitions. Menu option 8, or build-full-esp32-firmwares, builds the unified profiles plus necessary fallbacks. Menu option 9, or build-full-esp32-logging-firmwares, builds only those fallbacks. FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature set, full ElegantOTA where that target declares the required library, and LoRa OTA for every included role, including room servers, sensors, observers, and bridges. They use expanded A/B partition tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB and 16 MiB boards. Explicit *_lora_ota_no_external_sensors targets are not duplicated; their ordinary repeater or room-server build is the FULL, sensor-enabled counterpart. The *-full-usb-wifi-ota-* profile enables USB packet logging and MQTT, with a persistent logging.output selector; its verbose internal debug remains off. The fallback *-full-logging-ota-* profile enables USB debug and packet logging and has no MQTT target. Install a matching *-full-usb-wifi-ota-*-merged.bin or *-full-logging-ota-*-merged.bin over USB once to write the expanded partition table. After that, its matching non-merged FULL application image can be installed through USB, WiFi OTA, or LoRa OTA. Do not install a non-merged FULL image onto a node that still has its old partition table.

Implementer note: the bootloader (and any non-Arduino consumer) MUST locate the body extent by scanning for EndF, never by trusting a stored size - see the bootloader contract in Section 12.

"},{"location":"ota_protocol/#3-the-mota-container","title":"3. The .mota container","text":"

The distributed form (host-built, wire-transferred). Parsed by mota_parse() in MotaContainer.cpp.

off            size   field\n0              4      MAGIC = 6D 4F 54 41\n4              4      MOTA_TOTAL_SIZE  uint32 LE - total container bytes (incl. manifest, leaves[],\n                                       payload, trailer). Lets a node pre-reserve staging and compute\n                                       write_start = staging_region_end - MOTA_TOTAL_SIZE.\n8              M      MANIFEST         (Section 4; M = 197 fixed + leaves[], 4*BC; no length field - BC from payload_size)\n8 + M          P      PAYLOAD          (payload_size bytes; delta or full image)\n8 + M + P      5      TRAILER = 76 6B 34 39 36\n

MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5. The manifest M includes leaves[]; the manifest-minus-leaves prefix (mfl, sent over the wire as OTA_MANIFEST) is [8, leaves_off).

Staged (in-flash) form. Written bottom-aligned so TRAILER ends at staging_region_end. Identical bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the leaves[] slots (filled as blocks arrive - Section 7) and the 4-byte approval field (on owner consent - Section 4.2). Everything else is immutable.

"},{"location":"ota_protocol/#4-the-manifest","title":"4. The manifest","text":"

Fixed layout. Every field sits at a constant offset and is always present - base_hash, signer_pubkey and signature are zero-filled when not applicable (a full image / an unsigned container). Only leaves[] is variable (one 4-byte hash per block). So the manifest-minus-leaves (mfl) is always 197 bytes and the parser is plain offset reads - no conditionals. Parsed by mota_parse_manifest().

off  size   field            notes\n0    1      format_ver       = 0x02 application, or 0x03 privileged bootloader package\n1    1      flags            bit0 FULL; bit1 SIGNED; bit2 BOOTLOADER; bits3-7 reserved 0\n2    1      hash_algo        0x12 = sha2-256\n3    4      target_id        device/arch/role discriminator (Section 9)\n7    4      fw_version       MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre   (comparable uint32)\n11   4      image_size       size of the reconstructed image (BODY||EndF)\n15   4      payload_size     PAYLOAD bytes in this container\n19   1      block_size_log2  e.g. 0x0B = 2048 (new application default); 0x0A = deployed 1024\n20   4      merkle_root      sha2-256:4 over PAYLOAD blocks (Section 6) - also the manifest_id\n24   32     image_hash       sha2-256:32 of the reconstructed image - SECURITY anchor\n56   1      codec_id         0=full/raw, 1=detools-sequential, 2=detools-in-place\n57   32     hw_id            NUL-padded ASCII hardware tag (e.g. \"RAK4631\"); same tag => bootable-compatible.\n                             SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.\n89   8      base_hash        sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.\n97   32     signer_pubkey    Ed25519 public key. 0 if not SIGNED.\n129  64     signature        Ed25519 over manifest[0, 129). 0 if not SIGNED.\n193  4      approval         FF FF FF FF = not approved; 41 50 52 56 (\"APRV\") = approved\n--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---\n197  4*BC   leaves[]         BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)\n

The signature always covers manifest[0, 129) (the head + base_hash + signer_pubkey). approval is outside the signed region so it can be flipped in place on consent without breaking the signature.

Manifest-minus-leaves size (mfl) is a constant 197 bytes for every container (full or delta, signed or unsigned). At 197 bytes the manifest exceeds one packet, so OTA_MANIFEST is always sent multi-fragment (Section 8.4, 2 fragments) and reassembled by the fetcher.

The two versions are deliberately disjoint. Version 2 accepts application packages only and rejects the BOOTLOADER bit. Version 3 accepts only flags exactly FULL|SIGNED|BOOTLOADER; a non-bootloader v3 package is invalid. Consequently, deployed v2-only application parsers reject a bootloader package before they can mistake its raw 40 KiB payload for an application image.

"},{"location":"ota_protocol/#41-signed-region","title":"4.1 Signed region","text":"

signature covers manifest bytes [0, 129) - the head + base_hash + signer_pubkey. It does not cover approval or leaves[]:

"},{"location":"ota_protocol/#42-the-approval-field","title":"4.2 The approval field","text":""},{"location":"ota_protocol/#43-privileged-nrf52-bootloader-package-profile","title":"4.3 Privileged nRF52 bootloader package profile","text":"

A v3 bootloader package has a deliberately narrow, non-extensible profile:

The incoming embedded identity must exactly match the installed CRC-valid bootloader identity. Both scans consider every aligned structurally valid candidate so magic bytes in a literal pool cannot shadow the real manifest. Duplicate accounting counts each CRC-valid 44-byte base record before interpreting adjacent continuity metadata, so a corrupt or half-present BLM2 extension cannot hide a second identity; after exactly one base record is selected, malformed claimed continuity fails closed. A package must be signed by a key already in the device's trusted allowlist; unlike ordinary application packages, there is no unsigned manual-install exception. The signed outer fw_version must equal the embedded boot version. Qualified internal/QSPI targets may bootstrap a CRC-valid legacy-v1 installed bootloader once; MeshTower SD instead requires local BLM2 provisioning because it has no safe legacy media handoff. After bootstrap every remote successor must be strictly newer and match the live SoftDevice/application layout. Low-byte zero and all-ones boot versions are invalid. Remote rollback has no override and must use local DFU/SWD.

"},{"location":"ota_protocol/#5-payload-codecs-delta-base","title":"5. Payload, codecs & delta base","text":"

PAYLOAD is either the full reconstructed image (FULL) or a delta (!FULL).

codec_id Meaning Used by 0 full / raw PAYLOAD = reconstructed image (BODY||EndF). ESP32 A/B or an external SD/QSPI nRF52 target. 1 detools sequential random read of base + sequential write of result -> ESP32 A->B inactive slot. 2 detools in-place bounded scratch; rewrites the app region in place -> nRF52 single-slot.

For deltas, base_hash = the base build's EndF.body_hash (sha2-256:8 of its BODY). A node applies a delta only if base_hash matches its own EndF.body_hash. After applying, the result MUST hash (sha2-256:32) to image_hash before it is booted - the hard security gate.

A fetcher only requests firmware it can apply. Each node declares the codec(s) it can apply (set_apply_codec/set_apply_codec2): ESP32 accepts full + sequential (+ in-place). Internal-staging nRF52 targets accept only in-place because internal flash cannot hold a second full application image. Matched SD and raw-QSPI nRF52 targets accept full + in-place because external media holds the container. A .mota with an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an external folder may accept other codecs because that path captures bytes and never installs them.

Compression is internal to the detools patch and must be supported by the applier. Patches are produced by detools 0.53.0 (tools/mota -> detools.create_patch) and decoded on-device by detools' embeddable C decoder, vendored verbatim at src/helpers/ota/detools/ (see its README.meshcore.txt). That build enables only the self-contained NONE + CRLE compressions (no malloc/liblzma/heatshrink), so MeshCore deltas use --compression crle. Do not reimplement the codec - use the vendored decoder.

"},{"location":"ota_protocol/#6-merkle-tree-sha2-2564","title":"6. Merkle tree (sha2-256:4)","text":"

Verifies each PAYLOAD block against the signed merkle_root before the whole payload exists, so corruption/forgery is localized to a block. Implemented in MerkleTree.cpp.

"},{"location":"ota_protocol/#61-proofs","title":"6.1 Proofs","text":"

A proof for block i is the ordered list of sibling digests from leaf to root. Promoted levels contribute no element. Verification (needs BC to know the tree shape):

h = leaf_i ; idx = i ; n = BC ; p = 0\nwhile n > 1:\n    if (n is odd) and (idx == n-1):        # this node was promoted\n        pass\n    else:\n        sib, side = proof[p] ; p += 1\n        h = sha2-256:4( sib || h ) if side==left else sha2-256:4( h || sib )\n    idx //= 2 ; n = (n + 1) // 2\naccept iff h == merkle_root and p == len(proof)\n

Over LoRa, leaves[] are omitted from the manifest transfer. A serving node computes a block's proof on demand from its stored leaves[] and normally sends OTA_PROOF immediately after that block's paced OTA_DATA. OTA_REQ_PROOF remains the fallback for an older source or a lost proactive proof. The fetcher fills its own leaves[i] as each verified block lands.

"},{"location":"ota_protocol/#7-block-availability-staging-resume","title":"7. Block availability, staging & resume","text":"

There is no separate availability structure. Block i is present <=> leaves[i] is non-erased (!= FF FF FF FF). Because leaves[] live in the staged flash region, availability survives reboot.

Commit order per block (crash-safe): (1) verify proof, (2) write block payload to its offset, (3) write leaves[i] last. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched (idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning leaves[] for a persistent, reopenable store. A hybrid nRF52 transfer is the deliberate exception: its SRAM-backed payload suffix is volatile, so the application refuses to adopt that staged header instead of rebuilding partial progress.

Resume (OtaManager::resumeStaged + OtaStore::checkpoint/reopenFor): an interrupted fetch resumes from the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress every N committed blocks) is runtime-tunable (ota config checkpoint <N>, 0 = only finalized containers resume). Boot-time adoption is an automatic fetch decision: current autofetch must be enabled, the stored target must equal the node target, policy signed requires the signed bit, and an enabled running-version floor requires a strictly newer manifest. An explicit MID pull may deliberately resume an older or unsigned package and keeps target 0 as a MID-only wildcard. Stores keep leaves[] in RAM until flush and never auto-GC, preserving resumable progress. The debug/operator equivalent is ota dev resume <MID8>; after a reboot the MID is mandatory, while a no-argument form may only reuse a still-active session MID. It never uses the nullptr automatic-adoption path, so a malformed MID or no active MID fails closed. The CLI refuses resume while an application or bootloader update is armed, before reopening storage can reset the approved handoff state. Raw ESP32/nRF52 staging scans every valid header before adoption: address order is not creation order. Explicit resume selects a unique matching MID/target; automatic resume refuses multiple valid headers without erasing them. A leaf-marker read error is never treated as a missing block: reconnectable storage pauses and re-verifies after reconnection, while other storage reports failure and permits an explicit retry. Hybrid nRF52 staging cannot enter this resume path after an application restart, even if its flash prefix still contains metadata; the complete logical container must be fetched again.

Flash-store note (RX-safe writes): a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa RX, so the flash stores (OtaStoreFlashNrf52/OtaStoreFlashEsp32) coalesce writes to the erase unit (4 KB page / sector) and commit each once off the per-packet path. Ordinary stores keep RAM at O(one page), not O(image); a qualified hybrid nRF52 profile additionally reserves one fixed 64 KiB staging arena. A small delta that fits page 0 does zero flash I/O until COMPLETE.

"},{"location":"ota_protocol/#8-lora-ota-protocol","title":"8. LoRa OTA protocol","text":"

Carried in MeshCore packets with PAYLOAD_TYPE_OTA = 0x0C. Every OTA packet payload is:

[0]    ota_msg_type      (OtaMsgType, OtaFormat.h)\n[1..]  body              (fixed per type; encode/decode in OtaProtocol.cpp)\n

Message types:

ota_msg_type val routing purpose OTA_ADV 0x01 discovery tiny per-node beacon (discovery tier 1) OTA_QUERY 0x02 discovery ask a source for its catalog (discovery tier 2) OTA_HAVE 0x03 discovery the catalog reply (fragmented, digest-tagged) OTA_GET_MANIFEST 0x04 transfer request a manifest's fragments (want_mask) by manifest_id OTA_MANIFEST 0x05 transfer the manifest-minus-leaves, fragmented OTA_REQ 0x06 transfer request fragments from an adaptive flight of 1-4 blocks (want_mask per block) OTA_DATA 0x07 transfer one self-describing fragment of a block's data OTA_REQ_PROOF 0x08 transfer request/re-request a missing proof OTA_PROOF 0x09 transfer the merkle proof for one block OTA_GET_LEAVES 0x0A transfer request the target's leaves[] fragments (want_mask) - warm-start only OTA_LEAVES 0x0B transfer a fragment of the leaves[] array (for host-side seed leaf-diff) "},{"location":"ota_protocol/#81-two-tier-discovery","title":"8.1 Two-tier discovery","text":"

Because a node may serve many mOTAs (its own firmware plus an external folder - Section 10), discovery is split so the periodic beacon stays tiny regardless of catalog size:

Tier 1 - OTA_ADV beacon (10 bytes, constant). Flooded as a short burst at boot, then every advert_mins minutes (default 24h; runtime-tunable via ota config advert, 0 disables the periodic re-advertise). It is also emitted immediately whenever the served set changes (e.g. a motatool folder is attached/detached), so peers learn about newly-available firmware without waiting for the next interval:

seeder_id[4]    advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id\nn_motas         uint8 - count of complete servable mOTAs (saturates at 255)\nset_digest[4]   sha2-256:4 over the SORTED set of served manifest_ids (see below)\n

set_digest is a content hash of the offering, not a counter: canonical across nodes, and it changes iff the set of served mids changes. A peer that has already catalogued this {seeder, set_digest} ignores the beacon (steady state is query-free). For a single served mota, set_digest = sha2-256:4(mid).

Tier 2 - OTA_QUERY -> OTA_HAVE (on interest only):

OTA_QUERY  (flood):  seeder_id[4]  set_digest[4]  filter_target(uint32) want_fragments(uint32)\n                     # filter_target 0 = everything; want_fragments 0 = every fragment\nOTA_HAVE   (flood):  seeder_id[4]  set_digest[4]  frag_idx(1) frag_total(1) n_rows(1)  rows[]\n  HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)\n

have_count is the number of blocks the source holds (== block_count for a complete offered image). Receivers do not advertise partial or completed downloads as new sources.

A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as OTA_HAVE rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog complete only after all frag_total fragments arrive. If any are missing after the recovery timeout, it sends another QUERY whose want_fragments bitmap names only the holes. want_fragments is an append-only extension: an original 13-byte QUERY is still accepted and means \"send every fragment.\" The heavy manifest is fetched per-mid only on commit (Section 8.3).

Fragment numbers are canonical pages of the complete catalog sorted by manifest_id. filter_target may remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages or changes frag_total. Receivers derive the unfiltered page count and each page's row count from the source's advertised n_motas. A page counts as received only if both counts match that complete geometry; filtered rows are still cached, but omitted rows leave the page eligible for an unfiltered recovery query. This also prevents legacy filtered replies which renumber pages from completing an unfiltered catalog.

"},{"location":"ota_protocol/#82-anti-storm-mandatory-at-mesh-scale","title":"8.2 Anti-storm (mandatory at mesh scale)","text":"

If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS pattern), all in OtaManager:

Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.

"},{"location":"ota_protocol/#83-fetch-handshake","title":"8.3 Fetch handshake","text":"
fetcher                                   server (any node that has the mid)\n  OTA_GET_MANIFEST(mid, want_mask)  >     (want_mask=0xFFFF first; only missing fragments on retry)\n                               <-------   OTA_MANIFEST(mid, frag_idx, frag_total, bytes)   x requested frags\n  (reassemble manifest, verify, compute geometry: BC, block_size, payload_size)\n  for each adaptive flight of missing blocks (starts at 1, grows on clean flights):\n    OTA_REQ(mid, {block_idx, want_mask}[]) >  (one packet; all fragments first, only holes on recovery)\n                               <-------   OTA_DATA(mid, block_idx, frag_off, data) x requested frags/block\n                               <-------   OTA_PROOF(mid, block_idx, n_proof, proof) x requested blocks\n    (independently reassemble + verify each block, but remain RX-silent until the flight drains)\n    [after adaptive deadline: recover one block's holes, or OTA_REQ_PROOF for a missing proof]\n    (clean flight grows by one block; recovered flight halves the next width)\n  when all blocks present: verify full merkle_root + image_hash -> COMPLETE\n

Before allocating or writing the selected store, the receiver parses the reassembled manifest and requires its merkle_root to equal the requested/wire manifest_id and its target_id to equal the catalog or explicit-pull target that opened the receive slot. The wire envelope and HAVE row are advisory; they cannot label and stage a different manifest.

"},{"location":"ota_protocol/#84-message-bodies-transfer","title":"8.4 Message bodies (transfer)","text":"

All offsets after the 1-byte type. Encoders/decoders in OtaProtocol.cpp; constants in OtaManager.h.

OTA_GET_MANIFEST:  manifest_id[4]  want_mask(uint16)   # bit k = send manifest fragment k; 0xFFFF = all\nOTA_MANIFEST:      manifest_id[4]  frag_idx(1)  frag_total(1)  bytes[]     # up to OTA_MF_FRAG=176 B/frag\nOTA_REQ:           manifest_id[4]  { block_idx(uint16)  want_mask(uint16) }[1..4]\n                   # legacy: bit k requests its 160-byte fragment\n                   # v2: bit15=marker, bit14=allow transport DEFLATE,\n                   #     bit13=2 KiB descriptor, bits0..12=fragment bitmap;\n                   #     bit12 must be zero when bit13 is set (0xFFFF stays legacy)\nOTA_DATA legacy:   manifest_id[4]  block_idx(uint16)  frag_off(uint16)  data[<=160]\nOTA_DATA v2:       manifest_id[4]  block_idx(uint16)  descriptor(uint16)\n                   stream_id[4]  data[<=171]\n                   # deployed 1 KiB descriptor: bit15=marker, bit14=DEFLATE,\n                   #   bits13..10=fragment, bits9..0=complete encoded length minus one\n                   # negotiated 2 KiB descriptor: bit15=marker, bits14..11=fragment,\n                   #   bits10..0=complete encoded length minus one; DEFLATE iff that\n                   #   length is strictly less than the manifest-derived raw block length\nOTA_REQ_PROOF:     manifest_id[4]  block_idx(uint16)\nOTA_PROOF:         manifest_id[4]  block_idx(uint16)  n_proof(1)  proof[]   # n_proof x 4 bytes\nOTA_GET_LEAVES:    manifest_id[4]  want_mask(uint16)   # bit k = send leaves fragment k; 0xFFFF = all\nOTA_LEAVES:        manifest_id[4]  frag_idx(1)  frag_total(1)  bytes[]      # up to OTA_LEAVES_FRAG=176 leaf bytes\n
"},{"location":"ota_protocol/#85-sizing-against-max_packet_payload-184","title":"8.5 Sizing against MAX_PACKET_PAYLOAD = 184","text":"message fixed overhead payload/packet OTA_DATA legacy 9 B (type+mid4+idx2+off2) OTA_FRAG_DATA = 160 -> 7 raw frags/1 KiB or 13/2 KiB OTA_DATA v2 13 B (legacy header + stream ID) OTA_FRAG_DATA_V2 = 171 -> 6 raw frags/1 KiB or 12/2 KiB; fewer when compressed OTA_MANIFEST 7 B OTA_MF_FRAG = 176 -> signed manifest ~ 2 frags OTA_HAVE 12 B 10 rows x 16 B per fragment OTA_PROOF 8 B up to ~44 sibling digests (>> any real tree)

A served mota supports up to 1024 leaves in the default 4 KiB proof scratch (about 2 MiB of payload at the new 2 KiB default, or 1 MiB for a deployed 1 KiB container); larger self-images pass a bigger scratch buffer.

"},{"location":"ota_protocol/#851-negotiated-zero-hop-kissraw-radio-transport","title":"8.5.1 Negotiated zero-hop KISS/raw-radio transport","text":"

OtaDirectWire.h defines an opt-in direct transport which bypasses the normal 184-byte Packet payload without changing Packet layout or enlarging its pool:

RF frame = F2 00 || ota_msg_type || existing OTA message body\n           2 B    <----------- at most 253 B ----------->\n

F2 selects reserved MeshCore version 4, PAYLOAD_TYPE_OTA, and direct routing; old MeshCore parsers reject that version. The second byte MUST be zero: no path, transport codes, relay envelope, or Ethernet header is permitted. Maximum RF length is 255 bytes; v2 DATA retains its 9-byte OTA header and 4-byte stream ID, leaving 240 data bytes. Thus raw 1 KiB/2 KiB blocks need five/nine DATA frames. Only transfer opcodes 0x04..0x0B are legal; discovery ADV/QUERY/HAVE MUST retain ordinary framing. V2 descriptor bits, compression permission and stream identity are unchanged. The outer framing, not a new opcode or request-mask bit, selects 240-byte geometry.

A receiver MUST first validate a manifest obtained through ordinary OTA framing. It MAY probe direct mode only when ota config hops is zero, that manifest was observed at zero relay hops, v2 is usable, and the original temporary-radio physical session/profile generation remains valid. Direct responses MUST match that fetch's manifest, transport geometry and physical session. If direct support has not yet been confirmed, the first adaptive deadline without direct DATA MUST invalidate that probe and retry ordinary 171-byte v2 framing on the original session. After confirmation, a wholly empty later flight gets one admitted same-mode sparse retry before that downgrade. Local queue rejection MUST NOT consume the retry, and valid direct DATA resets its empty-flight evidence. The existing 160-byte legacy fallback remains available afterward. Reassembly for uncommitted blocks is reset when geometry changes; already proof-verified committed blocks remain staged. Missing-fragment masks continue to drive recovery.

Endpoints MUST consume or reject this reserved envelope before normal Packet allocation, raw logs and mesh bridges. They MUST NOT relay it or reinterpret it as routed OTA; malformed, nonzero-path and unsolicited traffic cannot create an OTA context. A shared bounded raw-radio TX owner retains the selected profile generation and the OTA cancellation epoch until actual TX completion. It MUST honor carrier sensing, duty/airtime accounting and adaptive pacing, and discard pending work after cancellation, profile replacement or temporary-session expiry. Direct response trains permit the proof immediately after the final completed DATA frame; adaptive sub-1.0 quiet gaps still apply. KISS queue acceptance is not RF completion, and actual F8 TX_DONE is not receiver delivery acknowledgement.

Manifest signatures, Merkle proofs, encoded-stream checks, staged bytes and explicit install authorization are unchanged. This is a transport optimization, not a new trust or installation path. MESH_ENABLE_OTA_KISS_DIRECT defaults on for ENABLE_OTA builds using the shared raw-radio scheduler, except primary ESP-NOW builds; -DMESH_ENABLE_OTA_KISS_DIRECT=0 explicitly disables it. Enabling the build feature alone does not bypass the runtime negotiation/session gates. The separate host seeder's safe probe/serve examples and lab TX lock are in tools/lora_ota/README.md.

"},{"location":"ota_protocol/#86-temporary-radio-and-transfer-boundary","title":"8.6 Temporary-radio and transfer boundary","text":"

OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while a matching tempradio or tempradio2 window is actually running. Direct transfers remain bound to their original profile and generation; ending or replacing that window does not silently move them to the other radio. A receiver selects missing blocks in serial order into a bounded request flight. Every session starts with one block. A clean completed flight increases the next request by one block; a flight requiring fragment/proof recovery halves the next width (4 -> 2, 3 -> 2, 2 -> 1). The default compiled cap is two blocks; the RAK3401 LoRa-OTA target caps at four, so it probes 1 -> 2 -> 3 -> 4. All rows are sent in one backward-compatible OTA_REQ, and no freed slot is refilled until the current flight is finished. It never serves partial blocks. A normal install receiver never re-advertises its completed download. An SD archive node is the deliberate exception: after a fully proof-verified container is published to its persistent archive, it registers that complete file as a MotaSource and advertises it as a new seeder. This keeps each active transfer as one transmitter and one receiver while still allowing active temporary-radio repeaters between them and persistent archive nodes to improve future availability.

TempRadio is treated as a private maintenance network. Active transfer packets use priority 0, bypass the public-flood receive holdoff, use the full transmit budget without overwriting the saved normal-radio airtime factor, retain the relay role's airtime-scaled transmit collision window, and do not schedule generic flood retries. Deployed firmware predating that TempRadio budget override can be accelerated manually with a saved get af / temporary set af 0 / restore sequence. The bounded serving queue admits at most two DATA/PROOF packets ahead of the radio while preserving at least four free packet-pool entries. CAD remains enabled to arbitrate the half-duplex channel, but its busy retry is scaled to one-quarter of a packet airtime and clamped to 5-50 ms instead of the ordinary 120-360 ms cadence. Discovery traffic keeps collision jitter and background priority. The fetch deadline uses the active radio's measured maximum-packet airtime, remaining DATA/PROOF packet count, dispatcher airtime factor, and longest observed path (falling back to the configured hop horizon before one is observed), with bounded guard time. Faster SF/BW settings therefore recover loss sooner; slower or multi-hop settings do not spuriously re-request a response still on air. These changes remove software waits and duplicate bursts; they do not remove the one required forwarding transmission per hop.

"},{"location":"ota_protocol/#9-identity-trust-versioning","title":"9. Identity, trust & versioning","text":""},{"location":"ota_protocol/#10-multi-mota-serve-the-external-folder-relay","title":"10. Multi-mota serve & the external \"folder\" relay","text":"

A node serves a set of mOTAs: its own firmware plus, optionally, an external folder of .mota files it relays without holding them in flash. To peers it simply \"has N mOTAs\"; the relay is trustless (fetchers verify everything). The serve side (OtaManager) keeps a lightweight registry of what it advertises and two resident \"views\": view0 (its own firmware) and one on-demand view loaded from a source when a request targets an external mota. Every fetch message carries manifest_id, so dispatch is a registry lookup.

The USB/TCP host-folder link can also be a pull destination (the reverse direction): ota pull <mid8> folder fetches a .mota off the mesh and streams it onto the host as <mid>.mota via the seeder STORAGE ops (OP_STAT/BEGIN/WRITE/SREAD/FIN, see MotaSeederProto.h), using a FolderMotaStore as the fetch's OtaStore instead of RAM/flash. This captures an exact copy of a device's firmware - e.g. to build a delta against firmware you don't have. Resume is bookkeeping-free: BEGIN 0xFF-fills the file and, on reconnect after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), STAT+SREAD let the fetcher recompute and refill only the missing blocks. TCP disconnect preserves the selected MID, target, and destination across dynamic-context idle cleanup. Metadata initialization or seed leaf-diff restarts from the manifest on reconnect; an initialized transfer reopens and rehashes the same MID before requesting missing blocks. Host I/O failure during final verification or publication pauses too; a genuine Merkle mismatch remains a failed transfer. Explicit cancel/detach still releases the session, and a different host transport cannot take over a paused capture without cancellation. The phone-oriented BLE link is deliberately source-only and does not register a folder destination.

"},{"location":"ota_protocol/#101-the-motasource-abstraction-otasourceh","title":"10.1 The MotaSource abstraction (OtaSource.h)","text":"

Transport-agnostic provider of one or more complete .mota as random-access bytes. The same serve code drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only read() differs.

struct MotaDesc {                      // catalog metadata + region offsets (no whole image in RAM)\n  uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;\n  uint8_t block_size_log2, source_caps;\n  uint32_t total_size, leaves_off, block_count, payload_off, payload_size;\n};\nclass MotaSource {\n  virtual uint8_t count();                                  // # mOTAs offered\n  virtual bool    describe(uint8_t idx, MotaDesc& out);     // metadata + offsets\n  virtual bool    read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len);   // random-access bytes\n  virtual bool    read_deflated_block(uint8_t idx, uint16_t block,\n                                      uint8_t* buf, uint16_t cap, uint16_t* len); // optional raw RFC 1951\n};\n

Legacy source paths retain the manifest-minus-leaves and four bytes per source block, bounded by OTA_PROOFGEN_SCRATCH, then stream payload blocks on demand. Non-PSRAM ESP32 Full instead retains roots of 32-leaf groups and rereads the selected group. Its complete workspace is bounded to 1,536 bytes while retaining 4,096 blocks and byte-identical proofs. Partial groups use the same odd-node promotion as the full tree. Receive pipelines, capture, serving and wire formats remain unchanged; larger custom leaf limits retain the legacy path.

With OTA_BOUNDED_SOURCE_PROOFS=1, optional read_quantum() and read_metadata() methods bound each metadata step. SerialMotaSource uses the current negotiated single-response capacity and rejects a chunk that no longer fits after a capacity change. Proof preparation yields between chunks, and queued jobs retain their buffers across radio backpressure. See the V3 Full qualification for allocation bounds and regression coverage.

"},{"location":"ota_protocol/#102-the-mota-seeder-transport-motaseederprotoh","title":"10.2 The mota-seeder transport (MotaSeederProto.h)","text":"

A MotaSource is fed by a host that serves a folder over the device's USB serial (the same console the CLI uses - no extra hardware), on an ESP32 WiFi companion or FULL ESP32 role over WiFi (TCP), or on an nRF52 Full Companion over an encrypted BLE GATT service. The host is the standalone Rust tool motatool (motatool serve --serial <port> / --tcp <host[:port]>, which also builds + verifies + inspects .mota). The device only emits request frames while actively serving a fetch, and reads the reply synchronously, so over the shared USB console binary frames coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:

request  (device -> host):  'M' 'S'  op(1)  args...                 xsum(1 = XOR of op+args)\nresponse (host -> device):  'm' 's'  op(1)  status(1)  payload...    xsum(1 = XOR of all prior)\n\nOP_COUNT     0x01   args: -            -> payload: count(1)\nOP_DESCRIBE  0x02   args: idx(1)       -> payload: MotaDesc wire (38 B)\nOP_READ      0x03   args: idx(1) off(4) len(2)  -> payload: len bytes\nOP_DEFLATE_BLOCK 0x09 args: idx(1) block(2) off(2) len(2)\n                         -> payload: total_encoded_len(2) + requested bytes\nMotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)\n                      total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)\n                      block_size_log2(1) source_caps(1) reserved(2)\nstatus: 0 = OK, non-zero = error (out of range / past EOF).\n

SerialMotaSource splits logical reads into replies of at most 192 payload bytes. A manifest's leaf table can exceed 256 bytes and new payload blocks are normally 2 KiB; requesting either in one transaction can overrun common USB CDC/UART receive rings even though the host successfully wrote the complete reply. Chunking is internal to the transport and does not change OP_READ or the MotaSource random-access contract.

Each response uses one deadline, starting after request transmission, for its magic, header, payload and checksum together. The deadline does not restart for each received byte. USB folder sessions use 600 ms; BLE/TCP folder sessions use 3000 ms. Each chunk of a larger logical read has its own deadline. A slow trickle can therefore fail sooner than before; a maximum 197-byte response takes about 17.1 ms on a 115200-baud UART, excluding host processing latency.

A shared USB text/control session explicitly opts into retaining a timed-out response's sync/header/payload/checksum position. No subsequent USB request is sent until that old response reaches its known checksum boundary, or an actual host-session reset clears it. The request supplies the payload length: the response itself carries neither a length nor a request ID. Late binary bytes therefore cannot become USB ota folder off control text. A complete reply followed by that ordinary shutdown line still works. If a software handoff or failed attach leaves a response outstanding, USB input is quarantined until its boundary; commands typed during that wait are discarded. Leaving a log-only USB/network handoff preserves that response for the tracked consumer rather than discarding its tail as raw input. A deliberate USB mOTA entry cancels startup probe and deferred ASCII timers so they cannot reclaim the stream or emit terminal text into an active transfer. If the sender abandons a partial reply, reconnect the USB host session to recover. Bare shutdown text inside a partial payload is inherently ambiguous and is not interpreted as control. This assumes one ordered reply per request; unsolicited duplicate or reordered binary replies cannot be unambiguously separated by this legacy wire format.

Dedicated UART, BLE and TCP sources retain their legacy retry/stale-drain behavior; their absolute response deadline still applies to every request.

OP_DEFLATE_BLOCK lets a host-folder source perform the optional transport compression without linking an encoder into the embedded seeder. len=0, off=0 queries the exact encoded length; subsequent chunks are at most 190 bytes, keeping the total response payload at 192 bytes. The host independently encodes each manifest payload block as ordinary raw RFC 1951 at level 9. It returns an error for an invalid range, an unsupported operation, or a result that is not smaller than the raw block; SerialMotaSource then serves the ordinary raw v2 representation. A supporting host sets source_caps bit 0 in every descriptor. Older hosts leave that formerly-reserved byte zero, so upgraded firmware skips the optional request instead of waiting for an old daemon that silently ignores unknown operations.

Manifest fragments are retained as bounded response jobs and admitted one at a time. Their source-side gap follows the active maximum packet airtime and dispatcher duty spacing, clamped to 100-1000 ms. The 100 ms floor protects fast radios' TX-to-RX turnaround; the cap keeps the receiver's one-second manifest progress/retry observation responsive. The source uses the same radio-aware 100-3000 ms drain/turnaround gap before an unsolicited block proof, but immediately serves a legacy receiver's explicit OTA_REQ_PROOF. Relay collision delay is a separate setting: active OTA floods honor the relay role's configured txdelay, and the deployment runner temporarily uses 0.3 on managed relays.

What to plug into --serial. Use the USB serial console of an OTA-enabled MeshCore node built with OTA_FOLDER_SERIAL. The node must have a working LoRa radio plus an ota folder on command; that command confirms it can host and advertise the folder. A KISS modem will not work with motatool serve --serial: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and mota-seeder request/response transport. The separate mota_kiss_direct.py source implements that full KISS interface. An ESP32 WiFi companion or FULL ESP32 role with active WiFi is the alternative source connection: use its dedicated seeder port with motatool serve --tcp <host>:5001. An nRF52 Full Companion can instead pair with a phone or Linux host, subscribe to its mOTA request characteristic, and use protocol-v14 CMD_BLE_MOTA_SOURCE. That BLE path is source-only; it does not expose the reverse FolderMotaStore capture operations.

Device CLI: ota folder on (attach + announce), ota folder (list), ota folder off. Build flag OTA_FOLDER_SERIAL (default stream = console Serial; override OTA_FOLDER_SERIAL_STREAM + define OTA_FOLDER_SERIAL_BEGIN for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a WiFiServer on the dedicated seeder port (OTA_SEEDER_TCP_PORT, default 5001) while WiFi is usable. On a companion it is separate from the app port (TCP_PORT, default 5000); on infrastructure roles it is separate from WebConfig and browser OTA on port 80. The node auto-attaches the source when a seeder client connects and detaches when it closes (no ota folder on needed over TCP). An already-active serial folder causes a TCP client to be rejected instead of silently replacing it. Verified on hardware: a RAK4631 relays a host folder to a Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (:5001) while the companion serves a phone on :5000 - every block merkle-checked.

The attach reply and bare ota folder report host=advertised/offered. The registry is RAM-bounded (OTA_MAX_SERVE, with the node's own firmware consuming one slot), so a host may correctly index more valid files than this particular firmware can advertise. Omitted entries are now reported instead of silently disappearing. Operators should split a large chain or use a higher-capacity/SD seeder when the two counts differ.

Transport-agnostic by design. The request/response semantics (COUNT / DESCRIBE(idx) / READ(idx, off, len) over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum + resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a reliable stream and the WiFi (TCP) transport reuses it as-is - both ends just treat the socket as a byte stream (on-device, SerialMotaSource runs verbatim over an Arduino Stream-compatible WiFiClient; motatool's TcpTransport mirrors its SerialTransport). The nRF52 Full Companion's BLE GATT path also reuses the exact frame and checksum. Device requests are notifications on a dedicated characteristic and host responses are ordered write-with-response fragments on a second characteristic. Keeping the same framing makes retries and corruption handling identical across USB, TCP, and GATT. The Linux reference implementation is tools/ble_mota/ble_mota_seeder.py; a phone app can implement the same transport-free catalog operations.

"},{"location":"ota_protocol/#11-cli-surface-otaclicpp","title":"11. CLI surface (OtaCli.cpp)","text":"

User-facing OTA data should travel via CMD_OTA_* companion binary frames; the text CLI below is debug/operator oriented and replies are snprintf-bounded into a 160-byte buffer.

Commands take intuitive aliases (matched by the first word; see is_cmd in OtaCli.cpp) so they're easy to type and read - status/neighbors/pull/drop/applydelta are the canonical names, the aliases are the recommended user-facing forms. Output is plain-language (a user-facing guide lives at ota_user_guide.md).

ota help | ? | h                   list the commands\nota status | st  (or bare `ota`)   plain-language: running fw, the one fetch session (state/%/id), serving, keys\nota ls | neighbors | nbrs | updates | n [page]   paged updates (queries sources; rows arrive async via OTA_HAVE)\nota get | pull | download <mid8|#index> flash [rescue] | folder [validate]\n                                      fetch by stable mid8 (preferred) or current page index\nota install | apply | applydelta   verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader\nota rescue install <base_hash16>  internal-flash nRF52 only: recover from failed app-side EndF validation\nota bootloader [status]           capable allowlisted nRF52 repeater: installed BL identity/caps + staged confirmation\nota bootloader install <MID8> <HASH16>\n                                      explicitly verify/arm one complete trusted v3 package; never automatic\nota cancel | drop | stop           drop the fetch; durably invalidate device staging, or retain a folder partial for resume\nota announce | adv                 serve self + send a beacon now\nota self | id                      print this firmware's EndF (body/image size, base_hash)\nota qspi | storage                 QSPI nRF52 only: JEDEC/SR1/stage/latched storage error (read-only)\nota folder | fold [on|off]         attach/detach an external .mota folder (host daemon) ; bare = list\nota config | cfg | set [autofetch|autoinstall|checkpoint] ...   show/set persisted policy\nota key | keys [add|rm <hex>]      trusted signer allowlist ; bare = list\nota dev ...                        bring-up helpers (stage/recv/serve/resume <MID8>/verify)\n

For a device-backed pull, current firmware returns success only after the persistent store can no longer be reopened; flash/SD/QSPI I/O or readback failure is reported as an error even though the in-memory manager session was dropped. For a folder pull, cancellation detaches the live transfer but deliberately leaves the host .part file available for a later resume. If the shared receive engine is currently performing the MeshTower SD auto-archive capture, cancellation likewise detaches that archive transfer and retains its .part file; it does not erase the unrelated manual-install store.

"},{"location":"ota_protocol/#12-apply-bootloader-contract","title":"12. Apply & bootloader contract","text":"

A signature, when present, proves author authenticity and must pass the device allowlist. Unsigned v2 application packages remain installable when local policy permits them. A v3 bootloader package is always signed and trusted. The one-shot approval marker records local consent before the bootloader may apply it.

Bootloader testing note: always test apply with a real different image (base != target). A same-image (X->X) \"delta\" trivially reproduces the target and gives a false positive.

"},{"location":"ota_protocol/#13-versioning-of-this-spec","title":"13. Versioning of this spec","text":"

The fixed byte layout has two intentionally disjoint profiles: format_ver = 2 for ordinary application packages and format_ver = 3 only for the exact privileged bootloader profile in Section 4.3. A parser must reject v2+BOOTLOADER, v3 without exact FULL|SIGNED|BOOTLOADER, and every other version. The multihash hash_algo separately allows swapping the digest family without a format bump. Unknown codec_id / ota_msg_type values are ignored (a node simply will not fetch what it cannot apply).

"},{"location":"ota_user_guide/","title":"Ota user guide","text":"

View this page on MeshCore Docs.

"},{"location":"ota_user_guide/#updating-your-node-over-the-air-ota-user-guide","title":"Updating your node over the air (OTA) - user guide","text":"

For 1.17.1.5, use the exact board/storage profile from OTAFIX 2.4.6 for nRF52 OTAFIX installations. New internal-flash hybrid receivers require its 64 KiB retained-RAM handoff; QSPI and microSD targets require their own matching bootloader profiles. Earlier preview versions mentioned below describe compatibility/migration history, not the current recommended download. Full Companion is a MOTA source and normally updates itself over USB.

Before a Companion upgrade, export its contacts when possible. ESP32, RP2040 and STM32 can retain complete legacy records ahead of an interrupted final record when no backup is reported present; nRF52 has separate migration rules. See legacy interrupted contact saves for the recovery limits and next-save behavior.

This guide is for node operators: how to update your MeshCore device's firmware over the radio, in plain language. No cables, no programmer - your node can download a new firmware from a neighbour and install it. (For the technical wire format, see the OTA protocol spec.)

LoRa OTA download and installation are present only in supported Keymind destination artifacts; the receiver must already be running one of those install-capable builds. Some internal-staging nRF52 targets use a lean lora_ota_no_external_sensors target, while matched external-QSPI boards can retain their normal full-sensor repeater features. Release filenames include an OTA marker, but capability must still be confirmed on the running device. A source can be an OTA-enabled infrastructure node or a source-only Full Companion backed by motatool. Intermediate repeaters do not need OTA-enabled firmware: current repeater builds transport OTA floods opaquely, subject to their normal forwarding filters, duplicate checks, and flood limits. OTA radio traffic is accepted, generated, and relayed only while tempradio is actually running on that node. Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.

The recommended temporary OTA settings use 250 kHz bandwidth, SF5, CR5, and a 120-minute window. For a North American node currently configured for 910.525 MHz, run this on every participating node:

tempradio 910.525,250,5,5,120\n

Use the node's current permitted regional frequency in place of 910.525 when necessary.

The scripted updater checks the firmware version of the receiver and every sender before deciding whether RX power saving can remain active. At SF5/BW250, an all-v1.17.1.5-or-newer path can use effective RXPS level 8 with the tuple-selected 64-symbol physical preamble. SF5/BW500 can use level 8 with 128 symbols. Other fast tuples remain at the shortest viable preamble, normally 32. A mixed, older, or unknown long-preamble path is put in continuous receive for the update, then the receiver's exact saved RXPS setting is restored. This preserves the RX benefit without risking missed packets from a legacy 32-symbol sender.

Can my node install the update? Choose a release-table artifact explicitly labelled LoRa-OTA capable, then confirm ota self and ota status expose install support; do not infer support from the filename alone. LoRa OTA firmware is available for supported ESP32 boards and nRF52 repeater targets. Every nRF52 installation also requires the OTAFIX bootloader built for that exact board; having an OTA-capable application image alone is not enough. An intermediate repeater only relays packets and needs neither an install-capable image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.

The following nRF52 repeater families gained firmware-side LoRa OTA targets in this release. Their ordinary repeater keeps its complete declared sensor recipe; the install-capable lora_ota_no_external_sensors sibling is smaller:

The RAK3401 RAK_3401_repeater_lora_ota_no_external_sensors image trims selected optional environmental/ranging drivers but does not disable I2C. It retains the INA219, INA226, INA260, and INA3221 voltage/current monitors, board display and RTC support, and both RAK12500 I2C and RAK12501/L76K UART GPS paths. The four INA drivers are only the retained voltage/current entries in the optional environmental-sensor table; they are not the only I2C users. The SSD1306 OLED, autodiscovered DS3231/RV3028/PCF8563/RX8130CE RTCs, and RAK12500 GPS use I2C as separate board peripherals. Install one GPS module in sensor slot A. Slot D's reset/PPS lines conflict with the RAK13302 radio's BUSY/DIO1 lines.

The firmware-configured INA3221 address and the RAK12500 address are both 0x42, so those devices cannot share one bus at those addresses. To install both, leave the RAK12500 at 0x42, strap INA3221 A0 to SCL for 0x43, and use a firmware build with -DTELEM_INA3221_ADDRESS=0x43.

The reduced RAK4631 profiles likewise retain the four INA monitors. The plain repeater and Serial2 bridge retain GPS: a RAK12501 can use sensor slot A or D, and a RAK12500 can use slot A or C. The runtime RS-232 bridge defaults to Serial2 so the UART RAK12501 can retain Serial1. The merged runtime image always reserves Serial1: a bounded silent probe cannot prove that a cold RAK12501 is absent, and turning the GPS setting off cannot stop a fitted module from driving the shared UART while WB_IO2/3V3_S remains powered. Use Serial2. Serial1 requires an explicit no-GPS/dedicated image. The reservation remains fail-closed even when an I2C RAK12500 is detected. The explicitly compiled RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors target omits the combined GPS provider because its bridge owns the UART used by RAK12501; consequently that legacy image does not expose the I2C RAK12500 path either, even though RAK12500 itself does not use the UART.

Selected nRF52 repeaters with dedicated external QSPI can now stage the complete package off-chip, so their normal full-sensor repeater build can install a full image or an in-place delta. The current matched families are XIAO nRF52840 and its XIAO-module derivatives, original LilyGo T-Echo, ThinkNode M1/M6, Wio Tracker L1, SenseCAP Solar, and the dedicated RAK4631 + RAK15001 slot-C target. The list also includes a separately wired W25Q16JV on RAK19007 with either RAK4631 or RAK3401 + RAK13302. These require the corresponding exact QSPI-aware OTAFIX bootloader; see the nRF52 QSPI guide.

The W25Q16 option uses one permanent RAK19007 wiring for either core: CLK/MISO/ MOSI tap IO-connector pins 26/27/28, flash CS uses J11 AIN1, and power uses J12 VDD (3.3 V) and GND. Add an approximately 10 kOhm CS-to-VDD pull-up and never power the flash from VBAT. The three SPI signals require underside pads or an IO-connector interposer; the easy J10/J11/J12 headers do not expose them. The flash consumes no WisBlock slot, so a supported GPS can remain in slot A.

The unified RAK repeater images additionally support a header-only W25Q16 layout, using J10 TX1/RX1 and J11 IO1/AIN1. This layout needs the matching compatible OTAFIX 2.4.10 bootloader and gives up UART1 GPS/PPS; I2C sensors remain available. See the pin table.

The ordinary full-sensor RAK_4631_repeater image remains too large for the safe internal in-place update limit. Without external flash, use RAK_4631_repeater_lora_ota_no_external_sensors; it trims selected optional environmental/ranging drivers while retaining generic I2C, battery monitoring, the four INA monitors, and target-compatible GPS. A RAK4631 fitted with RAK15001 in sensor slot C can instead use RAK_4631_repeater_rak15001_slot_c_lora_ota to retain the full sensor/GPS set and stage full images or deltas off-chip.

For the external Winbond breakout, use RAK_4631_repeater_w25q16_lora_ota on RAK4631 or RAK_3401_repeater_rak13302_w25q16_lora_ota on RAK3401 + RAK13302. They retain the full sensor/GPS recipe and require exactly a 2 MiB EF4015 W25Q16 plus the bootloader for that same core/radio/flash combination. Install wiscore_rak4631_w25q16 for RAK4631 or wiscore_rak3401_rak13302_w25q16 for RAK3401 + RAK13302 as a one-time prerequisite. The physical wiring is shared, but the two firmware identities are deliberately different and cannot be substituted for one another.

"},{"location":"ota_user_guide/#the-important-part-first-its-safe","title":"The important part first: it's safe","text":""},{"location":"ota_user_guide/#how-to-talk-to-your-node","title":"How to talk to your node","text":"

Connect to your node's console - usually a USB serial terminal at 115200 baud (or whatever tool you already use to manage the node). You type ota ... commands and the node replies in plain words.

The commands have short, friendly names (and most accept aliases, so you don't have to remember exact spelling): type ota help any time to see the list, or just ota for a status summary.

"},{"location":"ota_user_guide/#common-tasks","title":"Common tasks","text":""},{"location":"ota_user_guide/#1-see-what-im-running-and-whether-anything-is-going-on","title":"1. See what I'm running and whether anything is going on","text":"
ota status\n

Shows your current firmware version, your node's update \"target\" (its hardware/role id), and whether a download is in progress.

For a denser admin view - your firmware's content id (mid) and its body hash, the fingerprint of the set you're serving, live download progress, and the current policy - use:

ota stats\n

On a remote node this is admin-only (the remote command console requires the admin password) - send it from the app's repeater command screen, or the WiFi/serial OTA console.

"},{"location":"ota_user_guide/#2-find-updates-available-near-me","title":"2. Find updates available near me","text":"
ota ls\nota ls 2                 # page 2 when more than two updates are available\n

Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words - each with a temporary number, a stable eight-hex manifest ID, its version, whether it's a full image or a small delta, how many nodes have it, and how recently it was seen. For example:

Updates 1/1 (2 src; refreshing):\n 1) 838B8169 v1.2.3 delta [same target] 3n 5s\n 2) BF0AB0C4 v1.2.0 full [unsupported] 1n 12s\n

Each row shows the version, full-vs-delta, whether it fits your node, how many nodes have it, and how long ago it was seen. The fit marker:

Run it again after a few seconds - discovery happens in the background, so the list fills in. Nothing is downloaded yet; this is just looking around. refreshing means the command has just sent asynchronous catalog queries, so run it again even when an older row is already visible. Two updates fit in each remote CLI reply; use ota ls 2, ota ls 3, and so on for later pages. Catalog rows can change while replies arrive, so use the displayed manifest ID for scripts and important operations rather than a numeric position. (ota neighbors / ota updates also work.)

"},{"location":"ota_user_guide/#3-download-an-update","title":"3. Download an update","text":"

Pick one from the list by its stable manifest ID (a number also works for interactive use), and say where to put it:

ota pull 838B8169 flash            # stage it in this node's flash, to install here\nota pull 838B8169 folder           # capture it onto a connected motatool folder as <id>.mota\nota pull 838B8169 folder validate  # warm-start capture from a motatool --seed build (much faster; below)\n

The destination is required - ota pull 838B8169 on its own just shows the choices. flash is always available (stage here, then ota install). folder appears only while a motatool serve link is attached (it shows the link, e.g. folder: tcp 192.168.4.5); it streams the firmware straight onto the host folder - nothing is staged on this node. That's how you grab an exact copy of another device's firmware off the mesh (to a .mota file) so you can later build a delta against firmware you don't otherwise have. (ota get is an alias.)

Deliberate alternate image. An operator can choose a different target ID for the same physical board by selecting its exact manifest ID from ota ls, then running ota pull <MID8> flash and ota install. The target ID controls which image is discovered and fetched; it does not lock manual installs to the current role. The hardware identity, codec, image hash, and partition checks still run. Automatic updates follow the firmware's selected target (or its declared one-way successor), so use ota config autofetch off and ota config autoinstall off if you want to keep an alternate image without automatic convergence.

validate (warm-start, advanced). Capturing a full image over the radio is slow. If you have a similar build on the computer (e.g. a fresh recompile of the same firmware), run motatool with --seed <that.mota> and add validate: the node fetches just the target's block fingerprints, keeps every block your seed already matches, and pulls over the radio only the handful that actually differ - turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.

Where the seed comes from: it is the --seed <file> you pass to motatool serve - not a file you drop into the capture (--dir) folder, which is only the destination and starts empty. There is exactly one configured seed. When you run ... folder validate, the node asks motatool to begin the capture and motatool stamps that seed's payload into the fresh .part in the same step - so it is always the file you named, with no guessing. validate is the switch: a plain folder pull ignores any seed and fetches from scratch; re-running a validate pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is trusted - every kept block is checked against the target's own fingerprints, so a mismatched or missing seed just means those blocks are fetched over the radio (correct result, only slower).

The node fetches from one source as primary traffic, with bounded adaptive request flights. Every session probes with one manifest block, then clean flights grow 1 -> 2 -> 3 -> 4 blocks on RAK3401 OTA builds. New application packages use 2 KiB blocks; deployed 1 KiB packages remain supported. All blocks in a flight share one request packet, and the receiver stays silent until the source/relays finish returning them. A recovery halves the next flight. Retry timing follows the active SF/BW airtime, duty budget, and path length, so faster settings recover sooner without a fixed one-second request colliding with a valid multi-hop response. Mesh repeaters carry the packets only while their temporary-radio windows are active. Check progress with ota status.

If a folder pull loses its link mid-transfer, ota status shows paused - the host keeps the partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never falls back to flash. A TCP disconnect preserves the selected image even if it happens before its manifest arrives or during the final save. Reconnect through the same transport; an interrupted manifest or seed comparison restarts initialization, while a partial transfer rechecks its stored blocks. The paused capture keeps its OTA workspace until reconnection or cancellation. To stop a download you no longer want:

ota cancel\n

For a download staged on the node, success means current firmware also invalidated the persistent flash/SD/QSPI copy; it reports an error if that media operation cannot be verified. For a folder capture, cancel only detaches the live session and retains the host partial so it can be resumed or removed on the host deliberately. On a MeshTower SD auto-archive capture, cancel also detaches the live archive session and retains its card partial for the archive service to resume later; use ota cache off to stop new archive captures.

"},{"location":"ota_user_guide/#4-install-a-downloaded-update","title":"4. Install a downloaded update","text":"

Once ota status shows the download is ready to install:

ota install\n

The node verifies the firmware one last time, and if everything checks out it installs it and reboots into the new version. If the check fails, it tells you why and does not install. Unsigned images normally install only through this explicit command. The MeshTower V2 SD target is stricter and requires an allowlisted signature even for a manual application install, because it authorizes removable-media bytes for the bootloader. A signed image whose signer is not in the device allowlist is rejected; trusted signed images can auto-install only when that policy is enabled and the signed version is strictly newer than the running hash-valid EndF version. Manual ota install remains the deliberate equal-version/rollback override.

After it reboots, run ota status to confirm the new version.

"},{"location":"ota_user_guide/#updating-an-nrf52-bootloader-advanced-explicit-only","title":"Updating an nRF52 bootloader (advanced, explicit only)","text":"

This is available on specially marked no-external-flash nRF52840 lean repeater/bridge builds, on the legacy XIAO nRF52840/Sense raw-QSPI builds, and on the exact Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors microSD build, after a one-time exact-board ABI-3 OTAFIX installation over USB/BLE DFU or SWD. It is not the normal firmware update path. Check support first:

ota bootloader\n

The reply must show a CRC-valid installed identity plus ABI 3 and the exact storage/boot-update capability bits for that build (0x09 for MeshTower V2 microSD, 0x0A for the shared internal store, or 0x0E for XIAO raw QSPI). A bootloader package appears as bootloader in ota ls. It is never downloaded or installed automatically, even if both OTA automation settings are enabled. Use its stable ID explicitly:

ota pull <MID8> flash\n# wait until ota status says this bootloader download is ready\nota bootloader\nota bootloader install <MID8> <HASH16>\n

Copy both confirmation values exactly from the second ota bootloader reply. Ordinary ota install deliberately refuses this package. The privileged command requires an exact 40 KiB candidate payload in the fixed 41,330-byte container, a valid exact embedded identity/CRC and vector table, continued boot-update support, required BLM2/SOFT continuity metadata at canonical raw-image offset 0x9FB4, a boot version that exactly matches the package and is newer than installed BLM2, and a valid signature from a key already in ota key's trusted allowlist. It preserves the running application while OTAFIX replaces itself; blup:C8 in post-reboot ota status means success. Remote rollback is refused. Any node lacking this command or those capabilities must update its bootloader locally instead.

On an internal-flash target, the package shares the ordinary store below 0xED000 and bottom-aligns at 0xE2000; there is no separate reserved scratch bank. A valid live EndF must prove the current image ends at or below that address before any page is erased. If EndF is missing/corrupt or the app is too large, the pull is refused and local DFU/SWD is required. On the MeshTower V2 SD target, the application linker remains at 0xED000, but bootloader replacement needs temporary scratch beginning at 0xE0000. A hash-valid live EndF must therefore prove the complete current image ends by 0xE0000. A CRC-bound boot-settings bank must cover that complete image while also ending by 0xE0000. MeshCore binds both application and bootloader SD approval to purpose, exact raw geometry, and a normalized SHA-256 in a reset-retained MOTASDA2 record. Boot updates also bind the exact authenticated signed image hash in the E0000 token. OTAFIX consumes the retained record before media access, so a later card change or power cycle fails instead of authorizing different bytes. MeshCore never claims or writes raw sector 1. Both application and bootloader OTA wait until a matching BLM2 bootloader has been provisioned locally; preview.12 requires USB/BLE DFU or SWD first. Larger applications can continue to use normal application mOTA; only bootloader self-update is refused. See the nRF52 bootloader-update guide for the complete target inventory, storage layouts, and safety contract.

"},{"location":"ota_user_guide/#5-if-something-goes-wrong","title":"5. If something goes wrong","text":"

text ota pull <mid8> flash rescue # wait for ota status to say ready to install ota rescue install <base_hash16>

This is not a force option. It refuses a normally valid EndF, a different package hash, hardware or target mismatch, corrupt payload, and invalid/untrusted signatures. The bootloader independently hashes the running app and rejects a wrong base before writing the app. If the physical EndF is absent or the rescue commands were not already in the running firmware, recover over USB. Release chains should put this command in their first bridge and keep it in every bridge after that. The shared-internal bootloader-update builds are intentionally excluded: without valid live EndF, they refuse every internal pull before erase because their normal application can extend through 0xED000. Recover one of those builds over USB/BLE DFU or SWD. - If an install fails, the node won't boot a broken image - it lands in recovery mode: - nRF52: it appears as a USB drive; drag a known-good firmware .uf2 for that exact board onto it to recover. - ESP32: it keeps the previous firmware in the other slot and rolls back. - When in doubt, you can always re-flash over USB the normal way.

"},{"location":"ota_user_guide/#adjust-lora-ota-speed","title":"Adjust LoRa OTA speed","text":"

Use one saved speed factor for this node's LoRa OTA traffic:

get ota.speed\nset ota.speed 0.5\nset ota.speed .05\nset ota.speed 3\nset ota.speed 1\n

The range is 0.05 through 3, including decimals. 1 is the existing timing. At 0.5, adjustable OTA delays are twice as long; at 0.05, they are twenty times as long. At 3, adjustable delays are divided by three. Actual transfer speed also depends on packet airtime, the configured airtime budget, relays, and packet loss; 3 does not guarantee three times the throughput.

The factor covers OTA requests and responses (catalog, manifest, leaves, DATA, and proofs), OTA relay delays, discovery jitter, and automatic OTA adverts. Sources queue manifest and leaf replies and admit their fragments one at a time through the paced transmit queue. Repeated requests merge pending fragments; a full response queue leaves the request available for another source or a later retry. This preserves packet space for other mesh traffic. Below 1, the transmit queue also spaces out every OTA packet, including both profile copies, while letting ordinary messages run. Recovery waits grow for slower transfers; faster settings retain the existing retry allowance so they do not retry before a physical response train can finish. The radio's frequency, bandwidth, SF, coding rate, preamble, and airtime budget stay as set. The proof turnaround allowance also keeps its existing physical minimum. Dual-profile timing accounts for the participating profiles and reply copies; it does not vary with the scanner's current visit. Deliberate slow pacing is excluded from observer builds' stale-packet age, while congestion still ages queued packets normally.

Set the same factor on the source, destination, and OTA relays. It is a local setting, not a value negotiated with peers. A slow source paired with a receiver at 1 can cause unnecessary retries. The setting takes effect during an active session, survives reboot and temporary-radio expiry, and applies to OTA on either radio profile. set ota.speed 1 restores the previous pace. Ordinary chat, node adverts, and USB/WiFi source transfers keep their timing. Local verification of a staged image and leaf comparisons keep their normal maintenance rate even when LoRa pacing is slow. If the local transmit queue cannot accept a catalog, manifest, or leaf request, OTA keeps it pending without consuming a network retry. Successfully queued requests that receive no answer still have bounded retries. Blocked block requests also preserve the v2 transfer format and fragments already received; queue congestion alone does not trigger legacy fallback. Retry timing includes the primary channel when an RX-only temporary radio2 uses crossover to send requests there.

ota config speed 0.5 is an equivalent setter. get ota.speed, ota config speed, and ota speed show both the saved ota.speed setting and the effective packet pace. The latter is the slower of the saved speed and the sender's automatic loss-responsive packet-spacing factor; it does not replace the saved setting or change non-packet OTA timers. The ota config summary also shows both values. If no OTA workspace is active, the automatic factor starts at 1, so packet reflects the saved setting up to 1x. The setting is available on OTA-enabled repeaters, rooms, sensors, Companions, and seeder-only builds. Its separate /ota_speed settings file preserves the existing preference layouts and does not need an active OTA workspace.

"},{"location":"ota_user_guide/#optional-let-it-update-automatically","title":"Optional: let it update automatically","text":"

By default your node only discovers updates - it won't download or install on its own. If you want more automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.

ota config autofetch any        # auto-DOWNLOAD any compatible update for this node (still won't install)\nota config autofetch signed     # auto-download verified updates from allowlisted signers\nota config autofetch off        # back to manual (default)\n\nota config autoinstall trusted  # auto-INSTALL only a trusted signed version newer than the running EndF\nota config autoinstall off      # never auto-install (default)\n\nota config advert 1440          # re-advertise every N minutes while temp radio is running\nota config advert 0             # only advertise when a temp-radio window starts\n\nota config hops 3               # how far OTA travels: accept from / relay up to N repeater hops (default 3)\nota config hops 0               # only exchange OTA with directly-connected nodes (never relay)\n\nota config                      # show the current settings\n

With autofetch signed, add the author's public key using ota key add before expecting an automatic download. The node verifies the manifest's signature against that allowlist before planning, erasing, or writing staging storage. autofetch any remains the deliberate option for automatically downloading unsigned compatible packages; it does not authorize installation.

These policies also govern automatic adoption of an interrupted staged download after reboot. off leaves it untouched; signed requires the same allowlisted signature verification. Automatic resume also requires the stored target to match this node and its version to be newer than the running valid EndF. The signature check applies when an automatic download resumes after discovering a source or reconnecting its storage. Reissuing an explicit ota pull <MID8> remains the deliberate override for an older or unsigned partial. For bring-up/debugging, ota dev resume <MID8> performs the same explicit MID-bound re-adoption without starting a new network fetch. After reboot it requires the MID; bare ota dev resume is accepted only while an active/requested session MID still exists, and malformed or missing identifiers are rejected. Resume is refused while an application or bootloader update is armed, preserving its approved staging and pending reboot. If raw flash contains multiple valid staged headers, automatic resume leaves them untouched rather than guessing which is newest. Specify the intended MID to select its unique checkpoint; ambiguous matches are rejected. Temporary storage read failures no longer consume a download slot indefinitely: host storage pauses for reconnection, while non-reconnectable storage reports a storage error and can be retried.

A failed automatic download starts a cooldown before other automatic candidates are considered: one minute initially, doubling after further failures up to 15 minutes. Changing the offered image or source does not skip this wait. The timer uses elapsed uptime, and a completed download clears it. Manual ota pull, explicit resume, and archive captures keep their existing operator controls and are not restricted by this automatic-download cooldown.

Recommended for most people: leave both off and update by hand. Use autoinstall trusted only once you've added the signer's key (next section) and you trust them to push updates unattended. Automatic admission and final apply both reject zero, equal, or older signed versions; a dishonest catalog version cannot bypass the manifest check. Use manual ota pull plus ota install for an intentional rollback.

The MeshTower V2 SD OTA target has a separate, default-on archive policy. It saves all mOTAs it sees to the SD card so the repeater can seed them later; this does not install them and does not change the install-oriented autofetch default above. Use ota cache for status and ota cache off or ota config cache off to stop new archive captures. Already cached files remain available to peers. Manual ota pull commands take priority and an interrupted archive capture resumes later. See Preload many mOTAs from a computer for the required /mota/<manifest-id>.mota filenames and the complete TempRadio seeding workflow.

"},{"location":"ota_user_guide/#optional-only-trust-updates-from-specific-people","title":"Optional: only trust updates from specific people","text":"

If you'll use signed auto-download or auto-install, tell your node which signing keys to trust. The firmware author shares their public key (a hex string); you add it:

ota key add <public-key-hex>    # trust this signer\nota key list                    # show trusted signers\nota key rm <public-key-hex>     # stop trusting one\n

Only strictly newer updates signed by a trusted key are eligible for auto-install. Manual ota install permits an unsigned package after all integrity, hardware, base, and bootloader checks pass, except on the MeshTower V2 removable-SD path where all application installs must be signed and allowlisted. A signed package whose signer is not in the device allowlist is rejected rather than silently treated as unsigned.

"},{"location":"ota_user_guide/#sharing-updates-with-others-advanced","title":"Sharing updates with others (advanced)","text":""},{"location":"ota_user_guide/#relay-a-folder-of-firmware-from-a-computer","title":"Relay a folder of firmware from a computer","text":"

If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can hand out a whole folder of firmware files to the mesh - without storing them itself. Useful for seeding a new release to a remote area.

  1. Put the firmware files (.mota files - see below) in a folder on the computer.
  2. Install the helper tool once - the standalone motatool CLI (https://github.com/vk496/motatool) - then point it at your node and the folder - over the node's USB serial, over WiFi if it is an ESP32 WiFi companion or FULL ESP32 node, or over encrypted Bluetooth if it is an nRF52 Full Companion: git clone https://github.com/vk496/motatool && cargo install --path ./motatool # over USB serial: motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v # ...or over WiFi: the seeder is on dedicated TCP port 5001: motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v # ...or over paired BLE to an nRF52 Full Companion (protocol v14): python3 tools/ble_mota/ble_mota_seeder.py \\ --device MeshCore-MyCompanion --dir ./my_firmware/ It answers the node's requests; your node then advertises those updates to neighbours, who can ota get them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. Details: https://github.com/vk496/motatool.)

Check the device's attach reply or run ota folder: host=X/Y means the firmware is advertising X of the Y valid entries reported by the host. Serve registries are deliberately RAM-bounded on smaller builds, and the node's own firmware also consumes a slot. If X < Y, split the chain across seeders/folders or use a higher-capacity seeder; motatool saying that every file is valid does not mean every file fit on-device.

To stop, just stop the daemon - over WiFi the node auto-detaches when the connection closes; over USB you can also run ota folder off on the node. ota folder on its own lists what your node is offering. On a FULL repeater or room server, run start webconfig first if WiFi is not already active. Other FULL roles with browser OTA support can use the MeshCore-OTA access point from start ota and connect to 192.168.4.1:5001. Every LoRa participant still needs an overlapping tempradio window.

"},{"location":"ota_user_guide/#everyone-helps-share","title":"Everyone helps share","text":"

You don't have to be a gateway to help. Once any node finishes downloading an update, it automatically offers it to its neighbours too. So a new firmware spreads outward node-to-node, instead of everyone hammering the one node that had it first. Discovery remains background traffic; an actual transfer is primary traffic for the duration of its TempRadio maintenance window.

"},{"location":"ota_user_guide/#share-the-firmware-already-running-on-a-node","title":"Share the firmware already running on a node","text":"

OTA-enabled ESP32 and external-storage nRF52 repeater, room-server, and sensor builds automatically offer their running application during a TempRadio window. Both source and receiver need overlapping windows on the same radio settings. Adaptive RAK builds offer it only when the application has selected a usable, bootloader-matched external store. Internal-only nRF52 nodes do not automatically offer full images; they still need a computer-built delta to receive an update.

Start the overlapping TempRadio windows before explicitly publishing the source. Boards with an on-demand OTA workspace release it when the window ends, so the source is available only during the window. To check or publish it:

ota serve status\nota serve self\n

ota serve self selects the running application as the primary source and announces it. It replaces any manually staged primary image; attached host folders remain available. Automatic offering never replaces a manually staged primary. The receiver uses the normal ota ls, ota get <mid8> flash, and ota install commands. It must have compatible hardware, enough staging/application space, and the required OTA-capable bootloader. ESP32 reads the running A or B app partition; nRF52 reads only its application region. Private keys, ACLs, node names, and saved radio settings are not part of the shared image and are not copied from the source. Existing update paths preserve the receiver's saved data.

The generated package is full and unsigned, streamed directly from flash without keeping the whole image in RAM. This does not enable automatic download or installation, bypass trust checks, or generate deltas on-device. Trusted-only automatic installation rejects it. SD-backed nRF52 receivers also require a trusted signed application package for manual installation: those sources can export their image, but installation there needs a computer-packaged, signed .mota. Source-only Companions continue to offer host-provided files instead.

Eligible repeater, room-server, and sensor builds can compress their own-image blocks on the serving radio with a small, bounded raw-DEFLATE encoder. The receiver uses the existing tinf decoder; no second decoder is added. Each 2 KiB block is independent, and incompressible blocks, unsupported receivers, or insufficient encoder memory fall back to uncompressed transfer. Only the application image through its EndF trailer is shared, not unused partition space. This remains a full image, not a device-generated delta.

The radio does not run the computer packager's Zopfli encoder. Host-generated packages retain their stronger precompressed-block path. Allow a maintenance window long enough for the full image. ESP32 caches successfully read running-image metadata for the current boot and partition geometry, so repeated ota status requests do not rescan the application and interrupt radio servicing.

For an exact delta base from an internal-only nRF52, the existing diagnostic ota dev serve self export, followed by ota announce, remains available during TempRadio for capture onto a computer; it does not make that node capable of receiving a full image.

"},{"location":"ota_user_guide/#where-firmware-files-come-from","title":"Where firmware files come from","text":"

OTA distributes .mota files - a packaged, verifiable firmware image (full image or a small \"delta\" that only contains what changed). You get them by:

"},{"location":"ota_user_guide/#quick-reference","title":"Quick reference","text":"I want to... Command List all commands ota help See my firmware + any download ota status (or just ota) Admin: ids/hashes + serving + policy ota stats (admin-only remotely) Find updates nearby ota ls Download a listed update for installation ota get <mid8> flash Cancel a download ota cancel Install a finished download ota install Recover app-side no EndF on a legacy app-only internal nRF52 ota rescue install <base_hash16> Turn on auto-download ota config autofetch any Turn on auto-install (trusted only) ota config autoinstall trusted Trust a signer ota key add <hex> Relay a folder (gateway) ota folder on + the seeder daemon List what I'm offering ota folder Check whether I'm sharing my running firmware ota serve status Share my running firmware (ESP32 / external nRF52) ota serve self

(Older names still work too: neighbors/updates = ls, pull = get, applydelta/apply = install, drop/stop = cancel.)

"},{"location":"ota_user_guide/#a-few-terms","title":"A few terms","text":"

For the full technical details (the file format and the radio protocol), see the OTA protocol spec.

"},{"location":"packet_format/","title":"Packet format","text":"

View this page on MeshCore Docs.

"},{"location":"packet_format/#packet-format","title":"Packet Format","text":"

This document describes the MeshCore packet format.

"},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"

This is the current version-1 MeshCore wire packet structure. Older firmware may support only a subset of the path encodings and payload types described here.

[header][transport_codes(optional)][path_length][path][payload]\n
"},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (MAX_PATH_SIZE) Stores hop_count * hash_size bytes of path data if applicable payload up to 184 (MAX_PACKET_PAYLOAD) Data for the provided Payload Type

NOTE: see the Payloads documentation for more information about the content of specific payload types.

"},{"location":"packet_format/#header-format","title":"Header Format","text":"

Bit 0 means the lowest bit (1s place)

Bits Mask Field Description 0-1 0x03 Route Type Flood, Direct, etc 2-5 0x3C Payload Type Request, Response, ACK, etc 6-7 0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description 0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes 0x01 ROUTE_TYPE_FLOOD Flood Routing 0x02 ROUTE_TYPE_DIRECT Direct Routing 0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"

path_length is not a raw byte count. It packs both hash size and hop count:

Bits Field Meaning 0-5 Hop Count Number of path hashes (0-63) 6-7 Hash Size Code Stored as hash_size - 1

Hash size codes:

Bits 6-7 Hash Size Notes 0b00 1 byte Legacy / default mode 0b01 2 bytes Supported in current firmware 0b10 3 bytes Supported in current firmware 0b11 4 bytes Reserved / invalid

Examples:

"},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description 0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC) 0x01 PAYLOAD_TYPE_RESPONSE Response to REQ or ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message 0x03 PAYLOAD_TYPE_ACK Acknowledgment 0x04 PAYLOAD_TYPE_ADVERT Node advertisement 0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified) 0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified) 0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request 0x08 PAYLOAD_TYPE_PATH Returned path 0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop 0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets 0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted) 0x0C PAYLOAD_TYPE_OTA OTA-over-LoRa firmware distribution 0x0D reserved reserved 0x0E reserved reserved 0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description 0x00 1 1-byte src/dest hashes, 2-byte MAC 0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC) 0x02 3 Future version 0x03 4 Future version"},{"location":"pair_4p6_300us_validation/","title":"Pair 4p6 300us validation","text":"

View this page on MeshCore Docs.

"},{"location":"pair_4p6_300us_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"pair_preamble32_validation/","title":"Pair preamble32 validation","text":"

View this page on MeshCore Docs.

"},{"location":"pair_preamble32_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"payloads/","title":"Payloads","text":"

View this page on MeshCore Docs.

"},{"location":"payloads/#payload-format","title":"Payload Format","text":"

Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:

This document describes the shared payload envelopes implemented by the core. Application-specific request, response, control, and custom bodies can add their own formats.

NOTE: all 16 and 32-bit integer fields are Little Endian.

"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":""},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"

This kind of payload notifies receivers that a node exists, and gives information about the node

Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below

Appdata

Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node

Appdata Flags

Value Name Description 0x01 is chat node advert is for a chat node 0x02 is repeater advert is for a repeater 0x03 is room server advert is for a room server 0x04 is sensor advert is for a sensor server 0x10 has location appdata contains lat/long information 0x20 has feature 1 Reserved for future use. 0x40 has feature 2 Reserved for future use. 0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"

An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. Current CLI_DATA commands do not cause acknowledgement responses, neither discrete nor extra; their text reply is the application-level result. Repeaters still ACK the legacy plain-text form before processing it.

Repeater remote CLI keeps one volatile copy of the most recently completed reply, keyed by the authenticated sender, request timestamp, and command text. Repeating that same logical request re-sends the text reply without executing the command again. A retry must therefore preserve the original timestamp and command text. The cache is cleared by reboot and replaced by the next completed remote command; commands that intentionally produce no text reply remain silent.

Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"

Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.

Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"

Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.

Field Size (bytes) Description path descriptor 1 low 6 bits are the hash count; high 2 bits encode hash size minus one path count \u00d7 size encoded node-hash prefixes, each 1-3 bytes; the four-byte code is reserved extra type 1 low nibble is the bundled payload type, such as acknowledgment or response; high nibble is reserved extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 request sub type request data rest of payload application-defined request payload body

For the common chat/server helpers in BaseChatMesh, the current request type values are:

Value Name Description 0x01 get stats get stats of repeater or room server 0x02 keepalive (deprecated) 0x03 get telemetry request node telemetry 0x04 get min/max/avg get sensor node stats on time series data 0x05 get acl node ACL query 0x06 get neighbors node neighbors query 0x08 subscribe subscribe to telemetry push 0x09 ubsubscribe unsubscribe from telemetry push 0x0C tracker status encrypted Sensor dog tracker check to a configured owner's Companion; wire format"},{"location":"payloads/#get-stats","title":"Get stats","text":"

Gets information about the node, possibly including the following:

"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) permission mask 1 bitwise inverse mask to AND to permissions (0 = get ALL telem values)"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x04 (request sub type) start 4 starting time, seconds ago end 4 ending time, seconds ago reserved 2 should be zeroes"},{"location":"payloads/#subscribe-to-telemetry-push-sensor-nodes","title":"Subscribe to Telemetry push - (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x08 (request sub type) push tag 4 32-bit tag to be used in telemetry push _REPLY payloads timeout secs 2 subscription timeout (seconds) reserved 1 should be zero min deltas len 1 byte length of LPP encoded min_deltas min deltas (variable) LPP encoded min_deltas"},{"location":"payloads/#unsubscribe-from-telemetry-push-sensor-nodes","title":"Unsubscribe from Telemetry push - (Sensor nodes)","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x09 (request sub type)"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x05 (request sub type) reserved 2 should be zeroes"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body

Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.

"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table

txt_type

Value Description Message content 0x00 plain text message the plain text of the message 0x01 CLI data CLI command OR reply text 0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message 0x03 CLI command (since v1.18+) CLI command text (explicit)

For a room post, companion firmware uses its own monotonic clock for the on-air timestamp and preserves that timestamp across application retries. Room servers track post timestamps separately from login, request, and CLI traffic. They also remember recent accepted posts by sender, timestamp, and text: an exact retry is ACKed again without storing a duplicate, while stale or same-timestamp mismatches are rejected.

"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#companion-one-key-dm-introduction","title":"Companion one-key DM introduction","text":"

A Companion may send this request immediately before a normal private text packet when it has not received an ACK from that chat contact, and on each application retry. It lets the recipient learn the sender's full public key without a broadcast advert. The ciphertext contains:

Field Size (bytes) Description tag 4 unique sender timestamp marker 4 ASCII DMK1 name 1\u201332 sender name followed by NUL signature 64 Ed25519 signature by the sender

The signed bytes are the recipient's full public key followed by the tag, marker, and NUL-terminated name. The recipient verifies that signature against the 32-byte sender key in the anonymous request header before offering a chat contact for manual acceptance, or auto-adding it if dm.one_key is enabled. A receiver that does not implement DMK1 ignores the introduction; the following normal text packet still works when it already has the sender's key. The recipient's manual advert auto-add setting does not suppress the synthetic advert for a verified, addressed DMK1 introduction. The sender's full public key remains visible in the radio packet header, as with other anonymous requests.

With dm.one_key off, the recipient replies with an encrypted DMR1 response: the original 4-byte tag, ASCII DMR1, then its 64-byte Ed25519 signature over the sender's full key followed by that tag and marker. Only a matching pending introduction and verified signature stops repeat introductions.

"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details

The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).

The sender name is unverified message text. Group messages contain no sender signature, so any channel-key holder can choose any sender name.

"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details

The data contained in the ciphertext uses the format below:

Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (bit 0), request radio2 profile (bit 1) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)

Repeaters append an optional radio2 profile trailer when request bit 1 is set. Without that bit, the response is unchanged. Legacy responders omit the trailer even when requested.

"},{"location":"payloads/#trace","title":"Trace","text":"

Trace packets use direct routing. Their normal direct-path header is empty; instead, the intended route follows a fixed nine-byte trace header in the payload. Each forwarding node appends its received SNR multiplied by four to the packet's route-accumulator field.

Field Size (bytes) Description tag 4 Sender-selected trace identifier. auth code 4 Application-defined authentication/correlation value. flags 1 Low two bits encode the route hash size. Current senders use the legacy 1 << value interpretation (1, 2, 4, or 8 bytes); receivers also accept the packed 1-4-byte interpretation where it is unambiguous. route rest Concatenated node-hash prefixes for the requested direct route.

At the destination, the application receives the tag, auth code, flags, accumulated SNR bytes, and original route bytes. Trace therefore differs from a normal direct packet whose path is carried entirely in the packet route field.

"},{"location":"payloads/#multipart","title":"Multipart","text":"

The first payload byte identifies the inner payload and how many packets remain:

Field Size (bytes) Description remaining + type 1 Upper nibble is the remaining-packet count; lower nibble is the inner payload type. inner data rest Data for the inner payload type.

The core currently creates and consumes multipart acknowledgments. For that form, the low nibble is 0x03 and the next four bytes are the acknowledgment CRC. Other multipart inner types are reserved for application or future use.

"},{"location":"payloads/#ota","title":"OTA","text":"

An OTA payload contains one OTA protocol message and is normally sent by flood. Its message types, integrity fields, and transfer state are defined in the OTA-over-LoRa protocol. Builds without OTA support can still relay an opaque OTA packet when their routing policy permits it.

"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"

Custom packets have no defined format.

"},{"location":"power_saving_diagnostics/","title":"Power saving diagnostics","text":"

View this page on MeshCore Docs.

"},{"location":"power_saving_diagnostics/#power-saving-diagnostics","title":"Power-saving diagnostics","text":"

Use set powersaving.diag on to collect the real sleep-policy decisions in RAM, and get powersaving.diag to inspect them. This is available in Companion, Repeater, Room Server and Sensor, without a separate diagnostic build. It does not override any sleep guard. The production USB/DTR policy is unchanged.

"},{"location":"power_saving_diagnostics/#suggested-test","title":"Suggested test","text":"
  1. Record get powersaving, radio/RXPS settings, logging and bridge settings.
  2. Run set powersaving.diag on, then close the USB terminal for the test.
  3. Let the node run long enough to cover noise-floor calibration, queue work and scheduled tasks; a one-second snapshot can be misleading.
  4. Reconnect and run set powersaving.diag off promptly to freeze the window.
  5. Read get powersaving.diag, get powersaving.diag calls, get powersaving.diag time and pages 3 through 8.
  6. If necessary, clear and repeat with only one feature changed.

On ESP32, pages 9 and 10 also show SDK call-entry counts in sixteen 16-second buckets starting at start_ms. These cover the first 256 seconds; outside counts later entries. Flag 1 marks entries outside that horizon, flag 2 marks saturation and flag 4 marks a call without a millisecond timestamp. A bucket with calls can establish activity within an independently timed test interval only when its whole time range fits inside that interval. Whole-window calls alone can all precede packet traffic. Buckets count SDK entries, not successful returns, physical residency or a particular packet's wake event. Check the existing call/return/error counters too. Other platforms keep pages 0 through 8.

Reconnecting/reading creates real USB or serial work and can affect the final decision and counters. Prefer accumulated time over that final why value; check its age. Off freezes recorded values, not the node itself. Re-enabling after off starts a fresh window. Collection defaults off at every boot.

"},{"location":"power_saving_diagnostics/#interpreting-the-result","title":"Interpreting the result","text":"

radio2 means the scanner cannot safely idle right now: for example, a short remaining visit, calibration, pending reception or an unvalidated platform. On nRF52 and ESP32, settled long visits can idle the MCU with power saving enabled; the RF receiver stays on. ESP32 requires working radio GPIO wake and physically stopped WiFi/Bluetooth drivers. Short visits and active transports retain polling. See radio profiles.

The role reports the first actual short-circuit blocker. It does not query otherwise skipped hardware just to populate an exhaustive list. Board guards may add reasons. Per-reason durations therefore are observed guard time, not the independent energy cost of each feature. The state page's blocked time is the union; adding per-reason totals can double-count overlap.

calls and time instrument actual nRF52 event waits, ESP32 manual light sleep and generic RP2040/STM32 WFI paths. ESP32 automatic power-management yields are not direct wait calls. A return under 100 us is counted as short; it can be a legitimate pending interrupt/event, not an SDK failure. SDK errors have their own counter and detail. Time inside the wait includes interrupt/SDK overhead; it is not measured sleep residency. USB debugging/SWD can alter power and clock behavior, so compare like-for-like setups.

These are not current measurements. Radio continuous RX, RXPS parameters, FEM/LNA gain, GPS, displays and board regulators can explain large current differences even with good MCU sleep. Use a current meter and matched settings to attribute those loads. Counters are RAM-only, saturating, and consume at most 256 bytes of collector state. Each wait-call interval must be shorter than the 32-bit micros() wrap (about 71 minutes); the normal wait paths here are much shorter. Keep gaps between policy observations below the millis() wrap (about 49 days).

"},{"location":"power_saving_diagnostics/#esp32-bluetooth-off-and-cpu-sleep","title":"ESP32 Bluetooth off and CPU sleep","text":"

Switching Bluetooth off now completes a reversible host/controller shutdown, including when Bluetooth was saved off at startup. Existing pairings, address, name and PIN are retained when it is switched back on. A pending disconnect or failed teardown keeps the CPU awake rather than treating stopped advertising as a stopped controller. get bluetooth reports a pending change until that transition finishes.

NimBLE-Arduino 2.5.1 builds include the upstream host-timer shutdown fix (NimBLE-Arduino #1185). Stopping Bluetooth preserves an already queued timer event's callback until final host cleanup. Without this fix, switching Bluetooth off can reset an ESP32 through a null callback. The version/hash-checked repair is applied to a build-local source copy; the shared library installation is unchanged.

USB host sessions, logging, active WiFi/ESP-NOW, OTA and serial work keep their existing sleep guards. This change does not sleep an ESP32 through a live Bluetooth or WiFi connection. Such connections require SDK-managed modem and automatic sleep; lowering CPU frequency or yielding alone is not proof of light sleep.

The ESP32 Bluetooth and radio2 qualification report records the tested firmware, saved-pairing checks, SDK wait measurements and packet-test failures. SDK wait time is not a current measurement.

"},{"location":"power_saving_diagnostics/#esp32-processor-speed-and-temporary-test-controls","title":"ESP32 processor speed and temporary test controls","text":"

The WebConfig Processor speed selector and set cpu.freq auto|80|160|240 save the processor clock choice. get cpu.freq reports the choice, actual MHz and supported values. The board rejects unsupported clocks. Auto retains the existing policy: Companion normally selects 80 MHz with device power saving on and its configured nominal speed with it off. Other roles retain their nominal speed in Auto. A fixed speed stays fixed when power saving changes. The selection applies immediately; a failed apply or save reports an error and attempts to restore the previous selection. Returning to Auto clears the saved override. Existing preferences load as Auto.

A damaged CPU override falls back to Auto without discarding other valid preferences. An identifiable truncated timezone record is rejected as an incomplete preferences image, so it cannot silently replace a fractional timezone with an integer value.

Local USB/Bluetooth console commands provide temporary controls for controlled radio2 experiments. They are held in RAM and reset at reboot:

Command Values Default set powersaving.test sleep VALUE on, off on set powersaving.test slice_ms VALUE 2-20 ms 10 ms set powersaving.test guard_ms VALUE 0-20 ms 4 ms set powersaving.test min_ms VALUE 1-10 ms 2 ms set powersaving.test settle_ms VALUE auto, 1-50 ms auto set powersaving.test uart_quiet VALUE 5-300 seconds, off inactive

get powersaving.test shows the current controls; set powersaving.test reset restores their defaults. Sleep on permits existing power-saving decisions; the saved device power-saving setting and every normal transport, interrupt and radio deadline guard still apply. Settle changes only when profile sleep is allowed, leaving radio calibration timing unchanged.

UART quiet is available only for an ordinary hardware UART console, such as the V3's external USB bridge. After draining its acknowledgement, it stops the UART driver and parks its Companion session, preventing new push notifications to the stopped UART from holding the CPU awake. Received messages remain in the offline inbox; other transports keep their own sessions. It automatically restores the console at 115200 baud with a fresh, silent protocol session after the chosen interval. USB logging and OTA must be off. Native USB boards reject this command. A reboot also restores the console.

To separate clock speed from CPU sleep, hold the clock at 80 or 160 MHz and compare sleep off/on at the same RF settings. Use matching transmitter and receiver preambles, unique packets without retries, and diagnostic counters that prove SDK sleep occurred. Keep every failed packet in the result. These controls are experimental tools; a reported SDK wait is not a current reading or proof of a loss-free radio link.

"},{"location":"power_saving_diagnostics/#noise-floor-sampling-and-sleep","title":"Noise-floor sampling and sleep","text":"

Startup still collects a complete baseline promptly: 64 accepted samples, at least 50 ms apart, after the radio frontend has settled. Collection keeps the radio in continuous RX when needed, but the CPU may sleep between reads. Pending packet interrupts retain their normal receive owner. Rejected readings retry at 50 ms intervals, and the existing 30-second collection timeout bounds an attempt on a busy channel.

After a successful baseline, ordinary single-profile refresh waits five minutes. A rejected rise or failed attempt retains the last usable floor and retries after one minute. Gain/tuning changes and radio recovery still request immediate recalibration. Adaptive AGC maintenance with an unchanged RF path retains a valid baseline and its existing refresh deadline; it does not cancel a pending collection or postpone a failed-sample retry. Legacy fixed-cadence AGC resets still request recalibration. Dual-profile scanning keeps its existing timing and calibration policy.

ESP32 timed sleep uses a millisecond timer capped at the next sample deadline. Repeater, Room and Sensor also preserve millisecond queue, retry, RS232 standby, scheduled-radio and maintenance deadlines, so the last fraction of a second does not become a busy-wait tail. A future sub-second deadline is not reported as blocked work; a deadline that is already due still prevents sleep. nRF52, RP2040 and STM32 retain their interrupt-idle wake paths. Other sleep guards, including USB/logging, packet queues and bridges, still apply. These changes have host scheduling tests and firmware build coverage; their power benefit requires a new matched hardware/current measurement.

See the CLI reference for all page names and commands.

The nRF52 idle-sleep investigation describes an eligible-but-not-sleeping regression, its fix, controlled hardware evidence and the limits of CPU-state sampling without a current meter.

"},{"location":"power_saving_diagnostics/#rak13302-builds-with-an-unused-onboard-radio","title":"RAK13302 builds with an unused onboard radio","text":"

The RAK3401/external RAK13302 firmware now probes the private onboard-radio bus once during startup. This supports both a RAK3401 core (no onboard SX1262) and a RAK4631 core using the external RAK13302. It is adapted from agessaman's optional-radio sleep change. RAK's internal connection table identifies the private SPI, reset, BUSY and RF-switch pins used by the probe.

A matching six-byte chip signature triggers cold sleep with the RTC disabled. The RF switch stays off, NSS/reset stay high, and the temporary SPI/data/BUSY pads are released. Sleep confirmation reads BUSY only: another SPI transaction would wake the chip. Each BUSY wait is bounded to 20 ms, including when no chip is fitted. A timeout or mismatched signature does not block startup of the external radio. Debug-enabled startup logging reports asleep, absent, busy-timeout or sleep-failed when the USB debug output is available; a missing chip normally produces busy-timeout, not a verified sleep result.

This happens before the boot-voltage check, so a low-voltage shutdown cannot skip the unused-radio sleep attempt. It does not cycle the shared GPS/storage rail, change external-radio recovery, or enable its FEM before initialization succeeds. There is no runtime probing or automatic primary-radio selection.

Ordinary RAK4631 firmware is unchanged: its onboard SX1262 is the active primary radio and must not be disabled. Use the external RAK13302 image for that hardware arrangement, not a normal RAK4631 image. This startup change has host regression tests; sleep-current improvement still needs matched current measurements on the physical core/module combination.

"},{"location":"power_saving_diagnostics/#board-regulator-initialization","title":"Board regulator initialization","text":""},{"location":"power_saving_diagnostics/#user-selectable-regulators-and-crash-recovery","title":"User-selectable regulators and crash recovery","text":"

mcu.regulator and radio.regulator independently select auto, dcdc or ldo where the board supports those modes. auto preserves its existing board default. The choice is saved and takes effect after reboot; the two getters show requested/active mode, fallback, crash count and recovery availability. An explicit setter clears the corresponding fallback and streak, even when the same value is saved again. See the CLI commands.

Processor selection applies to the existing, circuit-supported nRF52 REG1 DC-DC path. It does not select REG0, change an external PMIC, or expose an ESP32/RP2040 CPU regulator control. Radio controls apply to the supported SX1262, LR1110/LR1121 and Meshnology W12 LR2021 implementations. DC-DC is offered only when that board's regulator circuit is supported. LDO-only boards reject dcdc; unverified LR2021 switching circuits keep their existing mode.

Integrated STM32WL targets retain their existing supply configuration. Their MCU and radio share the regulator, so independent selections need a coordinated implementation and board circuit verification before they can be offered.

Automatic recovery uses reset-retained RAM on supported platforms. A domain must have been armed in DC-DC mode before the crash, with a valid last battery sample of at least 50%, no more than 60 seconds old at its last heartbeat. The third consecutive qualifying crash selects LDO before that domain starts again and saves the fallback. MCU and radio streaks are separate; if both were using DC-DC during the same qualifying failures, both can fall back. This is a recovery action, not proof that a regulator caused the crashes.

Low/unknown battery, stale samples, cold starts, intentional reboots, sleep wakes and unclassified resets break a streak. Corrupt retained state cannot add a crash. Five healthy minutes clear the running streak; a later crash starts a new one. The saved LDO fallback survives normal resets and power cycles. It stays in LDO until a user explicitly selects a mode again, with no automatic return to DC-DC. Crash bookkeeping stays in retained RAM rather than writing flash at every heartbeat.

During timed Morse TX/RX, periodic battery sampling and failed-save retries wait until timing ends. Recovery heartbeats continue, and a deferred sample keeps its original timestamp, so it can become too stale to qualify a crash. Overdue sampling and retries resume on the next eligible service tick.

Battery eligibility follows the board's battery measurement and configured percentage curve. USB supply voltage alone does not establish that a physical battery is at 50%; validate battery-only starts and the board's absent-battery reading when qualifying recovery. Boards without a trustworthy battery reading should report it invalid. A getter reporting recovery unavailable still permits supported manual mode selection.

ESP32 WebConfig exposes supported controls under Radio \u2192 Advanced \u2192 Power regulators, showing active mode and fallback status. Unsupported domains and DC-DC options are hidden. Saving reboots only after the authenticated save completes successfully. Clear fallback on restart lets a user retry the current saved mode without first choosing a different one.

For hardware qualification, compare both modes with battery-only cold starts, normal reboots, sleep/wake, BLE/WiFi and radio TX/RX under load. Confirm that three injected qualifying watchdog/fault resets select LDO, that fewer than three and low/unknown-battery resets do not, and that an explicit setter restores the requested choice. Also check current consumption and voltage during TX; native recovery tests and successful firmware builds do not establish these physical results.

The regulator qualification record binds the compiled packages, software checks and Mercerwood bench results to their tested source revisions and lists the remaining physical checks.

"},{"location":"power_saving_diagnostics/#existing-board-defaults","title":"Existing board defaults","text":"

RAK4631 Companion shares the nRF52840 REG1 regulator path with the other RAK4631 roles. auto preserves its DC-DC default; the former Companion-only force-LDO flag has been removed. Saved mode selection is deferred until board ADC and primary filesystem initialization are complete, before display, radio and wireless startup. This retains boot-voltage protection and keeps REG0 and storage layout unchanged. The shared initializer uses the SoftDevice API when BLE's stack is already active and the hardware register otherwise.

RAK documents DC/DC support for the module. The related upstream battery-startup report #1878 concerned Repeater firmware, with BLE Companion reported working on an affected board. It did not establish filesystem corruption or a Companion-specific DC/DC failure. PR #1914 also changes boot-voltage/wakeup handling and is not proof that every failure in that thread has the same cause.

Host startup tests cover both SoftDevice states and deferred regulator selection with boot-voltage protection preserved. Firmware compilation is not hardware qualification: compare battery-only cold starts, low-voltage recovery, BLE operation and settings writes/reboots on the actual board/baseboard revisions before a fleet rollout. USB power can mask the reported startup symptoms. A current meter is still needed to quantify the power difference.

The following boards also now call their declared NRF52BoardDCDC initializer instead of bypassing it with NRF52Board::begin(). This applies to every role that uses the board class, not just Companion, and does not require enabling the runtime sleep policy. The nRF52840 hardware manages the enabled regulator according to load. This is REG1 only, not a blanket change to REG0 or an external LDO, PMIC output, or radio regulator.

Board Circuit evidence used for REG1 support Heltec T1 T1 V1.0 schematic, DCC-to-DEC4 inductors and capacitors. Heltec MeshPocket MeshPocket schematic, DCC-to-DEC4 L1/L2 and C5/C6. Heltec MeshSolar and MeshTower V2 HT-N5262M module circuit, L1/L2 and C1/C2. Heltec links this circuit from its MeshTower V2 resources; MeshSolar's datasheet identifies the module. LilyGO T-Echo T-Echo schematic, REG1 DC/DC network and explicit DC/DC-enabled design note. Ikoka Handheld, Nano and Stick nRF52 XIAO nRF52840 circuit, DCC L1 and DEC4 capacitors. The designers identify XIAO as the MCU for Handheld, Nano and Stick. Minew ME25LS01 Manufacturer datasheet, electrical schematic page 9: MCU DCC through ML2/ML3 to DEC4_6 and MC15/MC16. Nano G2 Ultra Manufacturer schematic published as the 19 June 2023 design, NRF52840 sheet: DCC through L13 (10 uH) and L12 (15 nH) to DEC4, with C41/C42. These components are not marked DNM.

The startup tests execute each real begin() with SoftDevice off/on and power management compiled out/in. Normal startup defers the selected mode until the saved settings and recovery state can be read, while retaining one common board initialization. The board class still defines the circuit-supported default; the settings layer applies it before display/radio startup.

Nano G2 Ultra was deliberately excluded from upstream PR #1246 because its LC circuit had not yet been confirmed. That discussion asks for circuit confirmation or testing; it does not identify a Nano-specific DC-DC failure. The published schematic supplies that circuit evidence. Nano now approves the REG1 DC-DC default through the shared board initializer, retaining saved LDO selection and crash fallback in the main firmware roles. KISS applies the board default directly. REG0, GPIO assignments, ADC conversion, peripheral startup/shutdown and USB/DFU identity are unchanged. The schematic title block does not identify a PCB revision; battery-only starts, current draw and radio load on actual hardware remain physical acceptance checks.

The Nano validation record binds the issue/circuit review, native checks and six passing build selections to source c6ccbb5ab: BLE/USB/Full Companion, Repeater, Room Server and KISS. No Nano hardware was flashed during these checks.

ThinkNode M3/M4/M6 and generic ProMicro remain unchanged pending exact circuit or physical-board confirmation; their DC/DC inheritance alone is not evidence. ThinkNode M1/M8 and Keepteen LT1 retain their plain NRF52Board initializers for the same reason. The MeshTracker X1's LR2021 regulator likewise remains unchanged. Do not turn on a switching regulator without its required inductor/capacitor network. The MCU and LoRa regulator choices are independent.

"},{"location":"power_saving_diagnostics/#meshnology-w12-transmit-preparation","title":"Meshnology W12 transmit preparation","text":"

The W12 adopts the GC1109 TX preparation from upstream PR #3579, but keeps the PA shut down in ordinary standby rather than permanently selecting TX mode. Before sub-GHz packet or Morse CW transmission, it temporarily enables the LF PA in standby, waits 1 ms, starts TX, then restores the normal standby mask. The TX mask stays unchanged during that restoration. Failure paths restore all affected DIOs and request radio recovery if the PA state cannot be confirmed. The 2.4 GHz path and other LR2021 boards do not use this LF prearm policy.

LF/HF FEM rail changes now settle for 1 ms instead of 100 ms. Packet-wake deep sleep retains only the selected FEM rail; terminal shutdown retains both rails off. This is separate from the W12's existing circuit-verified DC/DC setting.

Native fault-injection tests cover the mask transitions and power retention; RF envelope and battery-current measurements on a physical W12 are still needed to validate the TX improvement and quantify power use.

"},{"location":"pr7_review_validation/","title":"Pr7 review validation","text":"

View this page on MeshCore Docs.

"},{"location":"pr7_review_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"preamble_detection_investigation/","title":"Preamble detection investigation","text":"

View this page on MeshCore Docs.

"},{"location":"preamble_detection_investigation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"preset_test/","title":"Preset test","text":"

View this page on MeshCore Docs.

"},{"location":"preset_test/#temporary-radio-test-builder","title":"Temporary radio test builder","text":"

Choose a window and radio tuple to create a shareable temporary-radio test URL.

MeshCore \u00b7 default 48-hour temporary preset test

Scheduled Starts in \u2014 \u2014 Start End Display time zone Need to leave the test early?

Send this command to return to your saved radio settings in one minute:

Copy leave command KeyMind Cascade firmware and advanced commands

Use these only if your firmware supports the listed commands or you need dual-radio operation, scheduling, or clock correction.

Firmware schedule epochs:

Primary scheduling requires a Simple Repeater build. Companion tempradio2 and tempradioat2 require dual-profile firmware. If a command is unknown, use only the method your installed firmware supports.

Enter the test times in the selected time zone and choose the radio tuple. The generated URL converts the times to exact UTC instants, keeps the date punctuation readable, and keeps tz so the page displays them in the organizer's local time zone. With no URL settings, the builder starts at 5:00 PM tomorrow and ends at 5:00 PM two days later in the selected time zone, creating a 48-hour window.

Start date and time End date and time Selected time zone Detecting browser time zone\u2026 Use browser time zone

Loading time zone map\u2026

Click a region to select its IANA time zone. The initial selection comes from tz= when present; otherwise it uses your browser's time zone. Map design inspired by zones.arilyn.cc; boundaries from Timezone Boundary Builder and \u00a9 OpenStreetMap contributors. Test radio profile Frequency (MHz) Bandwidth (kHz) 7.810.415.6 20.831.2541.7 62.5125250500 Spreading factor 5678 9101112 Coding-rate denominator 5678 TX output for estimate (dBm) This estimate-only value does not change the TempRadio commands. Generate test URL

Copy generated URL Open generated page

Advanced use: start and end also accept ISO-8601 timestamps with explicit UTC offsets or Unix epoch seconds. If tz is omitted, the page uses the browser's time zone. The other URL parameters are freq, bw, sf, cr, and estimate-only tx.

"},{"location":"preset_test/#preset-test-title","title":"910.100 MHz / 500 kHz / SF8 / CR7","text":""},{"location":"preset_test/#stock-test-title","title":"Stock firmware: copy this tempradio command","text":"Copy tempradio command

Before sending it, run get radio and save the result. No clock setup is needed. Your saved settings return automatically.

"},{"location":"preset_test/#schedule-mode-title","title":"Scheduled command timing","text":"Choose the syntax supported by the node KeyMind Cascade \u00b7 +minutes (default) Absolute Unix time

Relative commands calculate whole minutes from now to the shared start and end. Both offsets use the node's same command-time snapshot, so no clock correction is needed. Copy the command shortly after it is shown.

Absolute commands use fixed UTC epochs. Verify or compensate for the node clock with the controls below before queuing them.

"},{"location":"preset_test/#continuity-plan-title","title":"Bridges and early-revert plan","text":"

The test will include bridges. If the Puget Sound area experiences a widespread power outage or loss of cellular service, we will end the test early. A stock node's one-minute temporary test profile then ends on that node's own saved radio settings.

"},{"location":"preset_test/#save-settings-title","title":"Before changing a radio","text":"

Run get radio and record the saved primary tuple. Both primary TempRadio methods return to that saved tuple; they do not store a return tuple inside the temporary command. On a Companion, also run get radio2 and get radio2.cross so you can restore any non-default secondary-profile setup.

"},{"location":"preset_test/#clock-check-title","title":"Check the node clock before scheduling","text":"

The primary tempradioat and Companion tempradioat2 schedulers use UTC Unix time. Run clock on every node that will be scheduled and compare it with the browser UTC time below. It should agree to within about a minute. A wrong clock can start late, start immediately, or cause the schedule to be rejected.

Browser UTC now \u2014 Browser Unix time \u2014 Node clock reported by clock (UTC, optional) Paste the node's clock reply immediately after it arrives. The firmware form 02:42 - 22/9/2026 UTC is also accepted. Leave this blank to keep the normal UTC schedule epochs unchanged. Apply clock conversion

No node-clock correction is applied. Scheduled commands use the normal UTC epochs.

This changes only the absolute tempradioat and tempradioat2 epochs shown on this page; it does not issue a clock-setting command. The correction is for this node and this open page only: it is never added to generated URLs, and is cleared on reload. If the node clock later syncs or jumps, delete and queue the schedule again.

Remote admin session

In a MeshCore client that supplies the sender timestamp, sync and then verify:

clock sync\nclock
Local USB or browser console

Copy this fresh, run it immediately, and then verify with clock:

time 0\nclock
Copy current clock command If the clock is ahead and cannot move backward

If setting the correct time returns ERR: clock cannot go backwards, run the command below. It resets the clock to an older fallback date and reboots the node immediately, so it sends no reply. After the node reconnects, copy and run the fresh local clock command above, verify with clock, and then add the Companion tempradioat2 schedule again. The reboot clears any pending scheduled entries.

If resetting or correcting this remote node's clock is not safe, leave it unchanged and use the optional node-clock conversion above instead.

clkreboot
Copy clock reset command

The Companion schedule must be queued while both its start and end are in the future and within the firmware's roughly 24-day scheduling horizon. Scheduled entries are held in RAM and disappear if the node reboots.

"},{"location":"preset_test/#join-now-title","title":"Companion: use both frequencies","text":"

These commands become available one hour before the official start. Their timeout shrinks so the Companion returns at the same end time.

Companion using both frequencies Dual profile Copy commands

rxtx permits transmission on the second profile; radio2.cross on copies ordinary Companion traffic across the primary and temporary profiles.

"},{"location":"preset_test/#schedule-title","title":"Schedule a supported node in advance","text":"

A Simple Repeater build can schedule its primary radio with tempradioat. A Companion can instead schedule its second profile with tempradioat2. The default KeyMind Cascade form uses +minutes for both endpoints. Switch to Absolute Unix time above for fixed UTC epochs and the node-clock correction tools. Both forms switch at the common start and restore saved settings at the common end. Room-server and sensor roles use the stock join command above instead.

Room server or sensor No advance schedule

Use the stock tempradio command above after its setup window opens.

Simple Repeater primary radio Primary schedule
Copy schedule commands

This is available only on a Simple Repeater build with primary scheduled-radio support. It changes the primary tuple at the start and restores the saved tuple at the end. If get tempradioat returns Error: unsupported, use the stock join command above instead.

Companion using both frequencies Dual-profile schedule
Copy schedule commands

Crossing is saved independently and remains on after the temporary second profile ends. Restore its previous value after the test; off is the normal default.

"},{"location":"preset_test/#cancel-title","title":"Cancel a schedule or adjust the timeout","text":"

For a primary or Companion schedule, the all form removes every temporary schedule in that family. To preserve another schedule, first run the corresponding get command and replace all with its displayed entry number.

Stock: leave in 30 minutes
Copy 30-minute command

This replaces the current temporary timeout with 30 minutes, then restores the saved primary tuple automatically.

Simple Repeater \u00b7 scheduled primary radio
Copy cancel commands

Before the start, this removes the queued temporary schedule. While it is running, it also restores the saved primary tuple after the command reply drains.

Companion \u00b7 before it starts
Copy cancel commands Companion \u00b7 while it is running Leave now
Copy restore commands

If get radio2.cross was not off before the test, restore that recorded value instead.

Leave in 30 minutes
Copy 30-minute commands

These commands clear any absolute second-profile schedule and give the test profile a fresh 30-minute lease. The saved second profile returns at expiry, but crossing remains on; afterward, restore the value you recorded before the test (off is the normal default).

"},{"location":"preset_test/#share-title","title":"Build a temporary radio test URL","text":""},{"location":"preset_test/#radio-estimates-title","title":"Radio estimates","text":"Nominal LoRa bitrate\u2014 Estimated sensitivity\u2014 TX output used\u2014 Estimated link budget\u2014

The bitrate is the nominal LoRa physical-layer rate; usable payload throughput is lower. Sensitivity assumes a 6 dB receiver noise figure and the standard LoRa SNR threshold for the selected spreading factor. Link budget is TX output minus that sensitivity, before antenna gain, cable loss, path loss, interference, and implementation differences.

"},{"location":"qr_codes/","title":"Qr codes","text":"

View this page on MeshCore Docs.

"},{"location":"qr_codes/#qr-codes","title":"QR Codes","text":"

This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.

"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"

Example URL:

meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n

Parameters:

"},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"

Example URL:

meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n

Parameters:

"},{"location":"radio_chirp_math_validation/","title":"Radio chirp math validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_chirp_math_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_dwell_policy_validation/","title":"Radio dwell policy validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_dwell_policy_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_profile_switch_validation/","title":"Radio profile switch validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profile_switch_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_profiles/","title":"Radio profiles","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profiles/#two-lora-profiles-on-one-radio","title":"Two LoRa profiles on one radio","text":"

radio2 lets one LoRa transceiver alternate between two frequencies or modulation settings. It is useful for listening to two networks, or keeping your usual channel available during a LoRa firmware update.

The second profile and radio2.cross both default to off. Saved settings survive firmware updates; selecting a profile does not change the crossing policy.

There are at most two active profiles. tempradio temporarily replaces radio; tempradio2 temporarily replaces radio2. These are time-shared receive windows, not simultaneous receivers. Receiving or transmitting a packet on one profile makes the other unavailable until that packet finishes.

The second-profile commands are shared by repeater, Companion, room server, sensor and standalone terminal-chat firmware with a LoRa radio. ESP-NOW-only devices reject LoRa profile commands. KISS is a raw host-controlled modem: it exposes equivalent RAM-only binary SetRadio2/SetTempRadio2 commands and logical KISS Data ports, documented in the KISS modem protocol. It does not use the Mesh CLI's persistent profile file or Mesh retry queues.

"},{"location":"radio_profiles/#quick-setup","title":"Quick setup","text":"

Keep the primary channel configured with radio, then add the second:

set radio2 910.5,500,8,5,rx\nget radio2\nget radio2.status\n

The tuple is frequency in MHz, bandwidth in kHz, spreading factor, coding-rate denominator, mode, optional preamble in symbols. 5 means coding rate 4/5.

"},{"location":"radio_profiles/#mediamesh-presets","title":"MediaMesh presets","text":"

Enable the saved MediaMesh profile with one command:

set radio2 mediamesh\n

This sets 911.150 MHz, 250 kHz, SF7, CR 4/5, rxtx, automatic preamble. For the same profile at 125 kHz, use:

set radio2 mediamesh-range\n

The RF settings match the presets documented by MeshCore Open TAK and Wadamesh TAK. Nodes exchanging media need matching radio settings.

Both leave the primary profile and radio2.cross unchanged. The center is 625 kHz above a 910.525 MHz primary. Automatic preamble remains scan-aware; neither preset fixes it at 32 symbols. See preamble for the switching measurement and recommendations.

On Companions, the default tx.media auto sends recognized binary photo/voice traffic only on an active rxtx MeshCore second profile, including tempradio2. Apps do not need to set tx.media. Ordinary traffic keeps its existing route. If the second profile cannot transmit MeshCore, media uses the normal channel/global route; set tx.media radio2 instead requires radio2 with no primary fallback. See application TX routing.

"},{"location":"radio_profiles/#discover-the-local-radio2-profile","title":"Discover the local radio2 profile","text":"

Clients can request a repeater's current MeshCore radio2 profile through public, zero-hop discovery. Set bit 1 (0x02) of the DISCOVER_REQ flags byte: 0x82 for the full public key, or 0x83 with the prefix-only bit also set. The type filter, tag and optional since fields keep their existing meanings. Omit since or send zero when requesting current settings; radio changes do not update the discovery timestamp.

The requested DISCOVER_RESP appends this version-1 trailer immediately after the public key, at payload offset 38 (full key) or 14 (8-byte prefix):

Field Bytes Meaning version 1 1 flags 1 Bit 0: configured; bits 1\u20132: mode (0 off, 1 rx, 2 rxtx); bit 3: temporary; other bits zero frequency 4 Little-endian unsigned integer, MHz \u00d7 1000, rounded bandwidth 4 Little-endian unsigned integer, kHz \u00d7 1000, rounded SF 1 Spreading factor CR 1 Coding-rate denominator (5 means 4/5)

For example, the MediaMesh preset reports frequency 911150, bandwidth 250000, SF 7, CR 5 and flags 0x05. An active tempradio2 reports its current settings with bit 3 set. Clients use automatic preamble; the trailer does not carry a preamble value.

When radio2 is off, unsupported, or using a foreign protocol, the trailer is only [1][0]. Full responses are therefore 50 bytes with a profile or 40 bytes without one; prefix-only responses are 26 or 16 bytes. Without the request bit, responses remain 38 or 14 bytes. A legacy responder's missing trailer means unsupported, not disabled. Clients should ignore unknown versions and incomplete trailers.

Replies remain on the main radio with zero hops and share the existing limit of four replies per two minutes. Existing visibility controls still apply. Discovery is public and unauthenticated. See control data for the base packet fields.

"},{"location":"radio_profiles/#channel-spacing","title":"Channel spacing","text":"

When setting a saved, temporary or scheduled second profile, the response warns if its occupied bandwidth overlaps the primary channel or comes within 6 kHz of either edge. This check uses both channel bandwidths, regardless of SF or coding rate. The change still applies. The warning also appears when reading the second profile, including while the switching timing self-test is pending.

The warning suggests a nearby center frequency with the same bandwidth, SF and coding rate that clears the buffer. It chooses the nearest whole-kHz frequency on the requested side of the primary channel (above for identical centers), checks the radio's supported frequency range, and tries the other side if needed. The suggestion is not applied automatically.

For example, with radio at 910.525 MHz / 62.5 kHz, a second 62.5 kHz profile at 910.550 MHz produces:

WARN radio2 overlaps radio1 (6kHz guard); try 910.594MHz\n

The center spacing must exceed (primary bandwidth + second bandwidth) / 2 + 6 kHz; touching that boundary also warns. Frequency comparisons use the stored RF centers rounded to the nearest Hz. This is a channel-spacing aid; choose the suggested frequency only when it also suits the network and local band plan.

Mode Receive on profile 2 Transmit on profile 2 rx Yes Only infrastructure replies explicitly configured with tx.reply ... force rxtx (also rx&tx) Yes Yes off No No

For transmission on both permanent profiles:

set radio2 910.5,500,8,5,rxtx\nset radio2.cross on\n

The same encoded message is queued separately for each allowed transmit profile. Each has its own direct/flood retry slots and channel-busy backoff. A forwarding echo on one profile cannot cancel the other profile's retry. Packet storage and the physical transceiver's airtime budget are shared; exhausted queues can still drop packets.

"},{"location":"radio_profiles/#keep-the-main-channel-during-an-update","title":"Keep the main channel during an update","text":"
set tempradio2 910.5,500,8,5,rxtx,120\nget tempradio2\n

This enables the second profile for 120 minutes (2 hours). Locally generated LoRa OTA requests use the temporary second profile. Infrastructure OTA responses follow tx.reply below (both TX-capable profiles by default). Ordinary local traffic stays on the primary profile by default. Use an OTA-capable build and the usual OTA setup on every participating node; a temporary profile does not add OTA support to firmware built without it.

Local settings take effect after a short reply allowance. On repeaters, immediate remote radio2/tempradio2 changes (including off) wait until their exact reply copies finish, with at least one successfully transmitted. A reply that cannot be queued or whose copies all fail cancels the change. Queued replies have a five-minute deadline; an already-transmitting copy may finish under the normal radio watchdog. This confirms transmission, not reception by the remote client. An active temporary profile keeps its radio generation until those tracked reply copies finish or are retired, but its remaining lease continues counting down during that wait.

Permanent remote changes are prepared in an uncommitted file before replying; the saved configuration is replaced only after reply transmission. If that replacement fails, the requested settings remain pending and the commit retries. An expired temporary profile returns to the last saved configuration during those post-reply storage retries; the uncommitted settings do not become active. Local get radio2.status reports this pending state. Roles without the tracked remote reply path require local USB for these immediate secondary-profile changes.

Temporary periods and schedules live in RAM and disappear on reboot. Waiting for a reply does not extend a temporary lease. Timer expiry also uses monotonic time, so setting the clock backwards cannot extend a temporary session.

"},{"location":"radio_profiles/#repeater-room-server-and-sensor-replies","title":"Repeater, room-server, and sensor replies","text":"

These roles default to tx.reply both: locally generated replies are queued on radio and on an active radio2 that permits TX. This includes replies to remote CLI/login/telemetry requests, ACKs, returned paths, delayed GPIO command completion, room subscription posts, and locally served LoRa OTA catalog, manifest, data, proof, and leaves responses. Each copy has its own retry state. Forwarded packets, periodic adverts, sensor pushes, and OTA requests continue to use the normal crossing rules.

get tx.reply\nset tx.reply both\nset tx.reply both force\nset tx.reply radio\nset tx.reply auto\n
Choice Replies transmit on both Primary plus an active rxtx second profile. Default. radio Primary only. radio2 Second only; no primary fallback. auto Normal radio2.cross behavior, including permanent/temporary isolation. off Neither profile. This also suppresses remote CLI replies and ACKs.

Append force to both or radio2 to allow these replies on an active second profile configured as rx. Without force, rx remains receive-only. Sending the command again without force removes the override. It never turns on radio2 off, changes the saved rx mode, or enables ordinary forwarding/TX there. Existing crossover packet filters still apply to the extra copy.

The explicit choices override radio2.cross, including off and mixed permanent/temporary profiles. radio means the active primary profile, including tempradio; radio2 also includes an active tempradio2. For example, an OTA source can retain its normal channel while allowing OTA and management responses on a temporary receive profile:

set tempradio2 910.5,500,8,5,rx,120\nset tx.reply both force\n

The source must have OTA support and an active temporary-radio session as usual. A node fetching an update still needs rxtx for its OTA requests. Forcing replies does not change OTA's receive gates, scan timing, or preambles.

This setting survives reboot; older saved profiles adopt both without force. The getter reports when the chosen second profile cannot transmit. Already queued replies retain their choice; disabling/changing a profile invalidates stale queued packets. A parameterized repeater tempradio command waits for both admitted reply copies to drain before changing the primary settings; at least one must transmit successfully. Those tracked replies omit alternate paths and transport retries so no late success copy can outlive the handoff. Queue capacity and the shared transceiver's airtime budget still limit delivery. OTA data/proof admission counts both copies against its queue credit and reserves receive capacity before allowing the response.

Companions use the per-contact/channel choices below; tx.reply is an infrastructure setting.

"},{"location":"radio_profiles/#companion-messages-on-both-profiles","title":"Companion messages on both profiles","text":"

To send ordinary Companion messages on both radio and an active tempradio2, configure the temporary profile in rxtx mode and enable crossing:

set radio2.cross on\nget tempradio2\nget radio2.cross\nget radio2.status\n

rxtx permits transmission on the second profile; radio2.cross on copies new messages between the two profiles. TX=a,b reports primary and secondary transmit counts. Crossing also applies to OTA traffic.

The crossing setting is saved and remains enabled after the temporary session ends or the node reboots. Run set radio2.cross off to restore the default isolation between profiles. The firmware picker's Companion tempradio2 instructions include both choices and the status commands.

"},{"location":"radio_profiles/#companion-tx-routing-by-contact-or-channel","title":"Companion TX routing by contact or channel","text":"

Companions can save a transmit choice for each contact and each configured channel. The commands apply to messages sent from the phone app, USB/BLE/TCP Companion clients, and the text terminal:

set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nget tx.channel 0\nset tx.channel #wardriving radio\nget tx.channel #wardriving\n

Channel indices match the app and the terminal's channels list; Public is normally slot 0. Names must match exactly, including capitalization and any leading #. Quotes always select a literal name, so \"1\" is a channel named 1, while unquoted 1 selects slot 1. Quote contact names beginning with key: to distinguish them from public-key selectors. Duplicate names are rejected: use the channel index or a contact's public key instead.

Choice Locally addressed traffic transmits on auto Existing behavior, including radio2.cross and temporary-profile isolation. This is the default. radio The primary profile only. radio2 The second profile only; sending fails if it is off or RX-only. both The primary profile and the second profile whenever the second is enabled for TX. off Neither profile for this contact/channel. Reception stays enabled.

Explicit choices override radio2.cross, including off and the isolation between a permanent and temporary profile. They do not turn on radio2 or change its rx/rxtx mode. For example, leave radio2.cross auto and use set tx.channel 0 both to send Public messages on both radio and an active tempradio2, while other channels keep their default routing.

radio means the currently active primary profile, whether permanent or tempradio; radio2 similarly includes tempradio2. There are still at most two active profiles. Getters show the saved choice and active= for newly initiated messages with the current profile configuration. A temporarily unavailable second profile is reported explicitly.

Contact selection uses the full stored public key after resolving a unique name or key prefix. It never routes by the short on-air destination hash. For key selection, prefix 12\u201364 hexadecimal digits (whole bytes) with key::

set tx.user key:0123456789AB radio2\nget tx.user key:0123456789AB\n

Replace the example prefix with your contact's key; use more digits if it is ambiguous. get tx.user and get tx.channel report override counts and query syntax. To return a target to the global behavior:

set tx.user \"Alice Jones\" auto\nset tx.channel 0 auto\n

A user's setting covers addressed texts, login/CLI/telemetry requests, replies, ACKs, and returned paths to that contact. Thus off also prevents ACKs to that contact. Channel settings cover group text and group data, including an explicitly routed group datagram. Contact choices do not filter other people's messages inside a group channel. Adverts, raw traces, raw packets, and OTA packets without a contact/channel destination retain global routing. Packets and their retries already queued retain the choice used when created; changing or disabling the radio profile still invalidates stale queued work. Message timeout estimates use the selected profiles' airtime, independently of the scanner's current receive profile.

Choices survive reboot and normal phone-app updates to the same contact or channel key. Replacing a channel's key or deleting/replacing a contact clears its choice. They use reserved bytes in existing saved records, so record sizes and the phone protocol are unchanged. Older firmware ignores these bytes and may reset the choices when it saves the records.

"},{"location":"radio_profiles/#companion-media-and-ssh-tx-routing","title":"Companion media and SSH TX routing","text":"

Companions also save independent application-data routes:

get tx.media\nset tx.media radio2\nget tx.ssh\nset tx.ssh radio\n

Both accept auto|radio|radio2|both|off and default to auto. An explicit application route overrides the channel's TX choice for that datagram only; tx.media auto chooses only radio2 whenever its active profile permits rxtx MeshCore transmission, including an active tempradio2. This overrides the channel/global route for recognized media even when an app has never configured tx.media. Otherwise, media uses the normal channel/global route. tx.ssh auto always preserves that normal route.

The explicit radio2 choice never falls back to radio, and requires an enabled rxtx MeshCore profile. both uses the profiles available for MeshCore transmission. off blocks transmission, not reception. Getters show the saved choice and current active= mask; auto shows active=radio2 while media automatically selects radio2, and active=channel/global otherwise, because the effective route then depends on the destination channel (or global rules for raw data). Settings survive reboot; a failed save returns an error and keeps the previous choice. Already queued packets retain their original route.

tx.media recognizes Open TAK photo/voice channel data type 0x5643, raw clip packets 0x56/0x57, and well-formed binary fetch requests 0x72. This covers CMD_SEND_CHANNEL_DATA and CMD_SEND_RAW_DATA, plus visible raw media in CMD_SEND_RAW_PACKET. Fully encoded encrypted group packets submitted through CMD_SEND_RAW_PACKET retain their existing route. Text announcements (VE3:, VM1:, VI1:), text fetch requests (VR3:), ordinary chat, contact ACKs, adverts, OTA, and unrelated raw data keep their normal routing. The format is defined in Open TAK's voice/media wire implementation.

tx.ssh uses a new opt-in application contract, not automatic inspection of encrypted SSH or arbitrary IP tunnels. A tunnel client must use either:

The firmware does not add/remove the prefix, decrypt the tunnel, or run an SSH server. Both endpoints must understand this framing. Untagged existing tunnel apps remain unchanged and are not affected by tx.ssh until they adopt the contract. Other channel data types do not acquire an SSH route merely because their contents start with SSH. Raw packets still require a direct path, including zero-hop; this does not enable raw flooding.

For a media-only side channel, use set radio2 mediamesh with the default radio2.cross off and tx.media auto. Ordinary text can stay on the primary profile without routing every raw application onto radio2. Choose set tx.media radio2 if media must fail rather than use the primary when radio2 is off, RX-only, or owned by another protocol.

"},{"location":"radio_profiles/#esp32-companion-ethernet-over-lora-kiss","title":"ESP32 Companion Ethernet over LoRa (KISS)","text":"

ESP32 Full Companion builds with a supported SX1262/SX1276 modem can carry raw Ethernet on radio2 while the primary profile stays MeshCore. This separate feature is named ethernet, not tx.ssh: it transports the whole Ethernet link, including ARP, TCP acknowledgments, IP fragments and SSH on any port. There is no Ethernet-to-MeshCore forwarding.

The mode is off by default. It requires an explicit RF tuple; it never chooses a frequency or starts WiFi automatically. For a dedicated lab link:

set ethernet.radio 909.5,500,5,5,512\nset ethernet on\nget ethernet\nget ethernet.radio\nget radio2.timing\n

Use an appropriate frequency and permitted power for your network. Both ends must match frequency, bandwidth, SF, coding rate and sync word. Ethernet uses sync word 0x16, explicit headers, normal IQ and a LoRa CRC. The optional preamble defaults to 32 symbols; it can be set explicitly (8\u201365528, subject to driver/timing limits). SX1276 requires SF7 or higher.

Do not assume 32 symbols is enough for a fast SF5/BW500 link. Run get radio2.timing after its switching self-test completes, and accommodate the slower end of the link. The lab example uses a 32.8 ms preamble and was tested on a Heltec V4/V3 pair, not an I/O-expander-controlled board. Both primary and secondary preambles matter if primary MeshCore traffic must also be heard while time-sharing. Longer preambles consume airtime; they do not prevent half-duplex collisions.

On that V4/V3 pair, the actual MeshCore-open SSH backend verified the remote host key, authenticated with a public key, opened a PTY and received exactly 4096 test bytes with a matching SHA-256 check. Radio1 stayed on the USA receive profile (910.525,62.5,7,5); its TX counter remained zero on both boards. Raw Ethernet still had losses, so this demonstrates TCP/SSH recovery, not loss-free RF or a minimum preamble guaranteed for every board. The bench used an explicit test-image TX-frequency lock; it did not transmit test packets on the USA profile.

The settings are saved in a checked atomic NVS blob. set ethernet off closes the host connection and releases only this feature's runtime radio2 lease; it does not rewrite the saved MeshCore profiles. A manually configured radio2 must already match the Ethernet RF tuple, including explicit preamble, and be rxtx; its protocol is borrowed only while Ethernet is enabled. An explicit saved radio2 off, a temporary/scheduled profile, or conflicting r2-/mt- channels prevents automatic takeover. Those channel aliases cannot be added while Ethernet is enabled. Clear conflicts or disable Ethernet first. Only one secondary profile can be active: Meshtext and Ethernet cannot share radio2 at the same time.

"},{"location":"radio_profiles/#attach-a-linux-ethernet-interface","title":"Attach a Linux Ethernet interface","text":"

Connect the Companion to station WiFi. Its station IP exposes a single KISS-over-TCP listener on port 5003, separate from Companion binary TCP 5000, OTA TCP 5001 and the existing text terminal on TCP 5002. Use tncattach on the host:

sudo tncattach -T -H 192.168.1.100 -P 5003 --ethernet --mtu 230 --noipv6 --ipv4 10.10.10.10/24\n

Replace the station IP. Attach the other end with a different IP in the same subnet, then use normal host tools such as ping or SSH. A peer running MeshTNC's Ethernet-over-LoRa mode uses the same raw Ethernet framing; configure its PHY to match this link. No custom SSH prefix or MeshCore packet wrapper is added.

Enable only on a trusted LAN. KISS has no authentication and permits raw RF transmission. The service binds to the station address, not the setup AP; do not expose port 5003 to the Internet. The ESP32 does not run an SSH server: SSH connects the attached host computers.

The host MTU must fit one RF frame: MTU 230 plus the 14-byte Ethernet header is 244 bytes. Ethernet frames of 14\u2013255 bytes are supported; larger frames are rejected, never truncated. There is no firmware fragmentation protocol, compression, retransmission or autonomous Ethernet forwarding. Queues are bounded and host TCP is backpressured. Host disconnects, WiFi/IP changes, disabling and profile changes discard stale queued/partial host traffic; an RF frame already transmitting cannot be unsent.

One transceiver time-shares reception between the two profiles. Short preambles on an unmodified peer can be missed while listening to MeshCore; TCP handles losses but performance is not equivalent to two physical radios. Host/codec and firmware-build tests do not establish an RF interoperability or throughput guarantee. nRF52 and non-Full Companion builds do not acquire this Ethernet service. tx.media and tagged tx.ssh remain independent MeshCore application policies; neither controls this raw Ethernet link.

"},{"location":"radio_profiles/#companion-channel-name-routing-aliases","title":"Companion channel-name routing aliases","text":"

Companions recognize the case-sensitive prefixes r2- and mt-, immediately after the optional leading #. Keep the prefix in the app's displayed name. For MeshCore aliases, the firmware strips exactly one prefix when deriving an automatic hashtag key:

#emergency       -> hashtag key for #emergency\n#r2-emergency    -> the same key for #emergency\n#mt-emergency    -> the same key for #emergency only in MeshCore fallback mode\n

Only the app's automatic, name-derived hashtag key is normalized; a supplied private key is not replaced. Non-hashtag names such as r2-Private also retain their supplied key. Empty or nested prefixes (r2-, #r2-mt-emergency) are rejected. MeshCore hashtag names remain case-sensitive.

When radio2 is configured (rx or rxtx), r2- binds the channel to radio2 for both send and receive, including emergency retransmissions. It overrides global crossing and cannot be changed with set tx.channel; rename the channel to remove the binding. RX-only radio2 cannot send. If radio2 is off/unconfigured, ignore the routing prefix and use ordinary MeshCore routing instead. The key stays the same as radio2 is enabled, disabled, or a temporary window expires.

If both the ordinary and r2- slots have the same key, a packet received on radio2 selects the r2- slot; a packet received on radio1 selects the ordinary slot. Global packet deduplication still delivers each packet once, not once in each slot. Receiving a radio2-only channel on the wrong profile does not mark that authenticated packet seen, so it can still arrive later on radio2.

The saved channel record retains the original displayed name and app-supplied key for editing and sharing. Runtime copies resolve the actual wire key. Repeater/room/sensor hashtag behavior is unchanged; those nodes can join the canonical MeshCore channel (#emergency). Older Companion firmware does not recognize these aliases.

"},{"location":"radio_profiles/#local-emergency-channel-notice","title":"Local emergency-channel notice","text":"

When a Companion app adds #emergency (or #r2-emergency), the firmware seeds its local chat with this notice, using #EMERGENCY as the sender:

#EMERGENCY: This is an emergency channel. Do not use for testing or small talk.\n

This is a local onboarding message, not an RF transmission or an emergency alert. It does not change the node's name. It is added only after the channel is successfully saved, not on every connection, reboot, key edit, or alias rename. Deleting and adding the channel again adds a fresh notice. Queued real messages are delivered first: the notice waits for an empty receive inbox and for held one-key DMs to be released, retrying as the app downloads messages. If later traffic fills the inbox, the local notice is the first channel message eligible for eviction. As with other pending received messages, it is RAM-backed until downloaded by the app; a reboot before download can discard it.

"},{"location":"radio_profiles/#meshtext-channel-chat","title":"Meshtext channel chat","text":"

MESH_ENABLE_MESHTEXT_CHAT enables the text-only Meshtext backend in the Companion. It defaults on for ESP32-S3 Full Companion builds using SX1262 LoRa and for the RAK4631 Full Companion starting with 1.17.1.10 preview3. An ESP-NOW primary and other builds default off. RAK4631 1.17.1.10 preview2 and earlier builds did not include this backend. This is not a replacement for stock peer firmware or a cross-protocol bridge.

In an enabled build, an mt- name always requests Meshtext, even if radio2 is off or setup is blocked. It never falls back to sending MeshCore frames. In a feature-disabled build, mt- remains an ordinary MeshCore naming alias while radio2 is off; with radio2 configured, MT channel changes/sends are unsupported. The r2- MeshCore behavior above is unchanged in either build.

With the region configured, join #mt-longfast, for example, through the app's normal hashtag-channel editor. A recognized preset name selects the stock peer channel name (LongFast) and, only for an automatically name-derived hashtag key, the expanded public AQ== key. This public key is not a secret. An explicitly supplied private AES-128 or AES-256 key stays private and unchanged. A custom name such as mt-Private uses the exact tail (Private) as its wire name and the supplied key; custom wire names are limited to 11 bytes and need a manual radio2 profile or another MT channel with a recognized preset. Preset names are ASCII case-insensitive; the mt- marker is still case-sensitive.

Recognized aliases and modulation are:

Alias tail Stock wire name BW (kHz) SF CR shortturbo ShortTurbo 500 7 5 shortfast ShortFast 250 7 5 shortslow ShortSlow 250 8 5 mediumturbo MediumTurbo 500 9 5 mediumfast MediumFast 250 9 5 mediumslow MediumSlow 250 10 5 longturbo LongTurbo 500 11 8 longfast LongFast 250 11 5 longmod or longmoderate LongMod 125 11 8 longslow LongSlow 125 12 8

The selected region must have room for the bandwidth; for example, 500 kHz presets are unavailable in the supported EU868 plan. Use these CLI commands:

get mt.status\nget mt.region\nget mt.slot\nget mt.hops\nget mt.retry\nget mt.rebroadcast\nget mt.node\nget mt.shortname\nset mt.node !12345678\nset mt.node default\nset mt.region auto\nset mt.region us\nset mt.slot 0\nset mt.hops 7\nset mt.rebroadcast off\nset mt.shortname HALO\nset mt.shortname default\nsend mt.nodeinfo 0\nget radio2\nset radio2 auto\n

mt.region accepts auto, us, eu868, eu433, or anz. auto infers US only from a primary frequency at least 902 MHz and below 915 MHz, EU868 from 869.4\u2013869.65 MHz, and EU433 from 433\u2013434 MHz. The US/ANZ overlap at 915\u2013928 MHz is ambiguous: explicitly select us or anz. mt.slot is a 1-based frequency slot, or 0 for the stock preset-name-derived slot; unavailable slots are blocked. Check get mt.status after configuration; a successful settings save does not mean the MT backend is active.

Outgoing MT messages default to a hop limit of 7. set mt.hops <0..7> changes this budget, and get mt.hops or get mt.status shows it. 0 sends only the initial over-air hop with no forwarding budget; larger values permit protocol-compatible peers to forward within that budget. The Companion itself does not relay MT packets unless explicitly enabled as described below.

get mt.node shows the effective Meshtext sender node ID. By default it is SHA-derived from the MeshCore public identity and remains stable for that identity. set mt.node !12345678 sets a saved override; exactly eight hexadecimal digits are required, with or without the leading !. Input is hexadecimal, not decimal, and is never silently truncated. IDs 00000000 through 00000003 and ffffffff are reserved and rejected. set mt.node default clears the override and restores the identity-derived ID.

The example ID is illustrative: choose an ID unique among your protocol-compatible peers. Collisions can merge different senders into one identity and interfere with packet deduplication. Changing the MT ID does not alter the MeshCore private key, reset the outgoing packet-ID sequence or send automatic traffic. Settings changes are refused while an MT transmission is pending. Neither node IDs nor text labels are authenticated.

set mt.shortname <label> saves a Meshtext-only NodeInfo name of 1\u20134 Unicode scalar characters, with valid UTF-8 and at most 16 encoded bytes. Combining characters count separately. get mt.shortname shows the saved/effective name. Stock NodeInfo accepts only four UTF-8 bytes, so the effective wire name is the longest complete UTF-8 prefix that fits. The saved value is not changed. set mt.shortname default clears the override and uses the last four hexadecimal digits of the effective MT node ID. This does not change the MeshCore node name, add a label to chat text or transmit anything.

After the first chat packet actually finishes transmitting on a configured MT channel, the Companion queues a separate, GPS-free NODEINFO_APP broadcast. Automatic metadata waits for a 250 ms gap after the most recent successful MT transmission, including retries, so a stock peer can re-arm its receiver. Explicit send mt.nodeinfo does not wait for this automatic-metadata gap. This happens once per used channel and sender identity per boot, not for every chat message. A rejected or failed chat send does not trigger it. There is no NodeInfo transmission just from booting, receiving traffic, changing settings or joining a channel, and there is no periodic NodeInfo timer. Changing the node name cancels old-name metadata retries; the next new successful chat announces the updated name without sending from the rename.

send mt.nodeinfo <channel slot> still explicitly queues one standard NODEINFO_APP broadcast on the selected configured MT channel. The slot is the Companion channel index, not mt.slot (the frequency slot). The packet contains the actual sender ID, the global MeshCore node name as its long name (UTF-8-safe truncated to 24 bytes), the effective short name, CLIENT_MUTE by default (CLIENT when mt.rebroadcast is on), and the no-DM flag. Both automatic and explicit NodeInfo omit GPS/coordinates, MAC, public keys and unsupported metadata. The usual radio2 TX-mode, pending-TX, airtime and profile guards apply. The reply says queued, not delivered or acknowledged.

Received NodeInfo is still ignored; this backend does not maintain a node database.

Outgoing Meshtext chat and automatic or explicitly requested NodeInfo set OK to MQTT on by default, allowing a compatible receiving gateway to upload those packets. This does not start an MQTT client, bridge networks, request a reply, or mark the packet as having arrived through MQTT. The 228-byte text limit is unchanged.

Region, frequency slot, hop limit, shortname, node-ID override and rebroadcast choice are saved atomically together; changing one preserves the others. Current 40-byte MT04 settings include all of them. New firmware accepts valid legacy MT01/MT02 settings and defaults the shortname to the sender-ID suffix and the node ID to the MeshCore identity; MT01 also defaults the hop limit to 7. Valid MT03 settings preserve both overrides. All legacy versions default rebroadcasting to off. Older MT firmware cannot read MT04, so downgrading does not preserve access to these settings. Invalid or unsupported stored settings block MT operation rather than silently selecting another network.

Private channels can be provisioned directly through the Companion CLI:

set mt.preset <longfast|shortturbo|...>\nset mt.channel <slot> <mt-name> <32/64 hex digits|AQ==>\nget mt.channel <slot>\ndelete mt.channel <slot>\n

set mt.preset accepts the recognized presets above and explicitly saves a manual rxtx radio2 RF tuple using the current region and frequency slot. It is not an automatic owner: deleting the channel does not remove that saved RF configuration. Set region/slot first; changing them later does not overwrite the manual frequency, so rerun set mt.preset if you intend to retune it.

set mt.channel accepts a 32-digit hexadecimal AES-128 PSK, a 64-digit hexadecimal AES-256 PSK, or the literal AQ== for the public key. It does not accept arbitrary Base64 private keys. The stored format interprets an all-zero upper 16-byte half as AES-128, including in a 64-digit input; use a nonzero upper half for AES-256. Slot numbers are zero-based and build-specific, as in the app. get mt.channel reports name, wire name and AES size, never the PSK. The firmware console masks key input echo; keep your client-side logs and stored keys private as well. SET and DELETE save transactionally and restore the previous channel if storage fails. DELETE applies only to an MT channel, not a MeshCore slot.

For a private channel named Team on a protocol-compatible peer:

set mt.region us\nset mt.preset longfast\nset mt.channel 1 mt-Team <your-32-or-64-hex-digit-key>\nget mt.channel 1\nget mt.node\nset mt.shortname HALO\nget mt.status\nsend mt.nodeinfo 1\nchannel 1 Hello\n

Replace the key placeholder with your actual hexadecimal PSK and send after get mt.status reports active. The peer must use the same preset, frequency, PSK and exact case-sensitive wire name Team; mt- is only the local marker. Alternatively, set mt.channel 1 mt-longfast <your-key> preserves a private PSK but uses wire name LongFast and can acquire automatic RF without set mt.preset, provided radio2 automatic ownership is permitted. This route is available for both 128-bit and 256-bit private keys even when the stock app cannot provision or export the latter.

When radio2 was never manually configured, a recognized preset can acquire an automatic rxtx profile. Its RF tuple is RAM-only and stays available for RX while the required channels exist. Removing the last MT channel, losing its last recognized-preset dependency, or detecting a conflict releases only the automatic profile. Explicit manual RF settings are retained: named aliases must match their BW/SF/CR and fit the selected region, but do not overwrite their frequency. The runtime MT overlay uses its required 16-symbol preamble; the saved manual preamble is retained.

With a Meshtext secondary, an automatic primary MeshCore preamble keeps its normal SF/BW-derived wire length (32 symbols for the USA SF7/BW62.5 preset), so scan recommendations do not silently lengthen normal-network transmissions. An explicit primary preamble is still honored. Automatic sizing for two MeshCore profiles is unchanged; mixed-network reception remains best-effort and time-shared.

set radio2 off is an explicit manual veto, including after reboot. Older saved off profiles are conservatively treated as manual. Local set radio2 auto clears the saved manual RF/off choice and permits automatic ownership after two seconds; it rejects pending changes, leases and schedules rather than cancelling them. get radio2 distinguishes auto-owned, off (manual) and off (automatic allowed). Temporary profiles, scheduled changes and reply commit barriers take precedence and can suspend MT operation. Automatic cleanup does not undo later manual edits.

All MT channels share this one secondary modem profile. Different recognized presets, or mixing any MT and r2- channels, are rejected instead of selecting whichever channel was edited last. Loaded conflicts also block MT traffic. Neither radio2.cross on nor MeshCore channel routes cross MT frames between protocols.

The app uses its existing channel index/text commands and receive events. Outgoing MT chat is plain text exactly as entered, with the sender node ID in the standard radio header. It contains no custom name envelope or NodeInfo. Incoming text uses the exact sender-ID prefix !12345678: before the received payload when passed to the MeshCore app. Changing the shortname only changes future NodeInfo, not chat or the packet's sender ID; set mt.node changes the actual Meshtext sender ID. Neither setting change emits advertisements. A successful send response means queued, not acknowledged by a remote node. This backend does not relay MT packets by default. It does not provide ACKs, administrative commands, direct messages, periodic node-info adverts or a node database. MeshCore emergency forwarding does not apply to MT traffic. Interoperability is with stock peers' PSK channel text using AES-CTR, not AES-CCM. AES-CTR channel traffic is not authenticated; successful decoding is not proof of sender identity.

The codec supports AES-128 and AES-256 and up to 228 UTF-8 bytes of wire text, without an added label/node-ID prefix. The standard app channel editor still provisions 16-byte keys. An MT-only 32-byte binary SET/opt-in GET extension is available for capable clients, with protection against legacy 16-byte edits truncating an existing 256-bit key; see Companion protocol. This does not give stock apps seamless 256-bit provisioning or sharing. Current V3 Companion receive events carry at most 165 text bytes including the node-ID prefix; longer incoming MT text is UTF-8-safe truncated for the app.

Radio1 and radio2 still time-share one physical radio. The device cannot receive either network while tuned to the other or transmitting; long MT airtime, particularly LongFast, can leave substantial MeshCore receive gaps. The compatible protocol's fixed short preamble also does not provide the scan-aware long preamble described below. Do not treat this as continuous coverage of both networks or guaranteed message delivery.

"},{"location":"radio_profiles/#heard-repeat-retries","title":"Heard-repeat retries","text":"

MT channel chat uses the Companion's saved flood.retry.count budget: 0 disables retries, and 1\u201315 allows that many extra transmissions after the initial send. Locally originated MT traffic is hop zero for the flood.retry.path gate, so a gate of 0 still permits it; the group-data gate does not apply to channel text. mt.hops 0 disables heard-repeat retries because peers cannot forward a packet with no remaining hop budget; the original chat and its first-use NodeInfo still transmit normally. Retries keep the exact encoded payload and packet ID, allowing peers to recognize duplicates. They use conservative, airtime-based flood spacing and the normal CAD and airtime limits rather than transmitting continuously.

get mt.retry is read-only diagnostics: it reports retry transmissions, matching heard repeats, exhausted retry sequences, pending work and the configured retry count. Use set flood.retry.count, not this getter, to change the budget.

A matching over-air copy with a lower remaining hop limit cancels the retry sequence. Merely receiving another message, an unchanged-hop copy or a packet with the same ID but different payload does not. A heard repeat indicates a compatible peer forwarded the packet; it does not confirm that a particular person or app received it. This does not add DM delivery receipts or ACKs.

NodeInfo follows the metadata retry cap: at most one extra send, only when flood.retry.count is nonzero and flood.retry.advert is on. Its wait adds one minute to the airtime-based delay, matching the conservative self-advert policy. Turning retry count off cancels retained retries. Channel, identity, settings or radio-profile changes invalidate retained work rather than sending it on a different network. The bounded pending-frame storage may reject a new send when full. Forwarding other people's decoded channel text is a separate, default-off option below. MeshCore repeater prefix/bridge IDs and flood-filter rules are not MT routing rules.

"},{"location":"radio_profiles/#optional-decoded-text-rebroadcasting","title":"Optional decoded-text rebroadcasting","text":"
get mt.rebroadcast\nset mt.rebroadcast on\nset mt.rebroadcast off\n

This saved option defaults to off. mt.maybe_rebroadcast is an alias for mt.rebroadcast; get mt.status also shows the current choice.

When enabled, only broadcast text successfully decrypted and parsed with a configured channel's key, and admitted to the local receive queue, is eligible. Unknown channels, wrong keys, malformed packets, DMs, NodeInfo, GPS and other non-text traffic are not relayed. A packet must have a valid remaining hop budget. AES-CTR is unauthenticated: decoding does not authenticate its sender.

Before queuing a relay, the Companion waits five additional packet airtimes calculated from that packet's actual length and the current radio2 settings, plus the normal flood jitter. Existing CAD, receive-busy and airtime-budget waits still apply; this does not replace or shorten them. Hearing another forwarded copy while waiting cancels the redundant relay. An unchanged-hop retry from the original sender does not cancel it.

The forwarded frame retains the original sender, packet ID, channel and ciphertext; only remaining hops and the relay-node byte change. It is sent at most once, without originated-message retries or an automatic local NodeInfo announcement. At most one relay occupies the existing bounded chat delivery slots, with no additional frame-sized buffer. A new local chat can displace an unsent relay. Full queues skip the relay, and work delayed more than a minute past its due time expires. Changes to the channel/key, identity or radio profile cancel stale work instead of forwarding it on another network. Turning the option off cancels pending relay work; settings edits are refused while a radio transmission is already queued or in progress.

Relays stay on Meshtext radio2. This is not a MeshCore bridge, and ordinary MeshCore forwarding rules remain unchanged. Enabling it does not add periodic traffic or cause NodeInfo to be sent just because something was received.

"},{"location":"radio_profiles/#choose-whether-traffic-crosses-between-profiles","title":"Choose whether traffic crosses between profiles","text":"
get radio2.cross\nset radio2.cross auto\nset radio2.cross on\nset radio2.cross off\n
Active primary Active secondary auto on off (default) radio radio2 Cross Cross Isolated tempradio tempradio2 Cross Cross Isolated radio tempradio2 Isolated Cross Isolated tempradio radio2 Isolated Cross Isolated

This table describes MeshCore profiles. A Meshtext secondary profile is always isolated, including with radio2.cross on.

New configurations default to off. An explicitly saved auto, on, or off setting is preserved across firmware updates and reboots.

These are the global rules used by Companion targets set to auto; an explicit contact/channel choice above overrides crossing for its own outbound packets. Recognized media uses the separate tx.media auto selection described above. Ordinary crossing respects rx. The infrastructure-only tx.reply ... force override above permits locally generated replies on that profile. With crossing allowed, a packet received there may still be forwarded on the primary profile. Existing routing, forwarding and packet-filter settings apply. An RX-only tempradio2 also blocks locally generated OTA requests under auto or off; they do not fall back to the normal primary channel. Use rxtx for a fetching node that needs to exchange requests and data. Infrastructure OTA responses follow tx.reply independently of this crossing table.

"},{"location":"radio_profiles/#mcu-power-saving-while-scanning","title":"MCU power saving while scanning","text":"

On nRF52, set powersaving on also permits interrupt-driven MCU idle during a settled radio2 or tempradio2 listening visit. The RF receiver stays on. This includes a Meshtext LongFast visit (SF11 / 250 kHz), whose minimum visit is about 37.7 ms. No extra command is needed.

The scanner first completes its switch self-test and both noise-floor baselines. It polls again for pending packets, calibration, changed settings, retries, or short visits. All normal USB, logging, bridge, OTA, queue and role sleep guards still apply. An nRF52 periodic interrupt bounds each wait to about 0.977 ms; admission reserves another 0.488 ms before the visit deadline and is rechecked immediately before sleeping. If that wake source is not available, scanning keeps polling.

ESP32 also permits MCU idle during settled long visits. It requires radio GPIO wake and physically stopped WiFi/Bluetooth drivers. Its timer slices are at most 10 ms, reserve 4 ms before the visit deadline, and require at least 2 ms usable time. Absolute deadlines are checked again after wake-source setup; expired visits, pending interrupts or setup failures retain polling. USB, logging, active wireless services and the other role guards remain blockers. Other MCU families retain polling until their timing is validated.

The integer admission gate requires at least 49 RTC2 ticks (about 1.495 ms) remaining after the work checks. A configured visit must also cover the loop work before that point. Eligibility is therefore per visit, not an SF/BW cutoff: a fast SF5 secondary can still idle the MCU when the primary profile gives it a sufficiently long listening visit.

For example, the RAK3401 bench with USA primary 910.525,62.5,7,5,32 and secondary 909.5,500,5,5,rxtx,384 had roughly 9.4/21.3 ms visits. MCU sleep was observed in 35% of 4265 debugger samples with power saving on, versus zero of 2129 with it off. This is a CPU-state spot check, not a current or Ethernet throughput measurement. Enabling power saving changed neither preamble.

Radio RX duty cycling remains suspended while scanning, then its previous setting is restored when radio2 stops. Meshtext's fixed 16-symbol preamble does not have the same sleep margin as a longer MeshCore preamble. Adding RF sleep inside a scan visit would introduce extra blind time, so this optimization saves MCU activity without shortening either listening visit.

"},{"location":"radio_profiles/#preamble","title":"Preamble","text":"

Append the preamble to the end of a command:

set radio2 910.5,500,8,5,rxtx,88\nset tempradio2 910.5,500,7,5,rxtx,120,128\nset radio 909.5,62.5,7,5,32\nset tempradio 909.5,62.5,7,5,120,32\n

Existing primary radio, tempradio, radioat and tempradioat commands accept the optional trailing preamble where those primary commands are supported by the role. Getters include the preamble. Omit it, or use auto/0, to calculate it automatically. Explicit values are used as entered, subject to the radio's limits and safe airtime arithmetic.

Automatic dual-profile preambles round up to a multiple of eight, with a 32-symbol minimum and a 512-symbol maximum. When a second profile activates, the radio first self-tests four successful RX-to-RX switches in each direction using the actual tuples. It defers the test while receiving, transmitting or busy, and never counts a failed switch. The 512-symbol automatic preamble is provisional until the test completes. The largest successful hop, rounded up with a 10% guard, then supplies the per-switch allowance for both the automatic preambles and the scan dwell. There is no fixed switch-time floor. The measurement is reset when either profile changes, including when a tempradio2 session starts or ends; it is not saved to flash. The rapid startup hops briefly interrupt reception on each channel; a new packet beginning during this test may be missed. An already detected packet defers the test instead of being interrupted.

Displayed preamble recommendations and warnings wait until the active pair has run for at least two minutes, measured from its first successful RX-to-RX hop. Four successful hops in each direction are still required. A pair change starts a fresh wait; an unchanged or rejected request does not. Use a temporary lease longer than two minutes if you want its recommendation before it expires.

This is a display-only delay. The bounded startup test, guarded maximum switch budget, automatic preambles, ordinary scanning and MCU-idle behavior do not wait two minutes. Live switch and scan diagnostics remain available, marked pending with wait=Ns until advice is ready. Waiting does not discard an early maximum or replace it with an average. No extra retunes are forced at the deadline; a successful normal hop completes the display wait.

Each idle scan starts with 4.6 symbols on the slower profile, then spends the remaining budgeted time on the faster profile, also with a 4.6-symbol floor. Slower means a longer LoRa symbol (2^SF / bandwidth), regardless of profile number. The slower preamble sets the return deadline. The calculation allows two slow visits within that preamble, reserving the self-tested, 10%-guarded switch time per hop. There is no extra fixed main-loop allowance. Fast-channel transmit preambles include sixteen acquisition symbols. For a primary SF7 / 62.5 kHz profile with a 763 us measured maximum switch (840 us after the 10% guard, from the BLE-on T1000-E trial):

Secondary profile Automatic secondary preamble Previously tested explicit preamble Primary preamble SF9 / 500 kHz 32 32 32 SF8 / 500 kHz 40 64 32 SF7 / 500 kHz 64 80 32

With preamble 32 on that slower profile, its visit is 9.421 ms and the faster visit is 21.667 ms, plus the actual switching time. Increasing the slow preamble also increases the time available on the fast channel. If the symbols are equal in length, the primary profile goes first. Detected packets hold the current channel until reception finishes.

The SF8 / 500 kHz value is calculated from the same formula as every other profile; the older pair-specific 88-symbol floor has been removed. The explicit values come from historical production validation: the final fast-channel attempts each received 50/50 packets at 4.8 slow symbols. Automatic values use the measured switch allowance below. Those explicit-preamble tests used the older 4.8-symbol policy; they do not validate the newer 4.6-symbol slow dwell. The 4.6/0.3 ms two-profile bench received 197/200 at SF7/62.5 + SF8/500 with explicit 32-symbol preambles. The newer four-channel 5.1/6.1/7.7 tests also had misses, so this is a timing policy, not a claim of zero packet loss. The automatic fast preamble covers the blind interval during the slow visit, both switches and the acquisition margin. These settings do not guarantee reception during overlapping packets or long pauses in the firmware loop. Transmitters on each channel also need a long enough preamble. Changing the receiver's setting does not lengthen packets sent by other nodes.

"},{"location":"radio_profiles/#chirp-calculator-and-warnings","title":"Chirp calculator and warnings","text":"

get radio2.timing (alias get radio.timing) reports the active pair in radio,radio2 order: dwell in chirps, recommended preambles in symbols, and the switching/loop allowances. During the two-minute advice wait it reports timing self-test pending, the remaining wait and the current provisional switch allowance/preambles, without a final need= recommendation. On nRF52 boards, enabling radio2 uses RTC2 for both retune timing and scan visits (about 31 us resolution); turning radio2 off stops RTC2 and returns to the usual micros() clock. Previously, the nRF52 micros() fallback counted 1,024 Hz RTOS ticks: a real sub-millisecond retune could be reported as exactly 977 us, and two ticks as 1,953 us. Those old numbers are timer artifacts, not measured RF costs. For SF7/62.5 on radio and SF8/500 on radio2, one live BLE-on read returned:

> chirps=4.60,42.84; need=32,40; switch=706us; loop=0us; test=4/4,4/4 ready; WARN recommended preamble: radio2=40\n

Later good hops in another BLE-on trial raised the measured maximum to 763 us (840 us with the 10% guard); the recommendation stayed at 40 symbols.

The measured switch duration is wall time, so an interrupt during the hop is included. Bluetooth's SoftDevice may still preempt application code; this clock correction does not stop it. Ordinary Companion/USB work in the main loop does not run inside the synchronous retune, but it can delay when the next scan visit begins. The zero fixed loop allowance is not a guarantee of lossless reception under arbitrary application load.

WARN recommended preamble identifies each profile whose recommendation exceeds the standard 32 symbols or its explicitly selected value. For example, with SF7/62.5 on the primary profile, set radio2 910.5,500,7,5,rx,32 initially replies with timing self-test pending. Once active and measured, get radio2 warns:

> 910.500,500.000,7,5,rx,32; WARN recommended preamble: radio2=64; short override: radio2\n

Successful set radio ... replies also include the recommended preamble for each affected profile, calculated with the requested settings. A short override note identifies an explicit value below the estimate; it is not silently rewritten. Warnings are also included in profile settings/getters, scan info, temporary settings and schedule acknowledgments. These preamble-bearing replies also include switch=547us (or the current measured value) after the self-test for that exact profile pair. The first ACK for a new pair says timing self-test pending instead: it is sent before activation, so it cannot contain the future measurement. Query get radio2 after activation to see the measured value beside the resulting preamble. off disables the dual-profile warning. get radio2.status shows self-test progress and the resulting guarded switch budget; its separate max field remains a lifetime diagnostic that can include failed/rolled-back retunes and is not used for calibration. If the recommendation exceeds 512, it additionally reports WARN need >512; an automatic preamble stays at 512, which may not be enough for reliable reception on that pair. Explicit preambles are not rewritten and still use the radio's ordinary validation limits. A timing readout does not change settings or transmit a packet.

The shared RadioProfiles::calculateChirpTiming() function accepts both tuples, optional visit times in microseconds, a per-switch allowance and loop margin. Using microseconds throughout:

T[i] = 2^SF[i] * 1000 / BW_kHz[i]\nslow = profile with the longer T (radio wins ties)\nL[slow] = max(requested_visit_us[slow], ceil(4.6 * T[slow]))\nL[fast] = max(requested_visit_us[fast], ceil(4.6 * T[fast]))\nO    = 2 * switch_us\nC    = L[radio] + L[radio2] + O\n\nP[slow] = round_up_to_8(max(32, 2 * C / T[slow]))\nP[fast] = round_up_to_8(max(32, (L[slow] + O) / T[fast] + 16))\n

Automatic selection uses the minimum visits to choose a preamble, then allocates the fast visit from half the slow preamble's duration. Diagnostics feed the actual allocated visits back into the same function, including longer visits caused by an explicit slow preamble. Too-short slow preambles that cannot fit both minimum visits remain rejected by the existing configuration validation.

The automatic scheduler uses these same role-specific minima and computes:

slow_dwell = ceil(4.6 * T[slow])\nfast_dwell = max(ceil(4.6 * T[fast]),\n                 floor(P_configured[slow] * T[slow] / 2 - slow_dwell - O))\n

For the tested SF7/62.5 + SF8/500 pair with slow preamble 32, this yields 9,421 us slow and 21,197 us fast (41.40 fast chirps) with the 1,075 us guarded switch allowance above. Swapping radio and radio2 swaps the allocations automatically.

Production scheduling and automatic preambles use the self-tested maximum successful switch plus 10%, without an additional fixed loop allowance. Subsequent successful hops can only raise that allowance until the next profile change. The measured recommendation is rounded up to eight symbols and capped at 512 for automatic settings. The calculator and warnings retain the uncapped recommendation so a slow board is not shown as safe merely because it hit the limit. Older V4 tests peaked at 836 us (827 us in the 4.6/0.3 ms run), and Indicator reached 8428 us; these are historical measurements, not default budgets for other boards. The model preserves two slow-profile return opportunities and the existing 16-symbol fast-profile acquisition allowance. These are conservative policies, not a fitted success curve, and neither packet overlap nor arbitrary application stalls can be made safe by a finite preamble recommendation.

See the implementation and validation notes.

"},{"location":"radio_profiles/#schedule-the-second-profile","title":"Schedule the second profile","text":"

Use UTC Unix timestamps (seconds), or +N for N whole minutes from now:

set radioat2 910.5,500,8,5,rxtx,START[,PREAMBLE]\nset tempradioat2 910.5,500,8,5,rxtx,START,END[,PREAMBLE]\nget radioat2\nget radioat2 1\nget tempradioat2\nget tempradioat2 1\ndel radioat2 1\ndel tempradioat2 all\n

Replace START and END with timestamps or +N, and the optional PREAMBLE with a number; omit the square brackets. Each schedule family has four slots. Times must be in the future and within 24 days. Overlapping temporary second-profile sessions are rejected. radioat2 changes the saved second profile when it starts; tempradioat2 restores the saved profile at its end. Pending schedules do not survive reboot.

For example, set tempradioat2 910.5,500,8,5,rxtx,+5,+15 starts in 5 minutes and restores in 15 minutes (10 minutes active). Both offsets are measured from the same node-clock snapshot when accepting the command, not from each other. set radioat2 910.5,500,8,5,rxtx,+5 schedules a saved change in 5 minutes. Offsets must be positive whole minutes. Absolute and relative endpoints can be mixed; bare numbers still mean UTC seconds. Existing clock requirements apply.

"},{"location":"radio_profiles/#return-to-one-profile","title":"Return to one profile","text":"
set radio2 off\n

This saves the disabled state and cancels all second-profile temporary activity and schedules.

set tempradio2 off\n

This ends temporary second-profile activity, cancels its temporary schedules and restores saved radio2. If the saved second profile is off, this returns to one profile. Packets/retries bound to an expired or changed session are discarded.

"},{"location":"radio_profiles/#power-saving-and-diagnostics","title":"Power saving and diagnostics","text":"

Dual mode suspends RX power saving and MCU idle sleep so receive visits can run promptly. The saved RX power-saving setting returns when profile 2 is off. The scan uses normal receive, following the CAD/RX measurements, with the updated 4.6-symbol dwell on the slower channel, followed by the remaining fast-channel visit. It does not use CAD for scanning. The separate CAD check before an initial transmission still follows cad.

SX1262 dual-profile mode keeps the oscillator running in XOSC standby between visits, so each retune does not need to restart the TCXO. It restores the prior standby policy when returning to one profile, together with the saved RXPS setting. Radio reinitialization temporarily uses RC standby until the TCXO has been configured again. Other radio families retain their existing behavior. This does not disable the TCXO supply or its initial startup delay, and it is not conditional on 500 kHz bandwidth. Keeping the oscillator on uses additional power during the short standby intervals.

The switch-time self-test is not a forced settling delay or a proof of RF acquisition. The earlier standalone keep-warm scan experiment averaged about 1.19 ms of overhead per hop (scan-cycle time minus both receive windows, divided by two). That experiment used a leaner receive path; it is not a measurement or guarantee of the production implementation. Measure the target board before shortening its timing margins or transmit preambles.

The production-path XIAO timing test measured 1.434 ms average / 1.467 ms maximum with warm standby, compared with 3.166 ms average using RC standby: about 1.732 ms saved per hop. It covered 12,000 retunes across SF7/8/9 at 500 kHz paired with SF7/62.5 kHz. These measurements include observing BUSY low, but exclude application/UI scheduling and are not a packet-delivery qualification or a guarantee for other boards, especially those using I/O expanders.

A same-image follow-up batching SF/BW/CR into one modulation command reduced warm switching from 1.444 ms to 1.158 ms average (batched maximum 1.198 ms), saving another 0.286 ms / 19.8%. RadioLib's modulation caches and LDRO calculation remain synchronized; frequency, preamble, packet guards, and RX restart are unchanged. A later HIL-only screening tested redundant standby/IRQ/buffer/packet setup and SPI transfer changes. Its fastest candidate measured 0.535 ms mean on XIAO but 8.203 ms on the Indicator, whose radio GPIOs go through an I2C expander. The validated production implementation now opts XIAO S3 WIO and Indicator LoRa into buffered 8 MHz SPI and a guarded fast-RX state machine. The heltec_v4_kiss_modem and Station_G3_ESP32_kiss_modem targets use the same guarded path only for KISS dual-profile scanning; their normal board roles keep their established HAL/timing policy. V4 uses its existing 160 MHz role override and Station G3 uses its base 240 MHz setting, but both still need an exact-image hardware timing check before a nominal 600 \u00b5s target is claimed. It skips redundant standby, IRQ mapping, buffer-base writes and modem queries only during an owned continuous RX-to-RX retune. Preamble/packet parameters (including the IQ workaround), stale-IRQ clearing, RX commands and BUSY waits remain. TX, CAD, sleep, reset, RXPS and failed commands revoke reuse; failed RX startup rolls back the profile. Sleep entry uses standby and wake restores the existing TCXO voltage/delay. Other variants do not opt in to faster SPI or the fast-RX state machine. Within a valid owned fast-RX retune, identical acknowledged SF/BW/CR/LDRO settings now reuse the previous modulation command: frequency-only hops omit SetModulationParams (0x8B). A changed effective LDRO value also forces a write, including when automatic LDRO policy changes. Ordinary setters and lost RX context invalidate that acknowledgement; initial setup, reset, sleep, failed commands and full RX setup conservatively reapply the tuple. Frequency, preamble/packet setup, IRQ clearing and BUSY checks are unchanged. This follow-up has same-image hardware measurements: frequency-only hops averaged 0.452 ms on XIAO and 7.601 ms on Indicator. The mixed-bandwidth timing figures above predate it. Four-channel SF6 scans still missed packets; faster switching is not proof of loss-free acquisition. See production and USB validation for current measurements, lifecycle tests and limitations. The Indicator result shows why each board now measures its own switch time. That measurement adjusts automatic preambles and dwell, but it cannot verify RF acquisition or lengthen packets transmitted by other nodes.

Single-channel noise-floor calibration and RSSI interference comparison are suspended while scanning. Retries use the radio's preamble/header activity indicators. LR2021 extra-SF side detectors are suspended and restored as well.

get radio2.status reports the active second-profile mode, MeshCore RX/TX counts for each profile, switch count, switch failures, longest measured switch and both effective preambles. A long switch time or rising error count indicates that the chosen settings need investigation on that board.

get radio2.scan shows which profile goes first, the two idle receive-window lengths in microseconds, and both effective preambles. It reports off in single-profile mode.

"},{"location":"radio_profiles_validation/","title":"Radio profiles validation","text":"

View this page on MeshCore Docs.

"},{"location":"radio_profiles_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"radio_receive_calibration/","title":"Radio receive calibration","text":"

View this page on MeshCore Docs.

"},{"location":"radio_receive_calibration/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"rak3401_1.17.1.7_legacy24/","title":"Rak3401 1.17.1.7 legacy24","text":"

View this page on MeshCore Docs.

"},{"location":"rak3401_1.17.1.7_legacy24/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"rak3401_mota_chain/","title":"Rak3401 mota chain","text":"

View this page on MeshCore Docs.

"},{"location":"rak3401_mota_chain/#document-moved","title":"Document moved","text":"

This document is now in Research.

"},{"location":"role_feature_switches/","title":"Role feature switches","text":"

View this page on MeshCore Docs.

"},{"location":"role_feature_switches/#feature-switches-by-role-11715-usa-cascade","title":"Feature switches by role - 1.17.1.5 USA Cascade","text":"

Use these settings with the exact board's canonical release image. Former logging, power-saving, FEM-gain, and rotated-display variants are now runtime choices where the hardware supports them. A setting cannot add missing radio hardware, MQTT code, storage, or an OTA partition. Check the download's .capabilities.json and the firmware picker. For exact old device/variant names, search the 1.17.1.5 variant map: it maps all 1,361 previous release entries to 1,325 covered choices or 36 excluded entries. Many old choices share one current image; the command tables below explain how to select their former behavior. Excluded entries have no download.

Search terms are alternative wording for finding these instructions, not CLI aliases. Use the commands in the role-specific tables and examples.

"},{"location":"role_feature_switches/#open-the-usb-web-console","title":"Open the USB web console","text":"

Search terms: USB terminal, serial console, ASCII terminal, browser terminal, web serial.

Open the MeshCore USB web console in Chrome or Edge, connect a data-capable USB cable, close other applications using that port, and select the device. Use 115200 baud when prompted. Full Companion, Repeater, Room Server, and Sensor start with an ASCII USB terminal. Run board, then ver on any role.

If a Companion app has already switched USB to its binary protocol, send:

+++MESHCORE-TERM-START\n

To hand the port back to a Companion app or USB MOTA host, send +++MESHCORE-TERM-STOP, close the console, and connect the app/tool. Dedicated USB/BLE/WiFi Companion images can start in binary mode; use the start token there too. KISS firmware uses its modem protocol and is outside this release.

The linked console runs in your computer's browser over USB. It does not need node WiFi or set webui on. WebConfig is a separate settings website served by supported ESP32 images. Both infrastructure and WiFi Companion WebConfig include a browser command terminal (set wifi.cli on), enabled by default on the LAN. The Companion browser CLI uses the same terminal commands as USB, including contact import, chat, recipient selection, and streaming replies. Full ESP32 Companion also exposes this terminal at TCP port 5002. See the terminal command guide.

"},{"location":"role_feature_switches/#which-old-variant-setting-should-i-use","title":"Which old variant setting should I use?","text":"

Search terms: old firmware variants, restore features, turn features on or off, enable MQTT, disable MQTT.

These controls apply to every board with the corresponding compiled feature; check the hardware requirements below.

Former choice Current image / runtime control USB logging or Station G2/G3 logging variant Same role's canonical image; set usb.logging on / off USB logging, WiFi MQTT, or both MQTT-capable image with USB logging; set logging.output usb, wifi, both, or off Companion WiFi-MQTT variant set mqtt.enabled on / off; configure broker slots through the CLI or WebConfig _ps power-saving variant set powersaving on / off; check get powersaving on every role _femoff / FEM-gain variant set radio.fem.rxgain on / off; separate TX switch where controllable RX boost variant set radio.rxgain on / off on a supported radio iKOKA rotated-display variant Full Companion: set display.rotation 180; 0 restores the board default Separate USB, BLE, WiFi, Ethernet, or Terminal Chat Companion Exact board's Full Companion where listed; supported transports are included, with SenseCAP selection below RS232 repeater variant consolidated into the ordinary repeater set bridge.enabled on / off on a build containing the RS232 bridge ESP-NOW bridge or primary ESP-NOW radio Select the matching hardware/role image first; bridge and primary-radio channel commands differ LoRa OTA / external-storage variant Still choose the exact receiver/storage image and matching bootloader; this is not a software on/off switch

Companion, Repeater, Room Server, and Sensor use the same command names for shared settings. Use get <setting> to check and set <setting> <value> to change them. A command requires its feature to be compiled into the build. set usb.logging on|off changes only USB logging. On MQTT-capable builds, set mqtt.enabled on|off changes only MQTT and keeps broker settings; set logging.output off|usb|wifi|both selects both outputs together. set usb.debug on|off separately saves verbose USB diagnostics. Debug output requires both USB switches on; RAW/RX/TX packet records need only usb.logging. Leave usb.debug off for USB-to-MQTT packet capture.

Current source also includes a saved USB logging watchdog: get usb.watchdog and set usb.watchdog off|on|auto. Missing state defaults to Auto, qualifying from 14 continuous healthy USB stats-polling days, not a cable or open TTY alone. Confirmed client loss can trigger USB-only repair; MCU resets require On and an entire physical-fault tier, growing from one hour to one week. This repairs the node's USB connection, not the host Pi or MQTT service.

"},{"location":"role_feature_switches/#full-companion-commands","title":"Full Companion commands","text":"Setting Enable / select Disable / restore Read back Device power saving set powersaving on set powersaving off get powersaving LoRa RX power saving set radio.rxps on set radio.rxps off get radio.rxps Radio RX boost set radio.rxgain on set radio.rxgain off get radio.rxgain External FEM RX gain set radio.fem.rxgain on set radio.fem.rxgain off get radio.fem.rxgain External FEM TX gain set radio.fem.txgain on set radio.fem.txgain off get radio.fem.txgain ESP32 USB logging set powersaving off, then set usb.logging on set usb.logging off get powersaving, get usb.logging nRF52 second USB logging port set usb.logging on reboot set usb.logging off reboot get usb.logging Verbose USB diagnostics set usb.debug on while USB logging is on set usb.debug off get usb.debug reports saved intent USB logging watchdog (current source) set usb.watchdog auto or on set usb.watchdog off get usb.watchdog; native USB and durable storage required MQTT master set mqtt.enabled on set mqtt.enabled off get mqtt.enabled, get mqtt.running, get mqtt.status ESP32 persistent WebConfig set webui on set webui off get webui ESP32 WebConfig browser console set wifi.cli on set wifi.cli off get wifi.cli ESP32 temporary setup AP start webconfig ap stop webconfig get webui Display rotation set display.rotation 90, 180, or 270 set display.rotation 0 get display.rotation Temporary MOTA radio window tempradio 910.525,250,5,5,120 normalradio tempradio

Saved settings apply immediately unless noted. Rotation and gain controls report unsupported hardware instead of creating that feature. Fresh Full Companion preferences enable device power saving and leave USB logging off; existing saved preferences win after an update. USB debug defaults off, including on upgrade from preferences without that field. Its saved intent survives switching the USB master off. Verbose messages still require compiled debug support; changing this preference adds no missing code and does not require a reboot.

ESP32: logging and binary Companion traffic share one USB port. Turn logging off before handing USB to an app/MOTA host. nRF52: the optional second CDC port is for logs; use the primary port for Companion/MOTA. Adding/removing the second port requires a reboot; the optional suffix shown above requests it. Full Companion includes complete ASCII RAW packet records and decoded RX/TX summaries, independent of verbose debug. The nRF52 log port is output-only; CLI/identity queries belong to interface 00. A USB-to-MQTT program that expects both logs and CLI replies on one port cannot use interface 02 alone.

"},{"location":"role_feature_switches/#mqtt-controls-shared-by-companion-and-infrastructure","title":"MQTT controls shared by Companion and infrastructure","text":"

Search terms: Companion MQTT settings, MQTT on, MQTT off, broker configuration.

Configure a broker through the MQTT tab or with the same commands on either role. For example:

set mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.enabled\nget mqtt.running\nget mqtt.status\n

set mqtt.enabled off disconnects brokers while preserving their settings. set mqtt.enabled on allows configured brokers to reconnect. An enabled switch is separate from a running connection: check get mqtt.running and get mqtt.status. The MQTT tab's Enable MQTT checkbox controls this same saved switch. Slot credentials and presets are available through get/set mqtt1.*, mqtt2.*, and the remaining supported slots; secrets stay masked in the browser. WiFi Companion accepts these commands through its USB terminal and browser CLI; Full Companion also accepts TCP port 5002.

set mqtt1.preset none disables just that slot. Publication controls such as set mqtt.status off do not disconnect brokers. USB logging is independent. Images without MQTT code do not show MQTT cards or accept MQTT controls.

"},{"location":"role_feature_switches/#companion-wifi-bluetooth-gps-and-board-exceptions","title":"Companion WiFi, Bluetooth, GPS, and board exceptions","text":"

Configure ESP32 WiFi with:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nget wifi.status\n

set wifi.powersave min enables WiFi modem sleep; none disables it. max is accepted only where the BLE/WiFi coexistence policy allows it. Read back with get wifi.powersave. This is separate from device power saving and RXPS. The assigned WiFi button/display switch controls WiFi services on supported boards. stop webconfig closes only the portal; it is not a WiFi master switch. On Bluetooth-capable ESP32 and nRF52 Companions, get bluetooth reports the running state and set bluetooth on|off saves the service preference. Plain off requires another active management connection; set bluetooth off force explicitly permits disconnecting the only client. See Bluetooth controls for reply draining and boot-transport restrictions. Ethernet controls remain specific to the board's recipe; there is no universal Ethernet-off command.

T1000-E Companion: USB data takes priority over Bluetooth. Once a computer enumerates USB, the firmware stops Bluetooth advertising and disconnects any Bluetooth client, even if no serial application is open. Bluetooth resumes 500 ms after USB disconnects, unless it was switched off manually. Battery power and a charge-only wall supply leave Bluetooth available. The saved Bluetooth setting, pairing PIN and bonds are preserved. get bluetooth reports the USB pause; set bluetooth on saves the choice for after unplugging.

SenseCAP Indicator Full only: set companion.transport wifi followed by reboot selects WiFi; set companion.transport ble followed by reboot selects Bluetooth. Check get companion.transport. USB remains available. Primary ESP-NOW Indicator images keep their mesh radio in either selection.

GPS-equipped builds: use get gps, set gps on, and set gps off on Companion and infrastructure. The Companion app's gps=1 / gps=0 custom setting controls the same GPS. See GPS tracking for location-sharing settings.

"},{"location":"role_feature_switches/#repeater-room-server-and-sensor-commands","title":"Repeater, Room Server, and Sensor commands","text":"

These roles use CommonCLI. Feature-dependent commands require the relevant hardware/build; MQTT is present in observer builds, and WebConfig is absent from some portable builds. The USB browser console still works without it.

Setting Enable Disable Read back Live USB logging ESP32 1.17.1.5: set powersaving off, then set usb.logging on; other platforms: set usb.logging on set usb.logging off get powersaving, get usb.logging Verbose USB diagnostics set usb.debug on while USB logging is on set usb.debug off get usb.debug reports saved intent Capture RX log to node storage log start log stop log prints the capture locally RS232 / ESP-NOW bridge master set bridge.enabled on set bridge.enabled off get bridge.enabled, get bridge.running, get bridge.type Independent RS232 in combined Full MQTT images set rs232.enabled on set rs232.enabled off get rs232.enabled, get rs232.running Independent ESP-NOW in combined Full images set espnow.enabled on set espnow.enabled off get espnow.enabled, get espnow.running MQTT periodic status publication set mqtt.status on set mqtt.status off get mqtt.status shows connection status MQTT packet publication set mqtt.packets on set mqtt.packets off get mqtt.packets SNMP on supported MQTT infrastructure set snmp on, then reboot set snmp off, then reboot get snmp MQTT raw packet publication set mqtt.raw on set mqtt.raw off get mqtt.raw MQTT receive capture set mqtt.rx on set mqtt.rx off get mqtt.rx MQTT transmit capture set mqtt.tx on (or advert) set mqtt.tx off get mqtt.tx MQTT master set mqtt.enabled on set mqtt.enabled off get mqtt.enabled, get mqtt.running, get mqtt.status ESP32 persistent WebConfig set webui on set webui off get webui WebConfig browser command terminal set wifi.cli on set wifi.cli off get wifi.cli LoRa RX power saving set radio.rxps on set radio.rxps off get radio.rxps Radio RX boost set radio.rxgain on set radio.rxgain off get radio.rxgain Controllable FEM RX / TX gain set radio.fem.rxgain on / set radio.fem.txgain on set radio.fem.rxgain off / set radio.fem.txgain off Corresponding get radio.fem.rxgain / get radio.fem.txgain GPS, when compiled set gps on set gps off get gps

Use set usb.logging on|off on every role. Adding the optional reboot suffix requests a reboot only when changing the USB interfaces requires it, as on nRF52 Full Companion. log start/stop records to storage independently of live USB logging. Use log erase to delete that capture.

Fresh ESP32 Full infrastructure installs default to USB logging off. Upgrades retain the saved choice. With device power saving enabled, an idle node can sleep after the setup AP closes and all radio services stop; live USB logging, an attached native USB host, and active MQTT/ESP-NOW still hold it awake.

For quiet packet capture in current firmware, use set usb.debug off and set usb.logging on. Verbose diagnostics default off for fresh and upgraded preferences without the new field. The debug choice is saved separately; turning logging off, or selecting logging.output wifi, does not erase it. Debug output is effective only when USB logging is on and the image includes the relevant debug code. RAW/RX/TX records remain enabled without debug.

For ESP32 1.17.1.5 USB logging, run these as separate commands in the role's text terminal (or remote admin CLI on infrastructure):

set powersaving off\nset usb.logging on\nget powersaving\nget usb.logging\n

Check for power saving off and USB logging on. This avoids the released ESP32 USB sleep bug, including the G3 report. Both settings are saved; turning logging off later does not automatically restore power saving. LoRa RXPS is independent. nRF52 logging does not need this ESP32 workaround.

With the G3 sleep correction, enabled live USB logging keeps ESP32 USB serviced and blocks light sleep, including when a host closes the port or disconnects. CPU idle/yield remains available. set usb.logging off removes that blocker; an attached native USB host still prevents sleep. On Full infrastructure, set logging.output usb and both enable the same USB blocker; wifi and off remove it. File capture with log start is independent. The original 1.17.1.5 binaries require the set powersaving off workaround described in the release note.

"},{"location":"role_feature_switches/#shared-mqtt-and-logging-output","title":"Shared MQTT and logging output","text":"

Search terms: MQTT settings, MQTT on, MQTT off, logging output, USB and WiFi logging.

On any Companion or infrastructure build with both MQTT and USB logging compiled:

Command USB logs MQTT bridge set logging.output off Off Off set powersaving off, then set logging.output usb On Off set logging.output wifi Off On set powersaving off, then set logging.output both On On

get logging.output reports the selection. Fresh unified Full infrastructure preferences select both; Full Companion starts with USB logging off. Saved settings override these defaults. To toggle only MQTT while keeping USB logging unchanged, use set mqtt.enabled off / on. Neither setting turns LoRa repeating off. Repeater forwarding uses set repeat off / on and get repeat separately. Repeaters can opt in to routed trace diagnostics with set repeat.trace on while repeat stays off; the saved exception defaults off and does not relay other payload types. See repeat-off traces. The selector changes USB master output, not saved usb.debug intent. Direct WiFi MQTT is independent of that USB verbosity preference.

The set powersaving off step above is the 1.17.1.5 ESP32 USB workaround. WiFi/MQTT-only logging does not need it while the Repeater/Room Server MQTT bridge is running: that sleep guard already exists in the released firmware. Use get mqtt.running to check that MQTT is running; an enabled preference alone is not the running state. set logging.output wifi keeps USB logging off and uses that MQTT guard.

For a custom broker on an MQTT-capable Companion, Repeater, or Room Server:

set wifi.ssid MyNetwork\nset wifi.pwd my-password\nset mqtt.iata SEA\nset mqtt1.preset custom\nset mqtt1.server broker.example.com\nset mqtt1.port 1883\nset mqtt.enabled on\nget mqtt.status\n

Set mqtt1.username / mqtt1.password if required by that broker. Configure only as many slots as the board supports. set mqtt1.preset none disables slot 1; other configured slots remain enabled, within the build's supported slot count. Disabled and partially configured slots remain saved across reboot. For a custom server URL, set mqtt1.port 0 clears the port override so the URI supplies it. set mqtt.status off disables status messages, not MQTT itself. See the MQTT reference for presets, TLS, credentials, and slot limits.

"},{"location":"role_feature_switches/#infrastructure-power-saving-and-bridges","title":"Infrastructure power saving and bridges","text":"

Use get powersaving, set powersaving on, and set powersaving off on every role. The preference is saved. Hardware and active USB/network services decide when sleeping is possible; the command names do not change between roles. Device power saving is separate from WiFi modem sleep and LoRa RXPS.

On RS232-capable repeater images, stop the bridge before changing its serial port or baud rate, then restart it:

set bridge.enabled off\nset bridge.baud 115200\nset bridge.enabled on\n

RAK4631: set bridge.uart 2 selects UART2 while the bridge is stopped. Canonical GPS-enabled builds reserve UART1 for GPS; UART1 requires a dedicated GPS-free image. ESP-NOW bridge: use set bridge.channel <1..13> and set bridge.format wrapped / raw where supported. Primary ESP-NOW mesh: use set espnow.channel <1..13> and reboot; this is a different radio setting.

For infrastructure WebConfig on the LAN, use start webconfig / stop webconfig. To force a setup AP on an observer, first run set mqtt.enabled off, then start webconfig ap. When finished, run stop webconfig and restore set mqtt.enabled on if you did not reboot.

"},{"location":"role_feature_switches/#updating-and-sending-mota","title":"Updating and sending MOTA","text":"

Search terms: mOTA, LoRa OTA, update over LoRa, wireless firmware transfer.

All released Full Companions can serve MOTA to other nodes. Close the console and run motatool serve --serial /dev/ttyACM0 --dir ./motas -v on the USB host. See Full Companion instructions for WiFi/BLE source commands and the bounded temporary radio setup.

Role / hardware Start self-update Stop / requirement ESP32 Repeater, Room Server, Sensor with WiFi updater start ota or start ota ap; WiFi starts as needed and WebConfig stops automatically; open the returned URL (normally port 80, /update) stop ota ESP32 Full Companion with two application slots start ota or start ota ap; returned URL uses port 8080, /update stop ota; single-slot Full builds use USB nRF52 infrastructure with Bluetooth DFU start ota enters the Bluetooth update flow Matching application DFU ZIP and board bootloader required Qualified LoRa OTA receiver Follow LoRa OTA directions Exact destination package, storage profile, and overlapping temporary radio windows

For nRF52 OTAFIX installations use the exact board/storage build from OTAFIX 2.4.6. Its retained-RAM handoff is required by the new internal-flash hybrid receiver images. Full Companions are sources and remain normally USB-updated.

The complete infrastructure CLI, role/build matrix, and Full Companion feature guide provide details.

"},{"location":"room_services/","title":"Room services","text":"

View this page on MeshCore Docs.

"},{"location":"room_services/#room-history-personal-mailboxes-and-information-board","title":"Room history, personal mailboxes, and information board","text":"

Room Server firmware keeps one room per radio. Existing app login, chat, catch-up, and Access Control formats remain supported. The information board and local Wi-Fi chat use the browser page below; the stock mobile app does not have an information-board screen.

"},{"location":"room_services/#personal-mailboxes","title":"Personal mailboxes","text":"

The single room also provides personal mailboxes. Each mailbox belongs to a complete public-key identity, independently of the public chat backlog. Regular room readers and writers can choose their own access policy and delivery mode; administrator permission is not required.

Access Who may leave mail Closed (default) The owner only Public Any authenticated room writer or administrator Private Up to eight selected full public-key identities, plus the owner

Read-only users can manage and read their own mailbox but cannot send mail. Room bans still apply to both senders and recipients. A private allow list does not grant room login or writing permission; approved senders must also have room writer access. Reading, acknowledging and deleting mail always use the authenticated owner's identity, never a name or shortened key supplied in a request. Changing access does not delete already accepted mail.

Mailboxes start closed and are created only when the owner saves a policy. There are at most 32 mailbox owners, 16 queued messages across the radio, four queued messages per owner, and eight selected senders per mailbox. Messages contain 1-512 UTF-8 bytes. A full queue rejects new mail without replacing old mail. Available filesystem space can impose a smaller limit.

The server stores mail separately from public chat. Mail is encrypted on each LoRa connection, but is plaintext in the server's filesystem: the room operator is trusted. This is not end-to-end encryption between sender and recipient. Other ordinary users cannot list or read your inbox. Mail bodies are not included in public chat, notices, or the Wi-Fi administrator dashboard. Wi-Fi room access uses local HTTP, so use a trusted Wi-Fi network.

"},{"location":"room_services/#mail-commands-for-regular-users","title":"Mail commands for regular users","text":"

Use an encrypted CLI request to the room, or send the same command as a direct message prefixed with !mail from a normal Companion app. These direct messages are reserved mailbox commands and never become public room posts:

!mail mode private\n!mail allow <complete-64-hex-sender-public-key>\n!mail deny <complete-64-hex-sender-public-key>\n!mail mode public\n!mail mode closed\n!mail delivery mailbox\n!mail delivery chat\n!mail settings\n!mail settings 1\n!mail send <complete-64-hex-recipient-public-key> dog lost\n!mail check\n!mail inbox\n!mail list 1\n!mail read <message-id>\n!mail read <message-id> <next-offset>\n!mail ack <message-id>\n!mail delete <message-id>\n

In a CLI-capable client, use mail ... without !. These commands affect only the requesting identity. CLI list/settings replies use entry cursors; continue at the returned next= while it is below total= or allowed=. List entries show the immutable message ID, byte length and sender key; a short route may show a sender prefix. Reads return readable text with id, off, next and total; continue at next until it equals total. Text chunks end on a UTF-8 character boundary. The route and command framing limit LoRa submissions to less than the 512-byte storage limit; the browser can submit a full 512 bytes.

Reading never removes a message. ack confirms receipt and durably removes it; delete deliberately discards it. Both retain a bounded retry receipt. Exact sender/request/body retries return the original ID, including after receipt or reboot; conflicting reuse of that request ID is rejected. Each owner retains up to eight submission receipts. Queued receipts stay retained; the oldest retired receipt is evicted when needed. A very old retry after its retired receipt has been evicted can enqueue again.

mail delivery mailbox suppresses automatic public chat and topic delivery to that identity, including retries and public backlog counts. It does not prevent explicit public-board reads, change routes or erase chat. Returning to chat resumes from the existing catch-up cursor. A copy already transmitted or queued can still arrive while switching modes. Unreadable saved mailbox policy blocks automatic chat rather than silently enabling it.

"},{"location":"room_services/#sleeping-tracker-example","title":"Sleeping tracker example","text":"

A dog tracker can keep its mailbox private, approve its owner's sender identity, and select mailbox-only delivery. The owner leaves dog lost while the tracker is asleep. At its next scheduled check-in, the tracker authenticates to the room, checks its inbox, retrieves that message in resumable chunks and confirms receipt only after handling it. Lost replies and another sleep cycle do not remove unread mail. Chat history and topics are not downloaded automatically.

Room Server provides the room-side queue and polling protocol. A tracker still needs client firmware that schedules check-ins and interprets authenticated commands. Ordinary mailbox text is never executed as a CLI command, and does not itself enable a tracker, wake it remotely or change its GPS reporting rate. For a reachable owner's Companion, the optional lost-status auto responder can also answer a private Am I lost? question with a saved Yes or No.

The opt-in Sensor dog tracker currently checks a reachable owner's Companion through tagged encrypted requests. It does not yet log in to a Room Server or automatically poll these mailboxes.

"},{"location":"room_services/#wi-fi-mailbox-management","title":"Wi-Fi mailbox management","text":"

Open /room, join with your room credentials, and open Personal mailbox. The page shows your complete browser mailbox address, public/private/closed access, selected senders and mailbox-only delivery. It provides sending, paged inbox metadata, selective body downloads, resumable reads, explicit receipt confirmation and deletion. Unsaved drafts survive failed requests. The page honors saved mailbox-only mode before its initial chat download; Refresh chat remains an explicit choice.

The browser has its own identity derived from its secret token and the server identity. It cannot claim your radio's mailbox using a shared room password. Mail sent to a radio key is retrieved by that radio; mail sent to the browser address is retrieved using that browser token. Keep that token to retain access to the same mailbox. Disconnecting clears downloaded private content from the page's memory; the queued server mail remains.

Administrators have a Mailboxes panel with paged owner metadata, queued counts, access policy, selected senders, blocking, and confirmed deletion or purge. Enter a full owner address to provision a radio's mailbox before it checks in. Policy changes compare the loaded revision, including creation revision zero; concurrent changes require a refresh. The dashboard does not download other users' message bodies. Inbox and administrator index pages use snapshot revisions to reject mixed pages after a concurrent change.

"},{"location":"room_services/#lora-mailbox-protocol","title":"LoRa mailbox protocol","text":"

Authenticated room users send encrypted PAYLOAD_TYPE_REQ requests. The usual four-byte request tag precedes each payload and is echoed before the response. All multi-byte integers are little-endian. Subtype 0x0b is separate from chat catch-up and board subtype 0x0a.

Operation Payload after tag Settings/check 0b 00 + expected revision u32 + allow-list cursor u8 Inbox index 0b 01 + expected revision u32 + entry cursor u8 Read 0b 02 + immutable message ID u32 + offset u16 Send 0b 03 + full recipient key (32 bytes) + body length u16 + complete body Confirm receipt 0b 04 + message ID u32 Delete 0b 05 + message ID u32 Allow sender 0b 06 + expected revision u32 + full sender key (32 bytes) Deny sender 0b 07 + expected revision u32 + full sender key (32 bytes) Access 0b 08 + expected revision u32 + mode u8 (closed=0, public=1, private=2) Delivery 0b 09 + expected revision u32 + mailbox-only u8 (0 or 1)

Start index/settings at revision zero and cursor zero, then use the returned revision on subsequent pages. next=255 means complete. Sends are single requests, without fragmented uploads; the complete body must fit the encrypted request route. Read responses contain raw bytes in chunks of at most 128, reduced as needed by the reply route. Continue by offset plus copied byte count. Reads never acknowledge receipt implicitly. Logical requests allow only zero trailing encryption padding.

Responses begin with subtype, operation and status. Statuses are: 0 success, 1 invalid, 2 storage unavailable, 3 write failure, 4 not found, 5 stale revision, 6 forbidden, 7 full, 8 successful duplicate, 9 conflicting request ID. Full response layouts are documented in RoomMailProtocol.h. Mutations require space for their complete confirmation before writing.

"},{"location":"room_services/#optional-persistent-chat-history","title":"Optional persistent chat history","text":"

History uses RAM by default. From a local console or authenticated remote administrator session:

get room.history\nset room.history on\nset room.history off\nroom.history.clear\n

Enabling persistence first saves the current RAM ring. The newest 32 posts then survive reboot. Disabling retains the archive and stops disk writes; the next enable replaces it with the current RAM ring. Clearing removes disk and RAM posts, leaving the topic and information board alone.

Accepted posts append 192-byte records. At 64 records the journal compacts to the newest 32. Its maximum size is 12,300 bytes; compaction temporarily also needs a verified snapshot of about 6.2 KB. This is bounded recent history, and its filesystem records are not encrypted at rest.

A post is saved before the server sends its ACK or advances the live ring, retry cache, or sender replay timestamp. Full storage and unsuccessful writes retain the previous chat and withhold the ACK. A recoverable incomplete or corrupt final append keeps its verified earlier records. Corruption before later complete records preserves the entire journal and blocks replacement. Unreadable, unsupported, or corrupt primary metadata is also preserved until storage is repaired.

A reboot after storage completes but before the sender receives the ACK can recover a valid post without confirming it to the sender. Exactly-once delivery across that crash boundary is not guaranteed.

Catch-up timestamps stay increasing across restored history and backward clock corrections without setting the radio's RTC from archived posts. New posts use a six-second monotonic delivery delay; restored posts can be delivered immediately. Set the clock correctly for useful displayed dates.

The Companion also withholds a room-message ACK when its receive queue cannot retain the message. Rejected delivery does not advance catch-up, schedule receipt persistence, or emit a new-message notification.

"},{"location":"room_services/#saved-acl-roles-and-moderation","title":"Saved ACL roles and moderation","text":"

An operator's nonzero setperm assignment survives reboot and is protected from automatic room-client eviction. Assigned read-only and read/write users appear in Access Control alongside administrators. Automatic guest-password read/write sessions remain transient; password-created administrators remain retained. The existing permission byte is unchanged.

Guest-password logins do not allocate new durable login replay records, so guest churn cannot fill the bounded replay store. Existing records remain authoritative and are advanced, including guest-password records created by older firmware. This change does not reclaim space in an already full 128-identity replay store. After reboot or transient client eviction, an identity without such a record can have captured guest login and message traffic accepted again. Assign an explicit read/write role with setperm <key> 2 when durable login replay protection is required. Room-post deduplication and per-sender post replay timestamps remain in RAM: captured old posts can also be accepted after a legitimate newer login restores a lost session.

ACL updates use the existing delayed settings save. Allow at least five seconds after setperm before removing power so the assignment reaches storage.

setperm <complete-64-hex-public-key> 1\nsetperm <complete-64-hex-public-key> 2\nsetperm <complete-64-hex-public-key> 3\nsetperm <complete-64-hex-public-key> 0\nroom.ban <complete-64-hex-public-key>\nroom.unban <complete-64-hex-public-key>\nget room.bans\nget room.bans 2\nset room.post.rate 12\nset room.poll.rate 60\nget room.post.rate\nget room.poll.rate\n

Roles are 1 read-only, 2 read/write, and 3 administrator; 0 removes the assignment. Guest-password login cannot demote an explicit administrator or upgrade an explicit read-only user. Empty passwords are not enabled credentials; public passwordless read-only login still requires its existing setting.

The persistent ban list accepts up to 32 complete identities, never names or prefixes. It excludes new logins and existing sessions, including posts, requests, path updates, ACKs, and pushes. It leaves existing posts and ACL assignments intact. Use another authorized administrator or a local console to undo a ban on your own identity.

Rates are per identity per shared monotonic minute. 0 disables a limit (the default); the maximum is 65,535. The post limit covers new chat posts. The poll limit covers room requests, including keep-alive, status, telemetry, ACL and board reads, and authenticated browser operations. Login does not reset counts. Exact accepted retries are not charged again. Old keep-alive retries cannot rewind catch-up or cancel a newer pending push. Rejected posts do not enter the retry cache.

Changing to a different identity creates a different user. These controls complement forwarding and airtime policies.

"},{"location":"room_services/#your-own-return-routes","title":"Your own return routes","text":"

Room readers, writers and administrators can use a private CLI request to inspect or change the room's route back to their own radio:

get outpath\nget outpath path\nset outpath A1B2C3,D4E5F6\nset outpath path\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath direct\nset altpath clear\n

Each hop is 2, 4 or 6 hexadecimal characters; all hops in a path use the same width. direct means zero repeaters. clear enables normal discovery again. outpath flood forces flood replies until you change that selection. The first command word is case-insensitive, and an optional Companion CLI prefix is preserved in the reply.

These commands always affect the authenticated requesting identity. They cannot select another user's public key, change room settings, grant roles, read passwords or run other administrative commands. Active public read-only sessions can manage their own routes too. Bans still block access, and management roles alone do not grant room-user command access.

Commands require encrypted CLI message framing. Typing them as ordinary room chat posts sends chat text instead. Use a CLI-capable client; this feature does not grant administrator status or add a non-admin command screen to the mobile app. No new protocol message type is required.

get outpath path reports the reciprocal route observed after the most recent flood login. It can return > path pending during its one-minute observation window. The read does not change the selected route; ordinary room discovery still learns a live route automatically. A route explicitly selected while waiting is protected from the delayed login PATH. set outpath path selects the observed route, or leaves the selection unchanged if none is available.

When the primary and alternate routes are valid and different, chat, topic updates and CLI/data replies send one encrypted copy along each route. The primary owns ACK tracking, catch-up and retries; the alternate has no separate retry loop. Identical routes send one copy. A missing packet for the alternate does not prevent primary delivery. A flood primary does not send an alternate copy; altpath flood also disables that secondary copy, so prefer set altpath clear when removing it. Keep-alive and post ACKs retain their existing primary-route behavior.

Routes are saved for already-retained ACL entries, including explicit readers and writers and password-created administrators. Ordinary guest-password and public-reader sessions keep route selections only for the current session; changing a route never makes their permissions permanent. Failed saves restore the previous route and report an error. Repeating an unchanged selection or an exact request retry does not cause another settings write.

"},{"location":"room_services/#choose-where-chat-catch-up-starts","title":"Choose where chat catch-up starts","text":"

Room readers, writers and administrators can skip older unread messages for their own radio using the same encrypted CLI request framing as route commands:

get room.catchup\nroom.catchup keep 20\nroom.catchup keep 0\nroom.catchup before 2026-10-10\nroom.catchup before 2026-10-10T18:30:00Z\nroom.catchup before 1791655200\n

keep 20 leaves the newest 20 unread retained messages to deliver and skips the older backlog. keep 0 skips all currently retained unread chat. Own posts do not count toward unread messages. A date skips unread messages strictly older than that cutoff; date-only values mean midnight UTC. A numeric cutoff is a Unix timestamp in seconds. A count larger than the backlog is a no-op.

Skipping advances only the requesting user's delivery cursor. It never deletes room history, rewinds a cursor or suppresses another user's delivery. A future date advances only through actual retained messages; new messages remain deliverable. Pending delivery and retries for a skipped post are cancelled; an outstanding topic remains pending. Delayed ACKs, routine keep-alives and normal reconnects cannot restore the skipped backlog. A copy already queued or transmitted can still arrive; skipping stops subsequent catch-up delivery.

Saved ACL users persist an advanced catch-up cursor through the existing filtered contacts store. Failed saves restore delivery state and report an error. Transient sessions retain changes only for the current session. History retention remains 32 posts; this command cannot recover older discarded chat.

"},{"location":"room_services/#administrator-controls-for-any-radio-user","title":"Administrator controls for any radio user","text":"

From a local console or authenticated administrator CLI session, use a user's complete 64-character public key to edit either route or their catch-up position:

room.user <complete-64-hex-public-key> outpath A1B2C3,D4E5F6\nroom.user <complete-64-hex-public-key> altpath 71CE82,BA09F0\nroom.user <complete-64-hex-public-key> altpath clear\nroom.user <complete-64-hex-public-key> keep 20\nroom.user <complete-64-hex-public-key> before 2026-10-10T18:30:00Z\n

Names and shortened keys cannot select a user. Administrators can change any existing radio user's routes without changing that user's role. Route and catch-up persistence follows the same rules as the user's own commands. The browser dashboard below provides activity, delivery and route details for all radio users; these views require administrator credentials.

"},{"location":"room_services/#local-wi-fi-chat-on-esp32","title":"Local Wi-Fi chat on ESP32","text":"

ESP32 Room Server images with WebConfig have an offline browser room page. It is disabled by default and uses the existing WebConfig listener:

set room.web on\nstart webconfig\n

Open http://<reported-radio-IP>/room. Without a usable router, use start webconfig ap, connect the phone to its access point, and append /room to the reported address. Existing setup AP time limits and Wi-Fi/OTA ownership rules apply. Starting OTA shuts down this listener. Compact images and boards without WebConfig retain LoRa chat and CLI access.

Enter the guest or administrator room password, or leave it empty for explicitly enabled public read-only access. Readers cannot post or edit notices; only administrators can publish or delete them. Each operation rechecks credentials and bans. Use private Wi-Fi: the local page uses HTTP, and network access alone does not grant administrator permission.

Room-browser password checks share one budget across all browsers and tokens. Five newly counted wrong passwords exhaust a 30-second window measured from its first counted failure. Until that window ends, every request with a nonempty password is refused, including correct guest/admin passwords and polls from an already connected admin tab. Correct passwords do not refund failures, and refused requests do not extend the window. The last four wrong passwords are remembered; repeats among those four do not count again. A wrong nonempty password still counts when read-only access admits it as a guest. An empty password never consumes this budget and remains usable for explicitly enabled public read-only access.

This limit belongs to /api/room. The WebConfig admin login at /api/login has its own address limits and shared budget for the same admin password; neither endpoint clears or bypasses the other's limit. The room page does not require a WebConfig session, and this browser limit does not change radio logins. If password requests are blocked, wait for the window to end or use the local console to manage the node.

The browser generates a secret token and its own moderation identity. It does not import a radio private key or inherit a claimed radio key's ACL role. The Access and history panel shows the complete browser identity for banning. Posts share the LoRa chat history and include the display name; combined name and text must fit 151 UTF-8 bytes.

Passwords stay in page memory. The token, sequence and display name are saved locally; use one active room tab per browser identity. Interrupted submissions retry the same sequence and body, then poll the owned result. Retry floors are bounded to twenty minutes and the current boot. The page stops retrying after twenty seconds and preserves an unconfirmed draft. After a restart or uncertain result, refresh and check before submitting again.

Authentication, storage and radio work run on the mesh loop. HTTP callbacks only admit a bounded request and return its owned result. No arbitrary CLI operation is exposed. Disable room access with set room.web off; stop the portal with stop webconfig.

"},{"location":"room_services/#wi-fi-room-administration","title":"Wi-Fi room administration","text":"

Connect to /room with the room administrator password and open Room administration, or visit http://<reported-radio-IP>/room#admin-panel. This management area offers:

The dashboard rechecks administrator credentials for each operation and rejects a write from an earlier boot. Route, catch-up, access, topic and history edits include the loaded value; a changed value requires a refresh instead of silently overwriting someone else's change. Each setting saves independently. Access changes use the existing delayed ACL save; the page reminds you to leave power on for at least five seconds. Removing a saved assignment does not ban guest-password access; use Block to deny that identity altogether.

Readers and writers cannot retrieve these radio-user activity/delivery views or management operations. Losing administrator access clears cached management data from the page. User names, messages and routes are rendered as text.

Choose chat shown in this browser filters the local display to the newest N retained messages or a chosen start date. It changes only the browser view; it does not move a radio user's cursor or delete posts. The administrator's selected-user catch-up controls do change that radio user's delivery position.

"},{"location":"room_services/#information-board","title":"Information board","text":"

Eight persistent article slots have titles up to 63 UTF-8 bytes and bodies up to 2,048 bytes. Index revisions and article versions survive reboot. Bodies use two bounded file banks per slot, streamed verification, and atomic index publication.

Available filesystem space determines how much fits. Small internal nRF52 filesystems cannot hold every article at maximum size alongside persistent history and settings. Leave space for the history snapshot and article/index replacement; a full filesystem rejects the change and retains existing data.

In /room, refresh the notice index and select an article to download. Unselected bodies are not pushed or downloaded. Chunks are at most 128 bytes; completed chunks remain available within the page after a lost connection. A new article version starts a new transfer. The administrator editor checks the version of the loaded article before saving to prevent lost edits.

Small notices also support administrator/local CLI management:

room.board.put 1 Trail conditions|Bridge closed until Saturday.\nget room.board\nget room.board 1\nget room.board.read 1 <version> 0\nroom.board.del 1 <version>\n

Index arguments are zero-based entry cursors; follow the returned next=. Long titles can reduce a reply to one entry. Put uses TITLE|BODY and still obeys the command transport's total length limit; use the browser for a full 2 KB article. Read replies contain bounded Base64 and ID, version, offset, total length and byte count. Continue at offset plus count. Deletion requires a current nonzero version; reusing a deleted slot assigns a newer version.

"},{"location":"room_services/#lora-board-read-protocol","title":"LoRa board read protocol","text":"

Logged-in readers, including read-only guests, can use encrypted PAYLOAD_TYPE_REQ requests to the existing single room. The usual four-byte tag precedes these payloads and is echoed before responses. Multi-byte integers are little-endian.

Operation Payload after tag Index 0a 00 + expected revision u32 + entry cursor u8 Article 0a 01 + article ID u8 + expected version u32 + offset u16

First index page: revision 0, cursor 0. Subsequent pages require the returned revision. Response: subtype, operation, status, revision u32, total count, start cursor, next cursor, then up to two complete entries. Each entry is ID u8, version u32, body length u16, title length u8 and title bytes. Continue while the next cursor is less than the total count; the index is complete when the next cursor equals the total count. An empty board returns total count 0 and next cursor 0.

Article response: subtype, operation, status, ID u8, version u32, offset u16, total body length u16, chunk length u8, then raw bytes. The route's encrypted reply capacity may reduce the chunk. Continue at offset plus length; offset equal to total returns successful EOF. Never combine different versions.

Statuses: 0 success, 1 invalid, 2 storage unavailable, 3 write failure, 4 not found, 5 stale version. A capacity too small for a full header may return only subtype, operation, and invalid status. On stale revision/version, reload the index and restart the affected transfer.

"},{"location":"room_services/#regression-coverage","title":"Regression coverage","text":"

Production handlers and stores are tested across ESP32, nRF52, STM32, Pico and generic filesystem APIs, including full storage, corruption, interrupted publication, queue rejection, ACL reboot/eviction, quotas, clock correction and allocation failure. ARM checks verify the new counters fit the existing client union.

The pinned official app JavaScript consumes real firmware room frames and login responses. Chromium executes the bundled browser page with real backend response fixtures, testing untrusted text, lost replies, draft preservation, selective reads, resume and UTF-8 limits. These browser fixtures do not claim a physical LoRa/Wi-Fi transfer.

"},{"location":"rs232_bridge_setup/","title":"Rs232 bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"rs232_bridge_setup/#rs-232uart-bridge-setup-and-diagnostics","title":"RS-232/UART bridge: setup and diagnostics","text":"

Two supported repeaters can exchange MeshCore packets over a wired UART link. Each repeater can use the LoRa settings of the network on its side:

LoRa network A <--> Repeater A <-- serial link --> Repeater B <--> LoRa network B\n

Use the exact board's repeater image with RS-232 support. Check get bridge.type: its answer must include rs232. This transport is optional; its presence in an image does not mean its UART is enabled. A Companion image or a room-server build without RS-232 does not gain the transport from these commands.

"},{"location":"rs232_bridge_setup/#configure-the-serial-link","title":"Configure the serial link","text":"

Use the UART and pin map for the exact board and image. Connect the two sides with the appropriate serial wiring and any required line drivers; a board's UART pins do not automatically provide RS-232 voltage levels. Avoid a UART already occupied by another peripheral.

Open each board's USB console. Run the following on both boards, using the same baud rate:

set rs232.enabled off\nset bridge.baud 115200\nset bridge.source tx\nset bridge.delay 500\nset repeat on\nset rs232.enabled on\nget rs232.enabled\nget rs232.running\nget rs232.status\nget rs232.transport\n

Use bridge.uart only where the board offers a supported choice, while RS-232 is stopped. Canonical GPS-enabled RAK4631 builds use UART2 (set bridge.uart 2); UART1 requires a compatible GPS-free image. The command rejects UART choices unavailable in the build or unsafe for an active peripheral.

rs232.enabled selects the UART independently of MQTT and ESP-NOW in combined images. The historical bridge.enabled alias can select ESP-NOW in MQTT images, so use the transport-specific rs232.enabled control for these checks.

bridge.source tx bridges packets transmitted on LoRa after the repeater's forwarding decisions. An existing bridge.source rx setup is also valid: it bridges received packets before those decisions. Normal scope, filters, duplicate suppression and repeat settings still affect mesh delivery. Configure each side's LoRa network separately; see the bridge CLI reference.

"},{"location":"rs232_bridge_setup/#distinguish-power-state-from-transport-activity","title":"Distinguish power state from transport activity","text":"

get rs232.status retains the existing heartbeat and automatic-standby report. It separates saved intent, actual UART state, standby/probe phase and confirmed versus legacy/unknown peer liveness. get rs232.transport adds the serial transport diagnostic view. A running UART means local startup succeeded; it does not prove the other board received a frame. With RS-232 explicitly disabled, its transport/counter commands may report diagnostics unavailable because there is no current UART bridge object.

Heartbeat and negotiated automatic standby are on by default. A fresh, session-matched heartbeat reply proves bidirectional communication with a supporting peer. Ordinary incoming mesh data proves only inbound reachability. See heartbeat and automatic standby before changing power settings. A diagnostic probe does not replace heartbeat negotiation, enable a disabled UART or establish peer authentication.

"},{"location":"rs232_bridge_setup/#test-each-serial-direction-without-lora-forwarding","title":"Test each serial direction without LoRa forwarding","text":"

Updated RS-232 bridge images include runtime diagnostics without a BRIDGE_DEBUG build flag or continuous USB logging. On both boards:

rs232 resetstats\nget rs232.transport\nget rs232.stats test\n

On A, send one probe:

rs232 test A1B2C3D4\n

OK - test queued id=A1B2C3D4; confirm on peer confirms local queueing only. On B, read get rs232.stats test: rx must increase and rxid must match A1B2C3D4. Then send rs232 test B1C2D3E4 on B and confirm that ID on A. These are two one-way tests. Local UART write completion alone is not peer receipt.

The optional ID is exactly eight hexadecimal digits, without 0x; omitting it uses uptime in milliseconds. Probes are transport diagnostics, not security tokens or MeshCore packets. They do not prove normal mesh delivery or authenticate a peer. There is no automatic echo, retry or LoRa forwarding. Older firmware ignores the extension and cannot report a received ID; use an updated peer on both sides. Sending a test and resetting counters require a local console and do not save preferences.

A write deadline records one failure. An entirely unsent probe is cancelled; if the UART already accepted a prefix, the bridge retains and drains the original suffix before starting another frame. Each main-loop pass does bounded work, without retransmitting the prefix. This avoids leaving an older receiver partway through a control frame. A late completion can increment sent while the earlier fail remains; confirm receipt using the other board's rx change and matching rxid.

"},{"location":"rs232_bridge_setup/#locate-a-mesh-forwarding-failure","title":"Locate a mesh forwarding failure","text":"

If both probe directions work, send one fresh identifiable message from a Companion on network A and compare these pages on both repeaters:

get rs232.stats\nget rs232.stats rx\nget rs232.stats tx\nget rs232.stats drops\n

Repeat in the other direction with a different message. Compare counter changes for the attempt rather than treating old totals as its result. Admission to the mesh means queued for normal processing; it does not prove a completed LoRa transmission or end-to-end delivery. If both repeaters already heard a message directly on LoRa, duplicate suppression can reject a later serial copy.

The RX page separates framing, checksum, packet parsing, partial-frame timeout and mesh-pool pressure. The TX and drops pages distinguish UART backpressure from stopped/busy state, filters and duplicate suppression. Byte counters include heartbeat and diagnostic traffic as well as mesh frames; they do not measure mesh payload alone.

Counters belong to the current bridge object. They survive automatic standby, but disabling or reconfiguring RS-232 can replace the object and clear them. Reboot and rs232 resetstats also start a new observation window.

The RS-232 diagnostic CLI reference describes the status fields, counter pages and their observation window.

"},{"location":"rs232_bridge_setup/#optional-short-usb-debug-capture","title":"Optional short USB debug capture","text":"

Record both get usb.logging and get usb.debug, then enable both gates with set usb.logging on and set usb.debug on. Send the probes or one fresh mesh message and restore both original settings afterward. The ordinary image emits bounded RS232: updates from its main loop at most once per second when diagnostic state changes. A busy USB console can skip an update; use the counter pages for exact values. Neither logging gate is required for the CLI counters or test probes.

These diagnostics identify local serial activity and rejection stages. They do not certify a physical cable, line driver, or end-to-end LoRa path without the corresponding peer and message checks.

"},{"location":"s3_memory_soak_validation/","title":"S3 memory soak validation","text":"

View this page on MeshCore Docs.

"},{"location":"s3_memory_soak_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sensor_tracker/","title":"Sensor tracker","text":"

View this page on MeshCore Docs.

"},{"location":"sensor_tracker/#sensor-dog-tracker","title":"Sensor dog tracker","text":"

Dog tracker mode is an optional mode in the Sensor firmware. It acquires a new GPS position, sends an encrypted check to one configured owner's Companion, and waits for that owner's saved lost status. The owner's answer controls the tracker's reporting rate and awake window.

Tracker mode starts off. It is compiled by default for GPS Sensor builds on nRF52, ESP32, and RP2040; compact STM32 builds exclude it by default. A build can override MESH_ENABLE_SENSOR_TRACKER. GPS hardware must be detected and available for position acquisition.

"},{"location":"sensor_tracker/#set-up-the-owner","title":"Set up the owner","text":"

Use a Companion build with lost replies enabled. Add the tracker as a saved contact on the owner's Companion, using its full public key. The responder accepts saved Chat or Sensor contacts for the encrypted tracker check.

Set the answer locally on the owner's Companion:

set lost.reply no\nget lost.reply\n

Use set lost.reply yes when the dog is lost. Use set lost.reply off to return an unknown status. These are owner settings; an incoming tracker check cannot change them. A normal private Am I lost? message also receives the saved answer where the text responder is available, but the automatic Sensor tracker uses an encrypted request with a matching response tag.

The owner must be reachable during the tracker's receive window. A Room or public message board does not substitute for the owner's Companion in this mode.

"},{"location":"sensor_tracker/#set-up-the-tracker","title":"Set up the tracker","text":"

Configure the Sensor through its local text console. Replace the key below with the owner's full 64-character hexadecimal public key:

set tracker.owner <owner-public-key>\nset tracker.interval 1800\nset tracker.lost.interval 60\nset tracker.path direct\nset tracker.mode dog\nget tracker status\n

Check clock on both devices before enabling the mode. If needed, use time <current-UTC-epoch-seconds> to set the tracker clock. The owner's Companion accepts map coordinates only when the request time is within five minutes of its own clock. Tracker acquisition waits for the provider's validated multi-sample GPS clock sync as well as its position, allowing cold-start clock recovery within the acquisition window. If clock sync cannot finish, an operator can still set the clock locally. Saved routing limits do not reset to grant floods after clock loss.

direct selects a direct radio path with no repeaters. If a repeater path is known, use comma-separated repeater prefixes, for example set tracker.path aa,bb or set tracker.path aabb,ccdd. All prefixes must have the same length: one, two, or three bytes. set tracker.path clear keeps the path unknown; the flood fallback delay still applies. Encrypted path replies can update the retained path.

Command Meaning get tracker status Show mode, saved owner status, intervals, flood lock, clock and storage state. get tracker.owner Show the configured owner's full public key. get tracker.path Show whether the route is direct, saved, or unknown, and its hop count. set tracker.owner <full-key> Save the destination identity. set tracker.mode dog Enable automatic checks and tracker power control. set tracker.mode off Stop automatic checks and restore normal Sensor operation. set tracker.interval <seconds> Normal interval, 60 to 86400 seconds; default 1800. set tracker.lost.interval <seconds> Lost interval, 60 to 86400 seconds; default 60. set tracker.path <hex\\|direct\\|clear> Save a known path or mark it unknown. tracker check Schedule a check now, if no check is already pending.

Tracker configuration and routing checkpoints use a separate durable store. Mode changes do not replace the saved manual GPS or power-saving preferences. If storage cannot safely retain configuration or a flood reservation, automatic tracker checks fail closed. Volatile filesystem recovery is unsuitable for this mode because a restart could otherwise reset the flood limits.

"},{"location":"sensor_tracker/#reporting-and-sleep","title":"Reporting and sleep","text":"

Each check asks the GPS manager for a new position and validated GPS clock sync. It discards the previous parser fix and previous time-validation samples, then stops once both are ready or after 120 seconds. An old cached position is not reported as a new tracker fix. If acquisition gets a position but clock sync times out, the fresh position is retained. If no position is acquired, the encrypted check marks the failure and carries no position. A retained parser fix or one unvalidated time sample cannot set the tracker clock.

The tracker then allows 20 seconds for the tagged owner response. No uses the normal reporting interval. Yes uses the shorter lost interval and requests a 120-second awake window, renewed by further Yes responses. This permits the tracker to remain reachable while the owner continues to report it as lost. An unknown answer does not replace the last saved Yes or No. Intervals run from the completed check; GPS acquisition and the response window add time.

Between active windows, tracker mode pauses radio reception when there is no queued radio work, temporary radio setting, update operation, or active console that needs service. The radio resumes before the next check. Unsolicited messages sent while reception is paused cannot be received; the owner should set the status and wait for the next check. A delivery ACK alone does not contain the owner's lost answer.

Tracker mode temporarily suppresses ordinary periodic advertisements, subscriptions, and Sensor alerts. Manual requests can still be served while the radio is awake. Leaving tracker mode restores the normal Sensor behavior.

This uses idle or light sleep while retaining runtime state. It does not use the nRF52 battery-protection SYSTEMOFF path described in nRF52 power management. Receiver power control depends on the hardware: T1000-E retains the GPS backup supply for faster fixes, and shared GPS power rails may prevent physically cutting receiver power.

nRF52 tracker sleep arms a real one-shot software timer before each System ON wait. Failure to allocate or queue that timer keeps the loop polling. Sleep chunks are at most 30 seconds so clock and watchdog housekeeping continue. ESP32 light sleep uses the board's existing timer wake. RP2040 uses the core's interrupt-driven idle wait; its physical tracker wake behavior is not yet qualified by these host tests.

"},{"location":"sensor_tracker/#flood-fallback","title":"Flood fallback","text":"

Automatic tracker checks retain a known direct path and try it while the owner cannot be reached. A failed check starts an outage; further failures do not restart that outage's six-hour clock.

The cutoff and flood checkpoint survive sleep and restart. Invalid or backward clock changes prevent fresh flood eligibility. A forward GPS or manual clock correction starts another conservative six-hour quiet wait; it does not reset the outage or the flood checkpoint. Repeater observations and unrelated public traffic cannot unlock a stopped tracker. Ordinary Room responses, login traffic, and manual messaging keep their existing routing.

"},{"location":"sensor_tracker/#encrypted-check-format","title":"Encrypted check format","text":"

This uses the existing encrypted peer PAYLOAD_TYPE_REQ and PAYLOAD_TYPE_RESPONSE datagrams. Request subtype 0x0C is inside the encrypted body; it is separate from the packet header's OTA payload type. All multi-byte fields are little-endian.

Request field Bytes Meaning Tag 4 RTC-based request identity, durably increasing across reboot. Subtype 1 0x0C. Version 1 1. Flags 1 1 for fresh GPS, 2 for failed acquisition, or 0 for no position; other flags are rejected. Latitude 4 Signed degrees times 10,000,000. Longitude 4 Signed degrees times 10,000,000. Battery 2 Millivolts; carried for reporting, not a lost-status decision.

The request has 17 logical plaintext bytes and zero padding to 32 bytes. Latitude and longitude must be zero when no fresh position is present.

Response field Bytes Meaning Tag 4 Exact request tag. Subtype 1 0x0C. Version 1 1. Status 1 0 Unknown, 1 Safe, 2 Lost. Awake lease 2 Seconds; the Companion requests 120 for Lost and zero otherwise.

The response has nine logical plaintext bytes and zero padding to 16 bytes. The client authenticates the configured owner's complete public key, requires the pending tag within its receive window, and caps an awake lease at 300 seconds. Short, oversized, nonzero-padded and unknown-version bodies are rejected. An encrypted returned-path packet may bundle the same response and provide a route for subsequent direct checks.

"},{"location":"sensor_tracker/#remaining-work","title":"Remaining work","text":"

This mode implements GPS checks and an owner's saved lost answer. It does not provide Bluetooth pings every 30 seconds, an automatic Room mailbox poller, public-board subscriptions, or a mailbox command interpreter. Room private mailboxes and the public board remain separate features. Mail text is not executed as a tracker or console command.

Battery life and reliable wake behavior still need measurements on the actual hardware, including GPS reception, repeater availability, and the chosen reporting intervals.

"},{"location":"separated_radio_modulation_cache_validation/","title":"Separated radio modulation cache validation","text":"

View this page on MeshCore Docs.

"},{"location":"separated_radio_modulation_cache_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_10_vs_100hz_full_sample_validation/","title":"Sf10 10 vs 100hz full sample validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_10_vs_100hz_full_sample_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_10hz_cr_detour_validation/","title":"Sf10 10hz cr detour validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_10hz_cr_detour_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_full_retune_twice_validation/","title":"Sf10 full retune twice validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_full_retune_twice_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_nrf52_double_write_settling_validation/","title":"Sf10 nrf52 double write settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_nrf52_double_write_settling_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf10_rollback_failure_validation/","title":"Sf10 rollback failure validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf10_rollback_failure_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf125_settling_limits_validation/","title":"Sf125 settling limits validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf125_settling_limits_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf5_250_dwell_validation/","title":"Sf5 250 dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf5_250_dwell_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_125_settling_validation/","title":"Sf6 125 settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_125_settling_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_5p1_trace_validation/","title":"Sf6 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_5p1_trace_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf6_channel_scan_validation/","title":"Sf6 channel scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf6_channel_scan_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"sf8_5p1_trace_validation/","title":"Sf8 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"sf8_5p1_trace_validation/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"solarxiao_power/","title":"Solarxiao power","text":"

View this page on MeshCore Docs.

"},{"location":"solarxiao_power/#solarxiao-carrier-profiles-mppt-and-battery-controls","title":"SolarXiao carrier profiles, MPPT and battery controls","text":"

SolarXiao Repeater and QSPI OTA Room Server firmware support the CN3791 Base, older BQ24650 Advanced and LTC4162/INA260 MPPT carriers in Full and Reduced builds. Use USB CLI or the authenticated administrator CLI over LoRa. Ship mode and saved-policy reset require local USB. Commands do not change RF settings.

"},{"location":"solarxiao_power/#automatic-detection-and-manual-overrides","title":"Automatic detection and manual overrides","text":"

There are three power profiles: auto, basic and mppt. Base and Advanced share basic because their onboard power telemetry and software controls are the same; their different analog charger designs require no different driver. The default is auto when no carrier selection has been saved. Saved MPPT selections remain manual overrides, and saved Base/Advanced selections load as basic. Use set solar.board auto to enable detection on such an installation. The firmware environment, hardware/OTA identity and flash layout are unchanged.

get solar.board\nset solar.board auto\nget solar\nget solar capabilities\nget solar readings\nget solar history\n

Auto mode checks the INA260 manufacturer's ID (0xfe = 0x5449) and device ID (0xff & 0xfff0 = 0x2270) at carrier address 0x40. An address ACK alone is not enough: another device such as INA219 can share that address. Detection only reads identity registers; it does not configure/reset either chip. The first probe occurs in the loop after initialization, then retries every five seconds until a valid identity is found. A positive result identifies MPPT for these carrier designs and is retained until reboot or a new board selection. Temporary communication loss does not relabel a previously detected MPPT.

Without a verified INA260, auto reports self-only and keeps retrying. It does not claim to distinguish Base from Advanced: neither has a digital identity, and missing responses can also mean an MPPT power/bus fault. Charger access, saved-override replay and native carrier telemetry stay blocked until a valid INA260 identity is found. XIAO self readings remain available throughout. The MPPT carrier's INA260 was previously read with USB power only; detection does not require the LTC4162 charger to respond at SOLAR/VIN.

Manual settings are available when the physical carrier is known:

Setting Behavior set solar.board auto Verify INA260 before enabling MPPT; otherwise self-only, retrying. set solar.board basic Base/Advanced self battery voltage and MCU temperature; no native I2C access. set solar.board mppt Enable native access without requiring INA260 detection; existing charger write identity checks still apply.

Legacy base and advanced command values are compatibility aliases for basic; they are not separate modes. Readbacks and help use the three canonical names. Existing Base/Advanced records load as basic without being rewritten; new Basic saves use the original Advanced record value, preserving the same self-only behavior across upgrades.

For Base or Advanced, basic reports the common hardware capabilities and live XIAO battery ADC and processor temperature. It does not claim which analog charger is fitted. The existing self telemetry stays on channel 1, with voltage and temperature as different LPP types. It adds no invented charger flags, current, watts, battery NTC temperature or panel measurements. get solar battery aliases the self readings; the MCU temperature is not a battery or charger temperature. If a node already reports battery voltage and MCU temperature on channel 1, this profile adds no further physical measurements. It adds carrier detection, manual overrides and capability reporting; the voltage/temperature history commands also already exist in builds that include telemetry history.

The 2025 Base Standard schematic uses a CN3791 analog charger with no digital interface or INA monitor. Battery voltage reaches the XIAO BAT input. Charge status drives an LED, and the other charger signals are not connected to XIAO measurement/control inputs. Base therefore needs no extra telemetry driver: only self battery voltage and MCU temperature are available, just like Advanced. Base Lite publishes a README but no schematic, so its exact connections are not independently qualified here.

The 2025 Advanced schematic has no INA monitor or digital charger interface. Charger status pins drive LEDs, NTC goes only to the charger, and current/voltage/MPP setpoints use hardware resistors, switches and a solder jumper. An OTA update cannot measure those signals or change those setpoints without additional hardware connections.

Manual Basic mode suppresses native LTC4162/INA260 polling, register reads/writes, replay and native measurements on channels 2..13. get mppt, set mppt ... and mppt ... explain that Basic Base/Advanced layouts have no digital charger interface. Pending auto mode only probes INA260 identity; it does not access the charger. The generic sensor scanner still reserves native addresses 0x40 and 0x68, so it cannot bypass that policy. Optional external sensor channels still start at 14. Historical samples retained from a previous carrier selection describe the old samples, not current sensor availability.

Voltage history reserves native channel IDs 2..5 at boot, even while Auto is pending or Basic is selected. This is schema allocation, not chip access or a measurement. It lets later detection, charger input power or a manual mode change supply valid samples without restarting or clearing unrelated history. Inactive readings remain missing, and empty channels do not appear in populated history results. Native and optional I2C voltage history use the existing get telemetry.volt.i2c <channel> [1-4] reader: four days of 30-minute RAM samples.

Where telemetry history is included, get solar history points to the existing get telemetry.volt [1-7] and get telemetry.temp [1-7] commands: 30-minute samples, seven days in RAM. History starts filling after boot and resets at reboot. Voltage trends can help assess a remote installation, but are not a measurement of solar power, charge current or charge state. Radio/status and history-transfer commands remain the existing role commands.

The selection is stored separately in an eight-byte, versioned checked record at /solar_board, using atomic publication and full public readback. A corrupt, unreadable or unconfirmed selection blocks native access and replay. Self ADC and MCU readings remain available. An explicit successful set solar.board recovers selection. With an unavailable filesystem, the selection cannot be saved and native access remains blocked.

When auto verifies MPPT or a manual MPPT selection re-enables native access, the firmware reloads any retained /solar_power charger overrides. Changing the carrier does not erase those overrides, change RF/PA settings, modify battery protection, or write charger defaults. The INA260 identity verifies the monitor; LTC4162 chemistry and cell-count checks still independently guard charger writes.

Radio target and role selection remain separate from the power profile:

Fitted module Repeater Room Server E22 M30S solarxiao_30S_repeater solarxiao_30S_room_server_lora_ota E22 M33S solarxiao_33S_repeater solarxiao_33S_room_server_lora_ota

All three power profiles work in these role builds. Room uses its existing onboard-QSPI OTA target, identity and application layout, with the matching board/storage-aware OTAFIX bootloader. The older plain *_room_server targets remain separate compatibility/recovery recipes; this native interface is qualified on the QSPI OTA Room targets. No legacy identity is remapped.

The published Base Standard schematic specifies M30S; the pictured 2W Advanced uses M33S. Charger detection cannot identify the PA variant or select its TX power limits.

A remotely deployed node must first receive compatible firmware before these commands can be used. Deployment still requires its installed target and bootloader compatibility to be checked when it is reachable.

See the Room Server and late-detection history validation and three-profile validation. This adds no measurements beyond existing self voltage/MCU temperature and has not been installed on the remote Advanced board.

"},{"location":"solarxiao_power/#mppt-carrier-commands","title":"MPPT carrier commands","text":"

Start with:

get mppt\nget mppt battery\nget mppt power\nget mppt alerts\nget mppt saved\nget mppt fields 0\n

fields returns three names per page with rw, ro or locked. Increment the page to enumerate the register interface. get mppt <field> returns units and the raw register. get mppt raw 0x43 reads a documented register; arbitrary register writes are not exposed. nan in a summary means unavailable, not zero.

"},{"location":"solarxiao_power/#identity-and-readings","title":"Identity and readings","text":"

The carrier bus uses SDA P0.09 and SCL P0.10. INA260 at 0x40 must match its TI manufacturer and device IDs. Its signed current, voltage and power readings do not require charger input power. The driver does not reset/configure INA260. LTC4162 at 0x68 uses little-endian SMBus words. Access normally requires power at SOLAR/VIN; USB alone can leave the charger unavailable.

get mppt info reports the chemistry/profile code, live cell count, last observed nonzero cell count, MPPT, JEITA, charger state and telemetry validity. The actual bench board identified as FST (chemistry code 6); this distinguishes it from FAD, FFS, Li-ion and SLA variants. The ID does not uniquely identify the M suffix or package/temperature grade. Cell count comes from hardware straps; zero means unknown while the charger is inactive. Software cannot change chemistry/cells.

Readings cover battery/input/output voltage, signed battery/input current, charger die and modeled NTC temperature, battery series resistance, charge/CV/ absorb timers, JEITA region, actual DAC settings, system/status bits and all three alert banks. A retained ADC bank is not presented as live while telemetry is off. Battery ADC voltage/resistance need a currently valid cell count. Questionable resistance and an open/disconnected NTC return unavailable in the summaries.

"},{"location":"solarxiao_power/#settings-on-the-4s-fst-carrier","title":"Settings on the 4S FST carrier","text":"

Use set mppt <field> <value>. Booleans accept on or off only.

Fields Behavior mppt.enabled Enable automatic solar maximum power tracking. charge.enabled Suspend/resume charging; does not disconnect the battery or system output. telemetry.force, telemetry.fast Force the charger ADC on, or use fast telemetry. These affect power consumption. input.limit_ma Input current limit; rounds down to the hardware step, never above the request; maximum request 3200 mA. input.minimum_mv Manual input-voltage floor; MPPT must be off. Rounds up in 140.625 mV steps, maximum request 35000 mV. jeita.enabled Select the fixed multi-region JEITA profile or basic temperature-qualified manual targets. charge.current_ma, charge.cell_mv Manual targets require JEITA off. Current rounds down in resistor-dependent steps, up to 3200 mA. This 4S board interface limits voltage requests to 3412.5..3600 mV/cell in 12.5 mV steps. thermal.start_c, thermal.end_c Die thermal regulation thresholds, -40..125 C; start must remain below end. termination.enabled, termination.current_ma C/x termination switch and nonnegative current threshold. termination.cv_seconds, termination.total_minutes 16-bit safety timers; zero disables the corresponding timer. alert.* Voltage/current/die/NTC/resistance thresholds and enabled alert masks; use field names/units from fields.

FST locks the six JEITA thresholds, regional current/voltage values, recharge threshold and absorb voltage/time. These remain readable. Manual target registers are continuously overwritten by JEITA when JEITA is on; read actual dac.* fields to see the active regulation targets. Basic temperature-qualified charging still uses the fixed outer temperature limits when JEITA is off.

All configuration writes require a fresh FST identity and a live cell count of 4 or unknown. Other variants remain readable but cannot receive this board's saved settings. Every setting uses a masked register update and verifies readback before success. Unrelated bits are preserved; one-shot BSR is not replayed by configuration writes. A failed write/save attempts to restore the previous field and reports whether rollback was verified.

"},{"location":"solarxiao_power/#saving-and-actions","title":"Saving and actions","text":"

Successful settings save an explicit override in a versioned, CRC-protected /solar_power record, using atomic file replacement plus public-file readback. The board reapplies only saved overrides after restart or charger power loss, with a five-second retry interval. It does not write default settings on first boot. JEITA-owned targets are not repeatedly written while JEITA is enabled. A failed/corrupt settings file blocks configuration saves and automatic replay.

These actions acknowledge the request, not completion of charging, resistance measurement or entry into ship mode. Read the corresponding status afterward.

See the bench acceptance report for exact firmware identity, measured readouts and test limits.

"},{"location":"solarxiao_power/#conversion-defaults-and-carrier-revisions","title":"Conversion defaults and carrier revisions","text":"

The supplied Standard schematic shows R5/RSNSI and R14/RSNSB at 10 milliohms, a 10 kohm NTC bias and a 10 kohm NTC. It shows an L42M single-cell charger, so it does not independently confirm component values on this 12 V FSTM revision. Defaults use those resistor values and the LTC4162 reference thermistor beta of 3490 K. Readings identify the model as reference until the corresponding calibration values are explicitly set.

Calibration settings, saved without changing hardware target registers:

get mppt cal.rsnsb_mohm\nget mppt cal.rsnsi_mohm\nget mppt cal.ntc_bias_ohm\nget mppt cal.ntc_r25_ohm\nget mppt cal.ntc_beta\n

Set these with the same set mppt syntax after confirming actual components. Allowed sense resistance is 0.1..1000 milliohms, NTC/bias resistance 100..1000000 ohms, beta 1000..10000 K. The simple beta temperature model assumes an unpadded NTC divider. It does not model series/parallel thermistor dilution networks. Changing calibration changes units/conversions, not already saved register codes. INA260 uses its integrated shunt and does not use these sense-resistor values.

"},{"location":"solarxiao_power/#telemetry-channels","title":"Telemetry channels","text":"

The following carrier channels apply to MPPT mode. Advanced mode reports the existing self voltage/MCU temperature on channel 1 and omits native channels.

Carrier telemetry requires the environmental telemetry permission. Channel 1 remains the XIAO internal ADC; it is separate from the 4S carrier battery and its existing startup/low-voltage policy is unchanged.

Channel Values 2 INA260 battery voltage, signed current, power 3 LTC4162 input voltage, signed current, computed nonnegative input power 4 Charger system output voltage 5 Charger battery ADC voltage and signed current 6, 7 Die temperature, modeled NTC temperature 8, 9, 10 Generic sensor integers: charger state, charge status, system bits 11, 12, 13 Generic sensor integers: limit, state and status alert masks 14 onward Optional environmental sensors, in discovery order

Unavailable values are omitted. Voltage history reserves channels 2..5 and marks unavailable samples invalid. The generic sensor scanner cannot claim carrier addresses 0x40/0x68; this prevents an INA219 driver from configuring the INA260 and keeps carrier channels stable across profiles and power states. No state-of-charge percentage or coulomb counting is inferred from voltage.

References: SolarXaio hardware, LTC4162-F Rev A, INA260.

"},{"location":"spiffs_regular_file_reads/","title":"Spiffs regular file reads","text":"

View this page on MeshCore Docs.

"},{"location":"spiffs_regular_file_reads/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"stats_binary_frames/","title":"Stats binary frames","text":"

View this page on MeshCore Docs.

"},{"location":"stats_binary_frames/#stats-binary-frame-structures","title":"Stats Binary Frame Structures","text":"

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"

The CMD_GET_STATS command uses a 2-byte frame structure: - Byte 0: CMD_GET_STATS (56) - Byte 1: Stats sub-type: - STATS_TYPE_CORE (0) - Get core device statistics - STATS_TYPE_RADIO (1) - Get radio statistics - STATS_TYPE_PACKETS (2) - Get packet statistics

"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"

The RESP_CODE_STATS response uses a 2-byte header structure: - Byte 0: RESP_CODE_STATS (24) - Byte 1: Stats sub-type (matches command sub-type): - STATS_TYPE_CORE (0) - Core device statistics response - STATS_TYPE_RADIO (1) - Radio statistics response - STATS_TYPE_PACKETS (2) - Packet statistics response

"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"

Total Frame Size: 11 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x00 (STATS_TYPE_CORE)\n    uint16_t battery_mv;\n    uint32_t uptime_secs;\n    uint16_t errors;\n    uint8_t  queue_len;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"

Total Frame Size: 14 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x01 (STATS_TYPE_RADIO)\n    int16_t  noise_floor;\n    int8_t   last_rssi;\n    int8_t   last_snr;       // Divide by 4.0 to get actual SNR in dB\n    uint32_t tx_air_secs;\n    uint32_t rx_air_secs;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"

Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":""},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x02 (STATS_TYPE_PACKETS)\n    uint32_t recv;\n    uint32_t sent;\n    uint32_t flood_tx;\n    uint32_t direct_tx;\n    uint32_t flood_rx;\n    uint32_t direct_rx;\n    uint32_t recv_errors;    // present when frame size is 30\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n    \"\"\"Send command to get core stats\"\"\"\n    cmd = bytes([56, 0])  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n    serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n    \"\"\"Send command to get radio stats\"\"\"\n    cmd = bytes([56, 1])  # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n    serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n    \"\"\"Send command to get packet stats\"\"\"\n    cmd = bytes([56, 2])  # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n    serial_interface.write(cmd)\n
"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n    response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n        struct.unpack('<B B H I H B', frame)\n    assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n    return {\n        'battery_mv': battery_mv,\n        'uptime_secs': uptime_secs,\n        'errors': errors,\n        'queue_len': queue_len\n    }\n\ndef parse_stats_radio(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n    response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n        struct.unpack('<B B h b b I I', frame)\n    assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n    return {\n        'noise_floor': noise_floor,\n        'last_rssi': last_rssi,\n        'last_snr': last_snr / 4.0,  # Unscale SNR\n        'tx_air_secs': tx_air_secs,\n        'rx_air_secs': rx_air_secs\n    }\n\ndef parse_stats_packets(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n    assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n    response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n        struct.unpack('<B B I I I I I I', frame[:26])\n    assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n    result = {\n        'recv': recv,\n        'sent': sent,\n        'flood_tx': flood_tx,\n        'direct_tx': direct_tx,\n        'flood_rx': flood_rx,\n        'direct_rx': direct_rx\n    }\n    if len(frame) >= 30:\n        (recv_errors,) = struct.unpack('<I', frame[26:30])\n        result['recv_errors'] = recv_errors\n    return result\n
"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n    serialInterface.write(cmd);\n}\n
"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n    battery_mv: number;\n    uptime_secs: number;\n    errors: number;\n    queue_len: number;\n}\n\ninterface StatsRadio {\n    noise_floor: number;\n    last_rssi: number;\n    last_snr: number;\n    tx_air_secs: number;\n    rx_air_secs: number;\n}\n\ninterface StatsPackets {\n    recv: number;\n    sent: number;\n    flood_tx: number;\n    direct_tx: number;\n    flood_rx: number;\n    direct_rx: number;\n    recv_errors?: number;  // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 0) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        battery_mv: view.getUint16(2, true),\n        uptime_secs: view.getUint32(4, true),\n        errors: view.getUint16(8, true),\n        queue_len: view.getUint8(10)\n    };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 1) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        noise_floor: view.getInt16(2, true),\n        last_rssi: view.getInt8(4),\n        last_snr: view.getInt8(5) / 4.0,  // Unscale SNR\n        tx_air_secs: view.getUint32(6, true),\n        rx_air_secs: view.getUint32(10, true)\n    };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n    const view = new DataView(buffer);\n    if (buffer.byteLength < 26) {\n        throw new Error('STATS_TYPE_PACKETS frame too short');\n    }\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 2) {\n        throw new Error('Invalid response type');\n    }\n    const result: StatsPackets = {\n        recv: view.getUint32(2, true),\n        sent: view.getUint32(6, true),\n        flood_tx: view.getUint32(10, true),\n        direct_tx: view.getUint32(14, true),\n        flood_rx: view.getUint32(18, true),\n        direct_rx: view.getUint32(22, true)\n    };\n    if (buffer.byteLength >= 30) {\n        result.recv_errors = view.getUint32(26, true);\n    }\n    return result;\n}\n
"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":""},{"location":"telemetry_decoder/","title":"Telemetry decoder","text":"

View this page on MeshCore Docs.

"},{"location":"telemetry_decoder/#telemetry-decoder","title":"Telemetry decoder","text":"

The decoder runs on the documentation website. GitHub's Markdown preview shows the instructions but cannot run the decoder's buttons or JavaScript.

Paste raw hexadecimal packet data copied from the Let's Mesh packet analyzer to decode a repeater's temperature, battery-voltage, external I2C-voltage, or radio noise-floor snapshot. The decoder also accepts the payload hex without its MeshCore packet header and the Base64 pages returned by repeater or room-server CLI commands. Decoding happens entirely in this browser; pasted data is not uploaded or sent anywhere.

The Source ID in a raw-packet result is the first eight bytes of the sending repeater's public key. Match that 16-character hex value against the public-key prefix recorded for your repeaters. It comes from the telemetry payload itself, so it is available even when only the payload was copied.

"},{"location":"telemetry_decoder/#send-raw-telemetry-packets","title":"Send raw telemetry packets","text":"

From an administrator CLI session, first configure a direct route to the MQTT observer that will receive and upload the raw packets. Use direct when the observer is a zero-hop neighbor:

set data.tx path direct\nsend telemetry.tx now\n

For a routed observer, provide its comma-separated hop hashes instead:

set data.tx path A1B2,C3D4\nsend telemetry.tx now\n

The shared route does not enable telemetry. Automatic telemetry is off on a fresh install; to explicitly keep the route manual-only before sending:

set telemetry.tx schedule off\nsend telemetry.tx now\n

The send telemetry.tx now command queues one TTB1 temperature packet, one TVB1 battery-voltage packet, and IVB1 chunks for every connected I2C voltage channel. TTB1 and TVB1 carry up to 165 samples. Each IVB1 carries 64, so a full 192-point/four-day channel takes three packets. Channels whose retained history is entirely zero are treated as disconnected and are not sent. The command requires at least one collected base sample and works even when the automatic schedule is off.

It also queues a N1B1 noise-floor packet once the main radio has completed a 15-minute history bucket. With radio2 enabled, it queues a separate N2B1 packet for that profile once it has a completed bucket. Each holds the latest 255 positions: up to 63 hours 45 minutes at 15-minute resolution. A position can be Missing; failed measurements do not silently shorten the timeline. The existing automatic transmission cadence is unchanged. Recording a new noise-floor position every 15 minutes does not transmit a packet every 15 minutes.

Schedule changes are saved transactionally; a failed write does not truncate the previous preferences. A legacy route must be successfully migrated into data.tx before its schedule can run. Corrupt, unreadable, or ambiguously committed preferences stop automatic and pending sends. get telemetry.tx shows fault=prefs, and manual sends return an error instead of acknowledging work that cannot run. Such a storage fault must be repaired before settings can be changed; the firmware does not overwrite an unmigrated legacy file.

"},{"location":"telemetry_decoder/#decode-a-packet","title":"Decode a packet","text":"

If you are reading this on GitHub, open the working decoder on the documentation website to paste a packet, load an example, or download CSV.

Enable JavaScript on the documentation website to use the decoder.

"},{"location":"telemetry_decoder/#analyzer-hex-examples","title":"Analyzer hex examples","text":"

The buttons load synthetic, protocol-valid zero-hop RAW_CUSTOM packets. A real snapshot can contain many more samples. Routed packets also contain path bytes before the telemetry payload magic; the decoder finds and validates the payload automatically.

"},{"location":"telemetry_decoder/#temperature","title":"Temperature","text":"
3E00545442311122334455667788800092651E0008000102354A4E5082\n
"},{"location":"telemetry_decoder/#battery-voltage","title":"Battery voltage","text":"
3E00545642311122334455667788800092651E000800010264C8FEFFDC\n
"},{"location":"telemetry_decoder/#external-i2c-voltage","title":"External I2C voltage","text":"
3E00495642311122334455667788800092651E00020800000004019026927109C427107FFF\n
"},{"location":"telemetry_decoder/#radio1-noise-floor","title":"Radio1 noise floor","text":"
3E004E3142311122334455667788800092650F00080044CB77DF\n

All four examples identify the source as 1122334455667788.

"},{"location":"telemetry_decoder/#cli-history-pages","title":"CLI history pages","text":"

The same page continues to decode the padded Base64 returned by these administrator commands:

Data Newest page Older-page example Samples per page MCU temperature get telemetry.temp get telemetry.temp 2 48 (24 hours) Battery voltage get telemetry.volt get telemetry.volt 3 48 (24 hours) I2C voltage get telemetry.volt.i2c 2 get telemetry.volt.i2c 2 4 48 (24 hours) GPS position get telemetry.gps get telemetry.gps 2 24 (12 hours) Main-radio noise floor get telemetry.noise get telemetry.noise 2 Up to 176 (44 hours) Radio2 noise floor get telemetry.noise2 get telemetry.noise2 2 Up to 176 (44 hours)

Paste either the complete reply beginning with > or Base64 alone. For noise replies, the optional two-character CLI correlation prefix is accepted too (01|Base64 or 01|> Base64). For example:

get telemetry.volt 1\n> EkDUcWoeMAAB5+bl5eTj4uLh4ODf3t7d3Nvb2tnZ2NfX1tXU1NPS0tHQ0M/Ozc3My8vKycnI/w==\n

Run get telemetry.volt.i2c without a channel first to list connected channels. For each channel, collect pages 1 through 4. Paste all four reply lines into the decoder at once; it merges their timestamps into one 192-point, four-day table and downloads them as one CSV. The same merging works for the three IVB1 analyzer packets from a full channel. Inputs must have the same telemetry type, source, LPP channel, radio profile, and sample interval.

Noise history retains eight days (768 quarter-hour positions) per profile in RAM and resets on reboot. Page 1 is newest; up to five pages are available as history fills. Paste the replies for one radio together to merge them. Main-radio and radio2 pages remain separate. A full noise CLI page is 117 decoded bytes or 156 Base64 characters, fitting the current CLI reply buffer over USB and administrator LoRa CLI. Radio2 history is read with get telemetry.noise2, including when it is not being transmitted.

An INA3221 exposes its three enabled hardware inputs as three consecutive LPP channels. When it is the only external sensor these are normally 2, 3, and 4, in hardware-input order. Other sensors can shift the numbers, so copy the IDs reported by get telemetry.volt.i2c rather than assuming them.

"},{"location":"telemetry_decoder/#reading-the-table","title":"Reading the table","text":"

Each noise position combines three fresh noise-floor estimates collected roughly five minutes apart within its UTC-aligned 15-minute bucket. If any estimate is -100 dBm or lower, the minimum wins, including a bucket containing both low and high values. Otherwise, if any estimate is -80 dBm or higher, the maximum wins. Otherwise the three estimates are averaged. Minimums round downward, maximums upward, and averages to the nearest encodable value. Values below -120 dBm clamp to -120 dBm; values above -64 dBm become MAX. Fewer than three fresh, valid estimates produce Missing. These are idle RSSI noise estimates, not received-packet RSSI measurements.

GPS history remains available through the administrator CLI, but telemetry.tx never puts GPS in RAW_CUSTOM packets. Location data therefore must come from a CLI Base64 page rather than analyzer hex.

For the byte-level layouts, see Read repeater telemetry history.

"},{"location":"terminal_chat_cli/","title":"Terminal chat cli","text":"

View this page on MeshCore Docs.

"},{"location":"terminal_chat_cli/#terminal-chat-cli","title":"Terminal Chat CLI","text":"

Below are the commands you can enter into the Terminal Chat clients:

For a timed RF carrier test on SX1262 or LR1110, use cw on 2.5 or cw2 on 2.5, then cw off to stop early. See carrier-wave RF checks for profile selection, limits, and status commands.

"},{"location":"terminal_chat_cli/#companion-wifi-browser-terminal","title":"Companion WiFi browser terminal","text":"

On ESP32 WiFi Companions with WebConfig, open the node's LAN IP address and select CLI. The browser runs the same commands as the USB terminal and Full Companion's TCP terminal on port 5002, including contact import, contact selection, messages, remote login/commands, and delayed RF replies. Run help to list commands compiled into the device. Enable the tab with set wifi.cli on and sign in with the node admin password. The setup AP does not expose this full terminal.

For example, copy the complete meshcore://... string from card on another Companion, then run these commands on the receiving Companion:

import meshcore://<full-contact-card-data>\nlist\nto <contact-name-or-prefix>\n

Replace the placeholders; a bare public key is not a contact card. Import is queued for signature validation, so use list to confirm the contact appears. Explicit import works with manual contact addition enabled and bypasses the automatic discovery type/hop filters. It still respects contact storage limits; a full contact table follows the configured overwrite policy. to remains selected for subsequent send, path, login, and cmd commands within this terminal session.

Only one USB, TCP, or browser text terminal owns the session at a time. An idle startup USB prompt can be borrowed by WiFi. Use disconnect or the browser's disconnect button to release it. Closing the page releases the session; if the connection disappears, it expires after 60 seconds without a poll. Long replies page into the browser automatically, and incoming replies continue appearing after a command finishes. Terminal scrollback starts at 4 KiB and can grow to 32 KiB for unread output, preferring PSRAM; it shrinks after reading and is freed when the session expires. If output exceeds the available buffer while unread, the browser reports the missing output.

USB protocol-switch and USB MOTA ownership commands still apply to the USB connection. Use the WiFi MOTA seeder on port 5001 for a host folder over WiFi.

"},{"location":"terminal_chat_cli/#local-maintenance-commands","title":"Local maintenance commands","text":"

Supported ESP32-S3/SX1262 Full Companions also support Meshtext channel chat:

set mt.shortname MIKE\nsend mt.nodeinfo 0\nchannel 0 Hello\n

Use a configured mt- channel slot. Chat carries only the entered text. After the first successful over-air chat transmission per used channel/identity per boot, the firmware queues a separate standard name/ID NodeInfo advertisement. Automatic metadata waits for a 250 ms quiet TX gap so the peer can resume RX. send mt.nodeinfo can also request one explicitly. Neither contains GPS or coordinates. No NodeInfo is sent just from booting, configuration, receiving traffic or a timer. The long name comes from the existing node name; get mt.shortname shows the effective four-byte short name. These commands are not added to smaller or feature-disabled builds.

MT chat uses flood.retry.count (0\u201315) to retry when no matching forwarded copy is heard. A heard repeat stops retries, but is not a recipient delivery receipt. NodeInfo has at most one retry, only with a nonzero count and flood.retry.advert on, with an extra one-minute wait. For example:

get flood.retry.count\nset flood.retry.count 3\nset flood.retry.advert on\nget mt.retry\n

get mt.retry shows retry/heard/exhausted counters, pending work and the configured count; it does not change settings. With set mt.hops 0, no peer can forward a copy, so heard-repeat retries are disabled. The original chat and first-use NodeInfo are unaffected.

This does not make the Companion relay other people's MT packets. See heard-repeat retries for the airtime limits, exact-packet matching and cancellation rules.

Local means a connection directly to the node: USB, BLE or WiFi/Ethernet Companion protocol, a TCP terminal, or the browser CLI on the LAN. These connections can use the following commands when the role and build include the corresponding feature:

Command Result stats-core, stats-radio, stats-radio-diag, stats-packets Runtime diagnostics get public.key Read-only node identity public key; Companion returns > followed by 64 uppercase hex characters get prv.key Node identity private key; Companion requires private key export enabled in the build get password Infrastructure admin password; ESP32 WiFi Companion reports management-password status without revealing it; other Companions report no admin password password <new-password> Change the saved admin password on infrastructure and ESP32 WiFi Companion erase Erase stored identity and settings; reboot for a fresh node get wifi.pwd Stored WiFi password on WiFi builds get mqttN.password, get mqttN.token Stored credentials for MQTT slot N get acl, log Infrastructure ACL or captured packet log; these stores do not exist on Companion get guest, set guest on, set guest off, get guests [page] Repeater guest-login control and current guest sessions get logins [page] Recent repeater login attempts, with 12-character public-key prefixes

The command text is the same for direct connections. A binary Companion app can send 0x42 (CMD_RUN_CLI_COMMAND) followed by the ASCII command, without switching into terminal mode. The response is 0x1D (RESP_CODE_CLI_REPLY) followed by the reply text. For example, payload 42 67 65 74 20 70 61 73 73 77 6f 72 64 runs get password. USB framing remains the normal Companion frame; BLE and TCP use their existing Companion transport framing.

set freq <MHz> is available on these local connections and through authorized LoRa administration, just like set radio. It saves the frequency without changing bandwidth, spreading factor, or coding rate and requires a reboot to apply. Infrastructure roles also apply the board's transmit-power limit.

Commands relayed to another node using cmd travel over LoRa and retain the remote restrictions on local-only maintenance and secret reads. These local permissions do not add an ACL or packet-log store to Companion. Use list or the binary contacts operations for Companion contacts. Interactive chat commands still use the text terminal or their corresponding binary protocol operations.

The browser CLI requires station/LAN mode, wifi.cli on, and the node admin login. Configuration forms keep password fields masked; explicit local CLI getters return their values, except ESP32 WiFi Companion's status-only get password. Existing USB/BLE/TCP clients and explicitly authorized remote CLI contacts retain their administrative command access. HTTP authentication does not add a password handshake to the raw TCP services.

"},{"location":"terminal_chat_cli/#companion-contact-and-channel-tx-profiles","title":"Companion contact and channel TX profiles","text":"

Choose where the Companion transmits to a contact or group channel:

set tx.user \"Alice Jones\" radio2\nget tx.user \"Alice Jones\"\nset tx.channel 0 both\nset tx.channel #wardriving radio\nget tx.channel 0\n

Values are auto, radio, radio2, both, and off. auto follows radio2.cross; explicit choices override it. Radio2 must still be in rxtx mode, and a radio2-only send fails when it cannot transmit. These settings persist and also apply to messages sent by the phone app. Use exact names, a channel index, or key:<unique public-key prefix> for a contact. See the routing guide for which requests/replies are covered and how to reset a choice.

"},{"location":"terminal_chat_cli/#companion-media-and-ssh-tx-profiles","title":"Companion media and SSH TX profiles","text":"
get tx.media\nset tx.media radio2\nget tx.ssh\nset tx.ssh radio\n

Each setting accepts auto, radio, radio2, both, or off, defaults to auto, and is saved across reboot. tx.media handles Open TAK binary media; tx.ssh requires a tunnel app using the explicit SSH application tag, not an existing untagged SSH connection. See the application routing guide for the framing and routing rules. Ordinary text keeps its existing route.

"},{"location":"terminal_chat_cli/#esp32-full-companion-ethernet","title":"ESP32 Full Companion Ethernet","text":"

Use set ethernet.radio <MHz>,<kHz>,<SF>,<CR>[,<preamble>], then set ethernet on. get ethernet reports state; set ethernet off closes the KISS service and releases its radio2 lease. Settings survive reboot. This is a separate whole-link mode, not the tagged tx.ssh application route. Normal USB/BLE/Companion TCP stay in Companion mode. The host instead uses KISS-over-TCP port 5003 on trusted station WiFi, with MTU 230. See Ethernet setup and radio2 restrictions.

"},{"location":"terminal_chat_cli/#companion-usb-mode","title":"Companion USB mode","text":"

Updated 1.17.1.6 USB Companion builds, including ordinary and Full builds, start in ASCII at 115200 baud. A valid framed app command automatically selects Binary Companion. You can open a terminal without a start token:

picocom --baud 115200 /dev/ttyACM0\n

Current source waits for input before printing the USB banner. Press Enter or type a command. This preserves the clean first response needed by binary apps; see USB client validation for release status.

For older firmware or a port already in Binary mode, use:

picocom --baud 115200 \\\n  --imap spchex \\\n  --initstring $'+++MESHCORE-TERM-START\\r' \\\n  /dev/ttyACM0\n

--initstring sends this exact terminal-start line automatically:

+++MESHCORE-TERM-START\n

The carriage return is required in Binary mode. Control tokens are recognized only as complete CR/LF-delimited lines, so the same text embedded in unrelated unframed input cannot switch modes accidentally.

Binary Companion frames can contain terminal control bytes. The spchex input map renders those bytes as bracketed hexadecimal during the short transition instead of allowing them to change the local terminal's character set or display state. It leaves high-bit bytes unchanged so a UTF-8 terminal displays emoji and non-ASCII text normally. Do not add 8bithex unless you explicitly want UTF-8 bytes displayed as sequences such as [f0][9f][91][8b]. Once the terminal banner appears, the start sequence has already succeeded; do not enter it again as a terminal command.

Send the following exact sequence to return to the binary protocol:

+++MESHCORE-TERM-STOP\n

If logging shares the primary USB port (ESP32), first run set usb.logging off. With logging still on, the stop token reports an error and keeps ASCII logging active. nRF52 Full's separate logging port can remain enabled.

An observable USB session reset restores ASCII after clearing the previous client's state. Native USB with DTR can detect a terminal closing; ESP32 hardware USB Serial/JTAG detects bus resets and physical host loss. USB-to-UART bridges usually cannot detect a terminal closing. Use the start token on ports that remain in Binary mode; idle time does not select ASCII.

Both modes use the same port at 115200. Selecting 57600 is not a portable mode switch: native USB CDC devices ignore the requested baud, while USB-to-UART devices really change the UART timing and receive corrupt data. Binary mode is the framed Companion API used by apps and meshcli; close the terminal before opening that port from an app.

All USB Companion builds automatically switch when they see a complete <-prefixed Companion frame at an empty prompt. The explicit start/stop tokens remain available. See Companion USB CLI and binary switcher for the state machine and limitations.

"},{"location":"terminal_chat_cli/#commands","title":"Commands","text":"

ESP32 and nRF52 Full Companions include an offline World English Bible lookup:

get reader 3:16\nget reader 2\nget reader 2:2-12\n

These select a verse, an entire chapter, or an inclusive verse range from the embedded corpus. Local USB/TCP/browser terminals stream the complete selection in small chunks, keeping the radio loop running while the terminal drains. A new command or disconnected session cancels unfinished output. Text uses plain ASCII punctuation with unchanged wording and capitalization.

In the app's USB/BLE CLI, each reply stays within the existing frame limit. Follow the displayed Next: get reader ... command to continue a long verse or advance through the selected chapter/range. The optional final number is the page within the first verse of that command, for example get reader 21:17 2. The continuation command retains the requested range's end. See Reader lookup and compression for the source, supported build profiles and memory costs.

set freq {frequency}\n

Set the saved LoRa frequency. Example: set freq 915.8. Available over local connections and authorized LoRa CLI; reboot to apply.

set tx {tx-power-dbm}\n

Sets LoRa-chip transmit power in dBm. The firmware rejects values outside the radio/board limit; external-PA boards can have a lower input-power ceiling than the radio chip itself.

set name {name}\n

Sets your advertisement name.

get bluetooth.name\nset bluetooth.name {name|default}\n

Shows or changes the complete Bluetooth device name without changing the mesh advertisement identity. A custom name may contain spaces and is limited to 31 valid UTF-8 bytes. default restores MeshCore-<node name>. The saved change takes effect after reboot. get ble.name and set ble.name are short aliases.

set pin {0-999999}\n

Saves the Bluetooth pairing PIN. For example, run set pin 654321, then reboot to activate it. set pin 0 removes the saved override and restores the build's default/generated PIN policy after reboot; it does not disable Bluetooth. Existing bonds can reconnect without PIN entry, so forget the phone's device entry and pair again if needed. See Bluetooth pairing PIN.

get bluetooth.mac\nset bluetooth.mac {address|random|random-every-boot|random-after-connect|default}\nget bluetooth.stealth\nset bluetooth.stealth {on|off}\n

Shows or changes the Bluetooth identity on Companion builds with BLE. A literal address must be BLE random-static, such as C2:11:22:33:44:55. random generates and saves one address; random-every-boot generates a new one on each startup (random everyboot is also accepted). random-after-connect keeps its address across unused power cycles, then rotates on the next boot after an authenticated connection. default or clear restores the factory address without changing the stealth flag. Reboot, forget the old phone entry, and pair again after an address change. get ble.mac and set ble.mac are short aliases.

The independent bluetooth.stealth flag defaults off and preserves the chosen address policy. With it on, the node is discoverable until its first authenticated pairing, then accepts only that bonded peer. Custom and saved random addresses keep the bond across boots; rotating modes reopen pairing when the address rotates, leaving stealth enabled. It still transmits the directed or allowlisted packets BLE requires for reconnection. Repeating on keeps the bond. Send off, then on, then reboot to reopen pairing manually. ble.stealth is the short alias. Flag changes require reboot.

set lat {latitude}\n

Sets your advertisement map latitude. (decimal degrees)

set lon {longitude}\n

Sets your advertisement map longitude. (decimal degrees)

set af {air-time-factor}\n

Sets the transmit air-time-factor. Deprecated - use set dutycycle instead.

get powersaving\nset powersaving {on|off}\n

Shows or changes Companion device power saving. On ESP32 this controls CPU and GPS idle behavior; it does not change LoRa RXPS or WiFi modem sleep.

get usb.logging\nset usb.logging {on|off} [reboot]\nget usb.debug\nset usb.debug {on|off}\n

Shows or changes persistent USB packet output and a separate verbose-debug preference in an ordinary USB-loggable Companion or Full Companion. usb.logging is the master; debug output requires the master and usb.debug to be on. RAW and RX/TX packet records need only the master. Use set usb.debug off with packet logging for USB-to-MQTT capture. get usb.debug reports saved intent, even while the master is off. Disabling the master does not clear it. Debug defaults off for fresh installs and older preferences without the new field; it still needs compiled debug support. Changing debug verbosity takes effect without a reboot.

Current source also accepts get usb.watchdog and saved set usb.watchdog off|on|auto. Auto is the default and requires 14 continuous healthy logging-client days before saving On. Exact USB stats polls, not merely USB attachment, renew the client lease. See USB logging watchdog for USB-only repair, increasing physical-fault reboot intervals, status fields, and ownership/storage safeguards.

USB Companion and Full start off on a fresh install with the logging master disabled to protect framed traffic. nRF52 Full changes its interface count after a reboot and keeps Companion on interface 00. Every ESP32 Full Companion has one TTY and needs no reboot: logging uses the active text terminal, disables framed Binary Companion on USB, and continues accepting set usb.logging off. Turning logging off leaves the port in the normal ASCII terminal, matching fresh firmware. Send +++MESHCORE-TERM-STOP, or let a Companion app send a valid framed probe, to switch it to Binary Companion afterward. Full Companion emits complete received frames as uppercase ASCII RAW: hex followed by decoded RX summaries where possible; TX summaries are separate. These are best-effort records, not atomic pairs or a durable capture. nRF52 interface 02 is logging-only: send CLI/control commands on interface 00. A single-port bridge that queries identity on its logging port needs separate control-port support before it can use that dedicated log endpoint.

reboot\n

Sends an acknowledgement, then reboots the Companion one second later. The delay gives either the USB terminal or the Full Companion TCP terminal on port 5002 time to deliver the reply before its transport disappears.

get radio.rxps\nget radio.rxps.config\nset radio.rxps {off|on|level 1-10 [preamble 16|32]|rx_us sleep_us}\n

Shows or changes LoRa receive duty cycling on supported radios. get radio.rxps.config also reports the saved level and preamble assumption so automation can restore a level-based preference exactly. Fresh Cascade builds select level 8 with a 16-symbol timing assumption. The preamble argument controls the RXPS calculation; it does not change the physical wire preamble. A configured level is the minimum: when faster SF/BW settings shorten the timing window, firmware raises the effective level only as far as needed, up to level 10. Starting with v1.17.1.5, SF5-SF8 packets normally use a 32-symbol physical preamble. Firmware selects 64, then 128, only when every shorter choice fails to enable RXPS at any level. The configured values remain unchanged, so every radio change recalculates from the saved minimum. A slower tuple returns to that exact level and the shortest viable wire preamble.

If neither adjustment leaves enough time for the radio to wake, RXPS stays logically enabled but receives continuously instead of rejecting the radio setting or starting an invalid duty cycle. The Companion terminal's get radio.rxps reports this as mode=continuous-fast and reports any effective-level or effective-preamble adjustment; get radio.rxps.config reports the persisted preference. A later compatible SF/BW change resumes duty cycling without another RXPS command.

For the SX1262+TCXO boards tested here, these fast combinations are the useful RXPS boundary profiles (CR does not change the RXPS preamble timing):

SF BW (kHz) Wire preamble Effective preamble Minimum effective level RX / sleep 7 500 32 32 7 2731 / 6101 us 6 250 32 32 7 2731 / 6101 us 5 125 32 32 7 2731 / 6101 us 5 250 64 64 8 1252 / 6424 us 6 500 64 64 8 1252 / 6424 us 5 500 128 128 8 626 / 6398 us 5 62.5 32 16 10 4096 / 6272 us

The 64- and 128-symbol rows require the v1.17.1.5-or-newer adaptive-preamble contract on every sender that may reach the RXPS receiver. Cascade/USA builds on a Heltec V4 and WisMesh Tag (RAK4631 target) passed 16/16 packets in each direction at both SF5/BW250/64 and SF5/BW500/128, CR5, 909.950 MHz. An older sender makes a long-preamble timing window unsafe, so use continuous RX for a mixed deployment. Retry packets use the same physical preamble as other packets.

get wifi.powersave\nset wifi.powersave {none|min|max}\n

Shows or changes the persisted WiFi modem-sleep policy on ESP32 WiFi Companion builds. Full Companion requires at least min while BLE is present and rejects none. The WebConfig WiFi card and the normal binary Companion protocol expose the same setting; binary clients do not need the terminal-start token. On an ESP32 Full Companion whose primary mesh radio is ESP-NOW, max is also unavailable because maximum modem sleep can make the station miss ESP-NOW broadcasts, which the access point does not buffer. A previously saved max value is capped to and reported as min, and a new max selection is rejected. Such a WiFi/BLE/primary-ESP-NOW build therefore uses min.

get espnow.channel\nset espnow.channel <1-13>\n

On builds whose primary mesh radio is ESP-NOW, this shows or saves the primary radio's channel. The default is channel 1. Use only a channel permitted in your region, and restart or power-cycle the node after changing it. Every primary ESP-NOW peer must use the same channel.

On Full Companion, the setup AP and infrastructure-WiFi station also share that channel, so the router's 2.4 GHz radio must remain fixed to it. Power saving does not permit separate channels. wifi.powersave max is unavailable on these ESP32 Full builds and min is the coexistence setting. This channel setting is also distinct from bridge.channel, which controls the optional ESP-NOW bridge transport. An updated LoRa-primary ESP-NOW bridge can join this raw primary transport by selecting the same channel and running set bridge.format raw; its default wrapped format remains the bridge-to-bridge protocol.

get radio.rxgain\nset radio.rxgain {on|off}\nget radio.fem.rxgain\nset radio.fem.rxgain {on|off}\nget radio.fem.txgain\nset radio.fem.txgain {on|off}\n

radio.rxgain changes the radio chip's boosted receive-gain mode. The FEM settings control the external receive-path LNA or transmit-path gain on supported boards. Changes are applied immediately and saved across reboots; changing either receive-gain path also recalibrates the radio noise floor. Boards without the respective control report it as unsupported.

time {epoch-secs}\n

Set the device clock using UNIX epoch seconds. Example: time 1738242833

advert\n

Sends an advertisement packet

clock\n

Displays current time per device's clock.

ver\n

Shows the device version and firmware build date.

card\n

Displays your 'business card', for others to manually import

import {card}\n

Imports the given card to your contacts.

list {n}\n

Lists favorite contacts first, then all remaining contacts. Each group is ordered by the most recent advertisement. Optional {n} limits the displayed contacts after applying that order.

show\nshow adverts {on|off}\nshow channels {on|off}\nshow emergency {on|off}\n

Controls unsolicited receive output in the USB terminal. Plain show reports the current settings, and show {category} reports one category. At boot, advertisements and ordinary channel messages are hidden while #emergency messages are shown. The three controls are independent, so show channels on does not override show emergency off.

These filters affect terminal printing only. Messages still enter the offline queue and are delivered through the binary Companion protocol. Changes remain active when switching between terminal and binary mode and reset to their defaults after reboot.

to\n

Shows the name of current recipient contact. (for subsequent 'send' commands)

to {name-prefix}\n

Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)

path\n

Shows the saved outgoing path for the current to recipient. This command and all path changes require a recipient to be selected first.

path direct\npath clear\npath {hop-hash...}\n

Sets the outgoing path used by subsequent login, send, and cmd commands. direct selects a zero-hop route. clear forgets the saved route, causing the next operation to use flood routing and allowing normal path discovery to learn a replacement.

Explicit paths use spaces, commas, or a mixture of both between hop hashes. Each hop must contain exactly 2, 4, or 6 hexadecimal digits, and every hop in one path must use the same width. Whitespace and hexadecimal letter case do not matter. The setting is saved with the selected contact.

For example:

to Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\npath\nlogin my-admin-password\n
login {admin-password}\n

Sends a remote login request to the current recipient. Select a repeater, room, or other remotely managed node with to {name-prefix} first. The password is masked with * while it is entered and must be 1-15 UTF-8 bytes; longer passwords are rejected instead of truncated. Login uses the route shown by path: a known or explicitly set route is direct, while an unknown route is flooded.

Login results arrive asynchronously. A successful modern response displays the remote ACL permissions byte and server protocol level. A wrong password, an unreachable target, or a server that does not support remote login normally produces a timeout because those nodes do not send a rejection packet.

cmd {remote-command}\n

Sends CLI data to the current recipient. Wait for the login result before sending the first command. The remote node applies its own ACL permissions, and any reply appears asynchronously as CLI -> from {name}. The response window is 300% of the route estimate. A routed send is displayed as DIRECT via path {hop,...} with the exact prefixes copied into the packet; in MeshCore, DIRECT is the route class for an explicit path, not a synonym for zero hops. A matched reply also shows its local round-trip time, measured from queueing the command through receiving the result. This includes both radio directions and remote execution; it is not execution-only CPU time. Only one terminal cmd can be pending at a time.

For example:

to Hilltop Repeater\nlogin my-admin-password\nLOGIN -> Hilltop Repeater accepted (ACL permissions 0x03, server v13)\ncmd ver\ncmd get radio\n

Repeater guest logins default to off. An admin can use cmd set guest on to enable them or cmd set guest off to disable them and remove current guest sessions immediately. These settings are saved and need no reboot. When enabled, the configured guest.password applies; its blank default permits passwordless guest access. Admin and assigned ACL access are unaffected. Room-server login settings are unchanged.

Use cmd get guests or cmd get guests 2 from an admin session to inspect the repeater's guest entries. Each page shows one full public key and its last activity time in UTC epoch seconds. Guests are held in RAM, so this is a list of accepted sessions rather than proof that a device is currently reachable. They are cleared by a reboot or set guest off and remain separate from get acl.

Use cmd get logins or cmd get logins 2 from an admin session to inspect recent login attempts. The repeater retains up to 16 distinct public keys in RAM, newest attempt first, with up to two rows per page. Each row shows a 12-character hex key prefix, the latest UTC epoch time, result, and attempt count. Repeated attempts update the same full key's row; different keys sharing a displayed prefix remain separate records.

Results distinguish accepted logins, wrong passwords, disabled guests, replay/state rejection, and unavailable ACL capacity. Only authenticated, complete login requests are recorded; anonymous owner, region, or clock requests are excluded. Passwords and shared secrets are never stored. This bounded history clears on reboot, survives set guest off, and does not show who is currently online. A zero timestamp means the node's clock was unset.

The exact commands and permissions depend on the target firmware. cmd does not run a command on the local Companion; it sends the text over LoRa to the selected node.

Incoming direct-route messages are labeled ROUTED, not DIRECT. Forwarders consume direct-route prefixes as the packet travels, so the destination cannot recover the reply's actual hop history from the received packet. Use trace when the return route itself must be verified.

send {text}\n

Sends the text message (as DM) to current recipient.

trace\n

Traces the saved round-trip route to the current recipient and displays the SNR at each hop. Select the recipient first with to {name-prefix}.

trace {name-prefix}\n

Traces a recipient directly without changing the current to selection. A trace requires a known direct path; use normal messaging or path discovery first if the terminal reports that no valid path is available. Only one terminal trace can be pending at a time, and a missing response is reported as a timeout.

For example:

to Hilltop Repeater\ntrace\ntrace Downtown\n

The displayed route uses one- or two-byte node hashes and per-hop SNR values. Saved three-byte paths are traced with two-byte prefixes because the trace packet format has no three-byte hash-size mode.

To trace an explicit route instead of a saved contact path, provide the prefix size followed by the complete ordered route:

trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n

Prefix separators may be spaces, commas, or any mixture of them. Each prefix must contain exactly 2, 4, or 8 hexadecimal digits for a 1-, 2-, or 4-byte trace respectively. Three-byte traces are not supported.

The prefixes are used exactly in the order entered. To receive the trace result, enter the complete outward route followed by its return route, as in the mirrored examples above. A route that does not return to this node will eventually report a timeout.

reset path\n

Resets the path to current recipient, for new path discovery. This is retained as an alias for path clear.

public {text}\n

Sends the text message to the built-in Public group channel.

channels\n

Lists the configured channel slots and names without exposing their secrets.

channel {name-or-slot} {text}\n

Sends a message to any configured channel by its exact name or numeric slot. Use the slot shown by channels when a channel name contains spaces.

For example:

channels\nchannel #rgdata Hello from Eugene\nchannel 2 Another message\nshow channels on\n

Messages are UTF-8. Emoji use multiple bytes toward the available message length, which also includes the sender-name prefix added over the air.

"},{"location":"tx_beacons/","title":"Tx beacons","text":"

View this page on MeshCore Docs.

"},{"location":"tx_beacons/#transmit-only-text-beacons","title":"Transmit-only text beacons","text":"

Up to 12 disabled-by-default slots can send a short text message using their own frequency, bandwidth, spreading factor, coding rate, power, and preamble. MeshCore beacons use the Public channel. Meshtext beacons use the public preset channel, such as LongFast or MediumFast, with its standard public key.

The default cycle is 24 hours. Choose 12, 24, 36, or 48 hours:

get beacon.interval\nset beacon.interval 24\n

Slots are spread evenly across the selected cycle: 1, 2, 3, or 4 hours apart. Slot 1 is due after the first spacing interval, slot 12 after a full cycle, then the cycle repeats. Empty slots are not compacted. Scheduling uses uptime, not UTC; correcting the clock does not compress the spacing. Reboot starts a fresh schedule, with no immediate startup burst.

"},{"location":"tx_beacons/#configuration","title":"Configuration","text":"
set beacon 1 meshcore 910.525,62.5,7,5,14,32 A MeshCore node is listening here\nset beacon 2 meshtext us longfast 14 A MeshCore node is also on LongFast\nset beacon 3 meshtext us mediumfast 14 A MeshCore node is also on MediumFast\nget beacon\nget beacon 2\ndelete beacon 2\ndelete beacon all\n

MeshCore tuple order is MHz,kHz,SF,CR,dBm[,preamble]; CR is 5 through 8, representing 4/5 through 4/8. The default beacon preamble is 32 symbols. Meshtext takes an explicit us, eu868, eu433, or anz region, a recognized preset, and dBm. Its frequency is calculated using the stock preset channel; its preamble is fixed at 16 symbols. Invalid modem, region, power, or text settings are rejected. Use settings permitted for your hardware and location. Power is checked against the driver's range at the beacon frequency and any explicit board limits. External-PA drivers interpret it in antenna dBm using their calibration range; the normal receive profile's power is unchanged.

Each message body is limited to 80 bytes and is prefixed with the node name. Settings are saved separately from ordinary preferences and channels. Configuration changes do not immediately transmit a beacon.

A structurally valid saved slot that the current board or firmware cannot transmit remains available through get beacon. Its scheduled opportunity is skipped, while supported slots continue normally. Use set beacon to repair that slot or delete beacon to remove it; unsupported Meshtext slots do not consume packet IDs. This also permits repairing an out-of-range power saved by older firmware. Corrupt images retain the existing safe backup recovery; without a valid backup they remain held. Unreadable or unknown-version files remain held without transmitting or overwriting them.

"},{"location":"tx_beacons/#radio-ownership-and-limits","title":"Radio ownership and limits","text":"

These are ordinary channel text messages, not MeshCore identity advertisements or Meshtext NodeInfo. Meshtext beacons request no ACK and have no forwarding budget. MeshCore Public uses one locally transmitted flood; other MeshCore nodes may relay that flood according to their own policies.

The transmitter briefly borrows the physical radio, sends once, and restores its normal receive settings, power, and RX power saving. It does not occupy, replace, or persist changes to radio2, including a temporary second profile. Meshtext beacon identity is separate from Companion Meshtext chat; beacon packet IDs are committed to storage before use to avoid reusing chat nonces. Keep manually assigned Meshtext node IDs unique; a manual chat ID can still collide with another sender, including the separate beacon sender. A missing or unreadable active beacon ID file stops Meshtext beacons rather than rolling back to a potentially older backup. MeshCore beacons remain independent.

Beacons share the normal airtime budget and yield to due MeshCore traffic. They do not interrupt a pending packet, Morse operation, carrier test, or noise-floor calibration. Busy-channel, preparation, and transmission failures skip that opportunity; there is no automatic RF retry or catch-up burst. A separate transmit-start guard permits at most one beacon attempt per hour. Receive restoration retains exclusive ownership until it is confirmed. An unavailable/suspended radio does not transmit a beacon.

The feature is available on supported ESP32, nRF52, and RP2040 LoRa builds where flash and runtime RAM qualification pass. Tiny STM32 images leave it disabled to preserve existing features and filesystem space. The Meshtext packet-format adapters cover SX1262 (including the Indicator LoRa variant), SX1268, LLCC68, SX1276 (including external-PA variants), LR1110, LR1121, LR2021, and STM32WLx. The STM32WLx adapter is available to explicitly enabled builds, but does not enable beacons in the space-constrained STM32 artifacts. An opt-in Wio-E5 Mini USB Companion link exceeded its unchanged application limit by 19,820 bytes; the three existing STM32 Sensor targets exceeded theirs by 37,000-42,736 bytes. Enabling those artifacts needs separate size work, not just a radio capability flag or a smaller filesystem. ESP-NOW-only boards have no LoRa transmitter and cannot send these packets.

Choose a region and preset the physical chip, board RF path, and antenna support. In particular, SX1268 requires a compatible band such as EU433, not US or ANZ. LLCC68 supports MediumFast (SF9/BW250), but not LongFast (SF11/BW250). Invalid modem combinations are rejected, not silently changed to a different preset or transmitted as MeshCore. Local RF rules and the board's power limits still apply.

Protocol entry and restoration use each chip's checked sync-word, CRC, explicit-header, and normal-IQ configuration. Beacon TX temporarily disables LR2021 receive side detectors and restores their saved configuration afterward. Enabling this feature does not enable Companion Meshtext receive/chat support on other roles.

Schedules are best effort. A busy or sleeping node can miss an opportunity; the feature does not promise delivery, RF sensitivity, or a particular retuning duration on unmeasured hardware.

"},{"location":"udp_bridge_setup/","title":"Udp bridge setup","text":"

View this page on MeshCore Docs.

"},{"location":"udp_bridge_setup/#wifi-udp-bridge-up-to-eight-ip-peers","title":"WiFi UDP bridge: up to eight IP peers","text":"

Supported ESP32 repeater images can bridge MeshCore packets directly over an existing WiFi network, without an MQTT broker. Each board has one UDP listening port and up to eight configured IPv4 address/port destinations. Eight is the per-board peer limit; it is not a limit on the router's number of WiFi clients.

LoRa network A <--> Repeater A <-- existing WiFi/LAN --> Repeater B <--> LoRa network B\n

The bridge starts off on new and upgraded installations. It operates only with a connected station interface and a valid local IPv4 address. It pauses during SoftAP or AP+STA operation, WiFi disconnection, browser OTA, and incompatible WiFi ownership transitions, then resumes when station operation is available. Opening a captive configuration access point does not create a UDP bridge network. USB/LoRa CLI access remains useful for inspecting a paused bridge.

With saved WiFi credentials, enabling UDP requests a connection to that network. The web page can share the same station connection. An unused automatic recovery AP returns to station mode when UDP needs the network; connected setup clients and unfinished browser/OTA work keep their link. An explicitly requested start webconfig ap stays active until released. Use stop webconfig when finished with that AP, then check get udp.status. Switching WiFi back on does not enable UDP if you saved udp.enabled off.

"},{"location":"udp_bridge_setup/#router-client-isolation","title":"Router client isolation","text":"

Client isolation is an optional access-point or guest-network policy, not a requirement of WiFi. When enabled it can prevent two wireless devices from talking directly, even though both can reach the Internet. A similar effect can come from VLAN rules or host firewalls. The bridge cannot bypass those rules.

Use a trusted SSID/VLAN that allows the two devices to exchange UDP, or ask the network administrator to allow the bridge peers and port. Disable isolation only where that network policy is appropriate. Simply changing the UDP port, enabling MQTT on a repeater, or configuring a second peer address does not make an isolation rule disappear. Some routers isolate only clients on the same access point; others isolate all clients or the entire guest network. Verify the policy on the router in use.

Reserve each board's DHCP address on the router. The bridge stores numeric IPv4 addresses, not DNS names; a changed lease can otherwise point a peer at the wrong device. No Internet port forwarding or public exposure is required.

"},{"location":"udp_bridge_setup/#configure-two-boards","title":"Configure two boards","text":"

Use firmware containing the UDP bridge. Connect both boards to the existing WiFi network using the normal WiFi settings. Read get wifi.status on each board to find its local address once connected. The commands below use example addresses; substitute the actual reserved addresses.

For a LoRa-capable SenseCAP Indicator, use the Full SenseCapIndicator-LoRa_repeater target on the original 8 MiB/OPI-PSRAM board. The distinct SenseCapIndicator-LoRa-N16R2_repeater target is for 16 MiB/QSPI PSRAM hardware. Both retain the LoRa expander and display; their PSRAM modes are selected before app startup and the images are not interchangeable. The Indicator Companion and primary-ESP-NOW images do not provide this bridge.

Both boards use the existing bridge.secret as their shared authentication secret. Set the same strong secret on both before enabling the transport. Do not paste it into shared diagnostic captures. The transport authenticates packets with HMAC-SHA256; it does not encrypt their contents. Use trusted LAN access and WPA2/WPA3 protection as appropriate for the network.

On board A, whose reserved address is 192.168.1.50:

set udp.enabled off\nset udp.port 4404\nset udp.peer.1 192.168.1.51:4404\nset bridge.source rx\nset repeat on\nset udp.enabled on\nget udp.status\n

On board B, whose reserved address is 192.168.1.51:

set udp.enabled off\nset udp.port 4404\nset udp.peer.1 192.168.1.50:4404\nset bridge.source rx\nset repeat on\nset udp.enabled on\nget udp.status\n

Each receiving board must also list the sender's listening address/port as a peer; this is an explicit allowlist, not discovery or a broadcast service. Both ports can be changed independently, but each destination must match that peer's actual listening port. Unicast IPv4 addresses and ports 1..65535 are supported; multicast, loopback, unspecified and broadcast destinations are rejected. Remove a destination with set udp.peer.1 off. Slots 1..8 have the same behavior, and duplicate endpoints are rejected.

The UDP settings are saved separately from the historical common preferences. An invalid or truncated record starts with UDP disabled. A rejected save keeps the previous runtime preferences; an uncertain filesystem result requires a reload before further changes. OK - UDP settings saved acknowledges saved intent, not network connectivity or confirmed peer delivery.

"},{"location":"udp_bridge_setup/#test-each-ip-direction-without-sending-on-lora","title":"Test each IP direction without sending on LoRa","text":"

Wait for get udp.status to show ready peer sessions. Each peer establishes an authenticated session using the shared secret, a fresh boot/session identity and a challenge exchange. Replay checks reject stale data and diagnostic probes.

From board A's local USB console:

udp test 1 A1B2C3D4\nget udp.stats test\n

On board B:

get udp.stats test\n

Board B's rx should increase and rxid should match A1B2C3D4. Reverse the test with a different ID to check the other direction. udp test 1 generates an ID from uptime; an explicit ID must have exactly eight hexadecimal digits. The peer number is the local configured slot, not a global board identifier.

A queued/sent result confirms only local acceptance by the UDP stack. UDP itself does not guarantee delivery. A received probe proves authenticated transport receipt on that peer; it does not prove LoRa forwarding. Probes do not enter the mesh packet pool, echo, retransmit or transmit on LoRa. Test and counter-reset commands require a local connection; normal configuration uses the existing authenticated CLI access policy.

"},{"location":"udp_bridge_setup/#diagnose-mesh-traffic","title":"Diagnose mesh traffic","text":"
get udp.status\nget udp.stats\nget udp.stats rx\nget udp.stats tx\nget udp.stats drops\nget udp.stats test\n

Status separates saved intent from network readiness and authenticated peer sessions. ready counts established boot sessions, not current peer liveness; an offline peer can retain its session until a restart or reconfiguration. Use a fresh received probe to establish current reachability. Counter pages distinguish framing/authentication problems, unknown source endpoints, replay, mesh filters/duplicates, packet-pool pressure and outbound queue/socket failures. udp resetstats starts a new counter window without changing saved settings or restarting the bridge.

If no peer session becomes ready, check both stored endpoints, matching bridge.secret, station connectivity, isolation/firewall policy and the peer's actual firmware. If sessions/probes work but mesh traffic does not, check bridge.source, filters, duplicate counters and each side's LoRa setup. bridge.source rx is valid; tx instead selects packets after LoRa forwarding decisions. These source/filter settings are shared with other compiled bridges.

The peer list contains direct destinations for locally received/transmitted LoRa traffic. An incoming UDP packet enters the local LoRa mesh; it is not relayed to other UDP peers, including through its later local LoRa TX callback. A hub therefore does not provide UDP-to-UDP transit between its peers. Normal mesh duplicate/filter checks still apply, including when several repeaters hear the same LoRa packet. The configured eight-peer fan-out is bounded; it does not increase LoRa capacity or promise delivery through an arbitrary IP topology. Avoid unnecessary parallel paths and loops.

For a short USB diagnostic capture, record get usb.logging and get usb.debug, then enable both:

set usb.logging on\nset usb.debug on\n

Restore both previous values afterward. Bounded UDP: updates are emitted from the main loop at most once per second when diagnostic state changes. The counter pages and probes work without either logging gate. Never include WiFi passwords or get bridge.secret in logs you share.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/","title":"Upgrading 1.17.1.8 to 1.17.1.9","text":"

View this page on MeshCore Docs.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#upgrading-from-11718-to-11719-over-wifi","title":"Upgrading from 1.17.1.8 to 1.17.1.9 over WiFi","text":"

For a supported ESP32 node that already has compatible application slots, use its own MeshCore-OTA access point and the matching application-only image. This measured V4 route avoids dependence on the site's WiFi credentials and LAN reachability. USA Cascade defaults remain 910.525 MHz, 62.5 kHz, SF7 and CR5; existing saved radio settings take precedence after an update.

Select the exact board, hardware variant and role in the firmware picker. Download the image before connecting to the node's update network. The release's manifests and SHA-256 inventory identify the exact files. Preserve identity and settings using the tools appropriate to the installed role before an upgrade.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#choose-the-installation-route","title":"Choose the installation route","text":"Installed node File and route Exact ESP32 board/role with compatible dual-OTA slots Matching application-only .bin through its working browser updater. Supported legacy dual-OTA layout needing expansion Exact board/role migration ZIP from the selected release's utility page; follow its README and use its supplied Full application. Layout without a matching migration package, single application slot, or no working supported updater Matching -merged.bin through that board's USB/serial flashing procedure. nRF52 Matching application UF2/DFU ZIP or a separately documented LoRa OTA route; this ESP32 browser procedure does not apply.

An application-only update cannot replace a partition table. Where available, get storage.layout reports the live layout; a version label does not establish it. For older firmware, use the partition lookup guide. The tested V4 already had two 6.25 MiB slots in the expanded 16 MiB layout. That result does not qualify a migration from legacy 1.25 MiB slots.

Never upload a -merged.bin or target-partitions.bin to an application-only browser updater. A merged image includes bootloader and partition-table offsets for USB/serial installation. A migration package's target table is a verification artifact, not an application.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#recommended-11718-v4-example-use-the-nodes-ap","title":"Recommended 1.17.1.8 V4 example: use the node's AP","text":"

The actual published Full V4 build tested was 9053038f. From an authenticated USB, Bluetooth or LoRa command session, send these commands one at a time, waiting for each reply:

stop webconfig\nstop ota\nstart ota ap\n

Stopping WebConfig explicitly is needed for this old build's HTTP-port handoff. Stopping a previously running uploader ensures the old start ota ap starts the AP route. The new 1.17.1.9 startup behavior should not be assumed on the old receiver.

Join the node's MeshCore-OTA WiFi network. Open the exact URL returned by the command, normally http://192.168.4.1/update for an infrastructure node. Confirm that the page identifies the intended node, select its matching 1.17.1.9 application-only .bin, and upload once. Keep the node powered through the transfer and its automatic restart.

After restart, reconnect with the normal configuration app or console and run:

ver\nget storage.layout\n

Compare ver with the selected image and confirm identity, ACL, radio settings and the other saved settings used by the node. A command unsupported by the installed image is not proof of an incorrect layout; use the lookup guide when necessary.

The old 1.17.1.8 page can remain at 100% or miss HTTP completion even after the new application installs and the node restarts. Check the running version before repeating an upload. Browser transmission progress alone does not prove successful installation.

For 1.17.1.9 receivers, start ota starts WiFi when needed, stops WebConfig to free its HTTP port, and reports the IP address and network to use. start ota ap explicitly selects the node AP even if a LAN uploader is already running. WebConfig's own terminal explains that the handoff closes the portal; send it through USB, Bluetooth or LoRa instead. On a compatible two-slot Full Companion, use the returned port-8080 URL; single-slot Full Companions require USB for self-update. Use stop ota if abandoning an update.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#lan-and-phone-examples","title":"LAN and phone examples","text":"

If an old 1.17.1.8 infrastructure node is already reachable on the intended WiFi LAN, retain that connection. Otherwise configure it through its existing WebConfig UI, then verify the live address with get webui. From a separate USB, Bluetooth or LoRa session:

stop webconfig\nstop ota\nstart ota\n

Open the exact returned LAN URL, for example http://192.168.1.40/update. The upload computer or phone must be able to reach that address. On 1.17.1.8, confirm the actual connection after changing credentials; a serial WiFi setter alone does not prove that WebConfig uses the new pair. If the LAN is weak or blocks client-to-client traffic, use the node AP route above.

A phone near the node can download the correct application while it has Internet access, join MeshCore-OTA, and use its browser at the returned address. Keep that WiFi connection even if the phone warns it has no Internet. This is the same intended node-AP route, but an actual phone browser upload was not physically tested in this run.

A phone hotspot is useful only if the node and uploading device can both join it and reach one another. Confirm the node's reported LAN URL before uploading; hotspot client isolation can prevent this. The controlled shared LAN test used a Pi access point, not a real phone hotspot, so a phone hotspot is not the qualified default recommendation. For a node with only LoRa reachability, use the LoRa OTA guide and exact matching target/storage package.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#when-partition-expansion-is-required","title":"When partition expansion is required","text":"

Use the exact migration ZIP selected for this board and role. Its README defines the supported old updater, live-source checks and preservation set:

  1. Upload that package's wifi-bridge.bin through the supported old updater.
  2. Join the migration network described in its README and wait for Expanded layout ready.
  3. Upload the same package's full-application.bin through its /update page.
  4. Verify the new layout, version, identity, ACL and supported saved settings.

Do not substitute a loose release application for the ZIP's supplied Full application: migration packages can carry a required older target identity. Do not combine stages from different packages. The bridge writes the partition-table sector; loss of power during that write can require cable recovery. See the complete migration directions for the exact preservation limits and supported WiFi/LoRa routes. Actual legacy-layout V4 migration was not physically tested in this run.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#stock-g2-migration-limitation","title":"Stock G2 migration limitation","text":"

The separate stock 1.14.1 G2 trial reached the 1e421ff4 Full application over WiFi after partition expansion, but its final isolated network check reached the client/DHCP deadline without a verified G2 client or any HTTP request. The complete cable-free migration is therefore not qualified. Plan for USB recovery if starting from that stock image.

The G2 was subsequently updated and verified through separate USB maintenance on release source 88c851080, with its original identity, ACLs, 22 common settings and saved extras preserved. That result does not qualify the failed WiFi-only final check. A legacy RX-gain loading correction preserves an unambiguous saved gain of on from compact stock preferences; ambiguous older zero padding retains the board default.

The detailed test report retains the original failures, host/network recovery and exact source revisions. Latest-source physical V4 testing was unavailable; the measured older upgrade routes below do not establish every board or starting version.

"},{"location":"upgrading_1.17.1.8_to_1.17.1.9/#measured-upgrade-scope","title":"Measured upgrade scope","text":"

The published 1.17.1.8 Full V4 installed newer applications over node AP and a controlled shared LAN, with automatic restart and preserved identity and ACL, despite missing old-page HTTP completion. On the earlier c5fe8a0e source, the unchanged native AP pages returned HTTP 200/OK for G2 and V4 uploads; the V4 upload completed in 16.392 seconds and its first ordinary clock reply took 1.010 seconds against the unchanged 5-second console deadline. These tests used compatible expanded layouts and do not qualify every board, browser, phone or starting version. See the 1.17.1.9 verification scope.

"},{"location":"usb_logging_watchdog/","title":"Usb logging watchdog","text":"

View this page on MeshCore Docs.

"},{"location":"usb_logging_watchdog/#usb-logging-watchdog","title":"USB logging watchdog","text":"

Current source includes a saved USB logging watchdog in Companion, Repeater, Room Server, and Sensor. It repairs the radio's USB connection; it cannot restart a Pi, restore SSH, or prove MQTT or disk delivery. Supported backends are ESP32 native TinyUSB/HWCDC and nRF52 TinyUSB. An external USB-to-UART adapter is not an observable native-USB backend. Unsupported targets compile out recovery timers and statefile transactions; their watchdog commands report unsupported rather than saving an unusable mode. Management still reports the logging master with USB support false.

"},{"location":"usb_logging_watchdog/#commands-and-defaults","title":"Commands and defaults","text":"
get usb.watchdog\nget usb.watchdog.last\nset usb.watchdog auto\nset usb.watchdog on\nset usb.watchdog off\n

Setters accept exactly one lowercase mode, save it before applying, and do not reboot immediately. They do not enable usb.logging or usb.debug.

Mode Behavior auto Default for missing watchdog state, including upgrades. Requires 14 continuous healthy logging-client days before saving on, with the retry tier reset to one hour. A previously confirmed client can receive USB-only recovery before promotion, but Auto never reboots the MCU. on Explicitly arms physical USB recovery and eventual MCU reboot. A confirmed stats client also gets USB-only recovery if its stats polls stop; missing polls alone cannot authorize MCU reboot. off No watchdog probes, automatic USB recovery, qualification, or MCU reset. Packet logging remains independent.

Existing valid saved On/Off choices win. Auto qualification is RAM-only and restarts on reboot, USB disconnect, observed stall, expired client lease, logging master Off, or an explicit mode change. A node that has never confirmed a logging client does not cycle USB in Auto merely because no cable is attached.

"},{"location":"usb_logging_watchdog/#confirming-a-logging-client","title":"Confirming a logging client","text":"

The existing meshcoretomqtt five-minute stats-core, stats-radio, and stats-packets polls renew a 15-minute lease. Only complete, exact stats commands received on the actual USB logging endpoint count; BLE, network, LoRa, management reports, and ordinary framed Companion traffic do not. No new heartbeat command is required. A cable, open TTY, DTR, or kernel USB ACK alone cannot qualify Auto.

logger=1 requires the USB logging master, an observed host and reader, a fresh stats lease, and no physical TX stall. Any program polling those commands can qualify: this is not application identification or proof that packets were parsed, published to MQTT, or written to disk. A one-time manual stats query can also establish the lease; use watchdog Off when later USB-only reconnection would be unwanted.

On nRF52 Full Companion, only the dedicated logging CDC (*-if02) establishes the lease. That endpoint accepts bounded stats-poll markers without replying with statistics or exposing arbitrary CLI/configuration commands. Queries and configuration still use primary CDC (*-if00), whose stats queries do not prove a logging reader. Existing single-port bridges needing identity/CLI replies still require separate control-port support.

The default nRF52 framework logger follows the same runtime USB logging and debug settings. Bluefruit flash-event diagnostics use the selected bounded logging stream, including dedicated CDC on Full Companion, so saving contacts does not insert ASCII into the primary Companion protocol. A disconnected or full logging endpoint drops diagnostic output without falling back to the control port. Explicit framework UART and RTT logger selections keep their existing destinations.

On ESP32 USB-Serial/JTAG builds, ESP-IDF diagnostics also follow both usb.logging and usb.debug. Driver tasks submit bounded text; the USB owner emits it through the same session and writer guards as functional output. Logging-off, host changes and congestion discard pending diagnostics instead of falling back to the raw SDK console. Fragmented diagnostics from concurrent SDK tasks remain best effort. UART and native TinyUSB routes are unchanged.

"},{"location":"usb_logging_watchdog/#recovery-timing-and-safety","title":"Recovery timing and safety","text":"

For a physical host/reader outage, or pending TX with no real completion, software USB recovery is requested after five minutes. Re-enumeration follows after another 60 seconds if health is not restored. These are one staged attempt per outage, not a repeated five-minute reboot loop. Busy backends retry at most once every five seconds without consuming an attempted stage.

After a confirmed client's stats lease expires, USB-only repair is due five minutes later: normally 20 minutes after its last stats poll. This applies in Auto and, once a client is confirmed during that boot, On. On with no stats client observed remains compatible with non-stats log readers. A gap in stats polls does not by itself reboot a healthy USB transport.

MCU reboot requires On, a physical USB fault lasting the entire saved tier, and the USB recovery grace periods. Tiers grow after reset authorization: 1, 2, 4, 8, 16, 32, 64, 128, then 168 hours (one week). An older app-only stats outage cannot shorten a later physical fault's interval. Ten minutes of healthy monitored operation resets a raised tier to one hour.

Native TX-completion events are progress evidence. On the pinned ESP32 HWCDC backend, an exclusive check of both the empty software queue and acknowledged hardware FIFO also completes a pending write: the SDK does not emit another TX event for the final FIFO acknowledgement. An empty software queue alone does not establish completion. Queueing, purging, reinitializing, or accepting the optional bounded minute probe is not proof of recovery. The watchdog never waits for a host. USB connection reports, especially HWCDC's host/reader flags, are SDK approximations and do not reliably identify an open TTY process. HWCDC therefore uses physical host presence with a 100 ms continuous-loss check rather than its unreliable open-reader heuristic. A brief status dip does not revoke a valid stats lease. A real USB bus reset invalidates the lease immediately; sustained host loss and actual TX stalls remain observable.

Recovery waits for active Binary USB clients, mOTA/update owners, unsaved contacts, radio tests/calibration/temporary-radio operations, functional CLI work, and selected/in-flight radio transmissions to clear. Stale ASCII output from a disconnected or stalled host does not indefinitely prevent recovery. Before MCU reset, the next tier must commit and pass readback; the firmware then rechecks transport health and ownership. Re-enumeration preserves the configured USB descriptors, but can disconnect host applications and lose best-effort packet records.

"},{"location":"usb_logging_watchdog/#persistence-and-status","title":"Persistence and status","text":"

The independent, versioned, CRC-protected /usb_wdg statefile does not change existing role preference offsets. Unreadable/corrupt state or a volatile fallback filesystem disables automatic recovery/promotion. Failed commits remain fail-closed; an explicit setter can retry or repair this statefile on durable storage, applying only after complete verification. It cannot make volatile storage durable. A verified backup repairs a conclusively missing primary after an interrupted rename; unreadable live state is not replaced automatically.

Current writes use the 33-byte UW2 format, including the latest watchdog event. Valid 20-byte UW1 files still load with their saved mode, tier, and counters; they upgrade on the next legitimate event/configuration save, not merely on boot. Each attempted software recovery or re-enumeration saves its event and counters once. Probes, idle ticks, ownership/backend deferrals, and status reads do not write flash. A failed save leaves the live event marked unsaved and automatic actions fail closed until explicit repair.

"},{"location":"usb_logging_watchdog/#last-event-review","title":"Last-event review","text":"
get usb.watchdog.last\n

This returns the latest event only, not an unbounded history. New events replace older ones; exporting periodic management reports can retain observations off-node. Mode changes preserve the record. Erasing the filesystem or restoring a backup can remove or roll back it.

The event includes an advisory Unix epoch from the node's RTC, uptime seconds at that recorded boot, a saturating event sequence, action, reason bitmask, and durability status. Uptime extends the millisecond clock across rollover. RTC time is not verified UTC: default clocks can start at a plausible fixed date, and a later time correction does not rewrite old events. Epoch zero is allowed and can mean no usable timestamp source. Use a synchronized node clock for meaningful calendar times; sequence and recorded uptime remain useful when clock time is wrong.

Reason bits are 1 host absent, 2 reader absent, 4 physical TX stalled, and 8 previously confirmed stats client inactive. Physical failures can set multiple bits; a stats-only expiry uses bit 8 and cannot authorize an MCU reset. Actions are soft-recovery, reenumerate, reboot-requested, and reboot-cancelled. Software actions mean the backend reported an attempt, not that USB was successfully repaired. A reboot request is persisted with the next backoff tier before authorization; health/ownership are rechecked after the commit. A subsequent veto records cancellation immediately in RAM. If OTA or another owner makes storage unsafe, its durable save waits for a safe application tick; power loss before that save can leave the earlier reboot intent on disk. Neither the intent record nor the existing reboot counter proves a completed physical reset. Power loss or a hardware main-loop watchdog can occur without any opportunity to save an exact trigger time; this record covers the staged USB logging watchdog.

get usb.watchdog includes these compact fields:

Field Meaning usb, log, host, reader, logger Backend supported; logging master; observed physical USB host/reader; confirmed healthy stats client. stall, stage, step Physical TX stall; stage 0 idle, 1 software wait, 2 re-enumeration wait, 3 reset pending; saved tier index 0-8. retry, idle, auto Saved MCU interval, current monitored fault age, and continuous Auto qualification age, in seconds. Retry is not an action countdown. defer, fs Ownership/backend deferral and verified durable persistence readiness. rec, boot Attempted USB recoveries and durable reset authorizations. Attempts save the event and recovery count together; a failed save can leave the live recovery count unsaved. The reset count can include a later safety/health veto; it is not a count of completed physical reboots.

The MGR2 management report exports the USB status and the same latest-event record as a frozen, authenticated public snapshot. Updated Python and browser decoders retain released MGR1 support; older decoder copies must be updated to read MGR2. Events appear on the normal management schedule, not as immediate alerts. Reading status does not renew the logging-client lease.

Concurrent readers receive one coherent status publication, including its tier, counters, and latest event. Reads never wait for a writer. If three bounded attempts cannot obtain a snapshot, status conservatively reports deferred recovery and no verified persistence, ages, counters, or event. It preserves published hardware/logging/mode intent and protects a possibly active reader. This temporary fallback cannot authorize a reset. The separate awake hint keeps an already durable watchdog policy serviced while publication is busy.

On nRF52, the CPU can wait for interrupts between watchdog checks in both on and auto modes. Its clock and wake interrupts continue running, so this does not restart recovery deadlines or disable the watchdog. Active USB work and unfinished transport detach/reattach still prevent that idle wait. Other platforms retain their existing sleep guard, including protection against sleep modes that restart the application clock.

"},{"location":"usb_logging_watchdog/#verification","title":"Verification","text":"

The two qualification stages below precede the subsequent upstream USB/GPS and repeater trace merge. Their firmware byte margins describe those earlier images, not the current merged source.

Initial watchdog qualification on 2026-10-03, before the latest-event extension, passed 109 USB regression tests, 143 native test cases, 11 management protocol tests, and the browser decoder checks. Tests execute the production policy, runtime, marker parser, role ownership guards, and native USB boundaries with simulated clock, filesystem, and FIFO faults. The 14-day qualification uses an advanced test clock, not a hardware soak. Bounded console output, unread hosts, session races, and exact stats markers are covered.

Those initial compiled checks passed Station G2 Full Companion, Repeater, and Room Server; XIAO nRF52 Full Companion and Repeater (full and reduced OTA qualifications); and ordinary RAK 3x72 STM32 USB Companion. The STM32 image retains all CLI features and its 32 KiB filesystem, but has only 368 bytes of app flash margin; its unsupported USB watchdog machinery is compiled out.

Latest-event verification on the same date passed 116 USB regression tests, 15 management tests with fatal AddressSanitizer/UndefinedBehaviorSanitizer checks, and the browser decoder tests. Coverage includes timestamp callbacks, clock corrections and rollover, legacy-state migration, commit failures, reboot-intent cancellation, deferred cancellation saves, and authenticated cross-language decoding of the development report formats. The extension was called MGR3 during those pre-release checks, then assigned to MGR2 because the earlier USB-only MGR2 layout had never been released. Current decoders support released MGR1 and the finalized MGR2 layout; this reassignment was not a migration of deployed MGR2 reports. The full native suite passed 1,713 cases during implementation; the final packet validator also passed a fresh 116-case routing suite and 5,855,168 previous/current validator comparisons under fatal sanitizers.

The extension compiled in standard Station G2 Full Companion and Repeater, XIAO nRF52 Repeater, and RAK 3x72 USB Companion. The normal build.sh STM32 recipe at version 1.17.1.9 retains all CLI features and the 32 KiB filesystem, with 372 bytes of app flash margin; longer version strings or future changes can still exceed that tight budget. No features or filesystem space were cut to fit this extension. This is not an all-board release qualification or a physical USB recovery test. No hardware was flashed, and verification artifacts stayed outside out.

The subsequent USB/GPS/repeater-trace merge passed all 1,718 native cases, 133 USB regression methods, 25 management/event/watchdog methods plus the browser decoder, and 10 GPS/trace/persistence methods. Companion preference tests exercise six platform/layout variants under fatal sanitizers. The compact final preference writer also matches the previous full image for 256 tail-value sets per variant and preserves committed files through every partial-tail write/readback boundary and rename failure.

Compile checks for that merge passed standard XIAO S3 Full Companion, Station G2 Repeater and Room Server, native-USB XIAO S3 USB Companion with all five session/progress hooks linked, and XIAO nRF52 Repeater. The final compact writer passed both the bare STM32 CI recipe and the normal 1.17.1.9 recipe; the latter uses 229,020 of 229,376 app flash bytes (356 bytes of margin), retaining all features and the 32 KiB filesystem. The bare recipe is tighter still. These checks do not qualify every board or physical USB recovery. No device was flashed and existing out artifacts were not changed.

Finalizing the event-capable layout as MGR2 passed 1,719 native cases, 16 management tests with real C++/Python/browser cryptography, the standalone browser decoder checks, and the bare STM32 Companion compile. Coverage retains MGR1, rejects retired formats and old prototype ciphertext, and verifies that invalid management sends release their packet without queueing it.

"},{"location":"usb_serial_backpressure/","title":"Usb serial backpressure","text":"

View this page on MeshCore Docs.

"},{"location":"usb_serial_backpressure/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"v4_pixel5_font_trial/","title":"V4 pixel5 font trial","text":"

View this page on MeshCore Docs.

"},{"location":"v4_pixel5_font_trial/#document-moved","title":"Document moved","text":"

This document is now in Test Results.

"},{"location":"old-releases/","title":"Old Releases","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/#old-releases","title":"Old Releases","text":"

Historical release notes and their supporting files are kept here for reference. Use the current 1.17.1.8 release notes for current firmware.

"},{"location":"old-releases/1.17.1.5-doc-audit/","title":"1.17.1.5 doc audit","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.5-doc-audit/#markdown-audit-11715-usa-cascade","title":"Markdown audit \u2014 1.17.1.5 USA Cascade","text":"

Audited on 2026-09-05 against firmware source 26303793, the resolved build configuration, and the qualified 1.17.1.5 staging manifests. The scope was all 71 tracked Markdown files present at the start, including root documents, docs, examples, tools, tests, variants, and vendored library documentation. Generated/ignored build directories and external repositories were outside that scope.

"},{"location":"old-releases/1.17.1.5-doc-audit/#corrections","title":"Corrections","text":""},{"location":"old-releases/1.17.1.5-doc-audit/#verification-and-limits","title":"Verification and limits","text":"

All original Markdown files were scanned for relative links, section anchors, source-path references, version/default claims, and role/OTA terminology. Flagged operator instructions were checked against CommonCLI, its observer extension, Companion command handlers, board configuration, and release manifests. Brace-expanded source references and SDK-relative paths were retained as such. Historical measurements, changelogs, upstream findings, contribution/security policies, and licenses were not rewritten as new validation results.

The documentation site passed a strict MkDocs Material build. Relative repository links and section anchors were checked separately, including documents outside the MkDocs source directory. Console and current bootloader links were checked on their primary sites. This was not a crawl of every third-party URL or a new hardware/firmware qualification; no claim is made that every board was physically tested or every protocol statement independently re-proved.

"},{"location":"old-releases/1.17.1.5-doc-audit/#original-file-coverage","title":"Original file coverage","text":"

\u201cRetained\u201d means no correction was identified by this documentation audit; it does not certify all historical statements as current hardware behavior.

File Disposition AGENTS.md Scanned; retained ALERTS.md Scanned; retained CHANGELOG.md Historical record retained CONTRIBUTING.md Scanned; retained MEMORY_MONITORING.md Scanned; retained MQTT_IMPLEMENTATION.md Corrected or clarified MQTT_INTERNALS.md Corrected or clarified MQTT_OWNERSHIP.md Historical record retained MQTT_SNMP.md Corrected or clarified README.md Corrected or clarified RELEASE.md Corrected or clarified RESTORE_UPSTREAM_NOTES.md Historical record retained SECURITY.md Scanned; retained STABILITY_TESTABILITY_HANDOFF.md Historical record retained UPSTREAM_BUGS.md Scanned; retained WEB_CONFIG_REVIEW.md Corrected or clarified arch/stm32/Adafruit_LittleFS_stm32/README.md Vendored documentation/license retained arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md Vendored documentation/license retained arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md Vendored documentation/license retained docs/WiFi.md Corrected or clarified docs/cli_build_matrix.md Corrected or clarified docs/cli_command_availability.md Corrected or clarified docs/cli_commands.md Corrected or clarified docs/companion_offline_queue.md Scanned; retained docs/companion_protocol.md Scanned; retained docs/companion_radio_full.md Corrected or clarified docs/docs.md Scanned; retained docs/esp32_memory_budget.md Scanned; retained docs/faq.md Corrected or clarified docs/filter_tool.md Scanned; retained docs/firmware_picker.md Corrected or clarified docs/flood_filtering.md Scanned; retained docs/full_companion_features.md Corrected or clarified docs/full_companion_usb_switcher.md Corrected or clarified docs/gps_tracking.md Scanned; retained docs/halo_keymind_settings.md Corrected or clarified docs/hardware_validation_checklist.md Corrected or clarified docs/host_cli_service.md Scanned; retained docs/index.md Corrected or clarified docs/kiss_modem_protocol.md Scanned; retained docs/lora_ota_automation.md Corrected or clarified docs/mbedtls-tls-footprint.md Scanned; retained docs/nrf52_companion_storage_recovery.md Scanned; retained docs/nrf52_power_management.md Scanned; retained docs/number_allocations.md Scanned; retained docs/ota_easy.md Corrected or clarified docs/ota_meshtower_v2_sdcard.md Corrected or clarified docs/ota_nrf52_bootloader_update.md Corrected or clarified docs/ota_nrf52_qspi.md Corrected or clarified docs/ota_protocol.md Corrected or clarified docs/ota_user_guide.md Corrected or clarified docs/packet_format.md Scanned; retained docs/payloads.md Scanned; retained docs/qr_codes.md Scanned; retained docs/rak3401_mota_chain.md Historical record retained docs/releases/1.17.1.5.md Corrected or clarified docs/spiffs_regular_file_reads.md Scanned; retained docs/stats_binary_frames.md Scanned; retained docs/telemetry_decoder.md Scanned; retained docs/terminal_chat_cli.md Scanned; retained docs/usb_serial_backpressure.md Scanned; retained examples/host_cli_service/README.md Scanned; retained lib/PsychicMqttClient/CHANGELOG.md Vendored documentation/license retained lib/PsychicMqttClient/README.md Corrected or clarified mesh-america/README.md Corrected or clarified test/README.md Scanned; retained test/test_mqtt_prefs_codec/FIXTURES.md Historical record retained tools/mota/README.md Scanned; retained tools/sensecap_indicator_font/README.md Scanned; retained tools/sensecap_indicator_rp2040/README.md Scanned; retained variants/sensecap_indicator-espnow/README.md Scanned; retained"},{"location":"old-releases/1.17.1.5/","title":"1.17.1.5","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.5/#meshcore-11715-dev-usa-cascade","title":"MeshCore 1.17.1.5 Dev - USA Cascade","text":"

Use feature switches by role for exact on/off commands for Companion, Repeater, Room Server, and Sensor, including MQTT, logging, power saving, GPS, and board exceptions. Open the USB web console for the default ASCII terminal on Full Companion and infrastructure images.

The firmware identifier and main release tag follow v1.17.1.5-halo-keymind-cascade-dev-<commit>, matching the 1.17.1.4 naming convention. Supplemental pages retain the repeater-room-, utility-, logging-, lora-ota-, and full-profiles- prefixes where those outputs exist. The Companion page is marked Latest; the supplemental pages remain development prereleases.

This release uses the USA/Canada radio preset and Cascade runtime defaults. The release build records the resolved frequency, bandwidth, spreading factor, and coding rate. The offline USA Cascadia fallback is 910.525 MHz, BW62.5, SF7, CR5. Saved node settings survive ordinary firmware updates.

"},{"location":"old-releases/1.17.1.5/#memory-corrections-in-aa20e927","title":"Memory corrections in aa20e927","text":"

All 583 published profiles (the 540-profile matrix plus 43 supplemental Full repeaters) were checked against the new runtime RAM policy. All 11 failing profiles have replacements, and the same queue sharing has been applied to the other 41 nRF52 Full profiles: 52 corrected profiles in total, initially built from aa20e927. The existing release tags stay in place; corrected filenames end in aa20e927, except Wireless Paper Full's capacity-restoring follow-up, 1e4d1e16. Companion remains Latest. Both Mesh America catalogs and the firmware picker point to the replacement files.

All 43 nRF52 Full Companions keep 256 offline frames normally and lend 128 slots to mOTA while needed. Wireless Paper Full now also shares its queue and keeps 350 contacts and 40 channels. The other six corrected ESP32 Full profiles below use 150 contacts, preserving their 256-frame queue and Full transports. Export contacts before upgrading if you have more than 150; entries beyond that limit may be unavailable and a later save may omit them. This warning applies to those six profiles and the superseded aa20e927 Wireless Paper image.

Wireless Paper Full's 1e4d1e16 update has 256 message slots normally and 128 while an mOTA session borrows its workspace. Idle WiFi listening leaves all 256 slots available. If more than 128 unread messages are queued, sync them with an app before starting mOTA. Source stop/disconnect restores all 256 slots and frees ESP32 scratch buffers. USB, Bluetooth, WiFi, the display and the partition layout are retained. The application is 1,869,976 bytes; use its app-only .bin for a compatible WiFi update, or its merged image for USB installation.

The T096 and T1 LoRa OTA repeater corrections allocate the diagnostic manual staging buffer only when requested, recovering about 4 KiB at startup. Ordinary mOTA uses its existing staging store. ota dev clear releases a manual stage; allocation failure reports an error before writing. These same-version repairs require an explicit exact-target manual LoRa installation or USB; forward-only trusted autoinstall does not select an equal-version repair.

Available RAM below means capacity before startup allocations, not free heap after boot. The Wireless Paper row includes its latest 1e4d1e16 update:

Failing original profile Original bytes Replacement bytes Required bytes Generic_ESPNOW_companion_radio_full 101,920 139,456 136,192 Heltec_Wireless_Paper_companion_radio_full 150,144 161,336 152,576 Heltec_Wireless_Tracker_companion_radio_full 148,496 186,032 169,986 Heltec_ct62_companion_radio_full 99,104 136,640 136,192 Heltec_t096_companion_radio_full_femon 54,724 74,060 73,728 Heltec_t1_companion_radio_full 55,044 74,380 73,728 Heltec_v3_companion_radio_full 143,840 181,376 173,056 Xiao_C3_companion_radio_full 98,880 136,416 136,192 heltec_tracker_v2_companion_radio_full_femon 133,136 170,672 169,986 Heltec_t096_repeater_lora_ota_no_external_sensors 61,588 65,676 65,538 Heltec_t1_repeater_lora_ota_no_external_sensors 61,964 66,052 65,538

Each replacement has a matching .memory.json with its ELF hash, RAM budget and firmware-file hashes. The complete audit distinguishes original binary/log calculations from new ELF checks. All 52 exact firmware builds, their native file pairs, and 150 native mOTA tests passed, alongside memory, allocation-failure, shared-queue and picker regressions. Wireless Paper's follow-up also passed complete TCP mOTA transfers with address/leak sanitizers, repeated disconnects, queue-full refusal, idle listening and USB/TCP ownership changes. Physical boot/load/soak validation remains pending.

"},{"location":"old-releases/1.17.1.5/#g3-usb-disappearance-with-power-saving-enabled","title":"G3 USB disappearance with power saving enabled","text":"

The published 26303793 firmware has an ESP32 native-USB sleep bug that matches a Station G3 report: USB disappears from the connected Pi after a few minutes, the display button stops responding, but the repeater still answers over LoRa. Hardware confirmation of this report is pending.

Cascade enables device power saving by default on fresh settings. After two minutes, the repeater can enter light sleep when no serial terminal is open, even though the Pi has enumerated the USB device. Light sleep stops USB service; the radio GPIO can still wake the CPU, while the G3 display button is only polled. Closing a terminal must not be treated as unplugging the USB host. Espressif documents the underlying native USB sleep limitation.

To enable USB logging on an ESP32 1.17.1.5 image with logging support, disable power saving first. Use the role's text terminal, or the repeater's remote admin CLI if USB has already disappeared:

set powersaving off\nset usb.logging on\nget powersaving\nget usb.logging\n

The read-back commands should report power saving off and USB logging on. For an image without USB logging, use just set powersaving off and get powersaving to apply/check the sleep workaround. Disabling power saving saves the setting and takes effect immediately; it does not erase the node or change LoRa RX power saving (radio.rxps). If USB does not reappear after disabling sleep remotely, power-cycle once and check that the port and display button remain usable beyond the two-minute mark. Ordinary reflashing can retain the saved power-saving setting, so check it during comparisons with another firmware build too.

On unified Full infrastructure, also run set powersaving off before enabling USB through set logging.output usb or set logging.output both. These are separate commands. Turning logging off does not automatically restore the saved power-saving setting. nRF52 logging does not need this ESP32 workaround.

WiFi/MQTT-only logging does not need set powersaving off while the Repeater/Room Server MQTT bridge is running. The original 26303793 code already prevents device sleep in that state. Select set logging.output wifi where supported and check get mqtt.running; get mqtt.enabled alone only checks the saved preference. MQTT-capable Full Companion now uses the same mqtt.enabled and logging.output commands as infrastructure. Disabling MQTT preserves broker settings; the MQTT tab exposes the same master switch.

The source correction keeps native USB serviced while a host is enumerated, including when its terminal is closed. Enabled live USB logging also blocks ESP32 light sleep, even without a host. FreeRTOS can still idle/yield; this does not disable the saved power-saving setting or Companion CPU scaling. With logging off, USB power from a charger still permits sleep.

The corrected G3 uses its user button as a light-sleep wake source alongside LoRa. Button activity keeps the CPU and display awake for at least two minutes; holding or pressing the button again extends the interval. The usual display timeout can keep the screen on longer. An intentional long-press power-off retains its existing action. After the wake interval, sleep is allowed when power saving is enabled, logging is off, no native USB host is attached, and no running bridge or pending work needs service.

Host simulations cover both native USB modes, terminal close/reconnect, logging toggles, OTA inhibition, simultaneous button/LoRa wake configuration, and the two-minute interval across timer rollover. The Station G3 repeater download remains the original 26303793 build; continue to use the logging workaround above on that image.

"},{"location":"old-releases/1.17.1.5/#t096-full-companion-bluetooth-and-menu-freeze-report","title":"T096 Full Companion Bluetooth and menu freeze report","text":"

A report against Heltec_t096_companion_radio_full_femon describes no usable Bluetooth despite a displayed PIN, followed by a freeze while stepping through the device menu. Reflashing both with and without a full wipe gave the same result. The exact freeze and Bluetooth behavior still need confirmation on a physical T096 with the source correction below.

The published 26303793 Full image leaves only 54,724 bytes (53.4 KiB) between the linked heap boundaries. Its static RAM check did not account for the allocations made at startup: the 160x80 color framebuffer alone needs 25,602 bytes, followed by task stacks, the packet pool, filesystem buffers, and message previews. This leaves inadequate runtime headroom. The pinned Bluefruit library also ignores failure to create its BLE/SOC worker tasks, and the sensor menu could write through a failed CayenneLPP allocation. Those are code paths consistent with this report; a PIN is not proof of a working Bluetooth radio.

The correction applies to every nRF52 Full Companion, keeping its offline queue at 256 frames during normal use. mOTA borrows the upper 128 slots for its roughly 19 KiB session state only when needed, leaving 128 message slots during mOTA use. Starting a source or a TempRadio discovery session acquires this workspace. Stopping or disconnecting the USB/Bluetooth source restores all 256 slots, even if the temporary radio window is still open. A discovery-only session returns the slots when its TempRadio window ends. No heap resizing is involved.

Unread messages keep their order across both transitions. If more than 128 frames are pending, mOTA refuses to start and asks you to sync unread messages with a Companion app first; it does not discard them to make room. OTA configuration and signer keys survive reuse of the workspace. While mOTA owns it, the normal queue overflow policy applies to the remaining 128 slots.

The T096 profile requires at least 72 KiB of heap space at link time. It retains 350 contacts, 40 channels, the color display, sensors, USB, Bluetooth, Bluetooth DFU, and USB/Bluetooth mOTA sending. This reserve is space for runtime allocations, not a measurement of free heap after boot. Bluetooth startup now checks both worker tasks and all registered services; a failed partial initialization does not repeat every five seconds. The sensor page reports low memory if its buffer could not be allocated.

Regression tests transfer complete mOTA containers through the actual shared context, check queue wraparound and message order, and cover source stop, disconnect, conflicting USB/Bluetooth ownership and repeated sessions. They also inject worker-task and GATT-service failures, verify that retries do not initialize the stack again, open the sensor path with a failed allocation, and use the real linker assertion to reject the old heap budget. The original T096 Full download has been replaced by the corrected aa20e927 image on the same release page. Another wipe is not a fix for the old compiled memory budget.

For the corrected T096 build, use its matching application UF2 through the existing USB bootloader. Preserve the bootloader and node data. Check Bluetooth discovery, pairing and message exchange on battery power and with USB attached; then step through every menu page, return from screen-off, and check USB and Bluetooth mOTA sending. Hardware validation is pending.

"},{"location":"old-releases/1.17.1.5/#firmware-roles-and-update-support","title":"Firmware roles and update support","text":"

Use the exact board's Full Companion image when available. Every Full Companion must pass linked-firmware checks for MOTA sending, its text terminal, and the platform's source transports. ESP32 supplies USB and WiFi sources; nRF52 supplies USB and encrypted Bluetooth sources. Full Companions remain USB-updateable and do not require LoRa self-installation. Optional ESP32 WiFi self-update uses port 8080 where the existing partition layout has two application slots; nRF52 retains Bluetooth DFU with a compatible bootloader.

Read Full Companion feature switches for on/off commands, USB logging, power saving, WiFi/Bluetooth selection, GPS, MQTT, MOTA sending, and update directions.

Infrastructure artifacts must have a verified wireless self-update path. Option 3 reports and omits infrastructure on platforms without a wireless updater. The report is ota-excluded-targets.txt. Source/seeder capability alone does not count as self-update support. ESP32 qualification checks both application slots and OTA metadata; nRF52 Bluetooth qualification checks the linked DFU service and its application ZIP. LoRa installation on nRF52 still requires the exact matching OTAFIX bootloader and storage arrangement.

Classic ESP32 capacity limits are recorded in each artifact's capability manifest. Generic E22, Heltec V2, Meshadventurer, and T-Beam OTA repeaters, plus the TLora MQTT repeater, use 50 neighbors where needed to preserve the required internal RAM reserve. T-Beam MQTT repeater profiles retain the full rule engine with 31 flood rules; constrained classic ESP32 ESP-NOW bridges with logging use 47. Meshadventurer Full uses 100 contacts, 30 group channels, and a 16-frame offline queue while retaining its MOTA and transport features.

Since 1.17.1.4, this branch also includes USB session and contact-persistence fixes, automatic nRF52 ExtraFS recovery, improved Indicator setup/display handling, compressed MOTA transport, and measured ESP32 memory limits. Qualified internal-flash nRF52840 receivers now reserve 64 KiB of retained RAM to extend application MOTA staging beyond the available flash prefix. This handoff requires the exact board's OTAFIX 2.4.6 bootloader. Install that bootloader before relying on LoRa updates from these new hybrid receiver images: they reject application containers larger than one flash page when the installed bootloader lacks the retained-RAM capability. QSPI/microSD roles and Full Companions retain their existing storage layouts. An interrupted hybrid download must be fetched again after an application restart or power cycle. See the protocol for the exact admission and reset checks.

"},{"location":"old-releases/1.17.1.5/#build-using-option-3","title":"Build using option 3","text":"

The equivalent reproducible command is:

OUTPUT_DIR=.releases/1.17.1.5-halo-keymind-cascade-dev \\\nbash build.sh build-firmwares-logging-matrix \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade \\\n  --require-ota --skip-kiss --resume --background\n

Or run bash build.sh, select option 3, set version v1.17.1.5-halo-keymind-cascade-dev, and retain USA Cascadia/Cascade. Option 3 enables the infrastructure OTA requirement by default. It merges supported runtime USB logging into canonical artifacts and builds the expanded ESP32 profiles. It runs one PlatformIO process at a time.

The background launcher prints its persistent log and status paths. Each target also gets a log in OUTPUT_DIR/build-logs/. Resume accepts only artifacts with the current capability schema and a successful qualification; infrastructure also needs a verified update method. Any failed target leaves the matrix unsuccessful and must be resolved before publication.

--allow-no-ota is an explicit development override for cable-only infrastructure. Those outputs do not belong in this OTA-required release. Use a separate output directory when changing the radio or settings profile. Use a fresh release output directory after changing the source revision or firmware version so packaging cannot mix old and new binaries.

"},{"location":"old-releases/1.17.1.5/#local-release-staging","title":"Local release staging","text":"

After the matrix completes successfully, scripts/package_cascade_release.py groups the qualified images using the previous GitHub release naming. Each group includes the firmware files, capability manifests, TSV/JSON inventory, checksums, build notes, and Full Companion directions. The staging directory's root FIRMWARE-PICKER-1.17.1.5.html opens the local downloads; the copies inside each release group use the planned GitHub download URLs. Staging does not push commits, create remote tags, or publish a GitHub release.

"},{"location":"old-releases/1.17.1.5/#published-release-set-and-small-slot-heltec-v4-updates","title":"Published release set and small-slot Heltec V4 updates","text":"

The capability JSON and target inventories now report both Bluetooth DFU and LoRa mOTA for all 60 nRF52 receiver profiles. The original report omitted the nRF52 LoRa method. Each affected manifest includes ota_update_requirements.lora with its storage type, accepted package types, exact-base requirement for deltas, and the matching OTAFIX bootloader link. Internal-flash receivers accept in-place deltas; supported QSPI/SD receivers also accept full application packages. The 20 retained-RAM receivers require OTAFIX 2.4.6's matching handoff support. These reports verify the released application bytes and storage layout; they do not certify the bootloader installed on a physical device. Firmware binaries and tags are unchanged. Full Companion mOTA sending remains separate from self-update capability.

Published firmware tags remain at source commit 26303793. Documentation can advance independently. The original matrix qualified 540 profiles (1,080 firmware files) from 551 attempts; 11 oversized portable attempts have expanded Full replacements. The variant map maps all 1,361 previous-release choices to 1,325 covered choices and 36 excluded entries. Multiple old choices map to the same current artifact.

For Heltec V4 with an existing compatible 1.25 MiB OTA application slot, the standard heltec_v4_repeater application is 1,179,200 bytes and the lean heltec_v4_repeater_lora_ota_no_external_sensors application is 1,232,024 bytes. Both fit that slot. Start the WiFi uploader with start ota ap, join MeshCore-OTA, and open http://192.168.4.1/update. Upload the application .bin; use stop ota to close an unused uploader. Their merged USB installers write two 6.25 MiB application slots. Expanded Full V4 applications require the larger layout and do not fit a 1.25 MiB slot.

"},{"location":"old-releases/1.17.1.6/","title":"1.17.1.6","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.6/#meshcore-11716-dev-usa-cascade","title":"MeshCore 1.17.1.6 Dev - USA Cascade","text":"

This release builds the current keymindCascade source after repairing the unit-test regressions. Its firmware identifier is v1.17.1.6-halo-keymind-cascade-dev-<commit>. The recorded commit identifies the source for every firmware file; binaries from earlier releases are not reused.

Use the firmware picker to match the exact board, radio, display and flash variant. The Companion release is marked Latest. Supporting release pages retain the repeater-room-, utility-, logging-, lora-ota- and full-profiles- prefixes where those outputs exist, and remain development prereleases.

The build uses the USA/Canada radio preset and Cascade runtime defaults. Build records include the resolved radio settings; the offline USA Cascadia fallback is 910.525 MHz, BW62.5, SF7, CR5. Ordinary updates retain saved node settings.

"},{"location":"old-releases/1.17.1.6/#published-matrix","title":"Published matrix","text":"

The GitHub release contains 553 qualified firmware profiles across five release pages. The initial matrix completed all 565 attempts: 537 passed, 16 failed, and 12 standard ESP32 profiles exceeded the 1.25 MiB portable slot. All 16 failures have passing repair builds; all 12 portable exclusions have qualified expanded Full alternatives. Smaller profiles that fit remain available.

The initial firmware uses 306feebe, the 14 nRF52 repairs use 283acd27, and the two Tracker MQTT repairs use 1274956f. Each firmware filename and target manifest records its source. Existing firmware assets and release tags were preserved when the repairs were added.

See the failure and repair report and portable-profile exclusions. Full alternatives require their matching expanded partition layout; they do not fit a deployed 1.25 MiB slot.

"},{"location":"old-releases/1.17.1.6/#two-lora-profiles","title":"Two LoRa profiles","text":"

radio2, tempradio2, radioat2 and tempradioat2 add a second profile to LoRa-equipped Companion, Repeater, Room Server, Sensor and terminal firmware. The second profile starts disabled. It supports receive-only or receive/transmit operation, separate retry ownership and backoff, and explicit off commands.

One transceiver alternates between at most two active profiles. Reception and transmission occupy that transceiver, so a packet on one profile interrupts listening on the other. This does not provide two simultaneous receivers.

By default, traffic can cross between two permanent profiles or two temporary profiles. A permanent/temporary pair stays isolated. radio2.cross auto|on|off controls this policy; receive-only still forbids transmission on profile 2. tempradio2 can carry LoRa OTA traffic while the primary profile remains available between packets. Use a firmware profile that supports the required OTA role.

Radio commands accept an optional trailing preamble, and their getters include the effective preamble. Automatic preambles round up in steps of eight. Paired with SF7/BW62.5/preamble 32, the automatic BW500 values are 48 symbols at SF9, 88 at SF8 and 120 at SF7. Configure transmitters with matching preambles too.

Dual-profile receive uses normal RX with the slower profile visited first. RX power saving and device sleep are suspended while scanning, then the saved settings apply again in single-profile mode. See the setup guide and command matrix and hardware measurements.

"},{"location":"old-releases/1.17.1.6/#other-changes-since-11715","title":"Other changes since 1.17.1.5","text":"

Feature switches and update commands are documented in feature switches by role, Full Companion features and the ESP-NOW bridge guide.

"},{"location":"old-releases/1.17.1.6/#validation-and-installation","title":"Validation and installation","text":"

The release is gated on the unit-test workflow for its source commit. CI repairs replace stale display assertions with compiled tests of bookmark saves across battery/USB transitions and message summaries in each inbox mode. The sleep harness checks both MQTT and dual-profile scanning.

The release packager checks each published profile's capability manifest, firmware-file hashes, native file pairs and runtime RAM budget. Infrastructure must have a verified wireless update path. The target manifests and exclusion report describe the actual published matrix; a build check is not a physical-board test.

Dual-profile timing was measured on a Heltec V4 with a XIAO ESP32-S3/Wio LoRa transmitter. Finite strong-signal tests and an OTA catalogue exchange passed; these tests do not establish weak-signal performance, simultaneous traffic reliability, a complete hardware OTA installation, or months of uptime.

ESP32 WiFi updates use the application .bin. The -merged.bin image includes boot and partition data for USB installation. nRF52 uses the board's .uf2 or native application DFU .zip; LoRa OTA also needs the exact destination package and a compatible OTAFIX bootloader. Check the per-profile capability manifest before choosing an update method.

"},{"location":"old-releases/1.17.1.6/#reproduce-the-release","title":"Reproduce the release","text":"

Follow-up builds repair the constrained profiles without relaxing qualification:

Use a clean checkout of the release commit and a fresh output directory:

OUTPUT_DIR=.releases/1.17.1.6-halo-keymind-cascade-dev \\\nbash build.sh build-firmwares-logging-matrix \\\n  --firmware-version v1.17.1.6-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade \\\n  --require-ota --skip-kiss --resume --background\n

Run only one PlatformIO process in that checkout. Let the matrix finish even when individual targets fail. The packager normally requires a successful full matrix. For an initial release with gaps, scripts/package_cascade_release.py accepts --allow-partial with --version 1.17.1.6, the original completed build status and source commit. It lists every failed attempt in BUILD-FAILURES.md and BUILD-FAILURES.json; each published file must still pass all qualification and memory checks. Repair the failures and add the corrected profiles afterward. PORTABLE-PROFILE-EXCLUSIONS.md and .json separately list standard ESP32 profiles that exceed the 1.25 MiB slot and are redirected to the expanded Full pass. Those size deferrals are not compiler failures. Qualified smaller images remain available; Full alternatives require their matching partition layout.

"},{"location":"old-releases/1.17.1.7/","title":"1.17.1.7","text":"

View this page on MeshCore Docs.

"},{"location":"old-releases/1.17.1.7/#meshcore-11717-usa-cascade","title":"MeshCore 1.17.1.7 USA Cascade","text":"

This development release uses 910.525 MHz, 62.5 kHz bandwidth, SF7, and CR5 with Cascade defaults. Saved radio settings take precedence. The firmware source is 2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1.

The release family contains 536 qualified firmware profiles on five GitHub release pages. The companion page is the main release; the repeater and room server, sensor and utility, LoRa OTA, and expanded ESP32 Full pages have the same version suffix. The firmware picker selects the exact board, role, storage and update file.

"},{"location":"old-releases/1.17.1.7/#installation","title":"Installation","text":""},{"location":"old-releases/1.17.1.7/#build-verification","title":"Build verification","text":"

The original matrix produced 532 qualified images and four STM32WL USB Companion flash overflows. Those four were rebuilt individually with packet diagnostics omitted and passed capability, flash size and memory checks. The repair record identifies them. The local release package also verified all 94 ESP32 migration archives and generated SHA-256 sums.

Automated qualification does not establish physical update compatibility on every board. Verify critical deployed devices with the matching image and recovery path before a fleet upgrade.

"},{"location":"releases/1.17.1.5-doc-audit/","title":"1.17.1.5 doc audit","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.5-doc-audit/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.5/","title":"1.17.1.5","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.5/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.6/","title":"1.17.1.6","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.6/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.7/","title":"1.17.1.7","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.7/#release-notes-moved","title":"Release notes moved","text":"

These notes are now in Old Releases.

"},{"location":"releases/1.17.1.8/","title":"1.17.1.8 USA Cascade","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.8/#meshcore-11718-usa-cascade","title":"MeshCore 1.17.1.8 USA Cascade","text":"

This release uses 910.525 MHz, 62.5 kHz bandwidth, SF7, and CR5 with Cascade defaults. Saved radio settings take precedence. The firmware source is 9053038ffa7f481e4b0af2bb8382210683a2c317.

The release family contains 536 qualified firmware profiles on 5 GitHub release pages. The Companion page is GitHub's Latest release; supporting pages have the same version suffix. The firmware picker and Mesh America catalogs select this family.

"},{"location":"releases/1.17.1.8/#changes-since-11717","title":"Changes since 1.17.1.7","text":""},{"location":"releases/1.17.1.8/#installation-and-partition-expansion","title":"Installation and partition expansion","text":""},{"location":"releases/1.17.1.8/#verification-and-known-limits","title":"Verification and known limits","text":"

The original matrix passed 526 targets and failed ten. The 526 passing packages were preserved byte-for-byte; only the ten failures were rebuilt using corrected build configurations. Five ESP32 Full Companions use 100 contacts. Generic ESP-NOW and Heltec V3 retain 256-frame queues; CT62, Tracker V2 and XIAO C3 use 224-frame queues. Their Full transports remain enabled. Two Indicator LoRa Full images retain 350 contacts and 512 queued messages, with a 320x320 internal render canvas scaled to the 480x480 panel. Three STM32 USB Companions use branch-based switch code generation to fit the existing app boundary. The downloadable REPAIR-PROVENANCE.json records each repair and its tooling commit.

All published firmware passed capability, flash size and runtime memory qualification. nRF52 UF2 files passed address/format checks. All 94 ESP32 migration archives passed package integrity checks. Each release page includes target manifests and SHA-256 checksums. The unit-test workflow for this source passed.

The Test Results archive includes measured reports and 1.17.1.8 Preview 1 qualification evidence. Reports for older firmware remain historical and do not qualify this release.

Qualification does not establish physical compatibility on every supported board. Review the exact image's capability and memory manifests, especially sensor and contact-capacity reductions. Reduced nRF52 LoRa OTA profiles retain only their documented sensor set.

The reported T1000-E/iPhone Bluetooth state where iOS shows a connection but the app keeps waiting has not been conclusively reproduced or resolved. This release includes the verified Bluetooth fixes above; it does not claim that report is fully fixed.

"},{"location":"releases/1.17.1.9/","title":"1.17.1.9 USA Cascade","text":"

View this page on MeshCore Docs.

"},{"location":"releases/1.17.1.9/#meshcore-11719-usa-cascade","title":"MeshCore 1.17.1.9 USA Cascade","text":"

Firmware source: 88c85108005a30a730fa7b78b2899b210754f3be. Release packages and their public assets passed validation. Hardware observations below retain the exact source and installation route tested.

USA Cascade defaults are 910.525 MHz, 62.5 kHz bandwidth, SF7 and CR5. Saved radio settings take precedence. Match the exact board, role and storage layout before installing an image.

"},{"location":"releases/1.17.1.9/#changes-since-11718","title":"Changes since 1.17.1.8","text":""},{"location":"releases/1.17.1.9/#firmware-profiles","title":"Firmware profiles","text":"

Choose the exact hardware variant and role in the firmware picker. Full includes the transports and optional sensors supported by that board's qualified recipe; it does not add peripherals that the hardware cannot support. Runtime controls select USB logging, power saving and included bridge modes without a separate image.

ESP32 normally has one public Full image per board and role. A migration ZIP may contain a different required target identity; use its supplied Full image for that migration. Station G2's ordinary Full image retains an Observer-named OTA identity, even though it is the combined Full profile.

nRF52 Repeater, Room Server and Sensor provide both Reduced sensors + LoRa OTA and Full supported sensors + LoRa OTA. Pick Full when its supported optional sensors are needed. Both keep the exact board/role/storage identity and need the matching OTAFIX bootloader. Full Companion remains the combined Companion choice. See profile details.

"},{"location":"releases/1.17.1.9/#installation","title":"Installation","text":"

An ESP32 merged .bin installs bootloader, partitions and application over USB. An app-only .bin requires a compatible existing layout. Partition expansion requires the exact board and role migration ZIP from this release family's utility page, or the matching merged image over USB. Follow the ZIP's README and use its supplied Full application. get storage.layout reports the live layout on firmware that supports it.

For the measured ESP32 upgrade route and examples, read Upgrading from 1.17.1.8 over WiFi. Download the selected image before joining the node's update access point.

Check the image's flood-rule capacity before updating a repeater. Move active rules into the supported slots and save them before installing a smaller table. An over-capacity policy is retained on storage but remains inactive until compatible firmware is installed.

Switching from TinyUSB to native hardware CDC changes the USB product name and Linux /dev/serial/by-id path. Update serial consumers to the new device path after the first upgrade. Hardware CDC descriptors are fixed in silicon; firmware cannot retain a custom TinyUSB product name. Do not identify a radio solely by a changing /dev/ttyACM number. See Espressif's explanation.

nRF52 application UF2 and DFU ZIP files do not install a bootloader. Use the exact board/storage OTAFIX mapping in the firmware picker. See the role controls, Full Companion guide, and USB web console.

"},{"location":"releases/1.17.1.9/#verification-scope","title":"Verification scope","text":"

The completed release contains 463 qualified firmware profiles across 5 stable release groups with 1988 verified public assets. All 114 nRF52 Full/Reduced sensor pairs and 94 exact ESP32 board/role migration packages passed checksum, capability, memory, identity and layout checks for firmware source 88c851080. Public asset digests and the exact release family marked GitHub Latest were independently verified.

Download the 1.17.1.9 USA Cascade family and its migration utilities. These verified releases supply the firmware picker and Mesh America catalogs.

Production-code host regressions cover the official configuration GUI's five-second command deadline, app protocol and ACL replies, USB lifecycle, OTA admission/MD5/completion/error handling, shared WiFi credentials, AP and MQTT ownership, and legacy RX-gain loading. Synthetic peripheral tests cover boards unavailable for physical testing. They do not establish every board's physical USB, radio, WiFi or power behavior.

The latest-source G2 ordinary Full Observer passed a separate USB application installation with complete application readback, exact normal runtime and EndF identity, and unchanged checked boot/partition/NVS/OTA-data/filesystem regions. Its read-only post-boot check matched the original identity, passwords, two Admin ACLs, all 22 common settings, seven supported bridge fields and saved extras. Core error flags were zero. MQTT was running with valid effective configuration; broker connectivity was not tested. Original TTY settings, three host services and the Pi network were restored.

The legacy stock 1.14.1 G2 WiFi-only migration is not qualified end to end. The earlier 1e421ff4 trial completed partition expansion and the supplied Full application's WiFi upload, but its final network check reached the client/DHCP deadline without a verified G2 client or any HTTP request. Final image/layout/identity/settings verification through HTTP remains unproven. The successful latest-source USB maintenance above does not qualify that cable-free path. Plan for USB recovery when starting from this stock image; do not rely on the migration trial as a verified LoRa or WiFi recovery procedure.

A published 1.17.1.8 Full V4 installed newer applications over its node AP and a controlled LAN on an already-expanded layout. Its old uploader could miss HTTP completion despite installation and restart; check ver before retrying an apparently stalled page. Current-source physical V4 testing was unavailable. Earlier V4/G2 AP uploads and quiet USB tests remain evidence for their recorded revisions. A bridge-setting command also missed its five-second deadline; later healthy uptime and readback did not turn that timeout into a pass.

The detailed browser OTA and release-candidate report retains the complete source-specific results, failures and recoveries. See also the native V4 console report, Linux Web Serial report, and USB backpressure results. These checks do not measure fleet reliability, MCU sleep/current draw, stack high-water or sustained RF throughput, or qualify every browser, phone, operating system, bootloader or starting firmware version.

"},{"location":"research/","title":"Research","text":"

View this page on MeshCore Docs.

"},{"location":"research/#research","title":"Research","text":"

Engineering investigations, design notes, diagnostic methods, and historical update-chain studies live here. For measured outcomes, see Test Results; for current installation instructions, use the documentation homepage.

"},{"location":"research/#memory-and-storage","title":"Memory and storage","text":""},{"location":"research/#diagnostics-and-update-chain-studies","title":"Diagnostics and update-chain studies","text":""},{"location":"research/companion_contact_cache/","title":"Companion contact cache","text":"

View this page on MeshCore Docs.

"},{"location":"research/companion_contact_cache/#companion-contact-caches","title":"Companion contact caches","text":"

Full Companions on ESP32 and nRF52 without PSRAM, plus nRF52 USB/BLE profiles with more than 300 contacts, keep 16 outgoing paths and 16 shared secrets in RAM. Every contact remains in the contact table; selecting a different contact loads its saved path as needed. This recovers about 25.4 KiB of internal RAM in the 350-contact ESP32 qualification builds.

See the qualification results for the six-board RAM comparison, physical tests, and shared-secret timings.

PSRAM boards retain their existing complete contact table in external RAM. Lower-contact legacy profiles and ordinary ESP32 USB/BLE/WiFi Companion profiles retain inline paths and secrets. High-contact nRF52 USB/BLE profiles (including Wio Tracker L1) enable the same cache, keeping all 350 contacts while making room for the shared 256-frame offline/DM pool. The build flag MESH_CONTACT_CACHE=0 or 1 overrides this policy for qualification; it is not a runtime feature switch.

The ordinary Wio Tracker L1 BLE build reproduced a runtime RAM failure with 48,964 bytes available against 59,968 required. Enabling the existing cache for high-contact nRF52 profiles recovered 30,200 bytes: the linked build has 79,164 bytes available and a 19,196-byte margin against the unchanged budget. It keeps 350 contacts, 40 channels and 256 normal offline frames. As on the cached RAK profiles, up to 15 held one-key DMs temporarily use queue slots; their storage is shared instead of reserving a second pool. This is a linked-capacity result, not a physical load/soak test.

"},{"location":"research/companion_contact_cache/#paths-and-persistence","title":"Paths and persistence","text":"

The 16 resident paths use least-recently-used replacement. Contact records keep small handles to their saved paths, including 1-, 2-, and 3-byte hop hashes. The existing 152-byte contact record and Companion app frame formats are unchanged. ESP32 uses /contacts3; nRF52 also supports its paged contact store and migration from /contacts3. An erase is not required.

A hot path needs no flash read. A cold path is read from its existing contact record. ESP32 reuses one open contact-file reader to avoid repeated SPIFFS metadata scans during synchronization and saving. It closes that reader before replacing the file or recovering a saved transaction.

Changing a copied contact preserves the old path for a pending app response or rollback. Dirty entries are saved before eviction. If storage cannot be read, written, or represented completely, the operation fails instead of sending along a different route. Contact I/O failures request bounded recovery between callbacks. A raw cached-path read fault first rechecks the current immutable paths without resetting accepted RAM changes or their save deadline. Persistent faults or uncertain file publication reload a readable live image, otherwise a readable backup, and discard only unrecoverable contact data if neither works. Recovery releases a pending contact-list snapshot before resetting the table but preserves its requesting route; the changed table revision starts a fresh list that can finish normally. Explicit session cancellation still stops the stream. nRF52 preserves healthy pages when another page is damaged. No archival .bad files are kept. If damaged contacts cannot be removed, the node remains usable with volatile contacts; explicit saves still report failure, while reboot/shutdown remain available. Identity, credentials, channels and unrelated settings are not erased. Allocation/capacity failures and shared filesystem-migration authority errors are not corrupt-contact evidence and do not authorize this reset. Failed source discovery also preserves contact files; if authority stays unknown, use volatile contacts instead of deleting readable pages or selecting a potentially stale view. The path checksum also rejects a cold path changed underneath a live handle; it does not add a new checksum to the legacy ESP32 on-disk format.

"},{"location":"research/companion_contact_cache/#legacy-interrupted-contact-saves","title":"Legacy interrupted contact saves","text":"

ESP32, RP2040 and STM32 Companions can load the complete records before a partial final record in a legacy /contacts3 file. This includes cached and uncached ESP32 builds. The file must contain at least one whole 152-byte record, and every whole record must be readable. The partial record is ignored; loading does not truncate the source. The next successful ordinary contact save writes the complete loaded contacts through the platform's existing transaction writer.

On ESP32 and RP2040, this salvage applies only when the presence check reports /contacts3.bak absent. A readable complete backup takes precedence over the incomplete primary. An unreadable backup keeps the existing bounded retry and recovery behavior; the partial primary is not used to declare that backup expendable. ESP32 also defers salvage when backup presence is uncertain. RP2040's filesystem API does not distinguish a metadata I/O error from a missing file. STM32 does not select a contact backup.

This recovers a readable prefix from an interrupted older in-place save, not arbitrary corruption. Legacy records have no file checksum, and the existing route validation and repair still apply. A file with no complete record, or one that remains unreadable after recovery retries, can still be discarded by contact-only recovery. Failure to remove damaged data leaves contacts volatile and explicit saves unavailable, without erasing identity or unrelated settings.

nRF52 retains its separate legacy-size validation and paged-store migration; this partial-tail salvage does not apply to nRF52. The existing ExtraFS recovery guide describes its separate mount and metadata recovery boundaries.

"},{"location":"research/companion_contact_cache/#shared-secrets-and-power","title":"Shared secrets and power","text":"

Both platforms reuse the 16 most recent secrets from RAM. Cache misses use a platform-specific policy:

Platform Miss behavior ESP32 Recalculate the secret without reading or writing a secret file. nRF52 Try a saved LittleFS entry, then recalculate if it is unavailable.

On the V4, a measured SPIFFS secret lookup took 132,427 microseconds while key exchange took 21,932 microseconds. ESP32 therefore uses recalculation on a RAM-cache miss. It avoids secret-file writes and reduces time awake for this measured workload. These are elapsed times, not measurements of energy in joules; other ESP32 hardware has not been physically benchmarked.

On a RAK3401, a LittleFS lookup took 7,812 microseconds versus 31,250 microseconds for key exchange. The nRF52 backend therefore uses saved flash entries to avoid repeat key exchange when a usable entry exists, including after reboot. Saved entries match the full peer public key and a fingerprint of the local key pair, and have a record checksum. Importing a different private or public key invalidates old results. A corrupt or missing saved entry is recalculated. The native tests use the firmware's actual Ed25519 key-exchange library and verify agreement with the peer's independently calculated secret.

Derived secrets are expendable. Their storage keeps a reserve for contacts, preferences and filesystem metadata; a full filesystem leaves the calculated secret usable in RAM. nRF52 packs 56 entries into a 3,844-byte page to use its 4 KiB flash blocks efficiently. Small filesystems may not have space to persist secrets for all 350 contacts. A miss without a usable saved entry still performs key exchange.

MESH_CONTACT_SECRET_FLASH_CACHE=0 or 1 overrides the miss policy for qualification builds. The optional ESP32 flash backend groups eight entries per file. The normal ESP32 policy does not create or use those files.

"},{"location":"research/companion_contact_cache/#inspect-the-cache","title":"Inspect the cache","text":"

From the Full Companion's ASCII USB terminal or authenticated local CLI:

get contact.cache\n

Cached builds report paths=16 secrets=16, miss=calculate or miss=flash, plus these boot-session counters:

Field Meaning ram_hits Shared secret reused from RAM. flash_hits Shared secret loaded from flash without recalculation. calculations Shared-secret key exchanges performed. save_skips Calculated secrets that could not be persisted, including storage reserve/backoff.

Uncached builds report paths=inline secrets=inline. The normal memory command on supported ESP32 Full Companions shows live heap and queue capacity.

get contact.cache.timing reports the last successful flash lookup and last key calculation in microseconds. Zero means no such operation has completed since boot. These timings exclude secret writes and are useful for comparing the work avoided on a valid flash hit.

"},{"location":"research/companion_contact_cache/#offline-messages-during-mota","title":"Offline messages during mOTA","text":"

Contact caches are independent of the offline message queue. Builds using the shared mOTA queue retain 256 frames normally and 128 while mOTA owns its workspace, then restore 256. No unread frames are discarded to start mOTA: synchronize the queue and retry if it exceeds the retained capacity. PSRAM-backed queues keep their existing capacity and allocation policy.

Each queue frame occupies 177 bytes. Retaining 32 instead of 128 would make another 16,992 bytes (16.6 KiB) available inside the shared storage. It would not increase free heap by itself: the normal 256-frame storage is statically reserved, and the current mOTA context already fits in the loaned half. More mOTA scratch data would have to share that space to obtain an additional RAM saving. These changes keep the existing 128-frame mOTA limit.

"},{"location":"research/companion_contact_cache/#build-the-six-capacity-trials","title":"Build the six capacity trials","text":"

The optional environments in platformio.nimble.ini combine NimBLE, these contact caches, and the shared mOTA queue at 350 contacts, 40 channels and 256 normal offline frames. They remain outside the ordinary release matrix. The ordinary six constrained release recipes still use their established 150-contact limits pending wider hardware qualification.

Enable platformio.nimble.ini as described in the NimBLE trial guide, then build one environment at a time. For example:

OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \\\n  Heltec_v3_ram_trial_companion_radio_full_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-cache-trial \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n
Hardware Trial environment Heltec V3 Heltec_v3_ram_trial_companion_radio_full_nimble Wireless Tracker Heltec_Wireless_Tracker_ram_trial_companion_radio_full_nimble Tracker V2, FEM on heltec_tracker_v2_ram_trial_companion_radio_full_femon_nimble Heltec CT62 Heltec_ct62_ram_trial_companion_radio_full_nimble XIAO ESP32-C3 Xiao_C3_ram_trial_companion_radio_full_nimble Generic ESP-NOW Generic_ESPNOW_ram_trial_companion_radio_full_nimble

The firmware RAM guard remains enabled. A successful linked budget is distinct from live free heap and physical Bluetooth/LoRa qualification on each board.

The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and coredump stay at the same addresses as the ordinary huge_app.csv Full layout. Install the trial's merged image over USB first to install its partition table. Subsequent WiFi updates must fit the new slots. An application-only update from the ordinary single-slot Full layout cannot change that layout or enable WiFi OTA.

"},{"location":"research/companion_contact_cache/#shared-contact-index-storage","title":"Shared contact index storage","text":"

Companion and terminal-chat builds share compact contact indexes in BaseChatMesh. Tables with at most 65,535 total slots use 16-bit sort and matching-result entries; larger configurations retain the original int type. Contact limits, PSRAM allocation fallback and saved/wire formats are unchanged.

On the existing 32-bit platforms, a static 350-contact table saves 764 bytes from these arrays; a static 100-contact table saves 264 bytes. A 350-contact PSRAM configuration saves 96 internal bytes and 716 PSRAM bytes. Cached nRF52 records also shrink from 96 to 92 bytes by moving runtime storage bookkeeping into an alignment gap. The matched RAK4631 Full build saves 2,200 static RAM bytes in total. These figures do not describe infrastructure ACL tables.

See the 2026-10-19 qualification report for source-bound builds, tests and remaining board-specific memory limits.

"},{"location":"research/companion_contact_cache/#regression-checks","title":"Regression checks","text":"
python3 -B test/test_contact_indices.py\npython3 -B test/test_companion_contact_lifetime.py\npython3 -B test/test_contact_cache.py\npython3 -B test/test_companion_uncached_storage.py\npython3 -B test/test_companion_contact_partial_tail.py\npython3 -B test/test_companion_contact_persistence_contract.py\npython3 -B test/test_companion_contact_stream_contract.py\npython3 -B test/test_nrf52_extrafs_contract.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_t096_full_memory.py\npython3 -B test/test_firmware_ram.py\n

The cache tests compile the production path/secret cache code and extracted production persistence, routing and request functions with filesystem/radio adapters. They exercise 350 routes, eviction, snapshot rollback, page migration, identity changes, read/write/rename faults, handle exhaustion, packet release on a failed path read, and recovery after a simulated reset between file renames. They do not simulate a physical flash power cut. The partial-tail tests also check readable-prefix preservation, backup precedence, deferred normalization on save and reload on ESP32, RP2040 and STM32 using filesystem adapters. Real flash power-loss and full-volume qualification remains separate from these host checks.

tools/hil/contact_cache_serial_stress.py checks all 350 paths over USB, replaces them with a different generation, and optionally exercises secret eviction. Use an otherwise empty test node and exclusive access to its Companion data port. --populate writes test contacts; --secrets transmits 23 directed LoRa datagrams to synthetic peers. For an ESP32 cache trial:

python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \\\n  --populate --generation 1 --secrets --expect-calculate --reboot\npython3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \\\n  --generation 1 --secrets --expect-calculate\n

Use the actual device path. Omit --expect-calculate for the nRF52 flash policy. The second invocation verifies the persisted paths after reboot.

"},{"location":"research/esp32_full_build_audit_2026-10-06/","title":"Esp32 full build audit 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#why-esp32-builds-still-have-options-besides-full","title":"Why ESP32 builds still have options besides Full","text":"

Audit date: 2026-10-06. Source baseline: 2ee62b981435df7dc4bcd79de39e61a4d5e98652 on keymindCascade.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#implemented-consolidation","title":"Implemented consolidation","text":"

After the audit, the ordinary ESP32 release policy was changed to publish Full only. The exact selector replay goes from 317 planned images to 172, removing all 145 standard/reduced slots. All 172 previous Full target identities, compiled PlatformIO environments, and Full overlay flags remain identical; no new Full compilation recipe or capacity requirement is introduced. The baseline findings below describe the policy before this change.

The ordinary board/role resolvers omit ESP32 reduced aliases. Every selected infrastructure Full image is built once, using its existing exact recipe and identity. Generated Full Companion keeps its registered USB/BLE/WiFi source environment. Resume packaging removes stale standard, minimal, transport, runtime-setting, and development artifacts instead of reintroducing them. Unverified ordinary Full images fail qualification. Other chip families keep their existing profile policies, and KISS remains a distinct host-modem role.

The measured T-LoRa UART/MQTT transport split and capacities within Full are preserved. Direct explicit recovery recipes and reviewed partition-migration packages remain available separately. The existing same-partition identity allowlist is deliberately retained for OTA compatibility checks; broadening release selection does not authorize an app-only update across layouts.

Not every board has a reviewed wireless expansion package. Targets outside the migration catalog require USB/serial for a partition-table change. The updated migration guide explains this distinction and the supported cable-free routes.

The complete baseline inventory contains all 636 primary logical names and four optional local HIL names, including their exact Full mappings. The post-change selector comparison records zero standard/reduced slots, zero duplicate identities, and no change to any previous Full source recipe.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#decision-being-audited","title":"Decision being audited","text":"

Prefer one Full image for each exact board and firmware role. A smaller image needs a demonstrated RAM limit or a demonstrated failure to fit the largest safe application layout for that board and role. An image failing an old, small OTA slot does not establish either condition.

This audit examines the resolved PlatformIO recipes and the release builder's generated aliases and routing. It distinguishes options a user can explicitly build from artifacts that the normal release matrix actually publishes. Existing measurements are identified by their original revision; they are not presented as fresh builds of the audit baseline. This is a source and evidence audit followed by release-policy consolidation and focused validation, not a rebuild or hardware qualification of every ESP32 target.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#what-full-means","title":"What Full means","text":"

Full is a feature profile within a firmware role. A Full Companion combines the board's qualified app transports and text terminal. A Full Repeater, Room Server, or Sensor keeps the supported infrastructure features, logging, and OTA support. These roles have different jobs and remain separate images. Different radios, pin maps, displays, and expansion hardware also remain separate exact board recipes.

Full does not mean every board has identical contact counts, queue lengths, or routing-table capacities. Reducing a table to meet a measured RAM budget can preserve the Full feature set. Nor does a compiled feature have to run all the time: runtime settings can turn off WiFi, logging, or other supported services without installing a different image.

There are two implementations with confusingly similar names. Named *_companion_radio_full targets are Full Companion even when the infrastructure ESP32_FULL_BUILD overlay is not set. Counting only that overlay would incorrectly call these images non-Full.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#why-the-other-options-exist","title":"Why the other options exist","text":""},{"location":"research/esp32_full_build_audit_2026-10-06/#genuine-ram-constraints","title":"Genuine RAM constraints","text":"

The T-LoRa V2.1-1.6 combined UART, MQTT, and ESP-NOW image has a retained linked measurement of 190,488 available internal bytes against 196,984 required bytes: a 6,496-byte shortfall. It therefore retains separate UART/ESP-NOW and MQTT/ESP-NOW Full images. This is evidence for a transport-combination exception, not for dropping Full on the entire board. See the firmware RAM policy.

Other constrained boards already run Full with board-specific capacities. The 2026-10-02 Companion audit records passing ordinary Full builds for Heltec V2/V3, Wireless Tracker, Tracker V2, CT62, XIAO C3, Generic ESP-NOW, T-Beam, and T-LoRa. Some margins are small: Generic ESP-NOW had 1,536 bytes and Tracker V2 had 2,254 bytes above the unchanged linked runtime allowance. A pass establishes that exact image's budget, not sustained runtime or room for future feature growth.

PSRAM does not eliminate all RAM limits. Bluetooth, WiFi, DMA buffers, and some allocations still require internal RAM. The guards exclude PSRAM from the internal budget; classic ESP32 additionally has a static DRAM-placement limit. See the classic ESP32 budget.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#older-installed-partitions-and-update-identities","title":"Older installed partitions and update identities","text":"

Portable standard and *_lora_ota_no_external_sensors images can fit a historical 1.25 MiB OTA slot. On qualified targets, Full fits after expansion, but ordinary app-only OTA cannot rewrite its partition table. LoRa OTA also checks the exact target identity and layout; merely renaming a Full image does not make it a valid update for an old installation.

This explains recovery and migration artifacts. It does not establish a hardware capacity exception. Once a supported device has migrated, its ordinary role image should be Full. A small migration bridge remains useful as a tool, rather than as a second everyday firmware choice. The migration guide describes the supported layouts, preservation checks, and current limitations of cable-free migration.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#legacy-transport-and-settings-recipes","title":"Legacy transport and settings recipes","text":"

Many USB-only, BLE-only, WiFi-only, Terminal Chat, power-saving, and FEM-default recipes still exist so an exact old name can be built. Where an exact Full replacement exists, the normal release matrix already suppresses many of these duplicates. Their presence in PlatformIO is not proof that they must appear in the firmware picker or that Full cannot fit.

The release builder also retains explicit --standard and explicit reduced OTA requests. These selectors intentionally override automatic Full selection. That is a compatibility/developer policy, not a measured RAM or maximum-flash finding.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#different-roles-and-development-tools","title":"Different roles and development tools","text":"

KISS modem firmware exposes a raw modem protocol; it is not the Companion application protocol or a MeshCore Repeater. Its existence is a role decision, not an argument that the board cannot support Full. Partition migrators, expanders, legacy-layout seeds, and simulation fixtures likewise have a specific test or installation job. The normal release matrix excludes those development tools. They should be considered separately from everyday non-Full node images.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#eligibility-rules-and-missing-qualification","title":"Eligibility rules and missing qualification","text":"

The infrastructure Full selector uses recipe metadata, including admin, MQTT, and ESP-NOW markers. Companion Full qualification also needs an exact combined recipe or a qualified replacement. These rules can exclude an option without measuring its RAM or largest safe flash layout.

An absent Full recipe, an eligibility-list omission, or an old compile failure is an unproven exception. It requires qualification or an explicit product decision; it must not be described as a demonstrated capacity failure.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#what-counts-as-the-maximum-partition","title":"What counts as the maximum partition","text":"

Flash is divided among boot metadata, applications, and persistent data. Infrastructure self-update requires two application slots; it cannot use the entire chip as one app slot. Companion Full can be a host-backed LoRa OTA sender without being a LoRa self-update destination, so some Companion layouts intentionally use one larger application slot and require USB for self-updates.

T-Beam 1W Full Companion keeps LilyGo's factory-compatible single 3 MiB app slot even though the board has 16 MiB flash. This is a boot/layout and source-role contract, rather than a failure of Full to fit its physical flash.

Physical flash Normal expanded infrastructure slot Upper dual-slot ceiling with only 64 KiB data storage 4 MiB 2,031,616 B (0x1F0000) 2,031,616 B (0x1F0000) 8 MiB 3,342,336 B (0x330000) 4,128,768 B (0x3F0000) 16 MiB 6,553,600 B (0x640000) 8,323,072 B (0x7F0000)

The upper ceilings assume 64 KiB before the first application, equal 64 KiB-aligned application slots, and 64 KiB of filesystem storage, with no additional coredump or mandatory data region. They are arithmetic bounds, not qualified partition plans. Required data, preserved offsets, or a larger minimum filesystem can lower the safe bound. The existing 8/16 MiB layouts retain substantially more storage and a coredump partition; their current slot sizes are therefore not the absolute physical maximum.

SenseCAP Indicator's 8 MiB preserved-data Full layout is a further exception: it keeps 2,621,440-byte (0x280000) OTA slots and the old filesystem/coredump addresses. Its first slot ends at the preserved filesystem boundary, so both usable OTA slots have that limit despite the larger chip. This is a data-layout constraint; it must be distinguished from an absolute flash-capacity limit.

A Full size failure against the legacy 1.25 MiB ceiling, or against an existing 8/16 MiB default slot alone, is insufficient evidence for a permanent minimal image. The audit must first establish the board's actual flash size, the role's update requirements, and its minimum safe persistent-data layout.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#baseline-audit-inventory-and-outstanding-qualification","title":"Baseline audit inventory and outstanding qualification","text":"

The primary repository configuration contains 500 concrete ESP32 environments and 136 builder-generated aliases: 636 logical target names. They are recipes and identities, not 636 separate everyday images. The canonical resolver keeps 374 names, including 43 KISS modems; the default non-KISS node selection contains 331 names.

Evaluating the real release-matrix selectors and the per-build Full promotion rules gives 317 planned ESP32 node artifact slots: 172 Full and 145 non-Full. These are pre-build selections, not an assertion that 317 current binaries were produced. Size-driven fallback, failed qualification, and the final package's capability-based consolidation can change the final output.

Planned non-Full group Count Why selected Finding under the requested capacity-only exception rule *_lora_ota_no_external_sensors 99 Generated reduced OTA identity explicitly selects standard Compatibility/profile policy; no per-target RAM or maximum-layout failure is required before selecting it Legacy images in the partition-migration inventory 24 Builder deliberately retains portable output during transition to expanded Full Old installed-layout compatibility, rather than maximum hardware capacity Other complete standard images with a supported Full route 22 Their names are outside the curated Full-only and migration promotion lists Missing policy promotion/partition qualification; not demonstrated capacity exclusions

Every primary canonical node target has a supported Full route, after mapping a reduced identity to its feature-rich base. That establishes routing, not that every current Full image has passed a fresh RAM/flash build or hardware validation. No retained current-baseline failure establishes that all 145 non-Full selections are necessary because Full cannot fit RAM or the largest safe partition.

The 22 complete standard choices outside the migration list are:

These are candidates for exact Full/layout qualification, rather than an approved instruction to relabel their old binaries. The T-LoRa transport combination exception above still applies.

All 52 canonical ESP32 Companion selections are already named Full; there are zero canonical non-Full Companions. All 21 standalone ESP32 Terminal Chat names map to exact Full Companion replacements. Most surviving old transport recipes are therefore direct-build compatibility choices rather than current release duplication.

This PC's ignored local configuration additionally loads four ESP32 radio bench fixtures. Three are excluded by the utility-name filter. The fourth, profile_four_tx_v4_rx, escapes that filter and adds one standard selection when the bench configuration is loaded. It is a local release-leakage risk, not a clean primary-configuration node option; it is excluded from the counts above and listed separately in the inventory.

The capacity evidence snapshot retains 38 historical linked reports for 14 ESP32 target identities, together with original failures, repairs, app sizes, and partition tables. All 38 reports pass their recorded budgets. Every bound artifact still present in this checkout matches its saved hash; eight referenced files are absent, so their saved reports cannot be fully rebound here. These results are historical evidence, not complete current-source qualification.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#method-and-policy-locations","title":"Method and policy locations","text":"

PlatformIO resolved the configuration once. The builder was then sourced with that cached configuration and a failing pio stub; the audit evaluated its actual selectors and intercepted build calls without compiling or flashing. The local configuration only adds HIL include files, so those 11 optional environments (four ESP32) were removed from the cached primary inventory without changing ordinary resolved options. A second selector replay confirmed the primary counts. Independent policy and memory reviews checked the results.

Relevant source locations at the audited baseline are:

The audit itself did not flash a device. The resulting consolidation changes ordinary release routing and resume selection; existing firmware recipes and capacity guards are retained.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#fresh-validation-of-the-consolidation","title":"Fresh validation of the consolidation","text":"Exact Full target Final app size / slot Linked internal available / required Result LilyGo_Tlora_C6_repeater_ 2,010,024 / 2,031,616 B 283,088 / 139,200 B Passed; 21,592 B flash margin Heltec_v2_companion_radio_full 2,275,368 / 3,342,336 B 184,680 / 173,840 B Passed; 10,840 B linked RAM margin

These sequential embedded builds use the audit baseline plus the release-rule changes, version v1.17.1.9-halo-keymind-cascade-dev-fullaudit. The C6 uses its supported expanded 4 MiB dual-OTA layout. The generated V2 Full Companion correctly compiles Heltec_v2_companion_radio_wifi; its named Full identity is preserved. Both produced verified capability manifests and firmware-bound RAM reports. No physical flashing or runtime qualification is claimed.

Focused release/profile regressions cover the real matrix scheduler, exact Companion recipe mapping, stale-artifact selection, preserved T-LoRa transport split, direct recovery access, and unchanged non-ESP32 choices. The complete test/test_build_profiles.sh suite passed, as did 69 Python tests covering selection, partitions, RAM, and packaging. The 58 selection/partition/RAM cases ran on native Linux; the 11 packaging cases ran on Windows Python 3.13 because Linux Python 3.10 lacks the packaging code's pre-existing hashlib.file_digest API. The selector evidence records these results and the fresh builds' bound artifact hashes.

"},{"location":"research/esp32_full_build_audit_2026-10-06/#recommended-release-rule","title":"Recommended release rule","text":"

Use Full as the everyday choice per exact board and role wherever it passes the existing RAM guards and the largest safe, supported partition layout. Retain capacity-tuned Full recipes when they preserve the board's features. Keep genuinely incompatible transport combinations separate, with their measured reason documented. Present migration/recovery tools separately from ordinary firmware and retain old names as compatibility aliases where useful.

Require a passing ELF-bound RAM report and the exact application/partition sizes for every capacity exception. Do not allow an unexplained name-based exception to masquerade as a hardware limit. Runtime startup, reconnect, service-toggle, OTA, and sustained heap checks remain necessary for Full qualification; build success alone does not cover them.

"},{"location":"research/esp32_memory_budget/","title":"Esp32 memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"research/esp32_memory_budget/#classic-esp32-image-memory-budget","title":"Classic ESP32 image memory budget","text":"

Classic ESP32 has 320 KiB of internal DRAM, but at most 160 KiB can hold statically allocated data. The remaining DRAM is available only through the runtime heap. Bluetooth, tracing, and SDK reservations further constrain the usable region. See Espressif's memory types documentation.

An image can therefore approach its static limit while PlatformIO reports less than 40% of the 320 KiB total in use. PSRAM does not expand the region used by ordinary internal .data and .bss allocations.

"},{"location":"research/esp32_memory_budget/#build-enforcement","title":"Build enforcement","text":"

Every classic ESP32 environment inheriting esp32_base runs scripts/check_esp32_dram.py. The release builder also adds the script when an ESP32 profile replaces the inherited extra scripts. The check runs before image generation, merging, and uploading, including cached nobuild uploads. It requires the ELF and linker map to be present for those cached operations. The check script must load after merge-bin.py so the merge target retains its pre-action. A merge also requires the bootloader/partition flash layout; nobuild cannot create a bootable merged image when that layout is absent.

The check reads dram0_0_seg from the final linker map. SDK reservations are already reflected in that region; Bluetooth's reservation is not subtracted a second time. The occupied span runs from the region origin to _heap_start, including .data, .bss, .noinit, and alignment gaps. The allowed region is the smaller of the linker's region and 160 KiB.

Builds must leave at least 8 KiB free within that static region. This is an additional project margin against near-full images, not an Espressif hardware limit or a prediction of free heap. A profile may require a larger margin with custom_esp32_static_dram_reserve in bytes; values below 8192 are rejected. Missing or unrecognized map boundaries also fail the check.

For the Heltec V2 build used to investigate the repeater report:

Shared repeater defaults Static span Static region Remaining Result Previous 255 scope slots 124,020 B 124,580 B 560 B Rejected Tuned 31 scope slots 108,308 B 124,580 B 16,272 B Passed

These measurements remove the board's existing scope-capacity override to exercise the shared defaults. The normal Heltec V2 profile already used 31 scope slots. Exact sizes vary with build flags and SDK versions.

To inspect an existing classic ESP32 build:

python3 scripts/check_esp32_dram.py .pio/build/Heltec_v2_repeater/firmware.map\npython3 test/test_esp32_dram.py\n

ESP32-S2, S3, and C-series chips have different memory maps and do not inherit this classic ESP32 check.

"},{"location":"research/esp32_memory_budget/#boot-versus-running-memory","title":"Boot versus running memory","text":"

The build gate checks static placement needed to load and start the image. It never counts heap-only RAM toward that static budget. Allocations made with new or malloc in global constructors consume heap and are not included in the static span. Moving a table there changes its placement, not total RAM consumption.

Successful build checks do not prove that every runtime configuration will boot. Hardware validation must still cover startup allocations and the enabled Wi-Fi/Bluetooth services, recording minimum free internal heap and the largest free internal block. Available heap changes during initialization; for example, the Arduino 2.x repeater releases unused Bluetooth controller RAM in initArduino(), after global constructors have run.

"},{"location":"research/firmware_choice_followup_2026-10-06/","title":"Firmware choice followup 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"research/firmware_choice_followup_2026-10-06/#further-firmware-choice-consolidation-audit-6-october-2026","title":"Further firmware choice consolidation audit - 6 October 2026","text":"

Baseline: pulled commit 08bf174ea30e2e075cdd9fc74111580662b461bb. The user requested fewer choices after the ESP32 Full-only release change. This follow-up combines menu/picker changes with a source, resolved-configuration, and retained-artifact audit. It did not build a matched nRF52 Full/Reduced pair or run hardware tests. Image-removal candidates below remain unqualified; the implemented changes simplify selection without changing firmware sources, partition layouts, or deployed update identities.

"},{"location":"research/firmware_choice_followup_2026-10-06/#implemented-selection-changes","title":"Implemented selection changes","text":"

The interactive board menu now uses canonical Full ESP32 targets and omits transport/default aliases, compact recovery recipes, and development utilities. On nRF52, normal names and aliases that already build the exact same qualified Full/Reduced pair appear once. Distinct external-storage and deployed OTA IDs remain selectable. Exact hidden names remain directly buildable.

Platform Supported exact names before Ordinary menu choices after ESP32 638 217 nRF52 403 209 RP2040 25 19 STM32 16 16 Total 1,082 461

These are menu targets, including KISS; they are not firmware-file counts. The current ordinary ESP32 release still has 174 Full node images (52 Companion, 58 Repeater, 47 Room Server, 17 Sensor). The pull added two Station G3 Sensor recipes to the previous 172-image plan. No Full image was retired in this follow-up. The selector evidence records exact menu/release agreement, zero duplicate menu names, and the remaining OTA identity constraints.

After selecting an exact board and role, the web picker skips logging, connection, and profile filters when they do not distinguish firmware builds. Logging and independent bridge controls remain in the result's setup directions; linked logging/bridge settings still select their corresponding commands. A sole install format is inferred; multiple install operations remain explicit. Real alternatives stay visible even after mutually narrowing filters are selected. The measured T-LoRa split and nRF52 sensor/storage alternatives remain available.

The complete build-profile suite, 28 ordinary release-selection tests, and two picker-package tests passed. Picker tests cover skipped requirements, real alternatives, install formats, existing share links, and runtime command rendering. The complete form was also exercised in the in-app browser using local replayed release metadata: a sole app-only Full download, explicit merged versus app-only installation, nRF52 Full/Reduced UF2 versus ZIP choices, runtime logging changes, and a partial share link with WiFi logging and ESP-NOW enabled. The correct files and commands remained selected. This used fixture metadata and did not download or install firmware, contact a device, or publish the website.

"},{"location":"research/firmware_choice_followup_2026-10-06/#nrf52-sensor-pairs-policy-versus-demonstrated-need","title":"nRF52 sensor pairs: policy versus demonstrated need","text":"

At this baseline, is_nrf52_sensor_ota_pair_target() in build_legacy.sh includes every nRF52 repeater, room-server, and sensor recipe, including external-QSPI and SD recipes. run_nrf52_sensor_ota_pair() requires Full to pass, then builds Reduced; a successful Reduced build cannot replace a failed Full build. Thus the pair is a blanket publication policy, not evidence that each board needs two images.

Classification Evidence Defensible action Same declared sensor functionality The 31 concrete recipes below enable none of the drivers removed by Reduced. GPS is outside the omitted set. Present Full as the ordinary choice. Treat image deduplication as a candidate until packaged applications are compared. Actual optional-driver difference Other recipes can enable environmental/ranging drivers; non-RAK recipes can also lose INA monitors. RAK3401/4631 Reduced keeps the four INA drivers and compatible GPS. Full for normal installs; Reduced can remain an advanced/recovery choice with its missing drivers stated. Disabling a sensor at runtime does not remove its compiled flash cost. Demonstrated internal-OTA headroom constraint The historical RAK3401 compact chain has several exact transitions with zero staging margin. Current staging capacity depends on the installed image and exact delta, not just the board's nominal flash. Preserve qualified reduced endpoints and exact-base update routes. This is not proof that every current Full/Reduced pair is necessary. Current measured mandatory pair No current matched Full/Reduced application and delta-package comparison was found in the retained local evidence. Do not claim a current pair is required by measured capacity, or claim Reduced can safely be deleted everywhere.

The baseline CLI matrix explicitly acknowledges that boards without optional sensor drivers still receive two named choices, with small or zero savings. This audit found no qualified artifact pair proving byte-identical firmware for those boards. The retained RAK3401 1.17.1.8 Reduced RAM report passes with 67,660 available bytes versus 66,904 required, a 756-byte margin; it is historical, not a current matched-pair comparison. The retained T1000-E reports concern Full Companion, outside this infrastructure pair policy.

Full already includes targeted RAM capacity tuning: T096/T1 repeaters use four flood rules, and RAK3401 unified uses 47. The baseline script records these as measured internal-RAM reductions while retaining the Full sensor support. They justify tuning within Full, not a second sensor profile by themselves.

"},{"location":"research/firmware_choice_followup_2026-10-06/#31-concrete-recipes-with-no-enabled-removable-sensor-driver","title":"31 concrete recipes with no enabled removable sensor driver","text":"

Source: out/esp32-full-audit-2026-10-06/platformio-current-resolved.json, the current resolved configuration retained for the baseline audit. Definitions and undefinitions were processed in order, and definitions equal to zero were excluded. The tested omitted set is the 12 environmental/ranging macros in apply_lora_ota_no_external_sensors_profile(), plus four INA macros except on the RAK recipes which retain them. This is a recipe-level result, not an image hash result or a count of canonical published outputs.

Heltec_mesh_solar_repeater\nHeltec_mesh_solar_room_server\nHeltec_tower_v2_repeater\nHeltec_tower_v2_room_server\nKeepteenLT1_repeater\nKeepteenLT1_room_server\nLilyGo_T-Echo_Card_repeater\nLilyGo_T-Echo_Card_room_server\nLilyGo_T-Echo-Lite_repeater\nLilyGo_T-Echo-Lite_room_server\nMeshTracker_X1_repeater\nMeshTracker_X1_room_server\nMesh_pocket_repeater\nMesh_pocket_room_server\nmuzi_base_duo_repeater\nmuzi_base_duo_room_server\nmuzi_base_uno_repeater\nmuzi_base_uno_room_server\nNano_G2_Ultra_repeater\nNano_G2_Ultra_room_server\nt1000e_repeater\nt1000e_room_server\nt1000e_sensor\nThinkNode_M1_repeater\nThinkNode_M1_room_server\nThinkNode_M3_repeater\nThinkNode_M3_room_server\nThinkNode_M4_repeater\nThinkNode_M4_room_server\nThinkNode_M8_repeater\nThinkNode_M8_room_server\n

ThinkNode_M1_repeater already declares external QSPI OTA. The plain MeshTower repeater name is an alias for the SD primary, so this list must not be blindly converted into 31 release removals. Aliases, full source options, board headers, storage layout, and deployed target identities must be resolved first.

"},{"location":"research/firmware_choice_followup_2026-10-06/#qualification-before-removing-an-image","title":"Qualification before removing an image","text":"

Start by showing Full for ordinary installation and moving Reduced to advanced or recovery selection. Preserve existing artifacts and update identities while qualifying actual duplicate removal. For each candidate:

  1. Build both profiles sequentially from the same source/toolchain, with the existing flash, RAM, capability, packaged-application, and OTA checks.
  2. Extract the DFU application's bin_file from its ZIP manifest and compare its complete bytes, EndF identity/body hash, image length, and layout. Outer UF2/ZIP hashes alone can differ because of packaging and are insufficient.
  3. If applications differ, measure the driver/capacity difference. For internal OTA, create exact-base deltas from representative installed Full and Reduced images and verify staging geometry, reconstruction, and the matched deployed bootloader. A passing local-install image does not prove a delta fits.
  4. Change publication validation and fixtures deliberately before emitting one artifact. validate_nrf52_sensor_pairs() currently requires exactly one qualified Full and one Reduced with matching target, hardware, version, source, and layout; it does not prove either member saves space or is needed. Preserve legacy endpoints needed by qualified chains.

Focused existing checks include test/test_build_profiles.sh, test/test_nrf52_ota_profiles.sh, test/test_ota_store_flash_nrf52_hybrid.py, test/test_rak_storage_policy.py, and the release-packaging tests. These check policy and boundary behavior; they do not substitute for matched image and exact-delta evidence. No PlatformIO process was started for this audit.

"},{"location":"research/firmware_choice_followup_2026-10-06/#storage-and-bootloader-choices-that-need-real-distinctions","title":"Storage and bootloader choices that need real distinctions","text":"

OtaFlashLayout_nrf52.h derives internal staging space from the trusted live EndF application extent, rounded to a 4 KiB page, and the effective bootloader ceiling. OtaStoreFlashNrf52.cpp adds 64 KiB of retained RAM only with the matching MOTARAMA capability and at least one available flash page. Its hybrid application path accepts deltas, not full application containers. A Full sensor firmware can still be the destination of a delta; the two uses of \"Full\" must not be confused.

The historical RAK3401 chain proves tight exact-package geometry under an older bootloader: multiple transitions have zero margin; steps 1-9 were physically passed, while the replacement final step has offline reconstruction and exact-bootloader simulation only. It does not compare the current sensor pair and must not be generalized to all nRF52 boards.

External NOR/SD removes the internal container-staging bottleneck, but does not increase executable internal flash or RAM. Existing runtime consolidation is already available for qualified RAK unified storage and MeshTower SD/internal fallback. Keep the exact NOR wiring, bootloader device/storage records, target IDs, and per-transfer store selection. MeshTower's old internal-only identity needs local migration; an alias does not authorize same-target LoRa replacement. Its internal fallback can reject a delta that fits on SD.

The source explicitly rejects raw OTA QSPI sharing a chip with the QSPIFLASH filesystem, and WisBlock OTA SPI sharing incompatible Ethernet/SD wiring. RAK3401 flash and the 1 W radio need distinct chip selects and the explicit bus handoff contract. Combining those recipes requires hardware/layout evidence, not just runtime sensor switches. Extra-filesystem and bootloader-scratch layouts likewise cannot be relabeled as the same OTA target.

"},{"location":"research/firmware_choice_followup_2026-10-06/#other-possible-menu-simplifications","title":"Other possible menu simplifications","text":"

The current ESP32 replay reports 174 Full choices, including the two newly pulled Station G3 sensor recipes. Its evidence file out/esp32-full-audit-2026-10-06/wifi_review_current_consolidation.json finds two equivalent source-option pairs: Heltec V4 base/Expansion Kit OLED repeaters, and their room-server recipes. Repeated identical I2C pin definitions are the only semantic option difference, but their mOTA target IDs differ. Simplify selection first; retain deployed update artifacts until an explicit migration is reviewed. The Expansion Kit TFT Companion has different display/sensor wiring.

Pico W already has a combined USB/WiFi/BLE Companion recipe; it is a candidate to replace transport-specific ordinary choices after fresh capacity and transport qualification. This audit has no current qualifying build result. STM32WL's retained flash reports support capacity tuning of its existing 256 KiB applications; they do not show a removable duplicate role. Repeater, room-server, sensor, Companion/Terminal Chat, and KISS remain different applications unless a separate combined application is designed and tested.

"},{"location":"research/firmware_memory_budget/","title":"Firmware memory budget","text":"

View this page on MeshCore Docs.

"},{"location":"research/firmware_memory_budget/#firmware-memory-checks","title":"Firmware memory checks","text":"

Every firmware environment runs scripts/check_firmware_ram.py against its linked ELF before producing or uploading an image. build.sh, including option 3, also requires a passing report before collecting release files. Native host tests do not use a microcontroller RAM budget.

The check reserves room for enabled runtime allocations as well as static data. A firmware image fitting its board's reported RAM total is insufficient: the display, packet pool, USB, Bluetooth workers and WiFi can allocate after startup. The T096 Full 1.17.1.5 report exposed this distinction. MQTT uses an explicit startup minimum plus runtime connection admission. A passing build report does not qualify its maximum TLS/WSS/outbox workload.

"},{"location":"research/firmware_memory_budget/#what-is-counted","title":"What is counted","text":"Platform Source of available runtime RAM nRF52 Actual __HeapBase and __HeapLimit; excludes SoftDevice, retained state, ISR stack and the dedicated 64 KiB mOTA arena where present ESP32, S3, C3, C6 Linked ESP-IDF memory-region, capability and reservation tables; only internal, byte-addressable heap counts RP2040/RP2350 __end__ to __HeapLimit, according to the selected linker STM32 _end to _estack, minus _Min_Stack_Size

ESP32 PSRAM, instruction-only RAM and RTC RAM never increase the internal budget. On chips other than classic ESP32, the late-reclaimed ROM stack region is excluded because its silicon-specific reservations are only known at boot. Classic ESP32 additionally retains its existing 8 KiB static DRAM check.

The policy adds allowances for task stacks, radio packet pools, screen objects and pixel buffers, filesystem/sensor allocations, enabled wireless stacks, MQTT connections, OTA scratch and transient allocations. ESP32 images with MQTT reserve the same 49,152-byte WiFi allowance even without WIFI_SSID or WIFI_OTA_SEEDER: saved runtime credentials can activate WiFi. Multiple WiFi features count that allowance once; PSRAM-backed TLS does not remove it. ESP32 images with RS-232 reserve another 7,168 bytes even when UART defaults off: 3,072 bytes for the source-bounded bridge object and allocation metadata, plus an unchanged 4 KiB for the UART driver and buffers. This keeps runtime UART enablement in the budget beside MQTT and ESP-NOW. T-LoRa V2.1-1.6 cannot fit all three transports: the qualified linked triple image had 190,488 available internal bytes against 196,984 required bytes under the earlier allocation estimates. The corrected equivalent requirement was 193,400 bytes with the earlier flat MQTT allowance, still 2,912 bytes above capacity. Its configured 16 KiB MQTT worker stack adds another 8 KiB under the current startup policy; these figures describe historical linked checks. Its ordinary release retains separate UART/ESP-NOW and MQTT/ESP-NOW Full images; routing capacities remain unchanged. A 160x80 ST7735 RGB565 framebuffer needs 25,602 bytes. T096 recipes instead select ST7735_BUFFER_COLOR_DEPTH=8: the RGB332 canvas needs 12,801 bytes, plus a source-bounded 1 KiB allowance for the complete sprite object and allocator metadata. Each frame expands one 160-pixel scanline into a 320-byte stack buffer and sends the same RGB565 wire format. The current primary UI colors remain exact; resolution, fonts, all 32 message previews, 350 contacts, 40 channels and the 256-frame offline queue are unchanged. Other ST7735 recipes retain the 16-bit default. A future sprite-object increase beyond the allowance fails a C++ assertion rather than silently reducing available heap. The existing loop stack and transient margin remain additive.

The ordinary T096 BLE FEM-on/off policy includes its receiving OTA source scratch and full-length previews: the reported 81,474-byte requirement becomes 69,697 bytes with the smaller canvas and explicit sprite-object allowance. The old 75,396-byte linked capacity therefore failed correctly; the check is not bypassed or relaxed to accept it. Full Companion's independent 72 KiB floor remains in place.

ESP32 and nRF52 SSD1306 use a source-bounded 2 KiB allowance for the same fixed 1,024-byte framebuffer, embedded driver and allocation metadata. The OLED auto-detection backend reuses that one buffer across SSD1306/SH1106 selection and retries. Other display allowances remain unchanged. nRF52 Full with that color framebuffer must have at least 72 KiB available before startup allocations. Headless and OLED devices use their own smaller totals. The JSON lists each component and checks the largest available region against the largest planned single allocation.

"},{"location":"research/firmware_memory_budget/#mqtt-startup-minimum-and-runtime-qualification","title":"MQTT startup minimum and runtime qualification","text":"

The mqtt_startup_allowance component keeps a 24,576-byte internal startup minimum for the default 8,192-byte MQTTBridge worker stack and two active clients. Each SDK client task is explicitly configured with a 6,144-byte stack and priority 5 on IDF 4 and 5; its maximum configured receive/transmit buffers total 1,792 bytes. These stacks and buffers total 24,064 bytes for two clients, leaving 512 bytes within the existing allowance. This is a minimum allowance, not a complete allocation bound for the clients, transport objects, TLS records, certificate processing, WebSockets or outboxes. The independent 16 KiB transient margin remains in the policy.

MQTT_TASK_STACK_SIZE above 8,192 bytes raises the startup requirement by its actual increase and raises the required contiguous allocation when needed. Smaller stacks and fewer runtime slots do not lower the existing allowance. The default runtime slot array has six entries on PSRAM builds and three on internal-only builds. The bridge can expose five active connections when PSRAM is present and two otherwise; MQTT_RUNTIME_SLOT_COUNT can restrict that capacity. A PSRAM board with unavailable PSRAM uses the two-connection cap.

The report's separate mqtt.maximum_slot_stack_buffer_allowance_bytes records 48,384 bytes for five exposed slots or 24,576 bytes for two, with larger worker stacks added to either. The five-slot figure extends the minimum by 7,936 bytes for each additional client task and buffer pair. It is not added to the startup gate: extra clients allocate lazily, and every connection attempt must pass runtime admission. It still excludes the maximum TLS/WSS/outbox workload. maximum_runtime_load_qualified and physical_validation_performed are false in these build reports. A passed startup check must not be presented as qualification of five simultaneous PSRAM connections or two internal TLS connections.

The mqtt report also names the production runtime admission thresholds. A cold SDK start requires at least 32 KiB of DMA-capable internal free heap and an 8 KiB contiguous DMA block. This includes a separate 16 KiB allowance for its SDK task stack/TCB, receive/transmit/reassembly buffers and transport or configuration objects, alongside the 16 KiB AES reserve. Those ordinary SDK allocations occur after admission and do not use the TLS allocation guard. A retained initialized SDK handle with a stopped task requires 24 KiB free and an 8 KiB contiguous DMA block. Its separate 8 KiB restart allowance covers the pinned 6 KiB task stack plus TCB and allocator overhead. SDK stop retains its receive/transmit buffers, configuration, transport objects and wrapper reassembly buffer, so restarting that handle does not need to reserve their first-initialization allocation again. A wrapper whose SDK initialization has not completed still receives the full cold allowance. This distinction also applies after WiFi loss stops the SDK tasks but keeps their handles.

All ESP32 clients use 896-byte receive, transmit and wrapper reassembly buffers from first initialization. Applying configuration cannot grow the retained SDK buffers or wrapper reassembly capacity, so uniform sizing permits later JWT reconfiguration without replacing the client. This adds 1,152 bytes to a non-JWT client's three buffers on an internal-only board compared with 512-byte buffers. The startup allowance already uses 896-byte receive/transmit buffers, and cold admission covers the ordinary allocations. WiFi recovery and explicit reconfiguration continue to retain clients.

A warm reconnect keeps its running task and buffers; it requires at least 16 KiB free and a 4 KiB contiguous DMA block. The per-client SDK stack is independent of MQTT_TASK_STACK_SIZE, which sizes the existing bridge worker. A TLS attempt without PSRAM requires at least 60 KiB free and a 17,408-byte contiguous block. PSRAM allocation fallback must leave the 16 KiB DMA reserve available. These thresholds describe free heap at the time of admission; they are not additional linked capacity and the TLS contiguous threshold is not a startup allocation requirement. Available linked internal heap cannot establish how much DMA-capable heap remains or how it is fragmented after boot. Only runtime observations can establish that.

Maximum-load qualification still needs physical boot and sustained reconnect, publish and configuration-change tests with the selected number of brokers, transport schemes, certificates and enabled UART/browser/OTA features. Record DMA free heap, largest DMA block and allocation failures throughout that workload. Host admission tests and passing ELF checks provide engineering evidence without claiming that hardware validation has been performed.

The earlier W12 Full MQTT RAM failure (182,768 available against 198,432 required) and standard portable-slot overflow (1,322,304 against 1,310,720 bytes) are historical measurements, not the current expanded Full result. The 2026-10-18 UTC recheck at 060119af8 passes with 200,640 internal bytes available against 198,432 required, retaining the 16 KiB transient margin. Its largest region is 167,872 bytes against 18,464 required; the packaged 2,280,072-byte application fits the expanded 6,553,600-byte OTA slots. MQTT is enabled and all ten declared capability checks pass, without declared reductions. This establishes linked capacity and packaged capabilities, not physical boot/soak or maximum TLS/WSS/outbox-load qualification. It also does not qualify an independently built compact portable-slot recipe.

The experimental RAK4631 combined Ethernet/UART2/adaptive-storage config adds an explicit 3,072-byte allowance for the UART bridge and its TX semaphore. Its Ethernet worker reserves 1,024 stack words (4,096 bytes) plus 256 bytes for task metadata and allocator overhead. Its bounded TCP transmit queue allocates 512 bytes only while Ethernet is enabled; another 64 bytes cover allocator overhead. The DHCP object, socket state, queue indices and SPI objects are static and already reduce the linked heap capacity.

Ethernet and external OTA share an exclusive ownership contract. Enabling Ethernet rejects a live or pinned transfer, releases idle external OTA buffers, and blocks encoder, self-source, manual-staging and folder-source allocation while Ethernet owns SPI. Disabling Ethernet parks the worker after socket/SPI cleanup; the main loop deletes that parked task synchronously and frees its TX queue before releasing ownership. The worker stack therefore cannot remain queued for idle-task reclamation while OTA allocates again. The budget counts the larger of the 4,928-byte Ethernet worker/queue or 11,344-byte external OTA workspaces, with the UART allowance alongside either mode. It keeps the 8 KiB loop stack, 63 flood rules, and reserved 64 KiB OTA handoff arena.

The shared RAM changes now reserve the lazy folder-source buffers as well: up to 4,096 bytes of source leaves and one 2,048-byte encoded output block, plus allocation overhead, for the RAK4631. These can coexist with own-image metadata and the encoder. The combined budget therefore reserves 11,344 bytes for external OTA, or 10,304 with the device encoder disabled, against the 4,928-byte Ethernet worker/queue. Ethernet and external OTA remain exclusive. Freeing static buffers increases available heap; their active allocation cost still appears in the memory check.

This combined config uses a source-bounded 2 KiB SSD1306 allowance. Its named 128x64 geometry needs one 1,024-byte framebuffer, reused on later starts; the driver itself is embedded in a linked global, measured at 156 bytes in the ARM test ELF. Including that already-counted object again plus 32 bytes for allocation overhead still leaves 836 bytes within the allowance. A compile-time assertion covers framebuffer, complete driver object and allocator overhead within that bound. This corrects a padded allocation estimate; it does not free actual RAM. The same source assertion and bound now cover ordinary nRF52 SSD1306 images as well; other display drivers keep their existing budgets. These are linked capacity checks, not qualification of a published or physically tested image.

"},{"location":"research/firmware_memory_budget/#shared-ram-reductions","title":"Shared RAM reductions","text":"

These changes apply to every board that compiles the corresponding feature. They retain stack sizes, radio packet pools, USB buffering, table capacities, stored settings, OTA block sizes and source-image limits.

Change Scope RAM reduction Repeater ACL session state Pure repeater builds on all platforms 36 bytes per client on 32-bit targets; 1,152 bytes for 32 clients Compact recent-repeater records Repeaters with recent history enabled 12 to 9 bytes per record; 1,536 bytes for 512 records Compact flood-rule flags and aligned counters Generalized repeater and room rule engines 200 to 192 bytes per rule; 504 bytes for 63 repeater rules or 248 bytes for 31 room rules One tagged flood-rule target name Generalized repeater engine on all platforms 332 to 300 bytes per extended rule; 2,016 constructor-time heap bytes for all 63 slots Streaming Merkle proof generation All OTA roles and platforms Removes full-leaf-table proof scratch; compressed output needs only one logical block Source buffers allocated on demand All OTA roles and platforms Leaves allocate only the selected image's digest count; unused leaves/output return to the heap

The ACL source-role pre-script contributes one common compiler definition for the entire build. Room/sensor and unknown roles keep the complete session layout. The persisted four-byte room synchronization field stays in repeater records, so existing ACL files and their checksums remain compatible.

Recent-history timestamps retain all 32 bits using explicit byte operations, avoiding unaligned multi-byte accesses on ARM. The table belongs to the mesh loop; network command handlers marshal their work to that loop. These byte operations do not provide synchronization for hypothetical concurrent users. Flood flags remain internal; FPF files serialize each field explicitly and never expose compiler bitfield layout. Multibyte counters remain aligned. Repeater rules store either the direct scope name or target region name in one 32-byte array because the CLI and stored-policy validator already reject both actions together. A spare flag bit identifies the name. FPF6 retains its single scope field. FPF7/8 retain both fixed-width name fields; the inactive field is emitted as 32 zero bytes. Load reads and validates both fields before selecting the shared name, retaining maximum name lengths and refusing a malformed two-target policy. Room-engine rows and on-disk formats are unchanged.

Rule deduplication compares configured fields explicitly, so neither live rate-window counters nor struct padding can make different rules share a slot.

OTA leaf capacity remains controlled by OTA_PROOFGEN_SCRATCH, independently of output-buffer size. The maximum source buffers are 4 KiB plus one block on ordinary nRF52, 8 KiB plus one block with SD storage, and 16 KiB plus one block on legacy ESP32 paths. Non-PSRAM ESP32 Full uses grouped source proofs instead: the complete workspace is bounded to 1,536 bytes, while retaining the same 4,096-block source limit. The check still adds one output block and allocation metadata; safety reserves and stacks are unchanged. PSRAM and other platforms retain the legacy policy. Explicit larger custom leaf limits also keep the legacy implementation. Generic folder sources need no output allocation for raw DATA or proofs; own-image raw serving retains its caller-owned compatibility buffer. Its nRF52 allowance remains in the budget even when the device encoder is off. Queued DATA, PROOF and MANIFEST responses pin their buffers through radio backpressure. Explicit cancellation or source detach releases the reloadable cache; an unpinned cache also expires after 30 seconds. Attached source descriptors remain advertised and reload on the next request. Allocation failure refuses source loading safely, or falls back to raw DATA if only compression output is unavailable. Receive reassembly and serving/inflate buffers remain separate.

On RAK4631, the OTA context's permanent allocation falls from 28,816 to 20,640 bytes, an 8,176-byte reduction before table savings. This is idle heap capacity, not an additional 8 KiB saving on top of the active-operation savings. The RAM policy reserves lazy buffers when active and separately checks the largest contiguous allocation. ESP32 history uses its actual default capacities: 256 records on classic ESP32 and 2,048 on S2/S3/C-series, unless overridden.

The matched RAK4631 Full combined trial on 8 October 2026 retained all sensor drivers, 32 ACL clients, 63 flood rules, 512 recent repeater records, the 8 KiB loop stack and the reserved 64 KiB OTA arena:

Linked measurement Before shared RAM changes After all five changes Available heap capacity before startup 73,580 bytes 83,300 bytes Reserved startup/active allocations 72,584 bytes 77,104 bytes Capacity beyond those reservations 996 bytes 6,196 bytes Application flash 585,700 bytes 586,260 bytes

The permanent allocations shrink by 9,720 bytes. The smaller ACL and flood tables reduce startup allowances by another 1,656 bytes, while the new 6,176-byte folder-source allowance counts buffers when active. The resulting capacity beyond reservations improves by 5,200 bytes; flash grows by 560 bytes. The reports are bound to ELF hashes a16c02e43c12d7dadd94b92b9b80cdd22eced7f67e085b1fffb62ea39f09fcce and f78110d1609334621098cb6db2039227c2f8fc9ac7f9e6ef7dc3cc63eb8d502f, respectively. These are linked capacity checks, not measured free heap after boot or physical Ethernet/OTA soak results.

The five changes pass representative repeater builds for RAK4631 combined Full, Heltec V4 (ESP32-S3), Heltec V2 (classic ESP32), and Seeed XIAO RP2040. RAK4631 room-server and Heltec V4 sensor builds also pass while retaining their complete ACL session layouts. At that measurement, Wio E5 Mini repeater compilation passed but linking exceeded flash capacity: the baseline already overflowed by 27,200 bytes, and those changes added 224 bytes for a 27,424-byte overflow. The newer 1.18.0.1 check below still fails; this cable-only STM32 image remains unqualified and is not release-ready.

Regression coverage includes unchanged ACL/FPF file bytes, full capacities, timestamp rollover, flag combinations, ARM access widths/alignment, streamed proofs against the existing reference algorithm, allocation failures, source reload, queued-response lifetimes, and concurrent compressed serving/receiving. Synthetic and representative build results are distinct from physical soak qualification of every board.

Generate matched Full/Reduced baseline and combined configs outside the repository without starting PlatformIO:

python3 scripts/generate_rak4631_combined_test.py\n

Run its printed commands one at a time and preserve each ELF and memory report before the next build reuses the environment directory.

The combined prototype starts with Ethernet off after every restart. Use the USB CLI commands eth on, eth.status, and eth off; get eth reports the requested mode. UART2 remains independently controlled by set bridge.uart 2 and set rs232.enabled on|off. Full retains every sensor driver; absent sensors do not require a different image.

With Ethernet on, OTA uses eligible internal-flash deltas. External full-image staging, self-serving, manual serving buffers and OTA folders are unavailable until Ethernet is off. Active or staged OTA work prevents a mode change. RAK15001 and Ethernet share chip-select 26 and cannot be used together; the prototype refuses that combination. External storage also needs a matching bootloader that permits internal OTA while Ethernet is enabled.

TCP sends and receives use bounded register operations; a stalled peer cannot hold the mesh loop waiting for an acknowledgement. DHCP and listener management run in the Ethernet worker. This is synthetic coverage and a build experiment, not a physical Ethernet/PoE soak qualification or a replacement release image.

RAK13800 Companion transport also uses a 1024-word worker by default. Its existing four-frame queue stays in the linked object; it adds no raw CLI TX ring. Reserve the worker's 4096-byte stack plus 256 bytes for its TCB and allocation metadata in addition to OTA source workspaces: USB/BLE folder sources can coexist with Companion Ethernet. Stack overrides raise both the total and contiguous allocation requirements. Worker shutdown must complete before its stack is reclaimed or the shared SPI/storage owner is released.

Full RAK Companion keeps every OTA target name while storing an eight-bit candidate fingerprint instead of a four-byte ID for each name. A lookup reconstructs candidate names in the existing 4096-byte heap workspace and checks their complete SHA-256:4 IDs before returning a match. Other targets retain their existing index. This trades bounded, infrequent display lookup work for flash space; no target names or firmware capabilities are removed.

The small-screen message layout retains complete 160-byte messages. Its expanded preview records add 2,816 bytes to the startup allowance and increase the contiguous history allocation budget. The V4 can allocate that history in PSRAM; the guard conservatively reserves internal capacity so that PSRAM availability cannot hide a RAM shortage. A target may replace the generic UI allowance only with MESH_COMPANION_SCREEN_STARTUP_BYTES; a source-side assertion then covers its actual startup screens plus ESP32 allocator overhead.

These are engineering allowances for supported configurations, not measured free heap after boot or a guarantee against every future allocation failure. Unknown platforms, unknown display drivers and missing linker metadata fail closed. MESH_MIN_RUNTIME_HEAP can raise a profile's requirement; it cannot lower the calculated requirement. Add an allocation allowance when adding a display, transport or other substantial feature.

Wireless Paper Full keeps 350 contacts and 256 offline frames by lending the upper 128 queue slots to its mOTA workspace during a session. An idle WiFi mOTA listener leaves all 256 slots available. More than 128 unread frames refuses the loan; sync messages with an app first. USB/TCP source detach or disconnect returns all 256 slots and releases the ESP32 proof/leaf scratch buffers. The display and simultaneous USB, Bluetooth and WiFi remain enabled.

"},{"location":"research/firmware_memory_budget/#nrf52-companion-flash-savings","title":"nRF52 Companion flash savings","text":"

The common nRF52 Companion policy now retains a 960-byte Ed25519 fixed-base table in place of the original 30,720-byte table. It evaluates the same signed radix-16 digits with constant-time table selection, using more doublings instead of storing 32 shifted rows. Compact SHA-512 and scalar arithmetic keep the same outputs and wire format. The larger RAM-built table used by infrastructure profiles is not enabled for Companions.

The policy also omits unused USB device MSC/HID/MIDI/vendor/video classes, SparkFun's unused automatic NMEA cache and the invisible SSD1306 vendor splash. For the exact hash-qualified CC310 archive, the domain wrapper retains P-256 for BLE Secure Connections and removes unused legacy curves. CDC, DFU, USB host, GPS navigation, all Companion fonts, Meshtext, contacts, channels, the Reader corpus and the 8 KiB loop stack remain available. Explicit board/user overrides and MESH_NRF52_COMPANION_TRIM=0 retain their requested values. Other platforms do not receive this Companion policy.

The RAK4631 Full comparison uses the released 1.17.1.10 preview3 ELF at 9ce2a076 and the same target with these changes and Reader chapter/range support. Flash is initialized linked code/data; static RAM is .data plus ordinary .bss, excluding separately reserved arenas.

Measurement Preview3 With Companion trims and Reader ranges Difference Linked flash bytes 710,404 652,908 57,496 fewer (56.1 KiB) Static RAM bytes 157,028 153,876 3,152 fewer (3.1 KiB) Linked heap capacity bytes 78,484 81,636 3,152 more UF2 space below the application ceiling 2,048 59,648 57,600 more

Physical RAK signing averaged 14.4 ms with the original table and 35.9 ms with the smaller implementation. All 24 signatures per image, including SHA-512 block-boundary message lengths, matched byte for byte and verified independently. The linked ARM emulator measures 1,928 bytes in signing versus 1,528 before; these are crypto function frames only, excluding callers, interrupts and other tasks. The loop stack stays at 8 KiB. A full firmware RAM check still applies.

CI runs scripts/check_nrf52_companion_crypto.py on the actual Full RAK ELF. It checks the 960-byte table and retained Reader corpus, executes two RFC 8032 vectors, and rejects crypto function stack use above 2 KiB. Host tests compare 1,791 scalars and 128 keypair/signature/shared-secret cases against the original implementation, plus scalar boundary/random and sanitizer tests. Clean Full builds also cover T1000-E and T096. These builds are synthetic coverage for those boards; the physical Reader/signing tests use the RAK4631.

Reader commands support get reader 2 and get reader 2:2-12. Text terminals stream at most 96 bytes per mesh loop, preserving progress under zero/short writes. The TCP console uses one nonblocking socket-send attempt for the stream, so a peer that stops reading cannot hold the mesh loop in WiFiClient retries. App CLI replies remain bounded and supply a continuation command. The physical RAK passed all 21 chapters, ranges, app-style continuation replies, a paused USB reader exceeding the 4 KiB queue, and disconnect cancellation.

"},{"location":"research/firmware_memory_budget/#empty-contact-path-cache-initialization","title":"Empty contact-path cache initialization","text":"

The contact-path pool now has an explicit runtime constructor. Its existing member defaults still initialize every empty handle and resident entry, and Slot() still resets released handles. This moves the local static pool from initialized .data to zero-initialized .bss: the vtable pointer and repeated NONE/255 slot markers no longer require a 5,780-byte flash load image. The pool is constructed on first use by the existing main-loop contact owner. Its layout, 390 handle slots, 16 resident paths, reference/snapshot behavior and persistent contact records are unchanged.

The RAK4631 Full release recipe at c0672b90, with the full v1.17.1.10-halo-keymind-cascade-dev version and commit suffix, measured:

Measurement Before Runtime pool construction Linked initialized flash bytes 716,448 710,908 Application image including EndF 716,528 710,988 Linked available heap bytes 79,388 79,388 Required runtime heap bytes 74,576 74,576

The net flash saving is 5,540 bytes after constructor/guard code. The image passes the existing 712,704-byte application ceiling, capability checks, RAM budget and DFU handoff checks. Its padded UF2 ends at 0xD3A00, below the unchanged 0xD4000 boundary. Sensors, BLE/USB/Ethernet, SD, logging, Meshtext, OTA, Reader data and the 8 KiB loop stack remain enabled.

The contact-cache test compiles a real static pool and checks its zero-initialized storage, alongside the existing sanitized eviction, snapshot, migration and transaction tests. The compiled ARM Companion check also rejects a pool in an initialized ELF section, so compiler promotion cannot silently restore the flash image. Its normal linked crypto vectors and Reader retention checks remain required.

"},{"location":"research/firmware_memory_budget/#release-evidence-and-regression-tests","title":"Release evidence and regression tests","text":"

Full Companions without PSRAM also use 16-entry path and shared-secret caches. The optional NimBLE capacity trials retain the same RAM guards at 350 contacts and 256 normal offline frames.

Each newly built firmware has a matching .memory.json report. It records the linked ELF SHA-256, available internal RAM, required RAM, largest region, and SHA-256 hashes for the actual firmware files and capability manifest. Packaging and resumed builds reject absent reports, failures, stale ELFs, missing files and changed firmware. Do not reuse a report for another build.

Run PlatformIO commands sequentially in this checkout:

python3 -B test/test_firmware_ram.py\npython3 -B test/test_sh1106_memory.py\npython3 -B test/test_t096_full_memory.py\npython3 -B test/test_st7735_framebuffer.py\npython3 -B test/test_nrf52_ble_startup.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_cascade_release_package.py\npio test -e native -f test_ota\n

Tests cover all resolved firmware environments' hooks, real ELF parsing, allocator table formats, excluded memory, allocation failure, package/report binding and the published T096 failing budget. Shared mOTA tests exercise complete transfers, queue wraparound, unread-message order, source ownership, stop/disconnect and repeated reuse. Bluetooth tests inject task and service startup failures. The manual staging buffer also has allocation-failure and repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source framing through complete transfers, idle polling, queue-full refusal, network loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers. The ST7735 tests check all 256 RGB332 values, exact UI primary colors, full 160x80 scanout and byte order on both SPI APIs, the unchanged 16-bit path, allocation failures, and retained-buffer wake/retry behavior. CI also compiles both ordinary T096 BLE FEM variants with their real runtime-RAM gates, not only the distinct Full Companion overlay.

For older releases without saved ELFs, an audit can compare their ESP allocator tables against a matching pinned SDK ELF and read reservations from the published application itself using scripts/audit_esp32_image_ram.py. Unrecognized layouts require another matching reference or a historical rebuild. An audit must identify original-log/linker calculations separately from new ELF checks and verify the published firmware hashes.

Physical validation remains necessary: boot with and without USB, pair and exchange Bluetooth messages, visit every screen, wake with the button, enable logging/MQTT, transfer mOTA, and monitor heap during a sustained workload. See memory monitoring for runtime diagnostics.

"},{"location":"research/firmware_memory_budget/#11801-known-risk-matrix-2026-10-18-utc","title":"1.18.0.1 known-risk matrix, 2026-10-18 UTC","text":"

The targeted recheck used v1.18.0.1-halo-keymind-cascade-dev, usa-cascade-fixed, the Cascade profile, and release-default debug/logging policy (DISABLE_DEBUG=0). PlatformIO ran sequentially; new artifacts and logs were isolated from existing out/ builds. Exact-identity expanded Full profiles were selected for ESP32 infrastructure. Required nRF52 receiver pairs built both Full and Reduced; a Reduced pass did not replace a Full pass.

Twenty-five selections passed, producing 31 verified profiles:

Selections Passing profiles RAK3401 and RAK4631 Full Companion 2 Wio L1 Full/BLE; T096 Full/BLE, each FEM-on/off 6 RAK4631 canonical and unified Repeater, Full/Reduced 4 RAK3401 unified and RAK13302/W25Q16 Repeater, Full/Reduced 4 MeshTower SD and T096 Repeater, Full/Reduced 4 RAK3x72, Wio-E5 Mini, Wio-E5 Dev and Tiny Relay USB Companion 4 W12 expanded Full Repeater, Room and MQTT Repeater 3 G2 Full Companion/MQTT Repeater, G3 r2 MQTT Repeater, V4 Full FEM-on 4

The separate 26th selection, wio-e5-mini_repeater, was an explicitly cable-only control using --allow-no-ota, not an eligible wireless release target. Its baseline at 3059d7994 overflows the existing 245,760-byte app region by 38,040 bytes. A private compact-crypto/unused-timer trial recovered 18,544 bytes but still overflowed by 19,496. That unqualified trial was not retained. The 16 KiB filesystem, host CLI and existing features were not cut to manufacture a pass; no failing image was published.

Three reproduced release-target failures were fixed:

The STM32 changes reuse equivalent fixed-loop crypto and omit unused Arduino PWM/tone wrappers while retaining the low-level TIM HAL, 100 contacts, eight channels, GPIO/ADC interfaces and 32 KiB filesystems. CI now links all four small STM32 Companions serially at release-version length (2ae109fea) and the exact G3 Full MQTT profile. Display tests cover repeated successful and failed initialization, recovery, destruction, addressing and rendering under ASan/UBSan, including a missing-cleanup negative control.

Passes retain their individual source SHAs (060119af8, 3059d7994, 2ae109fea, 29787c0b2) and file-bound memory/capability reports. The affected G2/Wio L1 selections in this matrix and all four STM32 Companions were rebuilt at 29787c0b2. The final 95 related host tests and artifact-hash checks pass.

"},{"location":"research/firmware_memory_budget/#pr21-contact-recovery-size-recheck-2026-10-18-utc","title":"PR21 contact-recovery size recheck, 2026-10-18 UTC","text":"

The combined PR17\u201321 merge tree a40b1f662 was linked with v1.18.0.1-halo-keymind-cascade-dev, usa-cascade-fixed, and the Cascade profile. All four unchanged 229,376-byte application limits and runtime RAM gates passed. These are build results, not on-device qualification.

USB Companion Application bytes Free application bytes Static RAM bytes RAK3x72 228,744 632 40,220 Wio-E5 Mini 228,760 616 40,252 Wio-E5 Dev 228,884 492 40,212 Tiny Relay 228,940 436 40,220

The earlier 508/452-byte margins above are historical checkpoints. Continue linking every small STM32 Companion at full release-version length: these margins do not justify relaxing the application, filesystem or runtime-RAM gates. Physical storage and boot checks remain in the hardware checklist.

Margins remain narrow: RAK4631 Full Companion has 932 flash bytes free; canonical/unified RAK4631 Full Repeater have 548/540 bytes above their RAM budgets. W12/G2/G3 Full MQTT have 2,208/2,176/1,816 bytes above their budgets. These are linked-capacity margins, not measured running free heap.

No new feature reductions were introduced. Existing declarations remain: RAK3401 unified has 47 flood rules, T096 Repeater has four, Reduced profiles omit selected optional sensor drivers, and nRF52 Full Companions loan half their 256-frame offline queue while mOTA is active. STM32 Companions remain cable-only; nRF52 Full Companion source support is not LoRa self-update receiver qualification. Exact storage/bootloader requirements still apply.

This is a targeted build/capacity pass, not the complete canonical release matrix at one frozen commit, physical hardware/upgrade/soak qualification, or publication of a 1.18.0.1 release.

"},{"location":"research/firmware_memory_budget/#wio-e5-mini-repeater-gcc-15-experiment-2026-10-18-utc","title":"Wio-E5 Mini Repeater GCC 15 experiment, 2026-10-18 UTC","text":"

The separate compiler experiment used source 6035f6631 and the same release recipe above. For the reported comparisons, firmware timestamps were fixed at 18-Oct-2026 and epoch 1792281600. The actual compiler executables reported Arm GNU 14.2.Rel1 / GCC 14.2.1 and Arm GNU 15.3.Rel1 / GCC 15.3.1; a private platform_packages override selected the latter. Production pins were not changed. Each PlatformIO build ran alone, with a fresh private build/output directory; no existing out/ artifacts were replaced.

These numbers are linker overflows beyond the 245,760-byte app region, not successful firmware sizes. The 16,384-byte filesystem, host CLI, logging policy, radio preset and existing feature declarations were retained.

Matched recipe GCC 14 overflow GCC 15 overflow Original Mini Repeater recipe 38,040 37,700 Compact crypto and unused timer wrappers 19,496 19,060 Compact plus best tested five-flag bundle 18,800 18,468

Here, compact means adding ED25519_COMPACT_SHA512=1, ED25519_COMPACT_SCALAR=1, ED25519_COMPACT_FIELD=1, RADIOLIB_TONE_UNSUPPORTED and HAL_TIM_MODULE_ONLY. CompactBase was already enabled in the original recipe. These private additions were not retained in the production Mini Repeater configuration. Their combined savings must not be attributed to the compiler upgrade alone.

The four-flag bundle was -fno-caller-saves -fno-tree-dominator-opts -fno-reorder-blocks -fno-reorder-functions. Adding -fno-tree-switch-conversion produced the five-flag winner. Full-source GCC 15 trials, all with the compact additions, measured:

Additional tuning beyond the four-flag bundle Overflow bytes None 18,500 --param=max-inline-insns-single=70 18,556 --param=max-inline-insns-single=30 18,556 --param=max-inline-insns-single=15 18,588 -fno-inline-functions-called-once 27,108 -fno-jump-tables 18,588 -fno-tree-switch-conversion 18,468 -Os instead of -Oz, compilation and link 18,500 -fno-inline 38,116 --param=max-inline-insns-size=0 --param=inline-unit-growth=0 18,500 -fno-tree-loop-optimize -fno-tree-scev-cprop -fno-tree-loop-distribute-patterns 19,228 -mslow-flash-data 27,628

The winning GCC 15 recipe reproduced the same 18,468-byte overflow in a second clean build. Its compiler-only improvement over the matched GCC 14 recipe is 332 bytes; the original recipe improves by 340 bytes. The 19,572-byte recovery relative to the original GCC 14 build includes the compact/peripheral and tuning changes, not just GCC 15. The winning linked flash extent is 264,228 bytes, still 2,084 bytes beyond the chip's entire 256 KiB flash even before reserving filesystem space.

Actual verbose commands were checked across application, crypto library, Arduino framework and final LTO link. Experimental compile options were installed through the framework's late build.framework_extra_flags.arduino hook, preserving board defaults and overriding its inline threshold before library/application environments were cloned. A post-script forwarded the same tuning to the LTO driver. The true -Os trial also changed build_unflags to remove -Oz, so the inherited removal of -Os could not silently invalidate that comparison.

Earlier mixed-scope post-script trials were preserved but excluded from the full-source comparison: those options reached framework/link stages, not the application/crypto compilation. One later link-only -Os control also measured 18,500 bytes; it is not the true full-source -Os trial above. No ABI-changing struct/enum options, fast-math assumptions, filesystem cuts, or capacity-check overrides were used. Every full-firmware attempt still failed at the flash link; none qualifies as a runtime-RAM pass, usable image, or hardware validation. GCC 15 was not adopted and no failing image was published.

A private Unicorn 2.1.4 harness also checked generated Cortex-M4 code for eight standalone crypto profiles: GCC 14 compact, GCC 15 compact, the GCC 15 four-flag bundle, its five-flag winner, and the four more aggressive trials in the last four table rows. All passed 432 positive function calls: 40 key generations, 40 signatures and 352 SHA-512 checks. Each profile used two RFC 8032 signing vectors, three additional reference signing vectors, and 11 SHA-512 boundary lengths in one-shot and three streaming modes. All 16 corrupted-expected-signature/digest negative controls were rejected.

The harness checked explicit Cortex-M4 CPU-model readback, allocated-section bounds within physical 256 KiB flash/64 KiB SRAM, read-only executable flash, non-executable SRAM, buffer canaries, unchanged inputs, per-instruction stack bounds, restored SP/callee-saved registers and bounded execution. Source and ELF hashes were verified. Maximum observed standalone call frames were 3,376 bytes for GCC 14 compact, 3,368 for the GCC 15 winner and most other GCC 15 profiles, and 2,960 with no inlining. These are not whole-application stack/heap budgets.

This is CPU instruction emulation, not a Wio-E5 Mini board model. The harness independently links repository keypair/sign/SHA-512 code through stable exported wrappers, with explicit -Oz at link; it is not the exact full firmware LTO image. It does not qualify signature verification, boot, interrupts, MMIO, radio, USB, flash/filesystem drivers, sleep, concurrent traffic, physical timing or cryptographic side channels. A passing standalone harness does not make any overflowing full-firmware image usable.

"},{"location":"research/firmware_memory_budget/#mini-repeater-approved-cut-experiment-2026-10-18-utc","title":"Mini Repeater approved-cut experiment, 2026-10-18 UTC","text":"

The follow-up used an isolated source worktree at 9b616f4d1, with the same fixed timestamps, release recipe, real GCC 15 executable, compact additions and five-flag bundle described above. Only Morse/CW tools and automated management reporting were approved feature cuts. No experimental firmware source/configuration changes were adopted in the production branch.

Each comparison retained live USB packet logging for MQTT (MESH_PACKET_LOGGING=1, compact and merged USB logging), persistent /packet_log logging, the host-service bridge, normal authenticated CLI, 50 neighbours, radio2 and the 16 KiB filesystem. The existing cable-only limitation was unchanged. The management cut also omits its shared data.tx routing implementation, not ordinary request/reply statistics. The Morse/CW cut removes carrier tools and their timer/state/direct-carrier hooks; Morse RX was already disabled for this target.

These are conservative linked flash extents from the final .data load address plus its size, measured from flash base 0x08000000, rather than PlatformIO's smaller sum of selected section sizes:

Private GCC 15 recipe Linked extent App overflow Saving from control Compact/tuned control, all existing features 264,228 18,468 0 Morse/CW tools off 256,832 11,072 7,396 Management reporting off 254,280 8,520 9,948 Both approved cuts 246,908 1,148 17,320 Both cuts and shared numeric GET handlers 246,748 988 17,480

The two-cut result reproduced in fresh control builds. The proposed GET helper groups 27 simple scalar keys using typed member pointers and NUL-separated key lists; SET handlers, capability/Observer checks, matching rules, replies and persisted layouts are unchanged. It defaults off in the prototype and was not adopted merely for its 160-byte saving.

Further full-source tuning did not close the remaining gap:

Additional option Without GET helper: overflow With GET helper: overflow None 1,148 988 -mword-relocations 1,148 988 -fno-ipa-sra 1,844 Not tested -fno-tree-pre 1,148 Not tested -fno-ipa-ra 1,772 Not tested -fno-tree-sink 1,348 Not tested -fno-ipa-pta Not tested 2,640 -fno-ipa-cp Not tested 1,744 -fno-tree-forwprop Not tested 1,404 -fno-split-wide-types Not tested 3,244

Separate, unapproved diagnostic controls establish the tradeoffs but are not candidates: additionally removing the host-service bridge leaves 1,620 bytes free on GCC 15 and 1,240 on GCC 14; additionally removing only persistent packet-file logging instead leaves 828 and 384 bytes free, respectively. Both controls keep live USB/MQTT packet logging enabled. Neither extra feature cut was adopted.

Focused host tests exercised the actual raw RX/TX logging emitters and packet codec, exact line framing, maximum packets, short/non-draining serial writes, drop recovery, persistent logging callbacks and failed-TX handling across all eight carrier/management/file-logging combinations. Disabled-management tests verify fail-closed routing, password erasure, no management filesystem access and retained shared Fleet/beacon context. The full legacy/compact GET-body oracle passes 80,406 comparisons plus fallback checks under ASan/UBSan, including distinct fields, NaN/Inf, limits, whitespace and capability cases; four negative controls fail as expected. All ten CLI contract tests pass. Independent source/evidence review found no blocker, and git diff --check passes.

These tests use host boundary stand-ins; they do not establish physical USB/MQTT broker integration, ARM runtime behaviour, board startup, radio timing, sleep or sustained load. No approved configuration fits yet, so there is no approved runtime-RAM pass or usable Mini Repeater image from this experiment. No hardware was flashed, no failing image was published, and existing build outputs were not overwritten. Production compiler pins, features and capacity checks remain unchanged; the private source/tests, configs, verbose logs and linker maps were retained for further work.

"},{"location":"research/firmware_memory_budget/#mini-repeater-production-recipe-after-file-logging-approval","title":"Mini Repeater production recipe after file-logging approval","text":"

After approval to also omit persistent packet-file logging, the three gates were adopted only for wio-e5-mini_repeater: MESH_ENABLE_CARRIER_TOOLS=0, MESH_ENABLE_MANAGEMENT_REPORTING=0 and MESH_ENABLE_PACKET_FILE_LOGGING=0. All three default to 1 for other boards/roles. No compact numeric GET handler was adopted.

The production recipe retains the existing GCC 14.2 package pin, adds the equivalent compact SHA-512/scalar/field implementations and unused Arduino timer-wrapper trims, and applies the five measured compiler flags to both compilation and the final LTO driver. The target-specific link hook is inert for other environments. Existing flash and runtime-RAM checks remain active.

Canonical build.sh build-firmware links at release-version length (v1.18.0.1-halo-keymind-cascade-dev, Cascade, USA preset, DISABLE_DEBUG=0) pass:

Canonical target Linked flash extent App limit Flash headroom Runtime RAM available / required Wio-E5 Mini Repeater 245,376 245,760 384 44,560 / 32,288 Wio-E5 Mini USB Companion regression control 229,096 229,376 280 24,256 / 17,408

These are conservative complete flash extents, not PlatformIO's selected section sums. The Repeater still reserves its original 16 KiB filesystem, 50 neighbours, radio2, authenticated administration, host-service bridge and runtime-controlled live USB packet logging for an external MQTT bridge. The Companion keeps its 32 KiB filesystem and existing features. GCC 15 is not required and no toolchain upgrade was adopted.

The omitted file feature is only node-local /packet_log capture and log start|stop|erase / local log dumping. Live raw RX/TX framing, USB logging controls, statistics, watchdogs and bridge callbacks remain. Old saved log files are left untouched rather than automatically erased. Management-disabled routing fails closed and retains shared initialization needed by independently enabled Fleet/beacon features.

Focused coverage now runs in CI alongside an exact cable-only Mini Repeater build at release-version length. It covers the feature gates, default-on behavior, raw logging and backpressure under all eight flag combinations, shared cooperative serial output, crypto equivalence, CLI permissions and Morse/radio recovery. Production-source and host-test review found no blocker.

Both flash margins are narrow: future changes must re-link, not assume these images fit. These results qualify linked capacity and host behavior, not physical USB/RF/MQTT integration, board boot/sleep or a sustained-load soak. The Mini Repeater remains cable-only (--allow-no-ota); no hardware was flashed, no release was published and existing outputs were not overwritten.

"},{"location":"research/firmware_memory_budget/#shared-contact-ram-and-rak4631-full-recheck-2026-10-19-utc","title":"Shared contact RAM and RAK4631 Full recheck, 2026-10-19 UTC","text":"

Companion and simple_secure_chat contact sort/match indexes now use uint16_t when the complete table has at most 65,535 slots; larger configurations retain int. Static 350-contact targets save 764 internal bytes; PSRAM targets save 96 internal and 716 external bytes; static 100-contact targets save 264 internal bytes. Cached nRF52 record placement also saves four bytes per runtime contact. Capacities, serialization, queues, stacks, features and fallback behavior remain.

At aed4c22e5, matched against 31e693ec2, RAK4631 Full saves 2,200 static RAM bytes and 488 actual DFU application bytes. Its application is 712,112 of 712,704 bytes (592 free); modeled RAM admission margin rises from 4,516 to 6,716. CRC sharing and Reader packing preserve formats/text; Full features and existing SD support remain enabled. These are linked/build capacities, not running-heap or physical storage measurements.

Five final-source canonical packages pass across nRF52, ESP32 PSRAM, RP2040 and STM32. RAK3x72 USB has four actual BIN bytes free, requiring unchanged hard gates. At that revision, V3 Full fails RAM admission on both baseline and final source; its 100-contact wrapper gains 264 bytes, reducing the deficit from 21,288 to 21,024. The direct 350-contact V3 terminal default build passes separately; it is not a canonical Full or release-version/preset package. No hardware was flashed. See the qualification report and its source-bound artifacts, software coverage and limits.

"},{"location":"research/firmware_memory_budget/#heltec-v3-full-bounded-source-repair-2026-10-19-utc","title":"Heltec V3 Full bounded-source repair, 2026-10-19 UTC","text":"

At eb9259f2a, canonical V3 Full passes with 189,616 internal bytes available against 183,584 required: 6,032 bytes of modeled margin. The pinned NimBLE adapter preserves 15 saved phones and 30 CCCDs. Existing Bluedroid pairings need one-time re-pairing. Its 100 contacts, 40 channels, 256 offline frames, MQTT slot, Full features and stack sizes remain unchanged.

Grouped source proofs retain the same 4,096-block capacity with a lazy workspace of 1,004 bytes, statically bounded to 1,536. The guard replaces only the old 16,384-byte leaf allocation with this bound; the output block, metadata, transient margin and all other allowances remain additive. Legacy/PSRAM paths and larger custom leaf limits retain the existing policy.

The clean V3 package and constrained RAK regressions pass; physical V3 testing still awaits USB attachment. See the V3 qualification report for artifact hashes, tests, upgrade behavior and hardware limits.

"},{"location":"research/linux_chromium_web_serial_tty/","title":"Linux chromium web serial tty","text":"

View this page on MeshCore Docs.

"},{"location":"research/linux_chromium_web_serial_tty/#linux-chromium-web-serial-after-pyserial","title":"Linux Chromium Web Serial after pyserial","text":"

A Station G2 browser timeout on firmware source 1e421ff4fc6a3b56d43db7a18aab94a72129b527 was traced to the Linux host's inherited TTY read settings. Chromium 139.0.7258.127 closed its receive stream with NetworkError / device lost before the first ver request. The radio was still running and an exclusive pyserial query answered in 21.330 ms without a reset or replug. This finding explains this controlled host failure; it does not establish that every reported USB hang has the same cause.

"},{"location":"research/linux_chromium_web_serial_tty/#why-an-idle-port-looked-disconnected","title":"Why an idle port looked disconnected","text":"

pyserial normally sets VMIN=0, VTIME=0 and performs its timeout handling with select. These kernel TTY attributes survive closing the port. With those settings, an idle nonblocking Linux read() can return zero bytes. pyserial's POSIX implementation sets these values and closes without restoring the previous attributes.

In the exact Chromium 139 serial implementation, port configuration preserves the prior VMIN and VTIME. Its receive handler interprets a zero-byte read as device loss. Setting VMIN=1, VTIME=0 makes an idle nonblocking read return EAGAIN, so the browser waits for data instead.

Upstream Chromium fixed this in commit 45ccdf6e on 2026-09-18, under bug 559872992. The change explicitly configures both read settings and adds a real Linux pseudoterminal regression for inherited settings. The presence of this fix upstream does not prove a particular installed browser contains it; the Pi's tested Chromium 139 predates the fix.

"},{"location":"research/linux_chromium_web_serial_tty/#controlled-g2-comparison","title":"Controlled G2 comparison","text":"

The same firmware, native Pi Chromium, and unmodified official flasher console were used for both attempts. Console source SHA-256 was 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9. Instrumentation observed and forwarded existing native API calls.

Attempt Host settings and observation Result 203 VMIN=0, VTIME=0. The receive stream was already lost before Enter. The native five-byte ver\\r\\n write nevertheless completed in 28.1 ms. FAIL. No displayed reply by the unchanged 5,000 ms deadline. 204 Only host VMIN changed to 1; VTIME stayed 0. The receive stream remained open. PASS. First ordinary ver reply was displayed in 154.1 ms.

No firmware or website patch, modem-signal change, reset, replug, added command retry, keepalive, or longer deadline was used to obtain the passing comparison. Before this comparison, stats-core reported uptime 1,728 seconds and error bitmask 2; this is evidence of continued operation, not an error-free radio qualification. The comparison does not qualify a fresh flash, a long idle window, another host OS/browser, or every USB transport.

Safe receipt names and hashes are retained for the comparison:

Receipt SHA-256 stock-browser-diagnostic-203-safe.json e41b0f009b8dce42ad9ee23ad843032b97a47fc2cfc5385aefbf093ac46a9788 controlled-browser-host-setting-safe.json 88527690ca8d8740fd415831123c45ac3249134aec3929f7fd2b0224ddbe8fce stock-browser-diagnostic-204-safe.json 6ba3249a23364a589653a0a1a4abb7e453a1cb2f095cec6138f3cebc171d5f39"},{"location":"research/linux_chromium_web_serial_tty/#hardware-test-preflight","title":"Hardware-test preflight","text":"

The tracked Linux Web Serial helper changes and verifies only VMIN=1, VTIME=0 immediately before a native browser test. It requires an explicit absolute TTY path and restores the complete original termios attributes after the browser closes, including on a Python exception or interrupt. Failed preparation also restores the original state. It sends no serial data and makes no DTR/RTS or USB-reset calls.

Pause other serial clients and finish all pyserial preflight queries before entering the context. Another opener can overwrite the read settings, and a second reader can consume the radio's reply. The helper closes its preparation descriptor before the trial, so it does not keep the port open across the stock browser's Disconnect action. Controllers must close their native reader, writer and port in their own finally cleanup before the context exits, and route termination into that cleanup. If the endpoint is replaced or disappears, restoration fails explicitly rather than applying old attributes to a different device.

Record browser version, TTY settings, native stream state/error, native write completion, raw receive-byte count and rendered reply separately. A zero-byte console display alone does not prove that the USB endpoint received no bytes. For example, the stock console buffers input until a complete CRLF line arrives. See the HIL instructions for the context-manager example and the hardware-free pseudoterminal test.

"},{"location":"research/nrf52_bluetooth_debug/","title":"Nrf52 bluetooth debug","text":"

View this page on MeshCore Docs.

"},{"location":"research/nrf52_bluetooth_debug/#nrf52-companion-bluetooth-diagnostic-firmware","title":"nRF52 Companion Bluetooth diagnostic firmware","text":"

The T1000-E diagnostic image is Full Companion firmware with the normal radio, sensor, pairing and recovery settings. Diagnostics are enabled with MESH_NRF52_BLE_TRACE=1. Ordinary builds compile them out. Diagnostic builds reserve about 6.3 KB of RAM for a 256-event ring and counters.

The trace records connection timing, security results, UART notification subscriptions, app receive/transmit lengths, queue overflow, local disconnect requests and each recovery trigger. It brackets Bluefruit's real event dispatcher so a handler that stalls can be distinguished from an event that never reached the framework. Maximum dispatch duration and Companion loop gaps are also reported. Callbacks write bounded RAM records without serial printing or allocation. PINs, keys, peer addresses and message contents are not stored.

"},{"location":"research/nrf52_bluetooth_debug/#capture-a-phone-failure","title":"Capture a phone failure","text":"
  1. Install the diagnostic UF2 for your exact board, or use its application DFU ZIP. This does not require replacing the bootloader.
  2. Connect with the phone normally and reproduce the failure. Keep the node powered. Do not forget the Bluetooth device, reboot, or factory reset before collecting the trace.
  3. Connect the node's USB cable to a computer and close other serial clients. Install Python and pyserial if needed.
  4. Export the diagnostic report:

~~~sh python3 scripts/capture_nrf52_ble_debug.py --port /dev/ttyACM0 --output bluetooth-report.json ~~~

On Windows use the correct COM port instead of /dev/ttyACM0. The exporter enters the Full Companion USB terminal at 115200 baud, reads diagnostics, then returns it to binary mode. It does not reset or change settings. 5. Send the JSON report with the approximate failure time and what the phone displayed. Export soon after failure because old events roll out of the ring. Reboot or loss of power clears the RAM history.

If the Pi's attached T1000-E is used, its serial identity is 34A9141999729D5D. Identify the physical board before flashing; an upstream application may have the misleading USB product name T1000-E-BOOT.

"},{"location":"research/nrf52_bluetooth_debug/#manual-usb-commands","title":"Manual USB commands","text":"

The debug commands are restricted to local Companion transport requests and are not exposed through remote mesh management. Replies and raw records are decoded by the exporter. Sequence gaps and overwritten records are reported. Diagnostic contention is counted and dropped rather than blocking BLE. The exporter retains unsupported-command replies from older format-2 images; missing newer readbacks do not prevent export of their event rings.

"},{"location":"research/nrf52_bluetooth_debug/#reading-the-result","title":"Reading the result","text":"Evidence Stage identified No stack connection event; advertising remains active Connection has not reached the application's Bluetooth event layer. Connection but security error or timeout Pairing or encryption setup. Secured link but UART notifications disabled Notification subscription or restoration. Secured link with no app RX; secured_no_app_watchdog App never opened its Companion UART session; the 15-second recovery fired. App RX followed by blocked/partial TX Reply delivery; the report identifies the queue/watchdog result. Dispatch entry with no matching exit and increasing age Software stack handler may be stuck; correlate with a later snapshot.

Reason 0x08 is connection timeout, 0x16 is local termination and 0x3e is connection establishment failure. Do not confuse decimal 22 (0x16) with hexadecimal 0x22 (link-layer response timeout).

The report can identify the failing software stage, but cannot guarantee the physical cause. No connection event alone cannot separate a central controller problem, RF interference and a peripheral controller problem. Dispatch timing starts after the SoftDevice delivers an event; it does not measure radio interrupt latency. A bench failure is not proof of the same failure on an iPhone. Use the diagnostic image on the affected node.

"},{"location":"research/nrf52_bluetooth_debug/#build","title":"Build","text":"

Run one PlatformIO command at a time in this checkout:

PLATFORMIO_BUILD_FLAGS='-DMESH_NRF52_BLE_TRACE=1 -DBLE_DEBUG_LOGGING=0 -g3' \\\nOUTPUT_DIR=out/t1000e-ble-debug \\\nbash build.sh build-firmware t1000e_companion_radio_full \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev-ble-debug --full\n

The Bluefruit dispatch hooks are applied to private build copies of framework sources. The shared PlatformIO framework cache is not modified. Retain the ELF alongside the UF2/ZIP for future SWD inspection if hardware access becomes available. Do not halt the CPU during a Bluetooth connection: that disrupts SoftDevice radio timing and invalidates the test.

"},{"location":"research/rak3401_1.17.1.7_legacy24/","title":"Rak3401 1.17.1.7 legacy24","text":"

View this page on MeshCore Docs.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#rak3401-11607-to-11717-without-a-bootloader-update","title":"RAK3401: 1.16.07 to 1.17.1.7 without a bootloader update","text":"

Qualified on MercerWoodMesh on 27 September 2026: all 15 application updates passed over LoRa, from the exact supplied 1.16.07 image, without a bootloader update. Use RAK3401-1.16.07-to-1.17.1.7-legacy24-mOTA.zip and verify it against the accompanying .zip.sha256 file before extracting. Follow the exact before/after hashes in its CHAIN.csv; this is not a single-step stock update.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#exact-compatibility-boundary","title":"Exact compatibility boundary","text":"

This is an application-only migration for target 2FA509C1, hardware RAK_3401_repeater_lora_ota_no_external_sensors. It is not a generic RAK4631 package. The installed application must have body hash 71F4026CBE4B8B74 (1.16.07, full image SHA-256 4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb).

The bootloader remains 0.9.2-OTAFIX2.4, with ABI 2 and codec mask 0x4. Its application staging ceiling is 0xD4000. Every package is a codec-2 CRLE in-place delta. DEFLATE is used only for transport by compatible intermediate applications; the old bootloader still applies CRLE.

The destination is built from source commit 2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1 with the explicit MESHCORE_NRF52_LEGACY_FLASH_OTA=1 build selection. This preserves the ordinary S140 linker and disables hybrid flash/RAM staging and bootloader self-update. It is deliberately different from the stock hybrid 1.17.1.7 release. The endpoint has:

Do not replace this endpoint with the stock ZIP. Do not send a bootloader or SoftDevice update. The ZIP/UF2 files used to construct and qualify the chain are not themselves LoRa update containers.

Future application updates for this node must also be qualified against the unchanged OTAFIX2.4 contract. Keep the legacy build selection; do not assume that a later stock release is an interchangeable target for this chain.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#what-smallest-means","title":"What \"smallest\" means","text":"

The search compares an explicit, hash-pinned set of application images and every allowed page-aligned in-place workspace for each increasing-version transition. The starting bootstrap is fixed: its 89,844-byte package is the only admitted first edge. Synthetic bridge version numbers are real compiled versions below 1.17.1.7; their binaries are not relabeled after compilation.

The bytes objective minimizes ideal serialized MeshCore traffic first, then install count. The packets objective minimizes ideal originated packets first, then serialized traffic. The older transport objective minimizes install count before traffic and is not equivalent to either.

Accounting includes manifests, block requests, proof requests/responses, DATA fragments, and MeshCore framing. It uses the actual pinned encoder for compressed blocks. It excludes discovery, retries, admin/control commands, other mesh traffic, flood fan-out, and LoRa preamble/coding overhead. A fixed linear relay path can be accounted for separately. These are finite-search minima, not a proof that no possible custom firmware or protocol could ever use fewer bytes. The ideal compressed counts also assume negotiation remains in that mode. An older receiver can fall back to raw DATA after stalls; the physical-test transmission totals include that extra traffic.

Best lossless byte count is not a guarantee of the fastest real radio link. Use the fastest preset that remains reliable over the actual route. SF5 / 500 kHz is a direct bench measurement, not a recommendation to switch an unreachable field node or unconfigured relay chain to that preset.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#selected-route","title":"Selected route","text":"

The completed search covers 52 images and 47,920 flash geometries. The packet-first route has 15 packages, totaling 2,340,160 container bytes. Its ideal direct-link traffic is 12,621 packets / 1,808,961 serialized MeshCore bytes. These totals include traffic in both directions, not just DATA. Fifteen is also the fewest installs among the searched candidates.

The byte-first route uses the same sequence of application images, with four different patch workspaces: 12,625 packets / 1,808,787 bytes. Saving 174 bytes costs four packets. The delivered packages use the packet-first choice; do not substitute packages from the comparison route.

Step Resulting version Container bytes Ideal packets Ideal MeshCore bytes 1 1.16.7.9 89,844 878 101,203 2 1.16.7.10 62,838 551 70,031 3 1.16.8.7 106,125 496 70,134 4 1.16.9.105 149,542 728 104,992 5 1.16.9.110 112,540 489 67,887 6 1.16.9.111 71,070 268 35,453 7 1.16.9.116 138,545 692 101,308 8 1.16.9.122 177,536 995 148,671 9 1.16.10.1 258,398 1,477 223,042 10 1.16.11.0 245,426 1,450 219,953 11 1.16.12.1 217,608 1,167 172,794 12 1.16.13.1 199,395 1,024 150,005 13 1.16.14.0 199,527 989 143,160 14 1.16.15.0 139,188 618 86,721 15 1.17.1.7 172,578 799 113,607

Some bridge versions also exist as different binaries. CHAIN.csv's exact body hashes and package checksums, not this version-only table, determine which package is valid at each step. The first two transfers use legacy 1 KiB blocks; subsequent transfers use negotiated 2 KiB blocks with transport DEFLATE and the unchanged CRLE bootloader format. The second transfer also omits explicit proof requests: that historical receiver waits for the current sender's proactive Merkle proofs. The original 1.16.07 receiver does not have this optimization.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#measured-mercerwoodmesh-replay","title":"Measured MercerWoodMesh replay","text":"

All 15 selected packages passed in order on the Pi's debug-wired RAK3401, starting with the exact 1.16.07 image and OTAFIX2.4. Every application payload crossed LoRa from the Heltec T096. There were no debug writes, application substitutions, failed installs, or resumed downloads between the 15 steps.

The direct bench used 909.950 MHz, BW 500 kHz, SF5, CR5, continuous receive, and a 128-symbol source preamble. Observations for both radios totaled:

These are bench measurements, not a field-link completion-time promise. The control commands were sent over USB; a remote field console adds LoRa control traffic. BENCHMARK.csv separates each step's ideal cost from its observed frame count and timing. Actual serialized radio bytes were not measured.

Independent debug-port readback matched the complete final application, unchanged bootloader, and unchanged MBR/SoftDevice. UICR matched the normal post-startup baseline throughout this replay. The initial pre-first-boot UICR dump differs because normal Nordic/RAK startup configures the reset pins and 3.3 V regulator; this is documented in the evidence, not a bootloader upgrade.

After a fresh reboot, the watchdog reported on. Both radios returned to 910.525 MHz / BW 62.5 kHz / SF7 / CR5, and a nonce-matched normal-channel trace through the exact target key prefix completed in 423 ms. The inaccessible field node has not been touched. The lab RAK remains on this repeater image; the Pi's mctomqtt.service remains stopped because it previously owned that serial port. Radios other than the RAK3401 and sending Heltec T096 were left untouched.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#field-preparation","title":"Field preparation","text":"

All destination commands below can be sent through the authenticated remote repeater console; USB/Bluetooth at the destination is not required. Keep a separate control connection available while the sender owns its companion's serial port. A local Full Companion radio attached to the sender runs the bundled motatool seeder. Resolve ports by device identity, not a remembered ttyACM number.

The qualified source is the Pi's Heltec T096, USB serial 651F8E496197F882, running Full Companion v1.17.1.8-halo-keymind-cascade-dev-d4b3bb56. It supports the extended 2 KiB transport and host-backed DEFLATE. The destination is RAK3401 USB serial 0B81C9C68D8D01B4. Older companion firmware or an older motatool is not an interchangeable sender for this bundle.

Before any change, record ver, ota self, ota status, get bootloader.ver, get system.watchdog, get radio.rxps, get powersaving, get rxdelay, get af, and ota config. Match the exact starting body, hardware, bootloader, and signing-key policy. An unsigned bundle is not suitable for a node requiring signed packages. Choose a stable-power maintenance window long enough for all 15 installs; do not begin an install while the destination is at risk of losing power.

Disable the destination system watchdog with set system.watchdog off. Allow its one-time hardware reset and wait at least 90 seconds; reconnect and prove that it reports off. A quick software reboot is not a substitute.

Use continuous receive on the destination and sender throughout the bridge chain (set radio.rxps off). Record and disable a second scanning radio profile on the sender if it would take the radio away from the OTA channel. Record rxdelay, af, power-saving, reach, and relay settings before changing them. Use hops 0 only when there are no intermediate relays. Obey applicable airtime/duty restrictions and restore all saved settings when finished.

The direct bench also uses set rxdelay 0 and set af 0 on both radios, ota speed 1 on the sender, and set powersaving off where supported. The earliest applications do not recognize the last command; continuous receive is still selected through set radio.rxps off. The source has set radio2 off throughout the transfer. Wait for configuration replies to finish before handing its serial connection to the seeder.

For the qualified direct bench configuration only, the destination uses tempradio 909.950,500,5,5,120; the tested current Full Companion accepts tempradio 909.950,500,5,5,120,128 for an explicit 128-symbol preamble. Historical destination parsers do not accept the sixth argument. The sender and destination must be on the same live radio preset. Do not assume the temporary preset survives a bridge reboot.

The newer historical receivers performed poorly with a 16-symbol source preamble in this test. Use the qualified longer setting. Also avoid frequent ota status or ota stats polling: these versions rehash the complete running application while answering, which can interrupt receive processing. The lab runner observes the sender's local log and checks the receiver near completion or at a 60-second interval. Set a field maintenance lease appropriate to the actual preset and route; the 120-minute value above is a bench setting.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#applying-a-qualified-bundle","title":"Applying a qualified bundle","text":"

Verify the outer archive hash and every entry in SHA256SUMS.txt. Verify each .mota using the included, hash-pinned motatool. CHAIN.csv is the authoritative ordered list of packages, manifest IDs, and before/after hashes. On the Pi, use tools/motatool-linux-aarch64 (make it executable after extracting if necessary). The x86-64 binary is for Linux/WSL, not native Windows. The provenance images, tools, and evidence are not sent over LoRa; only the selected .mota in the current step's serving directory is sent.

For every row, including after an interrupted run:

  1. Prove that ota self matches that row's exact before hash and that the bootloader reports apply OK. Do not select a row by version alone.
  2. Reconcile ota status. Resume only that row's known partial MID. If it is already ready, do not redownload it. Stop on an unrelated or ambiguous session. Never send a blind ota cancel.
  3. Reapply the agreed radio/receive guardrails after reboot. Start the sender with only this row's package in its serving directory: motatool serve --dir STEP_DIRECTORY --serial COMPANION_PORT --baud 115200 -v.
  4. Run ota ls until the expected version is advertised. The old listing shows a version, not a MID. Use ota pull EXPECTED_MID flash and poll ota status at a restrained interval.
  5. Require ready to install with the exact MID. Stop the seeder gracefully with Ctrl-C. Recheck watchdog off, then send ota install.
  6. Allow up to five minutes for the application to return. Require the exact after hash, expected version, and bootloader: apply OK before continuing. Stop if any check fails; do not try the next package against another base.

A retained previous container may be re-discovered after boot. If the exact previous MID is verifying, wait for it to become ready or idle. Only after re-proving the current running hash may that attached, completed previous MID be cancelled. An idle ota cancel does not prove that flash was erased.

After the final hash is proved, restore the destination, sender, and relays to their recorded normal settings and confirm normal-channel reachability. Re-enable the system watchdog and require it to report on. The current endpoint supports normalradio; coordinate channel restoration so that the remote node does not become unreachable during the handoff. The endpoint requires a temporary radio channel for OTA operations, so a normal-channel ota ls is not a reachability test. Use an ordinary mesh trace or authenticated remote command after restoring normal radio.

The existing rak3401_mota_chain.py pins older release archives. It must not be pointed at this new bundle or have its safety checks bypassed.

"},{"location":"research/rak3401_1.17.1.7_legacy24/#evidence-and-reproducibility","title":"Evidence and reproducibility","text":"

build_rak3401_legacy24_bundle.py checks every base/target hash, hardware identity, increasing version, block capability, flash workspace, and staging margin. It reconstructs every target with both zero- and erased-flash-filled scratch space, and applies every package with the actual OTAFIX2.4 release implementation compiled as a flash simulator. It remeasures transport with the pinned encoder. Its output remains explicitly offline-only until a separate physical qualification record is attached.

ACCOUNTING-CORRECTION.json records the proactive-proof accounting correction. It changed the ideal totals only; all package checksums and the selected hardware-test sequence remained unchanged.

The archive includes the 52 frozen search images, all 47,920 measured geometries, encoder binaries, per-step simulator logs, and raw hardware-test records. Only the selected .mota containers are served over LoRa. Lab scripts in evidence/ are tied to the Pi's USB/debug fixture and are not a remote field deployment controller.

Regression checks: 534 LoRa OTA tests ran successfully with one opt-in dependency-install test skipped; all 11 linker-selection tests passed.

All PlatformIO builds must be serialized, including builds in separate checkout directories that share the same PlatformIO package installation.

"},{"location":"research/rak3401_mota_chain/","title":"Rak3401 mota chain","text":"

View this page on MeshCore Docs.

"},{"location":"research/rak3401_mota_chain/#rak3401-1w-repeater-compact-lora-update-chain","title":"RAK3401 1W repeater compact LoRa update chain","text":"

Status: published prerelease for controlled, recoverable lab use. On 29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte identical to the corresponding transitions in the physically passed fd98bc90 chain. The replacement step 10 installs the current 3f6eddd5 endpoint. That replacement package and the hardened current runner have passed offline reconstruction plus the exact deployed bootloader simulator, but have not had a new physical end-to-end run. Use the lab gate and keep local DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates remain planning data, not physical qualification.

The prerelease is rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5. Use this asset:

RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\n

--verify-only remains the safe default. Live preflight and installation of this prerelease require --accept-test-candidate. That switch bypasses only the prerelease-status gate; it does not bypass the archive, identity, bootloader, route, watchdog, reachability, or post-boot checks.

"},{"location":"research/rak3401_mota_chain/#exact-destination","title":"Exact destination","text":"

This chain is intentionally specific to one firmware lineage and hardware target:

The exact starting application is the lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf RAK3401 asset. Its outer ZIP SHA-256 is 58ead2ea18efafda186bc9d983fe08cdf65f69b0e5a3a1c72c18fe35948a254c; its 566,912-byte firmware.bin SHA-256 is 4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb. The endpoint build ZIP SHA-256 is a9fca9be145a96feb650b57c43b5ed173d74d2f3194da12e31b1c54352635b56.

"},{"location":"research/rak3401_mota_chain/#retained-and-omitted-hardware-support","title":"Retained and omitted hardware support","text":"

Despite the legacy no_external_sensors target name, the endpoint retains the RAK12501 GPS provider and the common INA3221, INA219, INA226, and INA260 I2C voltage/current monitors. Install RAK12501 in sensor slot A. Do not use slot D with the RAK13302 1 W radio because the GPS reset/PPS signals overlap the radio's BUSY/DIO1 wiring.

The reduced profile omits optional external environmental and ranging devices: AHTX0, BME280, BMP280, SHTC3, SHT4x, LPS22HB, MLX90614, VL53L0X, BME680, BMP085, RAK12035, and BME680 BSEC. Board radio, display, buttons, battery telemetry, GPS, and the INA monitor family remain. No additional \u201cremove I2C temperature\u201d image is needed: the optional I2C temperature/environment drivers are already in the omitted set, while removing the remaining I2C/INA support would discard desired voltage/current monitoring and create another firmware identity without helping this route.

Do not use the chain on another target ID, hardware family, starting image, or firmware body hash. The runner checks all four.

"},{"location":"research/rak3401_mota_chain/#why-the-deployed-otafix24-and-old-staging-ceiling-work","title":"Why the deployed OTAFIX2.4 and old staging ceiling work","text":"

The deployed bootloader is not changed and no package uses the newer expanded 0xED000 ceiling. Each package remains bottom-aligned below the old bootloader's 0xD4000 scan ceiling. Its detools workspace is page-aligned and must satisfy:

0x26000 + inplace_memory <= staged_package_start < 0xD4000\n

The old bootloader already reads and validates that encoded detools geometry before its first destructive write. The previous application receiver was the limiting part: it reserved a fixed 0x98000 workspace before accepting a download. Step 1 therefore remains byte-for-byte identical to the physically passed 89,844-byte package. Step 2 is a 46,364-byte fixed-workspace package that installs the compatibility receiver. Later receivers stage above the real EndF image extent and let the bootloader enforce each package's selected workspace.

Every package in this candidate is an ordinary application container using format ABI 2 and in-place codec 2. The historical bridge applications scan the bootloader capability marker byte-by-byte, so they recognize the released 0.9.2-OTAFIX2.4 marker at absolute address 0xFCD16. That immutable marker advertises ABI 2, codec mask 0x0004, and internal storage profile 0. The 3f6eddd5 endpoint retains the matching legacy application-update fallback. Its separately compiled internal bootloader-update feature remains unavailable because privileged self-update requires a current ABI 3 marker with the exact storage profile. Installing this chain therefore does not replace the bootloader, and the final application can still accept a future valid ABI-2/codec-2 application update.

The exact installed bootloader is the 0.9.2-OTAFIX2.4 RAK4631 asset. Its outer ZIP SHA-256 is 5e29e7a8982cc2ed5f8868435556eb09387250fe5b9a7e8443bac325247dfed1; its 191,040-byte sd_bl.bin SHA-256 is a97f834388db0f1da6c29f6d8f458ff6d3e6a26034709044566384838919bb23. The exact simulator was compiled from release commit d73de8372e89b8ef352747c8bc7a1aaeab80fbfe; its executable SHA-256 is 9dacf24b1023fe2f4c620419417649ccc9538dd4a611b47bc19b7fedb97ceba6.

The schema-2 exhaustive search checked 1,799 candidate geometries and found 12 feasible forward edges in the declared exact 12-node inventory. It found one shortest ten-package route and selected the smallest total container bytes as the secondary objective. This minimum applies only to that declared inventory and geometry table, not every conceivable new bridge image. The selected route is:

Step From To Workspace Package Margin 1 1.16.7.0 1.16.7.9 0x98000 89,844 0 2 1.16.7.9 1.16.7.10 0x98000 46,364 40,960 3 1.16.7.10 1.16.8.7 0x91000 106,029 12,288 4 1.16.8.7 1.16.9.105 0x89000 149,927 0 5 1.16.9.105 1.16.9.110 0x8E000 111,312 16,384 6 1.16.9.110 1.16.9.113 0x8F000 120,624 4,096 7 1.16.9.113 1.16.9.117 0x8B000 139,637 0 8 1.16.9.117 1.16.10.0 0x7E000 174,202 20,480 9 1.16.10.0 1.17.1.3 0x6B000 271,569 0 10 1.17.1.3 1.17.1.6 0x76000 197,825 28,672

Total mOTA transfer data is 1,407,333 bytes. ROUTE.json, CHAIN.csv, and validation-results.json in the bundle pin the exact geometry and image hash for every transition.

Steps 1-9 are byte-for-byte identical to the transitions that passed on the physical RAK3401. The replacement step 10, from the physically reached FE65A6135A1E7B3F v1.17.1.3 body to 9BD7CF682EE065AE, has offline qualification only. All ten packages passed independent container checks, zero-filled and erased-workspace reconstruction, and the exact deployed 0.9.2-OTAFIX2.4 simulator. Those results prove the package bytes and apply geometry; they are not a physical claim for the replacement step 10 or the hardened runner.

The 171-byte transport DEFLATE support lands only in the final 3f6eddd5 application. The starting receiver and every receiver that accepts steps 1-10 lack that inflate path, so all ten legacy-chain transfers remain raw and the 1,407,333-byte total above is not automatically reduced over LoRa. A later update can negotiate transport compression after this endpoint is running; the application inflates before staging, and the immutable bootloader still applies an ordinary ABI-2/codec-2 container.

"},{"location":"research/rak3401_mota_chain/#host-requirements","title":"Host requirements","text":"

Install Python 3.9 or newer, meshcli 1.6.0 or newer, the current motatool, and an OTA-enabled Full Companion or repeater that can seed mOTA files.

"},{"location":"research/rak3401_mota_chain/#verify-offline","title":"Verify offline","text":"

No password or device is needed:

python3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --motatool /path/to/motatool \\\n  --verify-only\n

When given the ZIP, the runner checks its pinned outer hash and binds the extraction cache to that exact archive. An explicitly supplied extracted root starts at the pinned inner checksum list. In either case it checks complete inner coverage, all ten manifests, continuity, image anchors, both recovery images, and every container with motatool. Passing this command means offline and simulator qualification only; it does not unlock live use.

"},{"location":"research/rak3401_mota_chain/#guarded-prerelease-lab-command","title":"Guarded prerelease lab command","text":"

These direct-link arguments match the physical transition setup, but the current package replaces step 10 and the current host runner contains later cleanup and recovery fixes. Neither has been cleanly rerun through the whole chain. Keep local USB/SWD recovery available. The candidate switch bypasses only the prerelease-status block; all identity, checksum, bootloader, route, watchdog, reachability, and post-boot gates still run.

Restore the test start locally with recovery/test-start/RAK3401-test-start-v1.16.7-c1caa5ad.uf2, then run:

export MESHCORE_ADMIN_PASSWORD='password'\n\npython3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp 192.168.1.51:5001 \\\n  --source-cli-tcp 192.168.1.51:5002 \\\n  --source-shares-controller \\\n  --target-key 63d8df63 \\\n  --temp-radio 909.950,500,5,5,120 \\\n  --ota-hops 0 \\\n  --legacy-full-airtime \\\n  --motatool /path/to/motatool \\\n  --accept-test-candidate \\\n  --yes\n

--target-key accepts a full contact public key or a unique hexadecimal prefix of at least eight characters, such as the 12-character prefix shown by meshcli lc. The chain resolves it against the controller's saved contacts and uses the full key for every remote command and transfer step. Emoji or a later change in the repeater's display name does not affect selection. Each --relay can likewise use a full key or unique prefix instead of a name. The target still must be a repeater contact with the exact hardware, firmware, and bootloader required by this chain.

Keep the work directory. Rerunning the same command resumes only when the live EndF body hash matches an exact chain node. Never manually skip a package. --legacy-full-airtime temporarily sets the destination airtime factor to zero and restores it at the endpoint. Use that option only where the selected frequency and local duty-cycle rules permit a full transmit budget; omit it otherwise.

"},{"location":"research/rak3401_mota_chain/#superseded-b40d2e6c-prerelease","title":"Superseded b40d2e6c prerelease","text":"

The immediately preceding prerelease remains pinned for offline provenance:

Its first nine packages are byte-for-byte identical to the current release and the physical fd98bc90 evidence. Its different step 10 had offline qualification only. The runner recognizes this exact archive separately and keeps live use gated; it is not interchangeable with the current 3f6eddd5 asset.

"},{"location":"research/rak3401_mota_chain/#legacy-fd98bc90-physical-and-swd-qualification","title":"Legacy fd98bc90 physical and SWD qualification","text":"

The earlier local candidate remains the exact physical evidence source for this route:

RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip\n

All ten exact fd98bc90 transitions completed on the RAK3401 with its deployed 0.9.2-OTAFIX2.4 bootloader and a Heltec V4 source at 909.950 MHz / 500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD readback then matched the endpoint application, original bootloader, SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine packages in the b40d2e6c and 3f6eddd5 prereleases. All three candidates have different step 10 packages or endpoints, so the physical result and step-10 timing must not be attributed to either later prerelease.

"},{"location":"research/rak3401_mota_chain/#measured-direct-link-timing-for-fd98bc90","title":"Measured direct-link timing for fd98bc90","text":"

The first ready to install transfer ages were:

Step Blocks Transfer 1 88 598 s 2 45 42 s 3 103 81 s 4 146 105 s 5 109 76 s 6 118 94 s 7 136 102 s 8 170 127 s 9 264 187 s 10 169 193 s

The physically observed bulk-transfer total for those exact legacy packages is 1,605 seconds (26m45s). The log came from the pre-cleanup-fix runner revision. Step 1 was measured before the host began forcing source RXPS off and is therefore a conservative outlier. A new complete run with that fix is projected at roughly 18\u201319 minutes of bulk transfer, but that projection is not a substitute for the measured total. Allow about 70\u201375 minutes direct at BW500 for validation, ten transfers, installs/reboots, retained-store checks, and final restoration.

The legacy step 10's longer per-block time was not a V4 TempRadio delay: the source RXPS readback was off. Its v1.17.1.3 requester uses non-overlapping adaptive flights and a five-second recovery floor and logged more block reloads than step 8. The V4 itself intentionally schedules its local radio handoff after 1.5 seconds so the normal-channel command reply can drain; the host waits three seconds and then checks the live TempRadio state.

"},{"location":"research/rak3401_mota_chain/#live-progress-and-speed-sanity-checks","title":"Live progress and speed sanity checks","text":"

The runner watches motatool serve -v locally, so its quarter-progress reports do not consume LoRa airtime. At BW500 direct, physically measured steps 2\u20139 averaged about 1.1\u20131.4 newly requested payload blocks per second; the legacy step 10 averaged about 0.88 blocks/s, and step 1's 0.15 blocks/s was the known pre-RXPS-fix outlier. Use those only as bench sanity ranges, not guarantees. Relays, narrower bandwidths, interference, and recovery reads lower the rate.

Healthy output keeps advancing from [download] passive source progress x/y to source read all y payload blocks, followed by destination status ready to install y/y (100%) for the same manifest ID. Aggregate reads above the unique block count indicate recovery/retry traffic. A radio-scaled no-progress window causes one destination status check; the transfer timeout leaves the partial download staged for an exact-package resume. Stop and diagnose instead of installing if the manifest changes, progress remains flat through those checks, the final receiver count is not y/y, or the measured rate is far below the matching bandwidth/hop estimate without an understood RF cause.

For planning only, scale the conservative measured fd98bc90 26m45s bulk baseline by the qualified adaptive-preamble airtime and by each additional relay transmission:

Bandwidth, SF5/CR5 Direct / 0 relays 1 relay 2 relays 500 kHz 26m45s 53m30s 1h20m15s 250 kHz 46m51s 1h33m42s 2h20m33s 125 kHz 1h27m03s 2h54m07s 4h21m11s 62.5 kHz 2h54m07s 5h48m15s 8h42m23s

These are transfer-only estimates, not measured alternate-bandwidth or relay results. Packet loss, relay contention, discovery, administrative commands, staged verification, and ten reboot cycles add time. The bandwidth factors are not simple powers of two because the adaptive wire preamble is 128 symbols at 500 kHz, 64 at 250 kHz, and 32 at 125/62.5 kHz.

"},{"location":"research/rak3401_mota_chain/#manual-operation","title":"Manual operation","text":"

The automated runner is preferred because it binds every transition to the expected MID and body hash. The manual details below document the checks used during qualification and recovery; they are not permission to skip the runner's candidate gate. Keep a written copy of each original setting and never skip a step even when a later package appears in ota ls.

"},{"location":"research/rak3401_mota_chain/#1-verify-and-extract-the-asset","title":"1. Verify and extract the asset","text":"
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\nunzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip\ncd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5\nsha256sum -c SHA256SUMS.txt\nfor package in motas/*.mota; do motatool verify \"$package\" || exit 1; done\n

The outer hash must be 135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4, and the SHA-256 of the extracted SHA256SUMS.txt must be 93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4. Do not continue after any checksum or verification error.

"},{"location":"research/rak3401_mota_chain/#2-record-and-prepare-the-destination","title":"2. Record and prepare the destination","text":"

Before changing anything, save the complete replies from:

ver\nota self\nget bootloader.ver\nget system.watchdog\nget radio.rxps\nget powersaving\nget rxdelay\nget af\nota config\n

The starting ota self hash must be 71F4026CBE4B8B74, the target must be 2FA509C1, and the hardware must be RAK_3401. Disable the nRF52 system watchdog before the chain:

set system.watchdog off\n

The hardware watchdog cannot stop immediately. The RAK will reset once within about 60 seconds. Reconnect, wait at least 90 seconds from the command, and require get system.watchdog to report > off before continuing.

For a direct bench link, apply these temporary transfer guardrails to the RAK after every bridge reboot:

set powersaving off\nset radio.rxps off\nset rxdelay 0\nota config hops 0\ntempradio 909.950,500,5,5,120\n

Where local duty-cycle rules permit it, set af 0 can also remove the legacy firmware's saved airtime wait during this bounded maintenance window. Record get af first and restore that exact value after the final step. Current firmware grants the bounded TempRadio transfer budget without overwriting the saved airtime factor.

set powersaving off is an isolation guardrail, not the fix for the observed failure. The actual source failure was an oversized USB CDC reply; current firmware splits those reads. Some historical bridge builds may report RXPS as unsupported while already operating continuously. Verify the reported state and do not substitute a normal reboot for the one-time watchdog reset. Direct tests use hops 0: hops 1 makes the target echo each source response despite there being no intermediate relay, increasing half-duplex loss and legacy three-second retries.

The exact candidate cannot use fast RXPS throughout because its historical bridge applications predate the v1.17.1.5 adaptive-preamble contract. Keep RXPS off for all ten steps whether using SF5/BW250 or the faster direct-bench SF5/BW500 tuple. After the final endpoint is proven, a later update whose target, controller, source, and relays are all v1.17.1.5 or newer can use the generic LoRa OTA runner and its qualified adaptive-preamble boundaries.

Put the source on the identical TempRadio tuple. A current ASCII-first Full Companion recognizes motatool's initial ota folder on line directly:

motatool serve \\\n  --dir ./motas \\\n  --serial /dev/ttyACM1 \\\n  --baud 115200 \\\n  -v\n

The same command also works with older Full Companion firmware in binary mode, where the idle parser recognizes the identical preamble. Restart motatool for every step so the source emits a fresh catalog advert. Leave it running during the download and stop it with Ctrl-C only after the destination reports ready to install.

"},{"location":"research/rak3401_mota_chain/#3-install-all-ten-packages-in-order","title":"3. Install all ten packages in order","text":"Step Manifest ID Before hash Expected version After hash 1 C147BCEF 71F4026CBE4B8B74 1.16.7.9 42BC53A64288E845 2 BEB578FC 42BC53A64288E845 1.16.7.10 1AD2AA8B6C478DA6 3 6D8EF814 1AD2AA8B6C478DA6 1.16.8.7 D709A25308BC1831 4 40AC4CEB D709A25308BC1831 1.16.9.105 457BEDA5E6406C81 5 F6C8890B 457BEDA5E6406C81 1.16.9.110 E76BFA205634CAB8 6 82405E89 E76BFA205634CAB8 1.16.9.113 65328FC8A1FBED2D 7 80F40DB6 65328FC8A1FBED2D 1.16.9.117 AF7532E13337FADD 8 DBF6310D AF7532E13337FADD 1.16.10.0 22002359BBDFA76E 9 C2979E08 22002359BBDFA76E 1.17.1.3 FE65A6135A1E7B3F 10 B5011420 FE65A6135A1E7B3F 1.17.1.5 1FA6AAA3C28D8BD8

For each row, first prove that ota self exactly matches its Before hash, then inspect the manager before changing it:

ota status\n

Proceed to discovery only after that status is reconciled. When the manager is idle, run:

ota ls\nota pull MANIFEST_ID flash\nota status\n

If the first status reports this row's MID as partial, use ota pull with that same MID to resume it. If this row's MID is already ready, do not pull or cancel it; continue with the install checks below. If status reports the immediately previous row's MID as verifying staged blocks, wait through the bounded discovery window; require either that exact MID to become ready to install or the manager to become no download, and re-prove the current Before hash. Cancel only the same previous MID while it is visibly attached and ready. If it has become idle, send no cancel. Stop on any other MID, failed or incomplete state, ambiguous reply, or timeout. Never issue a blind ota cancel: it can discard a resumable or unrelated session.

Repeat ota status at a restrained interval until it says ready to install. If the manifest is initially absent, wait for a fresh source advert, run ota ls again, or restart motatool; do not pull a different ID. Stop the seeder and install:

get system.watchdog\nota install\n

The watchdog reply must still be > off. Allow the current default five-minute readiness window for the USB/LoRa identity to return; automation probes every 10 seconds and returns early as soon as the exact body is visible. Then run ver, ota self, and ota help. Require the row's exact After hash, require bootloader: apply OK, and require rescue install <hash16> in the help before exposing that bridge to the next package. This rescue-help gate applies to steps 1-9; step 10 is the endpoint and is not exposed to another package in this chain. A normal-radio ota status response of no download proves only that the manager is idle; legacy internal-flash firmware does not durably erase a retained container by accepting an IDLE ota cancel. Do not claim otherwise. When the same expected manifest reports verifying staged blocks after the next TempRadio starts, wait only for the bounded discovery window and require that it become ready to install or no download; stop on a changed ID, failed/incomplete state, or timeout. Prove the exact running body before cancelling an attached completed session. OTAFIX consumes the approval word during a successful install, so a retained endpoint container is inert and a later valid pull replaces it. Reapply the transfer guardrails and TempRadio tuple, restart motatool, and continue with the next row.

If the board boots but ota self says the EndF is invalid, stop. Only when ota status still identifies that row's complete staged package may the guarded ota rescue install BEFORE_HASH command be used. It is not a force option and must use that row's exact Before hash. If the board does not boot, recover the documented start/recovery UF2 locally over USB. Step 10 is the endpoint and has no successor package in this chain. If its EndF is invalid, use local DFU/SWD rather than attempting an unqualified LoRa package.

"},{"location":"research/rak3401_mota_chain/#4-intermediate-relays-and-restoration","title":"4. Intermediate relays and restoration","text":"

On every managed intermediate relay, save get rxdelay and get txdelay, then use the airtime-scaled collision window during the maintenance session:

set rxdelay 0\nset txdelay 0.3\n

Current OTA transfer packets honor that configured relay txdelay; setting 0.3 gives competing relays a small randomized, airtime-scaled forwarding window. The source's manifest-fragment gap independently follows active packet airtime and duty spacing, clamped to 100-1000 ms. These two delays solve different problems.

After step 10 is proven, restore each saved destination and relay value exactly, including rxdelay, RXPS, CPU power saving, af, ota config hops, and relay timing. Current full-parser repeater firmware accepts normalradio and restores the saved tuple after replying on the temporary channel:

normalradio\n

The b40d2e6c endpoint supports that command. If a recovery build unexpectedly replies Unknown command, shorten its lease to one minute with the same tuple:

tempradio 909.950,500,5,5,1\n

Wait for the lease to expire, return the controller and ordinary source to their saved normal channels, and then re-enable the RAK system watchdog:

set system.watchdog on\nget system.watchdog\n

A Full Companion source is the exception: its local TCP console supports normalradio and the automated runner uses it before restoring the shared Binary API radio tuple.

Final lab success requires version v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c, body hash 1FA6AAA3C28D8BD8, target 2FA509C1, hardware RAK_3401, and get system.watchdog reporting > on. If a relay cannot be restored before its TempRadio lease ends, wait for it to return to the normal channel and restore its saved rxdelay and txdelay there.

"},{"location":"research/rak3401_mota_chain/#two-relay-deployment","title":"Two-relay deployment","text":"

The first nine direct package transitions above were physically observed as the byte-identical first nine transitions of the legacy chain. Replacement step 10, this hardened host-runner revision, and multi-hop operation are not cleanly end-to-end physically qualified. A controlled, recoverable multi-hop lab can exercise two intermediate relays by listing them farthest-to-nearest and using three OTA hops. Run a non-mutating preflight first; even preflight requires the prerelease candidate override because the command connects to live devices:

python3 tools/lora_ota/rak3401_mota_chain.py \\\n  --bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \\\n  --work-dir ./rak3401-mota-chain-work \\\n  --controller-serial /dev/ttyACM0 \\\n  --source-tcp SOURCE_IP:5001 \\\n  --source-cli-tcp SOURCE_IP:5002 \\\n  --source-shares-controller \\\n  --target-key 63d8df63 \\\n  --relay 'Far Repeater' \\\n  --relay 'Near Repeater' \\\n  --temp-radio 909.950,250,5,5,120 \\\n  --ota-hops 3 \\\n  --motatool /path/to/motatool \\\n  --accept-test-candidate \\\n  --preflight-only\n

After preflight succeeds, rerun with --yes in place of --preflight-only. Confirm that the selected frequency and bandwidth are legal at the deployment location.

"},{"location":"research/rak3401_mota_chain/#rescue-and-completion","title":"Rescue and completion","text":"

The guarded ota rescue install <base_body_hash> command is present after step 1 and remains available through the step-9 bridge. It is not a force command: it refuses a valid normal EndF, a base mismatch, wrong target/hardware, or invalid payload. A failure before step 1 completes still requires local USB recovery because the deployed start image predates the rescue command. The final b40d2e6c endpoint uses the stricter shared-slot profile; its invalid-EndF recovery is local DFU/SWD, not the rescue command.

Before the first mutation, the runner saves the destination's RXPS periods, CPU power-saving state, RX flood delay, airtime factor, and OTA hop reach in the persistent work directory, flushing both the file and its directory entry before changing a guardrail. It also saves the managed source's exact RXPS preference in the chain-root source-rxps-settings.json before disabling it. Every step reuses that protected record, so a rerun after a killed host process restores the chain-start preference instead of treating the temporary off state as original. The record is accepted only for the same CLI endpoint. The runner retires that chain-root record only after it has proved the source is back on its normal radio and restored the exact preference at the verified endpoint. The runner reads the destination, controller, source, and relay versions before applying the same RXPS policy as the generic runner. The mixed historical chain therefore verifies radio.rxps off after every bridge reboot; a future all-v1.17.1.5-or-newer SF5/BW250 chain would instead keep RXPS on under the qualified level-8/preamble-64 boundary. It also verifies set powersaving off and rxdelay 0, plus af 0 when --legacy-full-airtime was explicitly selected. It restores every original value only after the exact endpoint is proven. An interrupted run deliberately leaves those transfer guardrails active; rerun the same command with the same work directory to resume and restore them. CPU power saving was not the cause of the observed folder-source failure: that was a USB CDC receive-ring overrun fixed by bounded serial reads. The runtime settings remain conservative isolation and fast-link reliability guardrails.

For every named --relay, the per-step runner also captures rxdelay and txdelay, writes them to relay-timing-settings.json, applies rxdelay 0 and txdelay 0.3 during transfer, verifies the readback, and restores both before the relay leaves TempRadio.

The runner also keeps the watchdog off across the chain, rechecks identity and OTA reach before every package, requires the exact post-boot EndF hash, and only re-enables the watchdog after step 10. Lab success requires endpoint body hash 1FA6AAA3C28D8BD8, target 2FA509C1, hardware RAK_3401, normal radio 910.525 MHz / 62.5 kHz / SF7 / CR5, and the watchdog verified on.

"},{"location":"research/rak3401_mota_chain/#external-radio-startup-and-manual-usb-recovery","title":"External-radio startup and manual USB recovery","text":"

RAK3401 is a distinct target from RAK4631. Its RAK13300/RAK13302 radio is on the WisBlock SPI bus (SPI1 in the Adafruit nRF52 core), with BUSY and DIO1 on P0.09/P0.10. Current builds explicitly enable those NFC-capable pads as GPIO and do not advertise the radio bus as an on-board QSPI flash device.

The RAK13302's FEM and boost supply use switched 3V3_S, but the SX1262 core uses unswitched 3V3. An MCU reset, a 1200-baud DFU touch, or disconnecting only USB may therefore leave a battery-powered SX1262 in its previous state. At startup, current firmware quiesces the FEM, reinitializes the dedicated SPI instance, asserts NRESET, and, if BUSY remains high, sends the Semtech NSS/GET_STATUS wake sequence without waiting on RadioLib first. The one-time board startup cold-starts 3V3_S; later radio retries leave that shared rail enabled so they do not reset GPS or sensor modules.

If BUSY still cannot be released, firmware must not enter a reset loop:

A permanently high BUSY after NRESET and the direct wake transaction indicates an electrically unavailable radio, loose module, or power-domain fault; an MCU reboot cannot manufacture a response. If local access is possible, disconnect the battery as well as USB and reseat the WisBlock module. Remote firmware will continue retrying without requiring that physical intervention or churning the USB device.

For an existing deployed tower using Preview 5, do not change the bootloader as part of this application chain. Its exact local recovery package is the Preview 5 RAK4631 ZIP. For a new RAK3401 installation or local recovery, use the OTAFIX 2.4.3 RAK3401 ZIP from the OTAFIX 2.4.3 release. These are local DFU/SWD recovery assets, not mOTA chain steps.

For a local Serial DFU recovery with the repository helper:

cd /path/to/meshfirmware\nMCFIRMWARE_NO_SUDO=1 ./mcfirmware.sh\n

Select the serial entry whose USB identity is WisCore RAK3401 Board, then choose Custom, nrf52, and the exact RAK3401 .zip. Use flash-update to preserve InternalFS preferences and identity. Use flash-wipe + flash only when erasing application data is intentional. The helper follows the same USB device into bootloader mode, so do not substitute another /dev/ttyACM* merely because its number appears first.

"},{"location":"test-results/","title":"Test Results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/#test-results","title":"Test Results","text":"

Completed validation reports, experiments, and test guides are collected here. Older page addresses redirect to these copies. Results are tied to the hardware and firmware versions named in each report; they do not automatically qualify a newer build.

"},{"location":"test-results/#radio-and-rf","title":"Radio and RF","text":""},{"location":"test-results/#companion-usb-ble-and-display","title":"Companion, USB, BLE, and display","text":""},{"location":"test-results/#build-storage-memory-and-checklists","title":"Build, storage, memory, and checklists","text":""},{"location":"test-results/battery_calibration_2026-10-22/","title":"Battery calibration 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/battery_calibration_2026-10-22/#shared-battery-adc-calibration-validation-2026-10-22","title":"Shared battery ADC calibration validation - 2026-10-22","text":"

Final integration baseline: 94a3b7e568781533510721670af24c162804a5ca on keymindCascade, plus the calibration changes. Earlier builds and link failures below used af106a0ee9edd8ee0113b7b364290e0944245b86; the final integration section supersedes those results for the targets it names. These checks do not constitute an installed-device or voltmeter acceptance test.

"},{"location":"test-results/battery_calibration_2026-10-22/#behavior-and-compatibility","title":"Behavior and compatibility","text":""},{"location":"test-results/battery_calibration_2026-10-22/#host-checks","title":"Host checks","text":"

All following checks passed using TMPDIR=/var/tmp (the machine's /tmp user quota prevented compiler temporary-file creation):

Check Coverage test/test_battery_calibration.py - 3 tests Production parsing, meter correction, existing absolute coefficients, pack references, effective multiplier, reset, unsupported/zero/saturated readings, bounds, actual MainBoard scaling and nRF52 policy precedence. test/test_battery_calibration_store.py - 1 test across four platform fixtures ESP32, RP2040, nRF52 and STM32 transaction implementations; reboot restore, every single-bit record corruption, truncation, write failure, uncertain publication, reset, missing persistent storage and backup recovery. test/test_battery_forwarding.py - 19 tests Actual infrastructure setter, failed-save rollback, nRF52 policy fallback, and refreshed battery-based forwarding decisions. test/test_heltec_r8_external_power.py - 1 test Existing R8 power estimate still uses the calibrated voltage. test/test_stm32_companion_profile.py - 21 tests Existing ADC conversion and sampling remain exact at default calibration; memory/profile constraints retained. test/test_rp2040_application_capabilities.py - 11 tests Existing upload artifact and capability validation.

The production calibration, store and forwarding harnesses use AddressSanitizer and UndefinedBehaviorSanitizer. git diff --check also passed.

"},{"location":"test-results/battery_calibration_2026-10-22/#earlier-firmware-builds","title":"Earlier firmware builds","text":"

Before integration, PlatformIO processes were run serially. Successful targets:

That earlier Mini Companion occupied 228,832 of 229,376 application bytes: 544 bytes remained. This validates the current build recipe, not every possible version string, optional feature combination or firmware target.

"},{"location":"test-results/battery_calibration_2026-10-22/#earlier-stm32-flash-blockers","title":"Earlier STM32 flash blockers","text":"

The following targets compile but fail the linker flash limit. Both failures were reproduced from an isolated, unchanged checkout of the baseline with the same build recipes and compiler versions:

Target Baseline overflow Calibration build overflow Additional bytes wio-e5-mini_repeater 1,168 2,852 1,684 wio-e5-mini_sensor 25,584 27,368 1,784

The calibration increase consists of code and constants; it did not introduce additional floating-point runtime helpers in the Repeater. Filesystem and application boundaries have not been moved, and no unrelated feature was removed to conceal these build failures. These results apply to the earlier source baseline above, before the Mini Repeater feature gates in 94a3b7e56. The integration checks below supersede that earlier Repeater result when a newer build succeeds.

"},{"location":"test-results/battery_calibration_2026-10-22/#final-integration-checks","title":"Final integration checks","text":"

After merging 94a3b7e56, 69 distinct focused tests passed: 42 calibration, platform-storage and related regressions, 19 battery-forwarding tests, and 8 Mini Repeater profile checks. The 23 calibration/store/forwarding tests were rerun successfully after the final reply compaction. Both new calibration suites are included in the unit-test workflow.

The first integrated Mini Repeater link exceeded flash by 432 bytes. Disabling guessed branch probabilities in that target's existing size recipe, sharing the existing save-failure replies, keeping the command key in constant storage and shortening calibration replies recovered 808 bytes. Calibration, routing, logging, 50 neighbours and the existing 16 KiB filesystem are retained. No extra feature was disabled to make this image fit.

The final merged-source builds passed their flash and runtime-memory checks:

Target Application bytes used Application limit Free flash bytes Heltec_v3_repeater 1,890,881 3,342,336 1,451,455 RAK_3401_repeater 559,616 815,104 255,488 wio-e5-mini_repeater 245,384 245,760 376 wio-e5-mini_companion_radio_usb 228,664 229,376 712

These small Mini margins qualify the stated recipes, not arbitrary optional features or longer build metadata. The earlier Mini Sensor overflow remains recorded above; that target was not rebuilt in this final integration campaign. No physical ADC, voltmeter, charging or installed-device test was performed.

The later unit-test workflow follow-up records release-wrapper failures that these direct recipe builds did not cover.

"},{"location":"test-results/bootloader_version_stock_audit/","title":"Bootloader version stock audit","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/bootloader_version_stock_audit/#stock-nrf52-bootloader-version-audit","title":"Stock nRF52 bootloader version audit","text":"

Checked 2026-09-29, against the 40 nRF52 variant families resolving to 26 board configurations in this checkout. This concerns get bootloader.ver, not bootloader installation or OTA eligibility. No devices were flashed, and no features, storage sizes, or partitions were changed.

"},{"location":"test-results/bootloader_version_stock_audit/#result","title":"Result","text":"

The real firmware reader successfully reads 26 distinct manufacturer-published bootloader images from 32 artifact records, including older RAK revisions, current and historical LilyGO images, and vendor HEX, UF2 and DFU ZIP packages. Repeated copies and identical normalized bootloader images are counted once. These are publicly supplied stock/recovery images, not proof that every manufacturing batch shipped identical bytes. No untouched live unit was read in this audit.

One format gap was fixed: Seeed's T1000-E LoRaWAN development-board bootloader declares UF2 Bootloader 1.00. The reader now accepts that two-component version and returns 1.00 unchanged. It does not invent 1.00.0. That development image is separate from Seeed's T1000-E recovery image, which reports 0.9.1-5-g488711a and already worked.

All other tested images already expose a readable UF2 bootloader record. Generic bare dotted-number searches remain deliberately unsupported: TinyUSB, nrfx, and SoftDevice can embed their own versions inside the same bootloader. The bounded standalone fallback still requires the explicit OTAFIX version shape. Ambiguous records, invalid vectors, and corrupt canonical OTAFIX metadata retain the existing rejection rules.

"},{"location":"test-results/bootloader_version_stock_audit/#published-images-actually-tested","title":"Published images actually tested","text":"

The exact source URLs, download hashes, normalized-image hashes, extraction layout and expected strings are pinned in stock_images.json.

Manufacturer image family Version captured from the image RAK4631/RAK4630 Arduino bootloader, historical through current 0.3.2-109-gd6b28e6, 0.4.1-13-g5e6690e-dirty, 0.4.2, 0.4.3, 0.4.4 Heltec HT-n5262, SDK and T114 vendor recovery HEX 0.9.0-2-g836c8dc-dirty LilyGO T-Echo 0.6.1-2-g1224915 LilyGO T-Echo Lite, Card, T-Impulse Plus (including historical builds) 0.9.2-dirty Seeed XIAO nRF52840 and Sense 0.6.1 Seeed XIAO nRF52840 Plus and Sense Plus 0.9.2-29-g6a9a6a3 Seeed Wio Tracker 1110 SDK/conversion bootloader 0.6.2-26-g949425a-dirty Seeed Wio Tracker L1 vendor recovery ZIP 0.7.0-22-g277a0c8 Seeed T1000-E recovery ZIP 0.9.1-5-g488711a Seeed T1000-E LoRaWAN development-board HEX 1.00 (newly supported) Seeed MeshTracker X1 recovery ZIP 0.10.0-18-gb93789f MTools Tec MeshTiny vendor bootloader UF2 0.4.5 BQ/Unit Engineering Nano G2 Ultra vendor HEX ZIP 0.6.4-dirty Muzi Works R1 Neo release HEX 0.9.2-31-g990aa7f-dirty

Use the embedded version, not the filename. For example, Seeed's XIAO file named ...bootloader-0.6.2...hex reports 0.6.1, and its L1 ZIP named ...bootloader-0.10.0...zip reports 0.7.0-22-g277a0c8. These are actual tested bytes, not renamed or inferred release numbers.

All these normalized images use the Adafruit nRF52840 F4000..FE000 bootloader window. HEX/UF2 address records identify the bytes. The boot-only Seeed DFU packages omit MBR/UICR address words; their bootloader binary is extracted using the manifest's sd_size/bl_size and the Adafruit nRF52840 linker layout. The runtime reader independently selects the installed MBR/UICR boot address; this audit does not hardcode that layout into the firmware getter.

"},{"location":"test-results/bootloader_version_stock_audit/#coverage-of-every-variant-family","title":"Coverage of every variant family","text":" Variant family Status Evidence or remaining gap gat562_30s_mesh_kit Unverified GAT documentation/firmware repository checked; no product-specific stock bootloader found. gat562_mesh_evb_pro Unverified Same GAT gap; RAK board configuration is not factory-image proof. gat562_mesh_tracker_pro Unverified Same GAT gap; custom vendor/OTAFIX builds also exist. gat562_mesh_watch13 Unverified Same GAT gap. heltec_mesh_solar Representative only HT-n5262 SDK image passes; Solar downloads found application images, not a board-specific stock bootloader. heltec_t096 Representative only HT-n5262 SDK image passes; T096 factory revision not established. heltec_t1 Representative only SDK image passes; vendor F&T archive contains application-only UF2s. heltec_t114 Image verified Both SDK HEX and T114-specific vendor recovery HEX pass. heltec_tower_v2 Representative only SDK image passes; vendor matching-firmware archive contains applications, not the stock bootloader. ikoka_handheld_nrf Representative only XIAO board configuration; XIAO module images pass, exact installed factory image not obtained. ikoka_nano_nrf Representative only Same XIAO representative coverage. ikoka_stick_nrf Representative only Same XIAO representative coverage. keepteen_lt1 Unverified Manufacturer product/download search did not yield a factory bootloader binary. lilygo_t_impulse_plus Image verified Both historical and newer vendor bootloader HEX images pass. lilygo_techo Image verified Vendor T-Echo bootloader passes. lilygo_techo_card Image verified Three distinct vendor historical/current bootloader images pass. NFC UICR repair application excluded. lilygo_techo_lite Image verified Vendor bootloader HEX passes. mesh_pocket Representative only HT-n5262 SDK image passes; exact Pocket factory revision not established. meshtiny Image verified Direct manufacturer bootloader UF2 passes; newer ADV/factory revisions require their actual image. meshtracker_x1 Image verified Seeed product-specific recovery ZIP passes. minewsemi_me25ls01 Unverified Manufacturer module page lists firmware as none; no stock bootloader binary found. OEM/dev-kit installations can differ. muzi_base Unverified Manufacturer publishes bootloader source, but its release assets found were for R1 Neo, not BASE. muziworks_r1_neo Image verified Manufacturer R1 Neo release HEX passes; do not substitute a generic RAK version. nano_g2_ultra Image verified Manufacturer's preinstalled-bootloader recovery HEX ZIP passes. promicro Unverified Nologo documents a Nice!Nano UF2 drive; its linked download requires Baidu access. Generic clones need separate factory images. rak3401 Representative only RAK4631 Arduino images pass; RAK3401-specific untouched image not obtained. rak4631 Image verified Five distinct published stock Arduino bootloader revisions pass. RAK4631-R/RUI3 is a different bootloader family. rak_wismesh_tag Representative only RAK core images pass; exact Tag factory image not obtained. sensecap_solar Representative only Seeed documentation identifies XIAO DFU; XIAO images pass, exact Solar factory image not obtained. t1000-e Image verified Both vendor recovery ZIP and separate LoRaWAN development-board HEX pass. thinknode_m1 Unverified Manufacturer repository checked; no factory bootloader binary found. thinknode_m3 Unverified Published factory UF2 is application-only (26000..52BFF), not a bootloader image. thinknode_m4 Unverified Published factory UF2 is application-only (26000..521FF). thinknode_m6 Unverified Published factory UF2 is application-only (26000..46CFF). thinknode_m8 Unverified No accessible product-specific factory bootloader binary found. wio-tracker-l1 Image verified Seeed's L1 recovery ZIP passes. wio-tracker-l1-1w Representative only L1 recovery image passes; exact 1W/ADV factory revision not established. wio-tracker-l1-eink Representative only L1 recovery image passes; exact e-ink factory revision not established. wio_wm1110 Representative only Published Wio Tracker 1110 and XIAO SDK images pass; the variant's XIAO config does not prove the shipped module's image. xiao_nrf52 Image verified XIAO, Sense, Plus and Sense Plus vendor images pass.

Representative/unverified products use the same shared reader and should report a version if their installed bootloader exposes one of the recognized records. There is no evidence yet for claiming an exact version or success on every untouched unit. ESP32, RP2040 and STM32 are outside this nRF52 flash reader; their getters were not changed and this report does not claim stock-version capture on those architectures.

"},{"location":"test-results/bootloader_version_stock_audit/#reproduce-the-artifact-tests","title":"Reproduce the artifact tests","text":"

Normal tests are offline and include all observed version strings, malformed and conflicting versions, dependency-version false positives, region/vector bounds, and the existing OTAFIX metadata/CRC/fallback cases:

python3 test/test_nrf52_bootloader_version.py -v\n

To replay the real images, download each url in stock_images.json into a cache directory using its file name, then run:

MESHCORE_STOCK_BOOTLOADER_CACHE=/absolute/path/to/cache \\\n  python3 test/test_nrf52_bootloader_version.py -v\n

This checks the exact download SHA-256, extracts only the bootloader window, checks the normalized-image SHA-256, and invokes the actual C++ reader with an exact expected version. It never downloads automatically or flashes a device. The ordinary regression cases cover every captured format even without the optional cached artifacts. RAK4631 and T1000-E repeater firmware builds passed.

"},{"location":"test-results/bootloader_version_stock_audit/#what-is-needed-to-close-the-remaining-gaps","title":"What is needed to close the remaining gaps","text":"

For each untouched unit, collect its DFU drive's INFO_UF2.TXT if present. To prove the application's flash reader can capture that exact revision, also obtain a read-only bootloader dump plus the MBR word at 0xFF8, UICR NRFFW[0], and flash geometry (or a full vendor HEX including those address records). Keep the bootloader unchanged while collecting this evidence. Only the identified bootloader region is needed; private application keys/configuration are not. If no version bytes or startup register value exist, report unknown; a regex cannot recover a version that was never embedded.

"},{"location":"test-results/bootloader_version_stock_audit/#manufacturer-sources","title":"Manufacturer sources","text":""},{"location":"test-results/cad_scan_validation/","title":"Cad scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/cad_scan_validation/#sx1262-channel-scanning-hardware-results","title":"SX1262 channel-scanning hardware results","text":"

Hardware test on September 12, 2026 (America/Los_Angeles). Heltec V4 transmitter; XIAO ESP32-S3 with Wio SX1262 receiver. Both used standalone diagnostic firmware.

The best tested scan setting was four channels with four-symbol RX-preamble windows. Three-symbol windows were unreliable even in the single-channel re-arm control. In the longer four-symbol runs, four channels received 80/80 packets, five received 79/80, and six received 52/60. These are measured limits of this implementation and test setup, not universal SX1262 channel-count limits.

"},{"location":"test-results/cad_scan_validation/#configuration-and-counting","title":"Configuration and counting","text":"

SF5, 125 kHz, coding rate 4/5, explicit header, payload CRC, and 32 preamble symbols. The programmed preamble is 8.192 ms. Channels start at 909.5 MHz and increase in 1 MHz steps; tests use the first N channels, up to 920.5 MHz. Test packets are transmitted individually with varied timing relative to the scanner. There are no overlapping transmitters in this experiment.

A success requires a CRC-valid packet with the expected sequence, channel, length, and test payload. Every included transmission has its own successful transmitter acknowledgement; delayed serial acknowledgements are recovered by sequence, without retransmitting. The one exploratory RX scan trial with shifted acknowledgements is excluded.

"},{"location":"test-results/cad_scan_validation/#completed-trials","title":"Completed trials","text":"Trial Method/window Channels Payload bytes Received / sent Mean complete scan baseline-fixed Fixed-channel RX 4 64 16 / 16 \u2014 cad-standard CAD + software RX 4 64 7 / 32 14.469 ms cad-auto CAD + automatic RX 4 64 16 / 32 14.468 ms rxfast-4 rxfast 4 4 64 32 / 32 6.100 ms rxfast-4-maxpayload rxfast 4 4 255 35 / 35 6.098 ms matrix-cad1-1 cad 1 1 64 7 / 8 2.643 ms matrix-cad1-10 cad 1 10 64 2 / 20 28.389 ms matrix-cad1-11 cad 1 11 64 5 / 22 31.297 ms matrix-cad1-12 cad 1 12 64 3 / 24 34.145 ms matrix-cad2-10 cad 2 10 64 3 / 40 31.040 ms matrix-fixed-12 Fixed-channel RX 12 64 24 / 24 \u2014 matrix-rx1-10 rxfast 1 10 64 0 / 20 7.555 ms matrix-rx1-11 rxfast 1 11 64 0 / 22 8.316 ms matrix-rx1-12 rxfast 1 12 64 0 / 24 9.072 ms matrix-rx2-10 rxfast 2 10 64 0 / 40 10.133 ms matrix-rx3-7 rxfast 3 7 64 2 / 35 8.892 ms matrix-rx3-8 rxfast 3 8 64 1 / 40 10.160 ms matrix-rx4-5 rxfast 4 5 64 39 / 40 7.630 ms boundary-rx3-1 rxfast 3 1 64 4 / 24 1.268 ms boundary-rx3-4 rxfast 3 4 64 2 / 24 5.080 ms boundary-rx3-5 rxfast 3 5 64 0 / 25 6.350 ms boundary-rx3-6 rxfast 3 6 64 2 / 30 7.621 ms boundary-rx4-4 rxfast 4 4 64 80 / 80 6.104 ms boundary-rx4-5 rxfast 4 5 64 79 / 80 7.630 ms boundary-rx4-6 rxfast 4 6 64 52 / 60 9.158 ms

rxfast N means a continuous-RX preamble observation window of N symbols, with minimal reconfiguration between frequencies. It is not CAD. cad N uses the SX1262 CAD detector with N symbols and automatic handoff to RX. CAD has no three-symbol mode.

Scan times describe uninterrupted sweeps. When reception starts, the radio stays on that frequency through the packet and cannot hear packets on the other frequencies. Serial status requests can add occasional timing outliers.

The maximum-payload trial ended after 35 confirmed transmissions when a serial acknowledgement was delayed. Its last TX acknowledgement and matching RX were subsequently recovered from the serial logs; all 35 were received. It is not a completed 64-packet trial.

"},{"location":"test-results/cad_scan_validation/#interpretation-and-limits","title":"Interpretation and limits","text":"

The initial four-channel fast-RX test received 32/32 64-byte packets and 35/35 255-byte packets. That is a successful strong-signal bench result, not a guarantee of zero loss. Ordinary four-symbol CAD took approximately 14.47 ms to cover four frequencies and was unreliable with an 8.192 ms preamble.

Reducing the observation window also reduces the time available for the modem to recognize a preamble. Dividing preamble duration by scan duration alone overestimates the number of usable channels. One- and two-symbol RX windows produced no preamble detections in the tested high-channel-count cases.

The three-symbol single-channel control repeatedly re-arms RX, just as a scan would, and received only 4/24 packets. It is distinct from continuous fixed-channel reception, which passed its baseline. This isolates a detection-window limitation even without a long multi-channel sweep.

CAD detection peak 18 and minimum 10 were held constant. These are the datasheet four-symbol SF5 thresholds; the one- and two-symbol CAD tests do not establish optimized thresholds or weak-signal sensitivity. CAD activity followed by no decoded packet can be a late packet detection, not necessarily a noise false positive.

Received signals were strong. These tests do not establish weak-signal range, adjacent-channel rejection, simultaneous-packet capacity, production firmware scheduling performance, power consumption, FCC compliance, or months of uptime.

"},{"location":"test-results/cad_scan_validation/#reproduction-and-evidence","title":"Reproduction and evidence","text":"

See the test harness. Compact validated results, timing counters, and final firmware/source hashes are in cad_scan_results.json. Detailed sequence-level logs and private lab transport helpers are retained locally under out/cad-scan/.

"},{"location":"test-results/carrier_wave_validation/","title":"Carrier wave validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/carrier_wave_validation/#cw-and-cw2-validation","title":"CW and CW2 validation","text":"

Tested on the MercerWoodMesh lab Pi on September 14, 2026 (Pacific), using base commit 9241ccc253ad2f8ac2c9a2fe756f59ba2f52505d plus the CW changes. Test images are marked v1.17.1.6-cw-test-9241ccc2; they are development images.

A separate RAK4631 sampled live received signal strength (RSSI) about every 10 ms, without needing to decode a packet. The transmitting boards ran the Full V4 repeater and Full T1000-E Companion builds. Both were configured for \u22129 dBm chip TX power, with profiles at 909.5 and 919.5 MHz, BW125, SF7, CR5, and 32-symbol preambles.

Check V4 / SX1262 T1000-E / LR1110 cw on 0.25 measured duration 0.25 s 0.25 s cw on 1.25 measured duration 1.25 s 1.25 s Manual stop approximately 1 s into a 5 s request 1.01 s 1.01 s cw on 1.25 after enabling RXPS 1.26 s 1.25 s cw2 on 1.25 on the second frequency 1.25 s 1.25 s Primary-frequency RSSI change while CW2 transmits 0 dB 0 dB Paired RX and TX recovery checks 5/5 passed 5/5 passed

The observer saw roughly \u221256 to \u221257 dBm during V4 carriers and \u221267 to \u221271 dBm during T1000-E carriers, against a \u2212112 to \u2212113 dBm baseline. Readings returned to baseline after timeout and manual stop. These are received signal readings, not calibrated transmitter output measurements.

RX recovery used CRC-valid LoRa test payloads and the DUT's receive counter. TX recovery used each DUT's MeshCore advertisement, received with valid CRC by the observer. Both DUTs finished in RX, with CW and radio2 disabled and no outbound packet pending. The observer has no autonomous transmission.

"},{"location":"test-results/carrier_wave_validation/#software-checks","title":"Software checks","text":""},{"location":"test-results/carrier_wave_validation/#sx1276-follow-up","title":"SX1276 follow-up","text":"

The SX1276 carrier feature from PR #11 is included alongside SX1262 and LR1110 support. It switches to FSK with zero deviation, applies the board's calibrated TX power, and rebuilds the runtime LoRa configuration when stopped. External-PA settings pass through the same DAC/drive-power mapping used for packets, including RFO selection.

The normal TX power command can adjust a running carrier for meter sweeps. RadioLib's SX1276 power setter enters standby, so the carrier is restarted afterward without extending the deadline. Failed power changes stop the carrier and restore the previous cached power.

Follow-up checks use base 9dd9881d520b4b791600a00fff1f8e1436921054 plus the SX1276 changes. Production-method regression tests cover both profiles, zero deviation, external-PA calibration, RFO, power sweeps, timeout, failed FSK entry, failed restoration, retry guards, and return to LoRa. The native CW paths and existing radio/profile/sleep/PA checks pass as well. Both GEPRC_Linkflow_900_repeater (external PA) and Heltec_v2_companion_radio_usb firmware builds pass, including size checks.

SX1276 RF output has not been measured in this follow-up. The physical RF results above apply to SX1262 and LR1110.

"},{"location":"test-results/carrier_wave_validation/#bench-qualifications","title":"Bench qualifications","text":"

The first CW2 request arrived before radio2's existing delayed activation. The harness now polls get radio2.status for the active mode before testing.

An initial V4 dual-profile packet probe used a 120-symbol preamble against 32-symbol profiles. A comparison around another CW2 burst received 0/8 before and 1/8 afterward with that mismatched probe, versus 8/8 before and 8/8 afterward with a matching 32-symbol probe. The configured preamble also sets the driver's preamble-detection hold timeout, so a much longer incoming preamble can outlast that hold and be interrupted by scanning. The final dual-profile recovery checks use matched preambles. This finding does not establish general packet-loss performance for every scan combination.

These checks establish RF activity, selected-frequency behavior, and recovery on the tested boards. They do not measure spectral purity, frequency error, SWR, calibrated power, or months of uptime. Carrier timeouts are serviced by the main loop and depend on that loop remaining responsive.

"},{"location":"test-results/carrier_wave_validation/#reproduction","title":"Reproduction","text":"

Append tools/hil/cw_observer.ini to extra_configs in platformio.local.ini, preserving existing entries, and build pio run -e cw_observer_rak4631. Flash that application onto a separate lab RAK4631. Do not run PlatformIO concurrently with another build or upload in the checkout.

Configure the lab DUT's primary profile as above, disable radio2, set TX power to \u22129 dBm, and reboot to apply saved primary settings. Then run:

python3 tools/hil/carrier_wave.py --dut /dev/serial/by-id/DUT \\\n  --observer /dev/serial/by-id/OBSERVER --role repeater --output cw-results.json\n

Use --role companion for the T1000-E. The harness changes lab radio2/RXPS settings and emits short RF bursts and packet probes. It is for test boards.

Compact evidence: carrier_wave_results.json. Command guide: carrier-wave RF checks.

"},{"location":"test-results/cli_settings_audit/","title":"Cli settings audit","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/cli_settings_audit/#cli-setting-dispatch-audit","title":"CLI setting dispatch audit","text":"

The 1.17.1.5 source retained get path.hash.mode in CommonCLI but lost its setter when radio preferences moved into CommonRadioPrefs. Infrastructure roles call CommonCLI directly, so the shared parser's setter was unreachable. The correction restores set path.hash.mode 0|1|2 there, validates the value, and saves accepted changes. Missing/invalid values report a value error and leave the setting unchanged.

The other radio setters removed by the original refactor had already been restored. A feature-guarded check found a second mismatch: get extra.sf exposed stored data on non-LR2021 radios while its setter was compiled out. Both commands now return Error: extra.sf requires an LR2021 radio there. Supported LR2021 set/get behavior is preserved. These are source corrections; existing release binaries need rebuilding/updating to receive them.

"},{"location":"test-results/cli_settings_audit/#coverage","title":"Coverage","text":"Surface Check Common infrastructure CLI 72 literal setter keys and 84 query keys; every query has a setter or an explicitly reviewed read-only/alternate-command exception MQTT/observer CLI 33 literal setter keys and 39 query keys, including Wi-Fi, timezone, alerts, display and watchdog settings MQTT slots All nine writable slot subkeys have query/set coverage; diag is intentionally read-only Repeater, Room Server, Sensor Shared CLI delegation and role-specific query/set coverage Companion Shared-radio allowlist and parser agreement, persistence wiring, terminal-specific setters, and role query exceptions Radio settings moved by the refactor All 13 shared setters remain present in infrastructure dispatch Literal command comparisons Checks key lengths so a setter cannot accidentally compare its value against the key's terminating NUL Feature guards Preprocesses common and observer handlers for minimal, nRF52 GPS/SD, ESP32 WebConfig, ESP32 MQTT, RS232/GPS, ESP-NOW, and LR2021 profiles before comparing query/set coverage

Read-only exceptions include runtime connection status, diagnostics, firmware role, bootloader identity and reset information. Alternate-command exceptions include the password command and set battery.alert on <region>. These exceptions are explicit in the test, rather than silently allowing all missing setters. Dynamic/delegated command families and hardware-specific features continue to use their dedicated tests; the inventory is not a claim that every build has every feature enabled.

"},{"location":"test-results/cli_settings_audit/#executed-behavior-tests","title":"Executed behavior tests","text":"

The native C++ fixture compiles the production get/set dispatch branches and the real setting bodies for 14 radio-related settings: radio, freq, af, dutycycle, int.thresh, cad, radio.rxgain, tx, rxdelay, agc.reset.interval, path.hash.mode, multi.acks, txdelay, and direct.txdelay. It runs local and authenticated on-air caller timestamps, checks round-trip values and save calls, preserves AGC interval rounding, and checks that rejected hardware applies do not save or change preferences. Hardware callbacks are mocked; these are not live-radio or flash-durability tests. The fixture also runs with and without USE_LR2021, checking supported extra.sf set/query/clear behavior, invalid values, and the unsupported-radio error.

The focused hash-mode test checks all valid modes, missing values, negative and out-of-range values, overflow, malformed numbers, whitespace, and similarly named unknown keys. Existing transport tests exercise Companion local/framed and on-air dispatch, local-only secret reads, and browser/stream routing under AddressSanitizer and UndefinedBehaviorSanitizer on Linux.

Run the CI checks locally with a host C++ compiler (Linux is required for the existing sanitizer-enabled transport suite):

python3 -B test/test_path_hash_cli.py\npython3 -B test/test_cli_settings_contract.py\npython3 -B test/test_local_cli_access.py\n

All 14 tests passed on the Linux VM. The workflow runs these commands on future pushes and pull requests. The new inventory and native radio tests also passed with the Windows host compiler.

pio run -e heltec_v4_repeater passed after both corrections, including the firmware's flash-size and runtime-RAM checks. The initial audit used host tests and build checks; subsequent hardware checks are recorded in the dual-profile validation.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/","title":"Companion ble refresh 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#companion-bluetooth-contact-refresh-2026-10-19","title":"Companion Bluetooth contact refresh, 2026-10-19","text":"

The production Heltec V3 Full Companion refreshed 92 contacts in a median 3.347882 seconds at 32.560 kbit/s, compared with 6.191396 seconds at 17.606 kbit/s for the earlier 60 ms firmware. All five complete refreshes and the 46-contact incremental refresh passed exact-record validation in each run. The new firmware reduced median complete-refresh time by 45.9% on this Mercerwood Pi connection.

Measured production source: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6, version v1.18.0.1-halo-keymind-cascade-dev-4e4f27a2, target Heltec_v3_companion_radio_full. This was the normal Full build, with no BLE benchmark override. These results measure contact synchronization on one V3 and one Linux central; phone performance and complete application refreshes remain untested.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#production-behavior","title":"Production behavior","text":"

The modern NimBLE-Arduino adapter now spaces notification attempts by at least 7 ms. Legacy NimBLE and Bluedroid retain their 60 ms setting. After the first successful encrypted, authenticated and bonded connection callback for a link, the modern adapter requests a 15-30 ms connection interval, zero peripheral latency and a four-second supervision timeout. It makes this request once per authenticated link. The central chooses the accepted connection parameters.

Required replies retain the existing bounded queues, notification-subscription and negotiated-MTU gates, retry behavior and stalled-link recovery. Each notification carries one complete protocol frame. Failed notification attempts retain the frame; successful queue acceptance removes it without waiting for a later callback. Contact-stream production retains its existing 5 ms pacing. The change preserves the Full feature configuration and contact capacities.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#hardware-method-and-results","title":"Hardware method and results","text":"

The same physical V3, Pi controller and 92 owned synthetic contacts were used for both firmware revisions. V3 Full has capacity for 100 contacts; eight slots were reserved. The fixture used zero path bytes and two modification timestamps so the incremental filter selected exactly 46 records. The complete fixture record-set hashes agree across both runs. Application-only upgrades preserved the contact fixture and node identity.

After the comparison, all 92 owned test contacts were removed. A fresh USB session verified their absence and that the other contact records were unchanged.

The earlier firmware was fcc4714e2b1294a0d76c0aab4fa3b95b1609b3d0. Its comparison run followed a controlled warm boot, recorded separately from the BLE measurement. That preparatory check verified the same node, all 92 persisted contacts and a reset uptime without core errors. The result measures the combined firmware revisions, including the intervening contact-storage fixes; it does not isolate the contribution of each individual change.

Measurement Earlier 60 ms firmware Production 4e4f27a2 Complete refreshes passed 5/5 5/5 Exact contacts per complete refresh 92 92 Median request-to-END time 6.191396 s 3.347882 s Complete-refresh time range 6.137166-6.331489 s 3.342054-3.435741 s Median protocol throughput 17.606 kbit/s 32.560 kbit/s Incremental contacts verified 46 46 Incremental request-to-END time 3.259745 s 1.696429 s Incremental protocol throughput 16.733 kbit/s 32.152 kbit/s

Each stream had to return a valid start header, the declared complete count, unique complete records, every expected owned record byte-for-byte, and a valid END watermark. Incremental results had to equal the timestamp-filtered complete inventory. All streams had zero duplicate contacts or unexpected restarts. The qualifier also checked the complete firmware version and public identity against USB, authenticated NUS access, and monotonic device uptime.

Throughput is application-protocol bytes divided by host-observed time from the GET_CONTACTS write through the validated END notification. One complete refresh contains 92 records of 148 bytes plus ten header/END bytes: 13,626 bytes. The figures include request latency and protocol framing. Unsolicited pushes are excluded from the byte total. These values are not BLE PHY bit rates.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#observed-connection-and-controller-limits","title":"Observed connection and controller limits","text":"

Metadata collected during the normal production run showed the connection start at 48.75 ms, latency zero and a 420 ms supervision timeout, then accept 30 ms, latency zero and a 4,000 ms supervision timeout. The complete-refresh contact callback gap medians were 30.79-30.87 ms; per-run p95 gaps were 56.97-58.40 ms. Callback gaps include host/controller scheduling and are not direct radio airtime measurements.

The Pi controller returned LE feature bytes [31, 0, 0, 0, 0, 0, 0, 0] from HCI opcode 0x2003: its Data Length Extension and LE 2M PHY feature bits were both zero. HCI LE Read Maximum Data Length (0x202f) returned status 0x01, unsupported command. This hardware result establishes the controller's reported capabilities; no DLE support was inferred from its Bluetooth version. A newer controller or phone may behave differently and requires separate measurement.

The monitor collector emitted only numeric connection/update, data-length and PHY fields. Raw monitor captures, addresses, keys, passkeys and contact records were not saved in the shareable evidence.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#native-and-build-evidence","title":"Native and build evidence","text":"

All six cases in test_nimble_companion.py passed. Four real-adapter configurations, including the production default and explicit 7/15/60 ms lab settings, execute a 128-record stream under controller rejection, queue pressure, notification-subscription changes and small-MTU pauses. They verify exact ordered delivery, retry retention, bounded queues, interval boundaries, timer wrap and connection-parameter request gating. The other two cases verify modern/legacy pacing policy and reject unauthorized or out-of-range benchmark overrides. These are native tests, separate from the 92-contact hardware measurements.

All 13 campaign targets compiled and passed their memory admission checks at the measured production source. The 11 Full Companions were V3, T1000-E, RAK4631, T096 FEM on, Mesh Pocket, XIAO nRF52, WisMesh Tag, V4.3 FEM off, T-Deck, XIAO S3 WIO and SenseCAP Indicator. RAK3401 and MeshTower SD repeater targets also passed, with full/reduced OTA packages. All 15 resulting capability manifests were verified. Compilation covers this build set; it does not qualify every board or every Full feature on hardware.

The retained V3 ELF SHA-256 is a05c0cd6d70a8146296d9b6c3edbe3e8602fe05901b4d05759454cf61ba7e902. It matches the passed Full memory manifest, whose application, merged image, capability manifest and license hashes also matched. Disassembly confirms the 7 ms comparisons in isWriteBusy() and checkRecvFrame(), and the authenticated updateConnParams call with interval units 12/24, latency 0, timeout 400 and connection-event length hints 0/0. This establishes the linked code; the monitor evidence above establishes the parameters accepted in this run.

"},{"location":"test-results/companion_ble_refresh_2026-10-19/#retained-safe-evidence","title":"Retained safe evidence","text":"

These artifact names and SHA-256 hashes identify the evidence used for this page. The page contains no private device identifiers or raw contact records.

Artifact SHA-256 v3-equivalent-warmboot.safe.json 6d816656a4ff5d2dc22f2175026078bb6b487445acbcc51a70fb3cd51cf87838 v3-60ms-92-after-warmboot-retry.safe.json 41a06fdb63cb63c08d63ca09be78c12e5997bec7fccc2a4a164acda56fd0fabf v3-production-fast-92.safe.json d0633f82862322d48194ccdb99a030a600365f1ecd3da729f32614aa80beb510 v3-production-fast-link.safe.jsonl fdac4c5d0ed73d2c1e3d1e37c2ef02ecc5831d7cb791099da1e3c97493825e64 pi-hci-capabilities.safe.json 7c103253e305b2345dfcbb9170faad4b67c91df5a475536798e1bf48d60b7980 v3-linked-ble-proof.safe.json cc04c5b0547a9911b9fb3a41fa33140c4647565b27d32c4b51c3d266c6ef7fbb v3-owned-cleanup.safe.json c1c7e59c30bf0c2e8bd3097007a2ddb457b5272d6c4b598564d8ac65059ef2c4 BUILD-RESULTS.json 03eb95513c6111804b59e6175892929bd4346be92c9b8942bed925442114cb7f"},{"location":"test-results/companion_contact_cache_results/","title":"Companion contact cache results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_contact_cache_results/#contact-cache-and-nimble-ram-qualification","title":"Contact-cache and NimBLE RAM qualification","text":"

Test date: 2026-09-08. Firmware source: b68aa1c6.

All six constrained ESP32 Full Companion trials pass the linked RAM budget at 350 contacts, 40 channels and 256 normal offline frames. Their final capability manifests confirm compiled Bluetooth/mOTA support and valid WiFi OTA layouts. These are the optional environments in the contact-cache guide. Ordinary release recipes retain their established contact limits pending qualification on those boards.

"},{"location":"test-results/companion_contact_cache_results/#linked-internal-ram","title":"Linked internal RAM","text":"

Every column below uses 350 contacts, 40 channels and 256 normal offline frames. Values are bytes available for runtime allocations after linking. The final margin subtracts the existing conservative runtime reservation; it is not a measurement of live free heap.

Hardware Bluedroid NimBLE NimBLE + mOTA queue loan Plus contact caches Final margin Heltec V3 143,776 158,896 170,152 196,168 23,112 Wireless Tracker 148,432 163,536 174,792 200,824 30,838 Tracker V2, FEM on 133,064 148,168 159,416 185,456 15,470 Heltec CT62 99,040 114,096 125,312 151,344 15,152 XIAO ESP32-C3 98,816 113,888 125,120 151,152 14,960 Generic ESP-NOW 99,776 114,800 126,032 152,064 15,872

NimBLE recovers about 14.7 KiB. Contact caches add about 25.4 KiB on top of NimBLE and the shared mOTA workspace. NimBLE alone fails the unchanged budget at this capacity on all six; adding the queue loan passes only on the original Wireless Tracker. Adding the contact caches passes all six, with 14.6-30.1 KiB of margin. The same Bluetooth runtime allowance is used in every comparison.

The shared mOTA queue stays at 256 frames normally and retains 128 while mOTA borrows its workspace, then returns to 256. A queue above 128 must be synchronized before that workspace can be borrowed; unread frames are not discarded to start mOTA. PSRAM-backed queues retain their existing policy.

The baseline builds use b041fc7f; the final builds use b68aa1c6, with the same ESP32 Arduino 2.0.17 toolchain and NimBLE-Arduino 2.5.1 where applicable.

The CT62, XIAO C3 and Generic ESP-NOW applications fit two 1.5 MiB OTA slots on 4 MiB flash. Their explicit trial layouts retain 896 KiB SPIFFS and the existing SPIFFS/coredump addresses. Install the merged image over USB when changing from the ordinary single-slot Full layout. The other three trials also retain verified dual WiFi OTA slots. All six remain mOTA senders.

Additional builds check the platform and default-policy branches:

Build Available bytes Reserved runtime bytes Margin bytes Policy T096 Full, FEM on 100,076 73,728 26,348 nRF52 path/secret caches RAK3401 Full 99,900 50,976 48,924 nRF52 path/secret caches Wireless Paper Full 187,360 152,576 34,784 ESP32 caches, existing Bluedroid V4 Full NimBLE 263,944 173,056 90,888 Complete contacts in PSRAM XIAO nRF52 legacy BLE Companion 84,368 38,688 45,680 Existing inline contact storage

The V4 test-only forced-cache build also passes its RAM budget. The V4 normally keeps complete contacts in PSRAM; forcing caches adds internal RAM overhead. The six-board savings above apply to boards without PSRAM.

"},{"location":"test-results/companion_contact_cache_results/#physical-checks","title":"Physical checks","text":"

The VM V4 used the test-only cache override. USB tests verified all 350 contacts and all 64 saved path bytes per contact, cold individual lookups, replacement of the complete path set, and persistence after reboot. A secret workload contacted 21 distinct synthetic peers, then the first peer twice: 22 calculations, one RAM hit, and no secret-file access under the ESP32 policy.

A sustained send test exposed a false receive watchdog recovery. RX could occur inside the main loop between two successful transmissions, while the watchdog observed TX at both loop boundaries. It counted from the start of the whole burst and could reset a working radio during a later CAD pause. 468cc3cf restarts the receive recovery allowance after each successful TX. The native regression fails before that change and passes afterward; it also checks that a stuck receiver still gets recovered.

The corrected V4 passed the same 23-datagram workload with zero radio error flags. It also passed a complete 350-path replacement, another send workload and a post-reboot path check with zero radio error flags. The replacement took about 127 seconds: dirty-cache eviction still performs synchronous SPIFFS contact-file saves, so bulk path updates remain a latency workload to qualify on the other ESP32 boards. After that run it reported 133,828 bytes free internal heap, a minimum of 130,976 bytes, and a largest free block of 122,868 bytes. These readings belong to the forced-cache V4 workload, not the six other boards.

After qualification, the VM V4 was erased to remove synthetic contacts and installed with the normal PSRAM-backed V4 NimBLE trial from b68aa1c6. It is named NimBLE-V4-VM, uses the PIN shown on its display, and has powersaving off for USB use. That clean setup reported 143,612 bytes of free internal heap and passed 201 local USB requests with zero radio error flags. The Mercer V4 and XIAO S3 were left on their previously qualified NimBLE builds.

The Mercer RAK3401 Full Companion verified 350 paths, eviction and persistence. Its first secret workload produced 21 calculations, one flash hit and one RAM hit with no skipped saves. After reboot, the same workload produced 22 flash hits, one RAM hit and zero calculations. Both completed with zero radio error flags. The final b68aa1c6 RAK image repeated the 350-path and saved-secret checks with zero radio errors and zero calculations. The RAK was returned to the published 26303793 repeater application with its original repeater name and USA Cascade settings; the Pi's MQTT logger was restarted after the exclusive USB tests.

"},{"location":"test-results/companion_contact_cache_results/#shared-secret-timing","title":"Shared-secret timing","text":"Hardware/filesystem Successful saved-secret lookup Key exchange Selected miss policy V4 / SPIFFS 132,427 us 21,932 us Recalculate RAK3401 / LittleFS 7,812-8,789 us 31,250 us Read saved entry, calculate on miss

The SPIFFS timing comes from the earlier forced flash-cache experiment. The final ESP32 policy does no secret-file I/O. Both platforms reuse the 16 most recent secrets from RAM. These are elapsed-time measurements, not energy measurements; timing on other ESP32/nRF52 boards remains to be measured.

"},{"location":"test-results/companion_contact_cache_results/#regression-checks-and-evidence","title":"Regression checks and evidence","text":"

The contact-cache suite exercises the production cache and persistence code under address/undefined-behavior sanitizers, with the real Ed25519 key-exchange implementation. Its cases include 350 paths, identity changes, snapshots, legacy migration, storage faults, handle exhaustion and packet release when a cold path cannot be read. The related contract suites passed 62 checks. The native queue/store suites passed 27 cases, the radio-liveness suite passed eight, and the partition-selection suite passed 11.

Hardware reproduction commands are in the contact-cache guide. The tracked harness is tools/hil/contact_cache_serial_stress.py. It requires exclusive access to the specified Companion data port.

Local evidence in the qualification workspace:

Each final RAM report is checked against its ELF hash. All 12 final builds pass their RAM and OTA capability checks. Physical testing here covers the V4 and RAK3401; the six constrained ESP32 boards still need pairing, WiFi/BLE/LoRa coexistence and sustained-load testing on their own hardware. No GitHub release assets or firmware-picker entries were replaced by these experimental builds.

"},{"location":"test-results/companion_lost_reply/","title":"Companion lost reply","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_lost_reply/#companion-lost-status-response-validation","title":"Companion lost-status response validation","text":"

Validated on 2026-10-10. These are production-path host tests, browser tests and firmware builds. No lab radio was flashed for this feature; this report does not qualify a sleeping tracker client or a live LoRa exchange.

"},{"location":"test-results/companion_lost_reply/#automated-checks","title":"Automated checks","text":"

The responder test executes the actual private receive callback, message composition/send functions, ACK handling and local CLI policy. Seven compiled variants cover normal and shared one-key DM queues, a text terminal, explicit STM32 enablement, default STM32 exclusion and explicit exclusion elsewhere. AddressSanitizer and UndefinedBehaviorSanitizer checks passed.

Coverage includes exact question matching, Yes/No loop avoidance, known full identities, temporary/unknown contacts, room/channel/CLI exclusions, a full offline inbox, duplicate requests, cooldowns, clock wrap, busy packet/ACK capacity, send failures and preservation of pending user messages/deadlines. Queueing the response preserves the shared send timer. ACK reception retains the existing shared-timer behavior; independent per-message expiry continues to protect pending user deliveries. Excluded builds have no responder state, automatic send, successful setting command, save or WebConfig capability.

Saved preference tests cover all three values, all 256 stored byte values, legacy images loading off, reboot, atomic save failures, recovery copies and runtime adapter preservation. Existing CLI, delivery, inbox, notifications and preference regression suites passed after updating adapters for the new append-only preference byte.

WebConfig tests execute the actual JSON and settings handlers with ArduinoJson. Chromium exercises Off -> No -> Yes -> Off, Companion-only visibility and the absence of the control on infrastructure roles. Existing browser, authorization, secret masking and generated asset checks passed. Both new suites are included in the unit-test workflow.

"},{"location":"test-results/companion_lost_reply/#capacity-and-reliability","title":"Capacity and reliability","text":"

The responder cache is 184 bytes on ARM and retains four complete public keys. The saved preference uses an existing padding byte in the tested ARM preference object and one new byte in its explicit storage image. No heap allocation is introduced by the responder.

Compact STM32 Companion images exceeded their existing application boundary when the responder was included. Those default to MESH_ENABLE_LOST_REPLY=0, preserving their existing features and filesystem layout. Explicit opt-in is tested synthetically and remains subject to the normal capacity checks.

The ordinary RAK WisMesh Tag BLE queue uses 240 frames, retaining 350 contacts and 40 channels and saving 2,832 bytes. Source comparison showed that its prior 256-frame recipe was already below the current runtime RAM allowance before the new 184-byte responder cache. Full retains its 256-frame shared queue. Sanitized tests exercise real FIFO wrapping, downloads and held-DM isolation at 240 slots, plus the production Full override.

Queueing an answer suppresses that request even if later delivery fails. A tracker needs a fresh timestamp for a later poll, must stay awake for the private answer, and must authenticate its configured Companion. Plain Yes/No answers do not contain a poll ID, so delayed replies cannot be precisely correlated with a newer poll. An unanswered poll means unknown.

"},{"location":"test-results/companion_lost_reply/#firmware-build-qualification","title":"Firmware build qualification","text":"

All ten final builds passed flash and internal RAM policy checks. Compact STM32 and both Full targets were also packaged using the normal v1.17.1.9-halo-keymind-cascade-dev label. Ordinary targets used their normal PlatformIO recipes. Full nRF52 UF2 boundaries and applicable Bluetooth DFU handoff checks passed. These are local qualification artifacts, not new published release assets.

Companion target Responder Flash used / limit (bytes) Link-time internal RAM available / required (bytes) RAK 3x72 USB Excluded 228,924 / 229,376 24,728 / 17,408 Tiny Relay USB Excluded 228,944 / 229,376 24,728 / 17,408 Wio-E5 Mini USB Excluded 228,888 / 229,376 24,704 / 17,408 Wio-E5 USB Excluded 229,008 / 229,376 24,736 / 17,408 T1000-E Full Included 607,480 / 708,608 87,064 / 44,864 RAK WisMesh Tag Full Included 649,408 / 712,704 82,112 / 44,864 T1000-E USB Included 426,756 / 708,608 48,248 / 38,944 Pico W USB Included 640,180 / 1,568,768 101,712 / 17,408 Heltec V4 BLE Included 1,550,217 / 6,553,600 263,448 / 118,560 RAK WisMesh Tag BLE Included 459,564 / 712,704 46,316 / 44,864

The STM32 historical-size table uses byte entries while retaining a 32-bit input file size; exhaustive tests reject oversized aliases and partial images. Tiny Relay and Wio-E5 use the size option already used by neighboring STM32 recipes. Existing terminal help was combined into fewer writes while retaining its text, order and 256-byte maximum per write. Supported images also show the new lost-reply commands. Contacts, channels and filesystem boundaries are unchanged on those compact boards.

"},{"location":"test-results/companion_qualification_f807_followup_2026-10-19/","title":"Companion qualification f807 followup 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_qualification_f807_followup_2026-10-19/#mercerwood-companion-diagnosis-follow-up-2026-10-19","title":"Mercerwood Companion diagnosis follow-up, 2026-10-19","text":"

Overall status: FAIL, incomplete. Across the selected measured sources, 1/2 Companions PASS, 1 FAIL. RAK 4631 passed USB, BLE and two-way LoRa at f807. T1000-E passed normal Full USB and BLE on battery, wall power and suspended Pi USB, but active Pi USB reproduced its BLE failure. It remains FAIL pending resolution of that operating condition. XIAO nRF52 remains a carried-forward failure.

Both selected boards first returned to Full f80751354ffc530461e87109ced4f575d3950a52. T1000 subsequently received normal Full 42f7df082fc4c7362ecd5c18c8644ab25b51b9e9, version v1.18.0.1-halo-keymind-cascade-dev-42f7df08, after the corrected bootloader. Its ZIP SHA-256 is 7fac141e40183ada315f70b296583eaf854fd819c76fbd896a1f0f2e4a405e5b. Checks ran on October 20 UTC across October 19-20 local time. Normal Full 72a74c784c38af72518bec7517e691ce9abccaa3 is now restored, with unchanged identity, zero core errors and MCU auto/DCDC. Its restoration smoke passed three USB opens/closes and 19 responses with clean framing. This is not new BLE, RF or full feature qualification; the earlier Full 42 results retain their original source binding.

Companion Source USB BLE LoRa Overall RAK 4631 f8075135 PASS PASS PASS PASS Seeed T1000-E 42f7df08 PASS battery/wall power/suspended USB PASS; active Pi USB FAIL not requalified; f807 passed FAIL Seeed XIAO nRF52 prior failure retained FAIL not qualified not qualified FAIL retained

This is a selected-board follow-up, not a 13-radio fleet qualification. Repeaters were outside scope. Earlier mixed-source results remain in the previous follow-up.

Before the bootloader comparison, each f807 USB run passed 100 query cycles, 10 reopens, radio2 checks, owned-contact CRUD, persistence through warm reboot and contact cleanup. Each closed 18/18 ports with clean final core flags and zero stable binary prefix discards or invalid lengths. ASCII setup and intentional reboot recovery remain separate transition evidence.

RAK BLE passed three 30-second sessions with USB-bound identity, board, full version, NUS services and getters verified. Uptime advanced, flags stayed clean and each session disconnected. LoRa passed an exact zero-hop DEV datagram in each direction between RAK and T1000, with packet-counter checks, owned-channel cleanup and restored radio leases. These finite checks do not establish phone interoperability, range or text delivery.

T1000's original f807 BLE run failed current-advertisement identity binding with zero sessions. A later 90-second Pi follow-up also completed none despite Bluetooth enabled, stealth off and clean USB core flags. The unpaired BlueZ cache retained T1000-BLE-PROBE while USB verified normal f807 and its configured MeshCore name. Cached naming may affect filtering without a current scan response; it proves neither the BLE cause nor a duplicate address or working connection.

Minimal BLE-only, payload/timing, clock-request, transmit-power and LFRC images were diagnostic probes, not Full acceptance. The LFRC probe used an explicit RC configuration and valid vectors; this does not establish a defective crystal or justify changing production clocks. Advertising/HFXO readbacks do not measure RF accuracy or prove over-air packets.

The official upstream companion-v1.18.0 T1000 BLE reference had matching ZIP/UF2 application bytes, base 0x27000 and SoftDevice requirement 0x0123. Its bounded Pi scan did not find the required current advertisement. Phone visibility was insufficiently bound to name/time. This application-only experiment retained the same bootloader/SoftDevice and cannot isolate them. Normal Full was restored. Reference ZIP SHA-256: 79e53c2ce13521ccda5db3621a68f79728d2f241c3a8345e4107b7ae6b5a374d.

Installed S140 7.3.0 readback CRC 733639BD matched the package bytes, also identical to Seeed's recovery package SoftDevice. That checks the covered image, not RF operation. C2 installation preceded a historical secured diagnostic link that never completed full BLE qualification.

A separate DFU defect was corrected in bootloader c0c421184a3cf1ca66b40b055101f20b8c587e26: active-high P0.06 had been configured pull-up/active-low, with RESET/P0.18 treated as another button. The correction adds qualified three-second hold/USB-first recovery while retaining explicit-request latency and other boards' two-button behavior. Eleven actual-function host tests, 13 DFU-entry checks and T1000/T096/T114 cross-builds passed. See candidate PR #1 and button-polarity evidence.

The reviewed combined bootloader transfer returned 0, but its installer stopped when the MSC disk was unavailable. That failed workflow remains failed. Later exact-device public INFO verified model Seeed T1000-E for Meshtastic, Board-ID nRF52840-T1000-E-v1 and version TEST_0x02040C01. Package CRC 6BA4D206 was validated offline; installed CRC was not exposed. Combined ZIP SHA-256: 8db09e6b7f253b1e54aae754220f0d1053d369c07bff299a26b384233de6e234.

Separate f807 application programming and its adapter command both returned 0. An independent USB check passed 15 queries/two reopens, closed 5/5 ports and verified normal f807, the same public identity, clean framing and zero core errors. With the corrected bootloader and same f807, an explicit-address 90-second BLE attempt still failed advertisement binding with zero sessions. No BLE fix or bootloader cause is established.

Final normal Full 42f7df08 installation succeeded. Its fresh USB run passed 100 queries, 10 reopens, owned-contact CRUD/cleanup, warm reboot and radio2 checks, with 18/18 ports closed, zero stable framing rejects and clean core flags. Its normal 25-second BLE discovery still failed with zero sessions. LoRa was not requalified at 42f7df08; the earlier f807 RF pass remains source-bound.

Combined serial delivery avoided legacy UF2 scratch 0xE0000..0xEA000, which overlaps internal ExtraFS. Its staging and f807 restore remain below ExtraFS. No filesystem-preservation claim replaces runtime checks.

A subsequent 90-second scan by the independently verified Bluetooth address also failed to reach a connection on normal Full 42f7df08.

After the user reported a reset, USB was present at preflight. Fresh USB passed 15 queries/two reopens on the same normal Full image. A 90-second Pi scan completed zero BLE sessions and captured zero target advertisements among 643 advertising event blocks. A separate 30-second passive scan captured one target advertisement among 3,375 blocks; direct public-service discovery timed out. A temporary saved random address produced two primary advertisements but no usable connection. Factory-default addressing was restored and verified over USB; Bluetooth remained enabled with clean core flags. No host bonds were removed. These are post-reported-reset observations, not a verified cold or battery-only test.

Fresh normal RAK and T096 USB/BLE controls passed on the Pi. An independent RAK nRF52 central diagnostic, version v4.1, then passed a single public ATT/NUS metadata comparison against the USB-bound T096. It read the expected public GAP name, recorded no security activity, and disconnected cleanly. The unchanged T1000 Full image then supplied a current primary advertisement at -63 dBm, but its one attempted connection ended with 0x3E before any NUS metadata or public read succeeded. The observer's uptime and watchdog counters advanced, with no late connections or security events; scanning and connections stopped cleanly. A central connected event does not prove the peer received its connection request; Nordic describes this initial-exchange failure.

This comparison strengthens a T1000-specific link finding across two central implementations. It does not identify a faulty component or establish a bootloader cause. That comparison did not reach pairing or password exchange. The temporary central is not Companion qualification. Its initial v4 scan-unit error was corrected before either trial; live v4.1 status verified a 3,000-unit, 30-second scan configuration. The RAK was restored to the exact normal Full f807 ZIP and passed fresh 15-query/two-reopen USB and one 10-second BLE session with the same public identity. T1000 remains FAIL. Its pin assignments match Seeed's T1000-E hardware list; no further pin change is justified by this evidence.

A later controlled Bluetooth off/on test kept USB management open and had the Pi client scanning before enable. Unlike the earlier fixed-duration discovery, this client attempted pairing 49 ms after its first current target advertisement, which arrived 84.331 seconds after the on request. This time the trace proves bidirectional SMP and ATT exchanges and a passkey request. The supplied PIN matched the active USB-reported PIN and the user's expected PIN. The link then ended with HCI Connection Timeout 0x08; BlueZ reported Authentication Canceled. No completed bond or Companion session, explicit PIN rejection, or stable-link pass is established. The parent evidence exporter subsequently failed to parse a traceback as JSON; that workflow failure remains recorded, separate from the independently analyzed radio trace. A further exact-address ATT connection was requested while Bluetooth was off, with its kernel scan already pending before enable. It failed initial establishment with 0x3E and read no NUS service. Bluetooth was restored on, with the same factory-address policy, stealth off, monotonic uptime and clean core flags. Firmware and host bonds were unchanged. These tests refine the failure to an unstable link that can sometimes reach pairing; T1000 remains FAIL.

A subsequent application-only isolation test installed two small public-NUS echo diagnostics built against source context 56b4d72e6. They omit Companion, LoRa, GNSS-driver and sensor processing, retain the board/framework/SoftDevice/bootloader, and feed every active watchdog channel. Both builds passed memory/vector/package checks and exact-device serial DFU/runtime verification.

Diagnostic Observation Result Minimal external LF crystal, 20 ppm No target advertisement in 60 seconds; uptime advanced 62.215 seconds with watchdog feeds advancing No connection or echo Same minimal app, internal LF RC, 250 ppm One target advertisement after 42.138 seconds at -60 dBm; connection attempt timed out; uptime advanced 59.209 seconds with watchdog feeds advancing No connection or echo Normal Full with four-second initial supervision timeout from RAK central v4.2 Current target advertisement at -62 dBm; initial connection establishment failed with 0x3E before public GATT reads or security activity FAIL T096 control with the same central parameters Public NUS service/characteristics and expected GAP name read successfully Diagnostic control PASS

The controller reported the configured 48.75 ms interval, zero latency and four-second initial timeout for both central trials. A central connected callback does not prove T1000 receipt of its connection request; the failed initial exchange does not test pairing on an established four-second link. The Pi's earlier attempt to extend a live connection saw no target advertisement in 90 seconds, so no update was applied. A host-only HCI-filter padding error was corrected before that valid trial and remains recorded separately.

These results make it less likely that the Companion workload or the external low-frequency crystal alone explains the failure. They do not distinguish common framework/SoftDevice behavior, the high-frequency radio clock, power or RF hardware. Both minimal images were removed. Normal T1000 Full 42f7df08 was restored and passed 15 USB query cycles/two reopens with the same public identity. The test RAK returned to its exact normal Full f807 image and passed 15 USB query cycles/two reopens and one 10-second secure BLE Companion session with the same identity. The four-second comparison had T1000 USB present before and after; it was not a battery-only test.

Physical battery-only testing subsequently passed on the unchanged normal Full image. The user confirmed unplugging USB, and the Pi verified the exact T1000 USB identity remained absent throughout each run. The first run completed fresh PIN pairing, authenticated bonding, encryption and a 30-second Companion session. Three subsequent one-minute sessions passed normal discovery by name and saved-bond reconnects. All four verified the same USB-bound identity, complete firmware version and Companion getters, negotiated MTU 247, advanced uptime with zero core error flags, and disconnected cleanly. The optional four-second timeout helper aborted at its live-handle guard before sending any command; it did not cause this success. The first connection negotiated normally from 420 ms to two-second supervision, and the reconnects used two-second supervision.

The user then reconnected USB after being asked to keep the radio in place without resetting it. The exact runtime endpoint remained present throughout both comparison trials, which opened no USB serial port, changed no firmware/preferences and retained the same bond.

USB-connected comparison Observation Result Normal name discovery, 25 seconds Zero target advertisements among 202 advertising event blocks Zero Companion sessions Immediate connection by independently verified address Ten target advertising event blocks, no target name scan response; one BlueZ Connect operation produced nine controller attempts, seven ending with 0x3E and two with 0x08; no successful encryption Zero Companion sessions Read-only USB check after BLE trials Same normal Full version and public identity, uptime 2,903 seconds without a decrease from the battery test, Bluetooth on, stealth off and zero core error flags Runtime check PASS

The USB comparison used the same normal two-second supervision parameters that worked on battery. After the user unplugged the hub end, with instructions to leave the magnetic end attached and the radio in place, three further 30-second sessions passed normal name discovery, encryption and Companion queries. Uptime advanced from the USB check, all sessions disconnected cleanly, and the Pi verified USB absence throughout. That makes seven successful battery sessions in total, with no firmware change, reset request or bond removal during this comparison.

The user then connected the same cable to wall power only. Three further 30-second sessions passed normal name discovery, saved-bond encryption, the same Companion identity/version/getters and clean disconnects. Pi USB remained absent throughout. Charging from that supply alone did not reproduce the failure. Supply voltage was not instrument-measured.

The battery/Pi USB/battery/wall-power sequence associates the failure with the Pi/hub connection; it does not identify cable/ground/RF effects, USB software activity or a particular faulty component. Position and cable attachment were requested but not independently measured. The MQTT gateway remained active before and after the comparison trials.

On the next Pi reconnection, a guarded Linux USB device authorization test deconfigured only the exact T1000, then restored its configuration. The physical cable remained connected as requested; no hub or port-power command was sent. Before and after deauthorization, 25-second scans captured no target advertisement. While deauthorized, the Pi captured three target advertising event blocks and briefly encrypted a link, but it ended with 0x08 before any Companion session completed. This did not restore stable BLE. Deconfiguration does not physically disconnect USB or establish that bus signalling and the peripheral USB block have stopped.

USB authorization returned to its original state. The first USB runtime read failed before opening a port because the recreated device node was not yet accessible to the normal user; this failed workflow is retained separately from the BLE results. After normal user access returned without a permission change, a separate readback passed on unchanged Full firmware with the same public identity, Bluetooth on, zero core errors and uptime 3,987 seconds. The gateway remained active.

The user moved the T1000 to another powered-hub port. The Pi verified the same USB serial identity at a different physical USB path, continuously present during both BLE trials. Normal 25-second discovery captured zero target advertisements among 358 advertising event blocks. A 90-second address-based scan captured zero among 580 and timed out before a connection attempt. Moving ports did not restore BLE. A read-only USB check between these trials passed seven queries with the same firmware/identity, Bluetooth enabled, zero core errors and uptime 4,635 seconds.

A guarded per-device USB runtime suspend comparison then changed only this T1000's idle policy. Linux reported the device suspended while its hub port remained active with port power-off prohibited. Authorization, configuration and device number stayed unchanged. No serial port was opened during either BLE phase, and no firmware, bond or reset change was requested.

USB runtime state BLE observation Result Suspended, cable still connected as requested Three normal-name, saved-bond sessions; same Full identity/version/getters, MTU 247, advancing uptime and clean core flags; three encrypted links and clean local disconnects 3/3 thirty-second Companion sessions PASS Resumed to original active policy Three target advertising event blocks; two controller attempts, one ending with 0x3E, the other briefly encrypted before 0x08 Zero completed Companion sessions

The original USB policy and idle delay were restored and verified, and the gateway remained active. This comparison makes active USB operation a stronger discriminator than charging alone or the individual hub socket. It does not distinguish USB software/peripheral behavior from electrical or RF effects of active signalling, and no waveform or supply-voltage measurement was made. Firmware remains unchanged.

Three subsequent handoff cycles temporarily allowed this device to autosuspend after two seconds idle. Each serial open woke USB and passed seven read-only queries. After serial close, Linux reported USB suspended, and a normal-name, saved-bond 30-second Companion session passed with the same identity/version, MTU 247 and clean core flags. All three cycles passed. Together with the direct suspend trial, six secure sessions passed with USB suspended.

The original active USB policy was restored again. A final seven-query USB readback passed, with uptime 5,259 seconds, Bluetooth on, unchanged identity/firmware and zero core errors. No persistent host configuration was installed. Automatic idle suspend is a tested candidate workaround for sequential USB/BLE use; it does not qualify simultaneous active-USB/BLE operation or establish a production firmware fix. A second computer with USB data active is the next comparison needed to separate the Pi/hub path from behavior common to the T1000's USB connection.

Further firmware isolation did not establish a reliable simultaneous active-USB/BLE fix. All native SDK runs below are public DIS/NUS nonce-echo diagnostics, not secure Companion qualification; they add zero qualified secure sessions. Results remain bound to their runtime builds and safe evidence digests.

Probe Source or build Completed observation Assessment MCU LDO, active USB Normal Full 42f7df08 Zero of three BLE sessions; MCU auto/DCDC restored, same identity and clean core flags FAIL Minimal USB IRQ 2/6/2 T1000-USBIRQ-v1 Zero echoes in each corrected trial; IRQ count-rate upper bounds 0.866/0.744/0.728 Hz including probe traffic No reliable fix; interrupt durations not validated Explicit HFCLK claim T1000-USBIRQ-v2 Ten echoes, then GATT protocol failure FAIL Modified Full clock prototype Base 72a74c78 Zero of three Companion sessions; current-advertisement identity gate failed FAIL; not retained as a production fix Active USB / suspended USB / active USB Native SDK v1 Zero echoes / three sessions with 90 nonce echoes / zero echoes Only suspended phase PASS Advertising channels 37/38/39 Native SDK v2 Target discovered on each channel; zero echoes in every trial No reliable fix Default/constant-latency power; LF RC/SYNTH Native SDK v3/v4/v5 Zero echoes in all four trials No reliable fix PHY and link policies Native SDK v6 Two isolated 30-echo harness passes, including an explicit 1M PHY request and default policy with longer connect allowance; both matching HCI traces recorded LL Response Timeout 0x22. Repeat and other policy trials failed No clean, repeatable fix TX +8 dBm and regulator selection Native SDK v7 Baseline and TX8/LDO: zero echoes. TX8/DCDC: one 30-echo PASS with clean HCI local-host close, then repeat FAIL after two echoes; ten-second echo-deadline retry FAIL after nine echoes with HCI/peripheral timeout 0x08 Intermittent success; not a solution CPU sleep versus busy idle Native SDK v8 Sleep: zero echoes and HCI timeout 0x08. Busy: one 30-echo PASS with HCI local-host close; busy repeat: FAIL after twelve echoes with HCI/peripheral timeout 0x08 despite a ten-second echo deadline Intermittent success; not a reliable fix Short actual connection interval, TX8/DCDC/sleep idle Native SDK v9 Channel37: 30 echoes and clean local close after update to 7.5 ms. All advertising channels: three sessions/90 echoes at actual 7.5 ms, but HCI recorded two teardown completions 0x22 about four seconds after host Disconnect commands, two failed-establishment attempts 0x3E and one local close. USB queries remained functional Diagnostic candidate; not three clean reconnect passes Same-image slow-interval control Native SDK v9 Actual interval updated to 75 ms before service-discovery failure; zero echoes. HCI: four initial failures 0x3E and one 0x08; final SDK readback still connected FAIL; controller/SDK observations retained separately TX 0 dBm recovery with fast interval Native SDK v9 Three payload sessions/90 echoes, but strict lifecycle: eight initial failures 0x3E, one 0x08, two 0x22 and one local close Strict lifecycle FAIL Quiet GATT initiation with fast interval Native SDK v10 Three payload sessions/90 echoes; data_exchange_passed=true, passed=false. Strict lifecycle: five initial failures 0x3E, two 0x22 and one local close Strict lifecycle FAIL; no reliable fix

A final native SDK v11 active/suspended/active comparison retained the same image and RAM phone-interval policy [12,12,0,400] (15 ms, zero latency, four-second supervision), TX +8 dBm, MCU REG1 DCDC, sleep idle and low-power mode, with advancing uptime and no reset observed. HCI verified actual 15 ms operation for all 270 nonce notifications. This is a connection-interval diagnostic, not phone interoperability testing.

V11 USB condition Nonce echoes Strict HCI lifecycle Median echo RTT Active, with application USB serial queries 90 across three payload sessions FAIL: two initial failures 0x3E, two 0x22, one local close 0x16 452.8 ms Runtime suspended, same image/policy 90 across three sessions PASS: three clean local closes 0x16 33.7 ms Active, no application USB serial I/O during BLE 90 across three payload sessions FAIL: two initial failures 0x3E, one 0x22, two local closes 0x16 394.0 ms

The active phase recorded 344 USB telemetry query replies, including 154 link replies reporting BLE connected at actual 15 ms. The suspended and active-idle phases performed no application USB serial I/O during BLE; configuration readbacks bracketed those trials. Active-idle failure strengthens active USB as the discriminator even without application serial payloads. It does not establish an electrical, RF, peripheral or host implementation cause. The exact harness source 19703941... is preserved and digest-matched for all three runs.

Across the v9 runs, 168 USB link-query replies explicitly reported BLE connected at actual parameters [6,6,0,400] (7.5 ms interval, zero latency, four-second supervision): 41 replies over 39.576 seconds in the first run and 127 across the three repeat-session windows. The complete runs recorded 196 and 334 USB rssi/link query replies respectively, including discovery and disconnect periods. Linux samples reported active USB throughout, with configuration retained before and after. These are concurrent diagnostic command-response checks, not Full USB or secure Companion acceptance. The 0x22 events occurred after explicit host Disconnect commands; payload completion and clean teardown remain separate results. The same-image slow control subsequently observed actual 75 ms before failing with zero echoes; production verification remains pending.

Controller and SDK counters remain distinct. TX0 recorded twelve controller connection-complete events versus SDK connection/disconnect deltas 5/5; quiet v10 recorded eight versus SDK deltas 3/3. No cause is assigned to those differences. The exact original v9 and slow-control harness snapshots were recovered with matching SHA-256 values 83be8cf2... and 3295d776..., and retained as reproducibility evidence. The completed v11 comparison is reported above.

The two-echo repeat ended in a harness timeout without a captured HCI disconnect, so that failure alone does not prove link loss. The separate nine-echo trial did record link timeout despite its longer deadline. Early missing-characteristic and D-Bus failures remain host-workflow evidence and are excluded from clean BLE comparisons. T1000 remains FAIL for simultaneous active USB and reliable BLE. Normal Full 72a74c78 was subsequently restored from unmodified source using application-only ZIP SHA-256 ba092c8e29b5b752a3d1667a0980910d4bec5815dfd0f07277b2ba3b8ce4e25a. Restoration and two additional USB opens returned 19 responses across three ports, all closed, with unchanged identity, advancing post-restoration uptime, zero framing rejects/core errors and MCU auto/DCDC with zero recorded crashes. The gateway remained active. These are restoration and USB runtime smoke results; historical Full 42 qualification remains source-bound, and BLE/RF were not requalified at Full 72.

Cold boot, battery-only restart, complete phone/feature testing and fleet qualification remain untested. Unplugging USB alone does not prove a restart. Safe JSON retains source bindings, workflow distinctions and evidence digests without device secrets or raw addresses.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/companion_qualification_followup_2026-10-19/","title":"Companion qualification followup 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_qualification_followup_2026-10-19/#mercerwood-companion-follow-up-2026-10-19","title":"Mercerwood Companion follow-up, 2026-10-19","text":"

Across the retained baseline and selected normal HEAD tests, 9/11 Companions PASS and 2 FAIL. Overall campaign status: FAIL, incomplete. A nonworking required axis counts as a board failure.

Baseline: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6. Selected normal HEAD refresh: 51538f0379076111c14d50b29618c5792e0490dc (v1.18.0.1-halo-keymind-cascade-dev-51538f03). This report covers 11 Companions, with no new fleet-wide inventory or repeater qualification claim.

The branch advanced to 811b2d7232aff86fd13075cee9ff6ac6471d2b6c during the campaign. Its four subsequent radio changes are outside the measured sources and have not been hardware-qualified by this report.

Later baseline RF tests closed the V4.3 and XIAO ESP32-S3 gaps: each exchanged an exact zero-hop DEV datagram in both directions with RAK 4631, packet counters advanced, owned channels were cleared and radio leases restored. A bounded transition drain preceded the unchanged strict handshake; it was not itself a qualification pass. Earlier setup failures remain in the record.

The retained 4e campaign now has 9/11 Companions passing all three required axes: 10 USB passes, 9 BLE passes and 10 RF boards through 9 two-way links. T1000-E BLE and XIAO nRF52 remain failures.

Companion Qualification source USB BLE RF Overall Heltec V4.3 51538f03 PASS PASS PASS PASS LilyGo T-Deck 4e4f27a2 retained PASS PASS PASS PASS RAK WisMesh Tag 4e4f27a2 retained PASS PASS PASS PASS Heltec MeshPocket 4e4f27a2 retained PASS PASS PASS PASS SenseCAP Indicator 4e4f27a2 retained PASS PASS PASS PASS RAK 4631 51538f03 PASS PASS PASS PASS Heltec V3 4e4f27a2 retained PASS PASS PASS PASS Seeed XIAO ESP32-S3 51538f03 PASS PASS PASS PASS Seeed XIAO nRF52 4e attempted; runtime unverified FAIL not runnable not runnable FAIL Heltec T096 4e4f27a2 retained PASS PASS PASS PASS Seeed T1000-E 51538f03 PASS FAIL PASS FAIL

The six retained baseline boards have not been requalified at the new source. Of the four refreshed boards, three passed every required axis; T1000-E failed BLE. Selected normal HEAD USB passed 100 query cycles and 10 reopen checks on V4.3, XIAO ESP32-S3, RAK 4631 and T1000-E. Each also passed owned-contact cleanup, warm reboot and radio2 checks, with 18/18 ports closed, zero stable binary framing rejects and clean final core flags. ASCII-to-binary setup and deliberate reboot transitions are recorded separately.

HEAD RF passed all three RAK hub links (V4.3, XIAO ESP32-S3 and T1000-E), one exact DEV datagram in each direction, with clean channel/lease cleanup. RAK also passed three 30-second normal BLE sessions, matched its USB identity/full version and disconnected cleanly.

V4.3 and XIAO ESP32-S3 initially failed normal HEAD BLE. Both traces show a successful exact-target connection followed by HCI encryption status 0x06, PIN or Key Missing, consistent with obsolete host keys after full ESP erase. A separately gated repair removed one proven target host bond each; it changed no other bond, reset no adapter and did not qualify BLE. V4.3 subsequently passed all three normal BLE sessions with USB identity/full-version matches, clean core flags and final disconnects. XIAO ESP32-S3 also subsequently passed all three normal BLE sessions with the same identity, version, core-state and disconnect checks.

The original V4.3, XIAO ESP32-S3 and RAK flasher commands returned 1 and remain failed workflow records. The ESP logs contain successful image-hash verification before native USB reset waits expired. Later exact-source clean USB qualification is separate evidence of working application runtime; it does not rewrite those return codes.

T1000-E BLE remains FAIL. Two normal HEAD discovery attempts, an MCU LDO trial, restored auto/DC-DC policy, a HEAD diagnostic image and the older cdacf670 reference all failed to produce the required current matching advertisement. The older Page Timeout pairing failure remains historical evidence rather than the reason for these discovery failures. No direct-link failure is counted where no connection was attempted.

The diagnostic firmware reported SoftDevice/interface readiness and advertising activity, with advancing loop timestamps, but those readbacks do not prove an over-air advertisement. The regulator comparison establishes no DC-DC cause. Normal HEAD was restored after the older-reference test; fresh USB evidence matched the same public identity, normal full version and original MCU/radio auto policy (both active DC-DC). Diagnostic collection does not count as normal BLE qualification.

A final read-only check at 2026-10-20 02:13:35 UTC confirmed normal HEAD on T1000-E, Bluetooth enabled, clean core flags and the original regulator policies. The active MQTT gateway still owned its original RAK 3401 primary USB interface; that port was not opened by the check.

C2 bootloader installation was verified at 2026-10-19 19:20:57 UTC. The same T1000-E old-reference trace at 20:04:22 UTC subsequently recorded one secured connection, with clean core flags; that trace did not qualify the full BLE test. The same older reference now fails advertisement discovery, so the failure is not isolated to the latest application build. All eight required C2 installation baselines remain separate from application qualification. Package manifest CRC validation is not an installed-flash CRC readback.

XIAO nRF52 is FAIL. The normal application transfer succeeded, but normal USB/runtime never qualified; BLE and RF cannot currently run. Its latest passive candidate-port capture returned protocol error -71 and zero bytes for all 24 device-descriptor reads and all eight address-setting attempts. No configuration or CDC request occurred, and no original UID or firmware mode was verified in that capture. This establishes failure before USB configuration, without assigning a cable, power, host, bootloader or application cause.

The passive observer issued no deliberate reset, serial open or image write. The kernel automatically issued 35 port-reset requests and two port-power off/on request sequences; accepted requests do not measure downstream power. The diagnostic image and staged 40b source hardening were not installed, so neither is claimed as a hardware fix.

These are finite campaign checks. USB covers the specified query/reopen/CRUD/warm-reboot/radio2 checks; BLE covers three 30-second sessions; RF covers zero-hop DEV datagrams and cleanup. They do not establish phone interoperability, cold boot, RF range, text delivery or every firmware feature.

Original histories remain in the earlier campaign report: Tag recovery, V3 discovery retry, Pocket watchdog observations, earlier setup aborts and failed XIAO runtime recovery. No earlier trial or physical-presence history is deleted.

Machine-readable results contain source-bound board records and SHA-256 evidence digests.

The unit-test workflow passed all 12 jobs for source 51538f03: GitHub run.

Later selected-board evidence, the T1000 bootloader button correction and the final Full Companion USB/BLE results are recorded in the diagnosis follow-up. The T1000 BLE and XIAO nRF52 failures remain open.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/companion_usb_ascii_validation/","title":"Companion usb ascii validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_usb_ascii_validation/#usb-companion-ascii-default-validation","title":"USB Companion ASCII default validation","text":"

The updated 1.17.1.6 Companion release covers 103 USB-enabled profiles: 52 ESP32 Full, 43 nRF52 Full, four RP2040 USB, and four STM32 USB. The BLE-only WM1110 image has no USB interface and is outside this change.

"},{"location":"test-results/companion_usb_ascii_validation/#behavior-and-cause","title":"Behavior and cause","text":"

Ordinary USB Companions previously started in Binary mode. Full Companions started in ASCII, but host-session cleanup could select or retain Binary mode for the next connection.

All USB Companions now select ASCII before framed dispatch begins. An observable USB session boundary cancels old USB operations and clears partial input, queued protocol output, frame ownership, and mOTA attachment before restoring ASCII. A network terminal can defer that restoration; a new USB binary client that connects during the deferral keeps its chosen mode. BLE and network Companion transports keep their existing binary protocol.

A complete framed app command still selects Binary mode automatically. An incomplete initial probe returns to ASCII after one second. Explicit terminal start/stop controls remain available.

Session detection depends on the hardware. Native USB with DTR can detect a terminal closing. ESP32 hardware USB Serial/JTAG observes bus resets and host loss, but cannot reliably observe a program closing its handle. UART bridges usually cannot observe that close either. Idle time never forces an active binary app back into ASCII; use the start token on a port that remains Binary. See the USB mode guide.

"},{"location":"test-results/companion_usb_ascii_validation/#automated-validation","title":"Automated validation","text":"

The 99 ESP32/nRF52/RP2040 profiles use d4a641ff. The four STM32 profiles additionally use the compatible Arduino 2.12 platform pin and build test in d2c52e4d. Both source revisions passed GitHub checks: main repair and STM32 repair.

All 103 builds passed capability and RAM qualification. Packaging checks bind each application to its capability and memory manifests, retain the original profile capabilities and download formats, and verify ESP32 application/merged image consistency. Replacement publication verifies public SHA-256 digests before deleting old images and checks that unrelated firmware assets retain their original hashes. Release manifests, checksum lists, picker links, and capacity-note source references follow the new binaries.

Publication completed with all 430 uploaded files verified through their public download URLs. The 412 superseded firmware/manifest files were removed. The remaining 1,837 assets kept their original hashes, and all five release checksum lists matched the final GitHub asset digests.

No USB Companion was attached to the local computer for this repair. These results are compilation and automated protocol tests, not physical USB tests across the board matrix.

"},{"location":"test-results/companion_usb_client_validation/","title":"Companion usb client validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/companion_usb_client_validation/#usb-companion-client-compatibility","title":"USB Companion client compatibility","text":"

The RAK4631 report was reproduced on the MercerWoodMesh Pi with the published RAK_4631_companion_radio_full-v1.17.1.6-halo-keymind-cascade-dev-d4a641ff image. The fixes described here are in the source and test firmware; the existing GitHub release assets have not yet been replaced with this fix. Hardware validation was completed on 2026-09-14 with test builds labeled v1.17.1.6-usb-client-test-0e5955f8 (base commit plus these USB fixes).

"},{"location":"test-results/companion_usb_client_validation/#port-selection","title":"Port selection","text":"Hardware Companion/ASCII port USB logging RAK4631 and other nRF52 Full Companions Primary CDC, Linux *-if00 Optional second CDC, *-if02 ESP32 Full Companions Primary USB serial port Shares that port; must be off for Binary Companion

+++MESHCORE-TERM-STOP changes the primary port to Binary Companion. Its OK - Binary mode reply does not move the client to a second port. Close the terminal before opening that same port with meshcli -s PORT infos. Do not use MeshCLI's repeater-mode -r option for a binary Companion connection.

"},{"location":"test-results/companion_usb_client_validation/#two-reproduced-failures","title":"Two reproduced failures","text":"
  1. Stock meshcore-cli 1.6.3 was tested with meshcore 2.3.10, whose serial client deliberately deasserts DTR. The nRF52 primary USB gate and response route both required DTR. A raw battery query returned a valid 14-byte response with DTR high and no bytes with DTR low.
  2. The automatic ASCII banner ended in >. MeshCLI recognized that > as a binary frame marker and consumed the real response's leading > as a length byte. Even forcing DTR high in a diagnostic client did not recover the first APP_START response. Stock MeshCLI exited without node information.

The nRF52 transport now accepts real USB input as evidence of a client, tagged with the current session generation. Close, line-coding, and device-reset events invalidate that evidence; an old reader racing cleanup cannot grant access to a newer session. The write path remains bounded and does not wait for an unread host. CDC1 logging retains its own DTR requirement.

The default ASCII terminal now suppresses its banner and unsolicited terminal events until ASCII input arrives. Enter or a command reveals the terminal. A binary probe receives a clean first response. Explicit START/STOP remains available, and an incomplete binary probe still times out back to ASCII.

On a shared logging port, STOP now reports ERROR: set usb.logging off before Binary mode while logging is enabled. It no longer acknowledges Binary mode and immediately reclaims the port for ASCII logging.

"},{"location":"test-results/companion_usb_client_validation/#hardware-validation","title":"Hardware validation","text":"

The RAK4631 Full test firmware passed with an unmodified installed MeshCLI and its normal DTR-low serial library:

The Heltec V4 Full Companion also passed using its ESP32-S3 USB Serial/JTAG interface, with USB logging disabled:

The shared-port logging policy was exercised on the V4 as well. With logging on, STOP returned the explicit error, a binary probe could not claim the port, and stock MeshCLI could not connect. After set usb.logging off, STOP worked, 10 binary replies were clean, and three stock-client reconnects succeeded.

After testing, the V4's complete original flash snapshot was restored and verified by the flasher. Its original v1.17.1-soak-v4-B firmware answered again, and the previously active memory-soak collector was restarted. This test interrupted that board's uptime; the restored firmware starts a new run. Fresh successful V4 telemetry was verified at 2026-09-14T19:46:01Z. The RAK4631 remains on the patched Full Companion test build with its separate logging interface enabled. The Pi's previously active services were restored.

The nRF52 DTR stress sequence is deliberately not used on ESP32. Changing those control lines during immediate close/reopen triggered the V4's hardware bootloader and produced an ESP boot message. Preselecting RTS before opening did not prevent that transition on this host. ESP32 reconnect results above use the unmodified client's normal connection sequence instead.

The test issues local information queries only. It does not transmit test chat messages over LoRa. Firmware installation and logging configuration are separate from the reusable test runner:

python3 tools/hil/companion_usb_client.py \\\n  --port /dev/serial/by-id/PRIMARY-if00 \\\n  --logging-port /dev/serial/by-id/LOGGING-if02 \\\n  --test-dtr --cycles 10 --output usb-client-results.json\n

Replace the example paths with the device's actual stable paths. Install meshcore-cli in the Python environment used for the test. Start after a fresh reboot so the default-ASCII check has a new session. On ESP32, omit both --logging-port and --test-dtr, and disable USB logging first.

"},{"location":"test-results/companion_usb_client_validation/#automated-checks","title":"Automated checks","text":"

The USB workflow runs the new compiled regression tests on subsequent changes.

"},{"location":"test-results/contact_memory_2026-10-19/","title":"Contact memory 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/contact_memory_2026-10-19/#contact-ram-savings-and-rak4631-full-capacity-2026-10-19","title":"Contact RAM savings and RAK4631 Full capacity, 2026-10-19","text":"

Shared contact indexes save RAM across Companion profiles and terminal chat. RAK4631 Full additionally saves 2,200 static RAM bytes and 488 application bytes, preserving its existing features and SD support. Five canonical packages and a direct terminal build pass. Heltec V3 Full failed its preexisting RAM-admission limit at this revision; the subsequent V3 Full repair is measured separately. No hardware was flashed for this contact-memory campaign.

Baseline source: 31e693ec209109a930dcf8aeef89dacbe9116a63. Final source: aed4c22e51201f0ee3586e26e1007194a74b015d. The machine-readable results bind package, ELF, memory-manifest and linked-check hashes to these revisions.

"},{"location":"test-results/contact_memory_2026-10-19/#shared-contact-savings","title":"Shared contact savings","text":"

BaseChatMesh uses uint16_t sort/match indexes when the complete contact table has at most 65,535 slots; larger configurations keep int. API counts retain their existing types. All Companion profiles and simple_secure_chat share this implementation across nRF52, ESP32, RP2040 and STM32. Infrastructure client/ACL tables use different record types and are outside this contact-table change.

Existing configuration Internal RAM saved by indexes PSRAM saved by indexes Static 350-contact table 764 bytes None PSRAM 350-contact table with internal fallback 96 bytes 716 bytes Static 100-contact table 264 bytes None

These totals include eight anonymous slots and 24 match results. PSRAM retains its existing 24-slot internal fallback and allocation-failure behavior. Cached nRF52 ContactInfo records also shrink from 96 to 92 bytes by moving the runtime-only storage_slot field into an alignment gap; other record layouts retain their sizes. Field-based serialization, contact/channel capacities, features, queues and stack allocations remain unchanged.

"},{"location":"test-results/contact_memory_2026-10-19/#rak4631-full-comparison","title":"RAK4631 Full comparison","text":"Measurement Baseline Final Improvement Actual application BIN extracted from DFU ZIP 712,600 712,112 488 fewer Application-region capacity 712,704 712,704 Unchanged Actual flash margin 104 592 488 more Ordinary static RAM (.data + .bss) 156,284 154,084 2,200 fewer Linked heap capacity 79,092 81,292 2,200 more Modeled heap requirement 74,576 74,576 Unchanged Modeled admission margin 4,516 6,716 2,200 more

.data stays at 1,660 bytes; ordinary .bss falls from 154,624 to 152,424. Separately reserved linker arenas are excluded from this section sum. The measured reduction includes final object alignment. Linked capacities and admission models do not measure running free heap or physical stack high-water.

Four duplicate CRC loops share the existing implementation without changing initial values, byte ranges or stored formats. Reader packing increases offline Zopfli iterations from 15 to 1,000 and selects 16-bit block offsets only when every block start fits. General 32-bit types/fallback, 4,096-byte blocks, scratch policy and all canonical text remain unchanged.

Reader flash data Baseline Final Saving Compressed payload 38,366 38,293 73 bytes Twenty-four block indexes 288 192 96 bytes Payload plus index 38,654 38,485 169 bytes

The complete image saves 488 bytes; Reader payload plus index accounts for 169. The Full marker and thirteen feature-verification entries are unchanged. Existing SD support, its 2,048-byte SD/FAT runtime reserve and protected authentication range remain intact. No physical SD operation was tested.

"},{"location":"test-results/contact_memory_2026-10-19/#source-bound-artifacts","title":"Source-bound artifacts","text":"Artifact SHA-256 Baseline DFU application 520cf40ec5ada891d3fe8081b23546ce777c712383f0479292bb8fac0017d4d7 Final DFU application c2080afe8874c71eb9a5d1fb253fec06463cac8a774fc776ff46453abf2da420 Baseline DFU ZIP ef641d49dc91db61f0a49912f191f6d81c2887a69950fa66a612f81b31f0e142 Final DFU ZIP d99d904430de75dd5e507a530267d933b8f0bda762afe86cdb150209f4df4499 Final ELF f002455df52a9cdf4278461685eaee3082cc14752f741428f6dfe2dcdc2bb2a7

Every admitted package's memory-bound file hashes were recomputed. Nordic UF2 payloads match the DFU applications, and initialized ELF flash-load bytes agree. The JSON records the individual hashes and actual packaged selectors/profiles.

"},{"location":"test-results/contact_memory_2026-10-19/#software-verification","title":"Software verification","text":"

Contact coverage totals 26 Python unittest methods: three cache/persistence harness methods, eleven persistence-contract methods, seven stream-contract methods, two index/layout methods, one lifetime matrix, and two RP2040/STM32 uncached-storage methods. They execute 24 ASan/UBSan C++ configurations (three cache, eleven index, eight lifetime, two uncached) and separately compile eight actual cached/uncached record layouts across the four MCU families. The lifetime matrix checks 284 assertions. Index execution uses a small stand-in record; separate layout checks compile the real header. Contact code is unchanged since b0f141615; results are retained in the tool transcript, with no separate contact log claimed.

Adjacent checks pass: 16 Reader tests, 19 Beacon tests, a radio-profile matrix with 32 scenarios, seven Morse-codec tests and sixteen persistent-format tests. These focused counts do not establish a complete hosted CI run.

Final linked ARM checks all bind to the ELF above:

The partitioned campaign invokes the unchanged linked Reader checker with all-page reconstruction; partitioning is not an additional release gate. Equivalent complete reproduction against the matching ELF uses Unicorn 2.1.4:

python scripts/check_nrf52_companion_crypto.py path/to/firmware.elf\npython scripts/check_nrf52_companion_ota_workspace.py path/to/firmware.elf\npython scripts/check_nrf52_companion_reader.py path/to/firmware.elf --every-page\n
"},{"location":"test-results/contact_memory_2026-10-19/#final-source-build-matrix","title":"Final-source build matrix","text":"

Canonical packages used v1.18.0.1-halo-keymind-cascade-dev, radio preset usa-cascade-fixed and profile cascade; Full selections required OTA. PlatformIO commands ran one at a time. For example:

bash build.sh build-firmware RAK_4631_companion_radio_full --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota\n
Requested target Actual application bytes Modeled RAM margin Result RAK_4631_companion_radio_full 712,112 6,716 PASS RAK_3401_companion_radio_usb 523,864 50,940 PASS heltec_v4_2_v4_3_companion_radio_full_femon 2,558,856 34,536 PASS, Full PSRAM profile Xiao_rp2040_companion_radio_usb 472,204 144,340 PASS RAK_3x72_companion_radio_usb 229,372 7,112 PASS; four actual flash bytes free Heltec_v3_companion_radio_full No admitted package -21,024 Verified preexisting RAM-gate failure

Matched baseline V3 Full also fails: 177,144 available against 198,432 required (21,288 short), versus final 177,408 (21,024 short). This wrapper preserves 100 contacts, gaining 264 bytes. The final failed ELF was cleaned by a later build; the failure is bound to its build log; that ELF hash is not recorded.

The direct pio run -e Heltec_v3_terminal_chat default build passes with 350 contacts: 263,800 available, 77,856 required and 185,944 margin. ELF SHA-256 is 1a193ae1bee829d0bf7d140310b8487d2a4671f0c85715d16cfb764850e06cb6. This qualifies a no-PSRAM/shared-terminal compile path; it is not a Full package or a firmware-picker release-version/preset qualification.

"},{"location":"test-results/contact_memory_2026-10-19/#retained-failures-and-limits","title":"Retained failures and limits","text":"

Intermediate b0f141615 failed the STM32 size check. Shared little-endian Morse serialization and an equivalent branchless CRC loop restore the packaged fit. STM32 static RAM falls from 40,256 to 39,992 bytes; baseline actual BIN was 229,284/229,376 bytes (92 free), although PlatformIO's section summary showed 444 free. Acceptance uses actual BIN/ELF extent. The final four-byte margin and RAK4631 Full's 592-byte margin require the unchanged hard gates for subsequent versions.

The separate legacy V3 BLE target has the same five unresolved WiFi setup/seeder symbols on matched baseline and intermediate source. Initial disk-quota failures and an unsupported V4 command flag remain separate failed attempts; successful retries establish the results above. No features, capacities or budgets were reduced to obtain these passes. There is no six-canonical-pass claim.

These are software/build/linked-code results. No board was flashed. Physical contact/client operation, PSRAM pressure/fallback, storage persistence, SD/radio operation, power, timing and full-firmware ISR/SoftDevice stack high-water remain outside this qualification.

"},{"location":"test-results/double_frequency_v4_validation/","title":"Double frequency v4 validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/double_frequency_v4_validation/#double-center-frequency-write-v4-transmitter-xiao-receiver","title":"Double center-frequency write: V4 transmitter / XIAO receiver","text":"

2026-09-14. Completed bounded comparison: neither single nor duplicate frequency writes reached 400/400. The duplicate adds about 86 us per hop. No production default was changed. Both radios are idle with HIL installed; the duplicate-write toggle is off. The earlier descending-SF sweep and its follow-up remain paused; no new exhaustive settling sweep was launched.

"},{"location":"test-results/double_frequency_v4_validation/#hardware-and-control","title":"Hardware and control","text":"

The V4 FEM uses the repository's production detection and TX/RX control. HIL restores RX after success or error on each direct transmit path. The GC1109 branch (not detected here) uses bypass according to GC1109 table 4. The detected KCT8103L uses its amplified path; antenna-port TX power was not measured. This is not a power-matched Indicator/V4 comparison.

"},{"location":"test-results/double_frequency_v4_validation/#exact-experiment","title":"Exact experiment","text":"

scanfreqrepeat 1 makes the HIL ExperimentalSX1262 repeat the same SetRfFrequency (0x86) immediately after the first successful frequency call, before modulation, packet parameters and RX restart. The second call skips the image-calibration check, but keeps normal BUSY/status handling. Only an identified RX-to-RX hop may repeat; ordinary setup/TX does not. Either write's failure propagates through the existing failed profile-apply path. The toggle defaults off at reboot and cannot change during an active scan.

Both modes use the same XIAO binary. Actual SPI command counts, not only software policy labels, are checked against one or two 0x86 writes per hop. The modulation cache remains on in both modes.

"},{"location":"test-results/double_frequency_v4_validation/#rx-only-timing","title":"RX-only timing","text":"

Six-second windows in single/double/double/single order, with reboot/clean setup between windows. No reference packets transmitted during this phase.

Frequency writes Hops Mean switch Actual 0x86 Actual 0x8B Single, first 142 451.965 us 142 0 Double, first 141 538.213 us 282 0 Double, second 141 538.170 us 282 0 Single, second 142 451.930 us 142 0

Weighted means: 451.948 us single, 538.192 us double: +86.244 us / +19.1%. All 566 hops had zero retune, RX-mode and cache errors. Maximum observed switch costs were 594 us single and 674 us double; these are samples, not hard latency guarantees. Unheld full-cycle means were about 168.942 ms versus 169.285 ms.

"},{"location":"test-results/double_frequency_v4_validation/#packet-checks","title":"Packet checks","text":"

Single first, then double, each up to 100 packets/channel and stopping on its first miss; a 400/400 result would have stopped all RF testing. Neither passed.

Frequency writes Valid before first miss Failed packet RF-run mean switch Hops / 0x86 writes Single 1 2, channel 2 452.719 us 235 / 235 Double 4 5, channel 2 542.063 us 255 / 510

Both failed packets were receiver timeouts, with TX success acknowledged, zero device-error flags and RX mode still active. Both settings reported zero retune, RX-mode, cache and RX errors. Ordinary processing after received packets caused one/four modulation rewrites respectively; pure RX-only timing above is the cleaner comparison of the duplicate cost.

All five good V4 packets reported -19 dBm RSSI. SNR was +9.0 dB in the single-write sample and +8.2..+9.2 dB (mean +8.675 dB) for four double-write samples. The previous Indicator run had 547 good packets at average -40.547 dBm RSSI, +8.311 dB SNR. Thus this V4 setup was about 21.5 dB stronger as seen by the receiver. Hardware/antenna/placement/power differences remain confounded; neither the Indicator nor close-range effects are proven causes.

One versus four successes before a miss is too small and stop-conditioned to establish a reliability improvement or packet-error rate. Repeating center frequency did not remove the observed failure at this setting. No results for lower SFs or added settling delays are claimed for the V4. A weaker received signal is needed before treating a transmitter swap as a clean comparison.

"},{"location":"test-results/double_frequency_v4_validation/#artifacts-and-checks","title":"Artifacts and checks","text":"

Base checkout 0e5955f889788a4317863e731b38ffe09e3d5441, branch keymindCascade, plus local uncommitted changes. RadioLib 7.7.1 pinned at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both builds/uploads succeeded and uploader verified written data. The 59 selected host/native tests passed (39 collectors, ten profile, four SX1262, five radio and one receive-mode), including FEM restore on error and frequency-repeat scope/error propagation. Temporary build overlay removed.

Actually loaded firmware SHA-256:

Raw evidence and byte-preserving SHA-256:

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/","title":"Esp32 ble radio2 sleep 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#esp32-bluetooth-shutdown-and-radio2-cpu-idle-2026-10-21","title":"ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21","text":"

The ESP32 changes cover two gaps identified while qualifying nRF52 CPU sleep: reversibly stopping the Bluetooth host/controller, and allowing bounded MCU light-sleep during a settled secondary-radio receive visit. This report separates host/build coverage, SDK wait measurements and physical packet tests.

Bluetooth transitions and quiet-window SDK wait checks pass. Three earlier strict RF trials with power saving on and matched preambles received five of six packets, including the trial after the first IRQ fix. They remain failures; radio2 is not fully qualified.

The first V3 CPU-clock sweep stopped during setup, before any of its 20-packet-per-frequency cases transmitted. Switching Bluetooth off exposed a NimBLE-Arduino 2.5.1 shutdown crash: PC zero in the host task, with the event address resolving to ble_hs_timer + 8. This matches upstream issue #1184 and its merged fix #1185. The backport removes premature timer deinitialization while leaving final cleanup intact. The earlier V4 Bluedroid checks do not qualify V3 NimBLE. The regression reproduces the original null callback, then passes 1,000 shutdown/cleanup cycles under ASan/UBSan for each timer backend. Library version, source hash and build-local middleware checks also pass.

Full Companion source 6236eec3 includes that repair and a preferences-reader fix for a truncated timezone record colliding with the new CPU footer length. Both timezone and CPU suites pass; damaged CPU overrides still fall back to Auto. V3 and V4.3 OLED FEM-off builds pass all 24 capability markers and their memory gates, at 2,118,680 and 2,556,504 application bytes respectively.

The V3 received one application-only update after its old application hash was verified. Complete new-image readback passed, with its partition table and OTA metadata unchanged. Starting with Bluetooth saved off, it restored Bluetooth on and completed three off/on cycles without a reboot or core error. Uptime advanced from 32 to 58 seconds across 20 health samples. Every Binary handoff revalidated board identity and source version. The final checked configuration and credential hashes matched the baseline, and the active PIN stayed unchanged. This physical check covers Bluetooth power transitions, USB control and checked preference preservation.

The recovery also exposed an already-ROM ESP32-S3 UART finish gap. The flasher now explicitly releases the boot strap before its final reset; its regression passes with both esptool command spellings. That separate fix is published in meshfirmware f9d03d1.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#behavior-and-safeguards","title":"Behavior and safeguards","text":"

Bluetooth off disconnects clients and completes host/controller teardown in the main loop. Bluetooth on rebuilds the service without releasing controller memory permanently or clearing successful bonds. Address, name, PIN and saved stealth policy remain intact. Pending or failed teardown prevents CPU sleep. The Bluedroid characteristics have deterministic TX/RX registration order; heap fragmentation must not change a paired client's cached handles. On SDKs using automatic Service Changed handling, a small asynchronous service change requests client rediscovery after authentication. A local SDK completion is not treated as proof that the client refreshed its cache. Only actual use of the current notification and receive handles permits skipping this request on a later connection from the same peer within that host session.

Companion, Repeater, Room Server and Sensor can idle during an eligible radio2 visit. The decision requires a settled, validated receive profile, working radio GPIO wake, no pending reception and physically stopped wireless drivers. An absolute scanner deadline is checked again at sleep entry. The timer budget is at most 10 ms, reserves a 4 ms early-wake scheduling guard and requires at least 2 ms remaining. Calibration, repair, ownership, queue, network, USB and role deadlines retain their existing guards. The receiver stays on. This does not enable sleep through an active Bluetooth/WiFi link.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#host-tests-and-builds","title":"Host tests and builds","text":"

The scheduling, board wake, dispatcher and real role integration suites pass. Coverage includes expired and wrapping deadlines, stale profile proofs, pending interrupts, failed wake setup and SDK errors. The BLE adapter tests exercise actual production code with ASan/UBSan, ownership differences, partial initialization/teardown failures and repeated off/on cycles. Existing nRF52 event/profile tests also pass; nRF52 hardware was not touched.

All 22 scripts in the affected Sensor/tracker/sleep CI step and all 57 in the Companion contact/channel step pass locally. The latter initially encountered a missing Python platformio module in one read-only metadata case; that script's six cases passed with the existing PlatformIO Python environment. No firmware build or hardware test was repeated for this environment correction. The diagnostic fixtures exercise the current production sleep branches rather than relying on obsolete source anchors.

The initial sleep/teardown implementation, before the cache fix, has Full V4.3 FEM-off Companion, V3 Companion, V4 Repeater, Room and Sensor builds that pass their capability and memory gates. ESP32-C6 BLE (Arduino 3.1.3), the NimBLE-Arduino V4 trial and RC32 Full (Arduino 3.3.11) also compile. RC32 exposed SDK service ownership and array-deletion defects; a version/hash-bound build-local repair fixes those sources without editing a shared SDK install.

The cache fix is published in 1087643ae. Its V4.3 FEM-off Full and RC32 Full builds pass all 24 capability markers and their application size gates: 2,551,592 and 2,152,840 bytes respectively, each within a 6,553,600-byte application slot. The other earlier builds do not constitute a full build matrix of this later source revision.

d34e65175 removes unnecessary delays between successful cache-recovery phases while retaining bounded failure backoff. Its V4 Full build passes, at 2,551,608 bytes. The same-size V4 build of f8b5b8301 adds only the channel-response padding fix described below. Bluetooth, radio, sleep-policy and role code are unchanged between those last two revisions.

At this checkpoint, Classic Heltec V2 BLE was below its required 8,192-byte static DRAM reserve. Historical checks left 2,552 bytes free on the baseline and 1,224 bytes on the initial sleep candidate. Removing an unnecessary WiFi-driver reference recovered 1,208 bytes, raising the later candidate to 2,432: 120 below baseline and 5,760 short of the reserve. Final cache/padding revisions were not remeasured on V2. The gate was retained. The later 22 October V2 cache repair closes the RAM build failure with 12,440 static bytes free and unchanged capacities; physical V2 operation was not measured by that repair.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#physical-v4-observations","title":"Physical V4 observations","text":"

The test board is a Heltec V4.3 OLED using its matching FEM-off Full Companion image. The first candidate reports source suffix 278c9ffb; its published equivalent after history cleanup is 61eecbd97. An application-only write and complete readback preserved the partition table, OTA metadata, identity, settings and credential digests. No whole-flash backup, bootloader write or other-device update was performed.

With Bluetooth/WiFi off and power saving on, only this V4's USB data was suspended. After the host-loss grace period, a 90-second diagnostic window recorded 4,635 SDK light-sleep calls and returns, zero SDK errors, zero short waits, zero vetoes and zero pending-wake returns. Cumulative call time was 35.523299 seconds; the longest call was 9,869 microseconds. The mixed profiles were 910.525 MHz / BW62.5 / SF7 / CR5 and 916 MHz / BW125 / SF9 / CR5. USB authorization and runtime policy were restored. Core errors remained zero. These are SDK wait measurements, not electrical current or residency readings.

Two initial saved-bond Bluetooth off/on sessions passed. Further testing exposed both a host-side notification-acquisition problem and a real firmware GATT-handle ordering defect. The host driver now uses ordinary StartNotify. The original failures are retained; clearing a client cache is not accepted as proof that firmware handles remain stable or an existing cache migrates.

On d34e65175, both authenticated NUS connections passed with the old opposite handle cache retained. The test independently checked board, exact version, role, public identity, node name, address and advertising policy. Switching off while connected disconnected the client; the test required completed off before re-enabling and reconnecting. No cache was cleared and no new pairing was made. The cache changed automatically to TX value 0x002A, RX value 0x002D; saved pairing-key digests were unchanged. The image also started successfully with Bluetooth saved off. No electrical power measurement was made.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#two-frequency-packet-controls","title":"Two-frequency packet controls","text":"

The peer is an independently identified ESP32 XIAO S3 WIO. Every transmitted packet uses a new encrypted nonce, full sender/destination identities and a zero-hop direct route. The receiver's USB data is absent during the sleeping RF window; queued packets are checked only after USB resumes. WiFi and Bluetooth are stopped, crossover is off, and physical RX counters must advance on both frequencies. No failed packet is resent automatically.

Earlier runs used a transmitter preamble of 128 symbols while the receiver reported 32. An awake two-packet control received zero packets with zero SDK sleep calls. Disabling radio2 then received the one primary test packet. Source inspection found that the receiver's 32-symbol preamble guards expire after about 99/198 ms, while a 128-symbol transmitted preamble alone lasts 262/524 ms on these profiles. The guard can clear preamble detection and permit a scan hop before the header arrives. These mismatched trials remain recorded; they do not establish a CPU-sleep regression.

With both sides at the verified 32-symbol preamble and peer TX at 2 dBm:

Control Unique packets received SDK waits Result USB active, CPU awake 2/2; one on each profile 0 Pass, including exact cleanup USB suspended after 121-second host-loss grace, power saving on 5/6; all three primary and two secondary 777 calls/returns; zero SDK errors, short waits, vetoes or pending returns Strict RF check failed: final secondary nonce missing

The sleeping trial's measured profile RX deltas were 100/2, including ambient traffic. Both frequencies carried verified private packets while the MCU idle path ran, but the missing sixth packet prevents an all-pass result. These finite samples do not establish a loss rate or attribute that loss to CPU sleep.

A predeclared paired comparison then used the same d34e65175 image, settings, 32-symbol preambles, TX power and suspended USB in both modes. Each interval sent six new nonces once. A first preparation attempt was refused before any RF transmission; its clean restoration and zero-transmission receipt remain preserved. Bounded admission of the power setting was moved before USB suspension; no packet retry was introduced. Power saving also caps the CPU at 80 MHz; switching it off restores the nominal 160 MHz clock on this V4 Full build. This comparison measures the whole power-saving policy, so it does not isolate light sleep from foreground CPU speed.

Same-image control Unique packets received SDK calls/returns Result Power saving off 6/6; three on each frequency 0/0; all diagnostic error/veto counters zero Pass, cleanup and PS-on restoration verified Power saving on 5/6; primary sequence 2 missing, all three secondary packets received 820/820; zero errors, short waits or pending returns; five vetoes Strict RF check failed; cleanup and PS-on restoration verified

The second sleeping trial had profile RX deltas 103/3, including ambient traffic, and seven receive errors. The different missing packet rules out a deterministic final message-slot loss. This small comparison warrants a wake-path investigation; it does not isolate interference, scanner timing or an individual GPIO event as the cause. No unchanged RF trial was repeated after it.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#gpio-wake-and-packet-ownership","title":"GPIO wake and packet ownership","text":"

ESP-IDF 4.4.7 documents that GPIO interrupts are not generated during light sleep. GPIO wake is a separate mechanism. The board temporarily uses a level wake source and then restores a rising-edge interrupt; the SDK's GPIO driver and S3 wake implementation do not replay a software callback when that interrupt type is restored.

The wrapper previously checked only its software event. If a completed packet left DIO high without a callback, the loop could fail to claim its FIFO before a later rearm or retune. This is a concrete ownership gap, but the aggregate packet results do not prove which missing nonce encountered it.

1ba4e2830 adds an ESP32-only physical IRQ fallback to both pending and claim checks. It uses that registered radio module's own pin and HAL, validates ownership, null pointers, NC, pin range and the SoC validity mask, and adds no RAM fields. Claiming consumes any software event too; the unchanged hardware RX_DONE check still gates FIFO reads. It changes no frequency, preamble, wake mask or sleep budget. Other platforms retain their software-event behavior.

All 12 interrupt-recovery host tests pass with ASan/UBSan. They execute the production receive and reconfiguration paths with an asserted DIO and no ISR, combined and delayed callbacks, independent radio pins, invalid pins and non-ESP32 variants. Removing either fallback fails an executed regression assertion. The V4 Full build passes all 24 capability markers and its memory/size gates, at 2,551,736 bytes within the same 6,553,600-byte application slot. The RC32 Full build on Arduino 3.3.11 also passes all 24 markers and its memory/size gates at 2,152,968 bytes. These are build checks, not physical RC32 qualification.

The application-only installation and full readback of this image passed, including unchanged partition table, OTA metadata, identity, saved settings and credential digests. One new six-packet qualification trial then exercised the changed IRQ code with the same radio settings, USB suspension and power-saving policy. It received 5/6, missing secondary sequence 6: another strict RF failure. All six sends completed once; none was resent. Profile RX deltas were 79/2, including ambient traffic, with five direct packets and two receive errors. The interval recorded 741 SDK calls/returns, zero SDK errors, short waits or pending returns, and two safe vetoes. Cumulative call time was 5.710857 seconds; the longest was 9,900 microseconds. Cleanup and PS-on restoration passed.

Independent source audits found no additional concrete clock/deadline defect: ESP32 micros() and the scanner use the ESP Timer domain; the deadline is anchored before the late query and checked again after wake setup. Pending packets and latched preamble/header activity guard the next retune. The current 4.6-symbol scanning model is not a zero-loss guarantee. The observed losses remain unresolved; neither interference nor the IRQ gap is established as their cause. No further unchanged RF trial or arbitrary timing/preamble change was used to obtain a pass. Further qualification needs a control that holds CPU speed fixed and correlates scan/preamble/IRQ timing with individual packets.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#channel-cleanup-and-response-padding","title":"Channel cleanup and response padding","text":"

The single-profile and sleeping reports also recorded cleanup failures. The single-profile power restore was refused while the radio was busy; a separate bounded cleanup restored 22 dBm and removed only the hash-matching test channel. The later sleeping run restored power and cleared its channel but failed a raw channel-response digest comparison.

A separate read-only audit compared every meaningful channel name and every key byte twice on both devices. All matched, and both temporary slots were empty with zero keys. Raw differences occurred only after the terminating NUL in the fixed-width channel-name field: one channel differed between repeated reads on each device and all 40 differed between devices. The getter used strcpy without clearing the unused name bytes, exposing stale reply-buffer padding. The original failed receipts were retained; no channel data was rewritten to force a digest match.

The shared Companion fix zero-pads and bounds that field while retaining key offset 34 and the existing 50/66-byte frame layouts. All nine affected host tests pass. The actual-handler regression covers empty, short, maximum and unterminated names, dirty output buffers, Meshtext on/off and 16/32-byte keys; the original implementation fails its negative control.

A read-only hardware audit on 1ba4e2830 also passed: both devices' meaningful channel names and every key byte matched across two reads, both temporary slots were empty with zero keys, and every V4 name field had zero padding. The peer's firmware was not changed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#v3-fixed-clock-comparison-with-20-packets-per-frequency","title":"V3 fixed-clock comparison with 20 packets per frequency","text":"

After the NimBLE repair, the V3 completed all four predeclared cases on 6236eec3. Every case sent 20 unique packets per frequency once, with the same 32-symbol preambles, 2 dBm peer TX power and default 10/4/2 ms sleep dials. Power saving stayed on; CPU speed and the temporary manual sleep switch were controlled independently. RX power saving was saved off for these comparisons; the profile scanner itself uses continuous RX during each visit.

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns over the whole diagnostic window 80 MHz Off 18/20 17/20 0/0 80 MHz On 20/20 19/20 270/270 160 MHz On 18/20 14/20 18/18 160 MHz Off 19/20 16/20 0/0

All 160 transmissions reached TX_DONE, with no packet retries or duplicates. All four strict delivery checks failed. Settings, channels and credential hashes were restored; both radios finished with zero core error flags. Losses with manual sleep off show that sleep is not required for the observed failure. Ambient traffic differed between cases, and these samples do not establish a CPU-speed relationship or a packet-loss cause.

The On rows do not yet prove sleep during packet traffic. The UART quiet control stopped the driver but left its Companion session logically connected. Incoming messages then queued push notifications to that stopped transport; the existing pending-output guard correctly prevented sleep. Whole-window SDK calls could have occurred before traffic began. The delivery counts remain valid, but sleep comparison qualification requires retiring that UART session and timing evidence inside the RF sequence. No pending-work guard is bypassed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#uart-session-repair-and-timed-v3-comparison","title":"UART session repair and timed V3 comparison","text":"

The campaign continued on 2026-10-22 with 7d0709bf0. UART quiet now retires only that USB protocol session after draining the ACK, including its pending reply owners and USB Reader output. The offline inbox and other transports remain active. A successful UART restart restores a fresh silent session; failed restarts keep the interface parked and retry. The integration regression reproduces the original stuck notification, then retains and downloads 40 actual receive frames without weakening sleep guards.

ESP32 diagnostics now retain sixteen 16-second SDK-entry buckets, with explicit outside-window, saturation and missing-clock indications. The collector stays within its 256-byte budget; nRF52 layout and behavior are unchanged. The physical comparison requires a positive bucket wholly after the first confirmed TX_DONE and before the last TX request, including host/device clock uncertainty. This establishes SDK entries during the packet sequence, not sleep through a particular packet or electrical residency.

Both Full Companion builds passed 24 capability markers and their memory gates. V3 and V4 application sizes are 2,120,088 and 2,557,400 bytes. Application-only updates passed full readback, unchanged partition/OTA metadata and checked configuration/credential preservation on both boards. No Bluetooth lifecycle or earlier quiet-only test was repeated solely for documentation changes.

The new V3 comparison again sent 20 unique packets per frequency per case, with no RF retries, the same 32-symbol preambles and default sleep dials:

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns SDK calls in wholly contained RF-time buckets 80 MHz Off 17/20 15/20 0/0 0 80 MHz On 18/20 19/20 10,925/10,925 4,625 160 MHz On 18/20 16/20 10,164/10,164 4,585 160 MHz Off 20/20 16/20 0/0 0

All 160 sends reached TX_DONE once; there were no duplicate deliveries. All four strict delivery checks failed. On cases have positive SDK activity during the RF sequence, with zero SDK errors, short returns or pending waits. In the 80 MHz On comparison, serial blocking fell from 109,265 ms on the old image to 1,368 ms after session parking. This supports the console-fix mechanism; varied ambient traffic still prevents a causal delivery comparison. Neither CPU speed nor manual sleep consistently removed packet loss. Original settings, channels, identity and checked credential hashes were restored, with clean core flags.

V4's first new comparison stopped during setup, before Bluetooth-off was sent and before any case or RF transmission. Its strict stable-Binary check rejected one unexpected prefix byte after a valid health response. Seven valid push frames were accepted; the rejected byte was not retained, so its origin was unknown at the time. That original failure remains recorded.

Two diagnostics-only attempts on unchanged 7d0709bf0 also stopped before Bluetooth-off was sent, with zero RF transmissions. Bounded raw capture retained the late SDK line E (2905571) wifi:timeout when WiFi un-init, type=4. Source inspection identified an SDK timer firing after shutdown; this message does not establish that WiFi deinitialization timed out. The raw SDK log bypassed disabled USB logging and broke strict Binary Companion framing. All three attempts restored checked settings/channels and verified a fresh MQTT PUBACK increase. The latest immutable diagnostic receipt is v4-7d0709bf-suffix-completed.private.json. These setup failures supply no new V4 packet-delivery evidence.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#sdk-logging-repair-and-v4-scanning-controls","title":"SDK logging repair and V4 scanning controls","text":"

The SDK logging fix in d73599bc6 shipped in 3fd9b392. It prevents raw SDK logs from bypassing the disabled USB-logging policy and corrupting the shared Binary Companion stream. All 101 USB host tests pass. The V4 application is 2,558,856 bytes, with SHA-256 ddfd4f40a97da40bdeda2c00fb18ebc2d4d365590d4bc63496a425d16975906d. Its installation passed full readback and checked configuration preservation.

Two new scanning cases completed on this V4 image, with 20 new nonces per profile, matched 32-symbol preambles and 2 dBm peer TX:

CPU Manual CPU sleep Primary received Secondary received SDK calls/returns SDK calls in wholly contained RF-time buckets 80 MHz Off 19/20 18/20 0/0 0 80 MHz On 19/20 15/20 4,121/4,121 3,511

Both strict delivery checks failed. All 80 sends reached TX_DONE once, with zero retries or duplicate deliveries. SDK errors, short returns and pending waits were zero. The awake losses establish that CPU sleep is not required for this symptom; these samples do not identify the cause of individual misses.

The user redirected testing toward the scanner. The 160 MHz On case was intentionally stopped during its USB host-loss grace period, before any RF transmission; 160 MHz Off was not entered. This is not a completed four-case comparison. The interrupt receipt remains recorded in v4-3fd9b392-user-steered-completed.private.json (SHA-256 e102200af7e2503bb0362e65afad0d947d5c3aeb220425a589d0588032530c52). Full restoration and a fresh MQTT PUBACK increase passed after that stop.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#fixed-tuple-controls-and-receive-path-defect","title":"Fixed-tuple controls and receive-path defect","text":"

Two fixed-tuple controls on the same 3fd9b392 image each received 20/20 unique packets with radio2 explicitly off. The primary physical radio was temporarily tuned first to 910.525 MHz / BW62.5 / SF7 / CR5, then separately to 916 MHz / BW125 / SF9 / CR5. Both runs used 80 MHz, manual CPU sleep off, saved power-saving intent on, RX power saving off, 32-symbol preambles and 2 dBm peer TX. The target's USB data was absent after the same 121-second host-loss grace. Each run retained the original seeded 40-slot cadence, leaving the opposite profile's 20 slots silent. SDK calls, duplicate deliveries and RF retries were zero. The immutable control report remains failed because the peer's original 22 dBm setting was not restored within three bounded busy-response attempts; MQTT recovery passed. A separate recovery then restored 22 dBm and verified full peer settings, credential hashes and channel semantics without another RF transmission. The original failure is retained in fixed-tuples-v4-3fd9b392-completed.private.json; successful recovery is in peer-tx22-restoration-verified.private.json.

These finite controls narrow the investigation toward scanning or overlapping traffic. Radio2 time-shares one transceiver: a valid primary reception can occupy it throughout a secondary packet's preamble. The idle-scan timing model does not guarantee zero loss under simultaneous traffic. The controls do not distinguish that limitation from a receive-path defect for a particular nonce.

A separate host regression executing the real receive functions with ASan/UBSan proved a stale-IRQ defect. Servicing a packet through its asserted physical DIO could leave a late software event. Subsequent empty RX_DONE probes could then restart continuous RX over a newer packet's preamble or valid header. The narrow ESP32 SX126x continuous-RX guard in a5293fa50 preserves that new reception while retaining normal activity timeouts. All 15 interrupt-recovery, five receive-contract and six RX-retry tests pass. The V4 build passes all 24 capability markers and its memory gate at 2,558,936 application bytes. Its application SHA-256 is cc16e44ccd24e64724a2bd17f59f684117e411077e4c0a7d04611be328b7ccfc. The application-only installation passed complete readback, with the partition table, OTA metadata and checked configuration preserved. The immutable receipt is flash-v4-a5293fa5-completed.private.json (SHA-256 ef61cf8dfc77f8907d621f7f0493949ff6888b017c277eefb309247ccf88b278).

One new awake 80 MHz scanning run then sent 20 unique packets per profile on this changed image, with manual CPU sleep off and the same matched preambles, TX power and USB host-loss grace. The V4 received 19/20 primary and 20/20 secondary packets. All 40 sends reached TX_DONE once, with zero retries or duplicate deliveries. SDK calls, returns and all time buckets were zero; core error flags remained clear. The missing primary nonce keeps the strict RF result failed. Full restoration passed, including a fresh MQTT PUBACK increase. The immutable receipt is v4-a5293fa5-awake-completed.private.json (SHA-256 466bdf0bb08f10665007d156e1ec37c074e1c576e6ac5fc401bcd449179ee774).

The stale-IRQ regression proves a receive-path bug and the new guard repairs that case. This physical sample does not establish which historical misses encountered it or show that all scanning losses are fixed. A controlled two-transmitter overlap diagnostic is recorded below; it does not establish a cause for these historical misses.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#receive-activity-across-rx-restarts","title":"Receive activity across RX restarts","text":"

A further host regression executing the production receive methods exposed a separate scan hazard. Reading a completed packet and successfully rearming RX cleared the hardware IRQs but retained the previous packet's _activityAt and _headerSeen latches. At the next due scan, the detector could mistake the new packet's preamble without a header for the old packet's consumed header. It reported idle before processing that preamble, permitting a retune over the new packet. The regression reproduces that sequence and shows that resetting the latches on successful RX entry protects the next preamble.

The change resets those existing fields only after successful receive starts in CustomSX1262, CustomSX1268, CustomLLCC68, CustomSTM32WLx, CustomLR1110 and CustomLR1121. It covers normal starts and, where supported, fast resumes and duty-cycle entry. Failed starts retain their prior state; the change adds no fields and leaves existing activity timeouts and receive/retune guards unchanged. Independent source review passed. Five new regression methods cover 24 ASan/UBSan variants, including reset-removal controls that reproduce the retune over a new packet for all six handlers. All 19 affected host-fixture tests and 26 interrupt-recovery, receive-contract and receive-retry checks pass. The new suite runs in the required unit-test workflow. Physical RF results on the new image are recorded below. The host proof does not attribute historical missing nonces or establish a complete RF pass.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#rx-restart-installation-and-overlap-diagnostic","title":"RX-restart installation and overlap diagnostic","text":"

The V4 image built from pushed source e4119a120 passes its capability and memory gates at 2,558,984 application bytes. Its application SHA-256 is fc1a835c45ebc5e7a9fec95c757d8606e145bbc92fa8d365ba62d96a7930286d. The application-only installation passed full readback and checked configuration preservation. The receipt is flash-v4-e4119a12-completed.private.json (SHA-256 b06d8b6ef8d9df9aa881d6d1ca2f6096d19c8bcc6d4cbb2873fb20d47a9f85c5).

Two overlap conditions completed on this image at 80 MHz with manual CPU sleep off. Each condition sent 20 unique primary/secondary pairs; all 40 sends reached TX_DONE once, with zero retries or duplicate deliveries and zero SDK counters in both conditions.

Condition Primary received Secondary received Radio2 scanning 16/20 2/20 Fixed 916 MHz, radio2 off 0/20 (outside receive frequency) 20/20

All 20 scanner timing bounds were inconclusive. The nominal 140 ms secondary write occurred 227.239-263.257 ms after the primary write (median 244.363 ms), while the first counter reply confirming primary TX_DONE arrived 523.533-903.982 ms after that write. That reply bounds completion; it does not timestamp the RF event. The controller synchronously saved the report before the secondary write and after each counter reply, widening these bounds. The analysis uses 101-byte wire airtimes of 397.824/652.288 ms, a complete 148.480 ms secondary preamble and a 2 ms margin. No pair proves that the whole secondary preamble fell inside the primary payload reception. The timings do not exclude desense or attribute historical misses. The ordinary alternating-profile check is recorded below.

Three earlier setup attempts sent no RF: the first two stopped before mutation; the third encountered a pending lease, with separate V3 automatic cleanup recovery passing. The overlap experiment completed with an inconclusive status: neither condition has a qualified timing pair, so masking was not demonstrated. Full restoration of all three boards passed without errors, and a fresh MQTT PUBACK verified recovery. The final host receipt is /home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/overlap-v4-e4119a12-completed.private.json (SHA-256 c1aee5132b447734df3fe9c663f4f3ae7a8b980f3165de43f96c2653d05200e1). No firmware qualification pass is claimed.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#ordinary-awake-check-after-both-receive-fixes","title":"Ordinary awake check after both receive fixes","text":"

The predeclared e4119a120 check sent 20 new packets per profile, once each, with the ordinary alternating schedule and no deliberate overlapping sender. It received 17/20 primary and 18/20 secondary packets. All 40 sends reached TX_DONE once; duplicates, RF retries and SDK sleep calls were zero. The strict RF result remains failed. The physical counters recorded 120 successful raw receives: 35 owned direct packets and 85 ambient floods, with seven separate receive errors. These totals do not identify the cause of each missing nonce. The immutable receipt is v4-e4119a12-awake-completed.private.json (SHA-256 9f2f8acecc634ebaf7b9e9aa21e51079b9872316e571a886d3866bfedabab558).

That receipt also retains a cleanup failure: restoring automatic radio2 permission was rejected while the temporary primary restore was still pending. Fresh MQTT recovery passed. A separate guarded operation subsequently restored automatic permission with one setter and no RF transmissions, then verified the checked settings, credentials and channel semantics against the baseline. Its receipt is v4-auto-restoration-after-e411-awake.private.json (SHA-256 4dbf10eeac81e8897c8a4f9fa4d7b62ac20e74c91b467a501f31fa3a1d8bd0ec).

Both receive defects remain proven by executed host regressions, but the physical results show that additional losses remain. The following diagnostic records bounded activity and transmission times on the radios themselves. It distinguishes other-profile ownership, unresolved preamble activity, read/rearm gaps and delays between scanner calls without treating host counter replies as RF timestamps. No arbitrary preamble or CPU-sleep setting change or replacement packet was used to turn a failed result into a pass.

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#on-radio-capture-after-the-receive-fixes","title":"On-radio capture after the receive fixes","text":"

Source 9b70827d0 adds an opt-in, bounded RAM trace for this diagnosis. It is compiled out of normal firmware. The private V3 sender and V4 receiver images record physical interrupt bounds and receive activity without additional SPI reads or asynchronous USB output. Trace capture does not qualify a release image by itself.

Two controller failures are retained separately. The first rejected a valid pending-frequency response before sending any RF. After correcting that parser, the absolute-slot attempt sent and received one primary packet, then stopped with 39 packets unsent. Its next frequency change took 2.847 seconds, exceeding the 2.553 seconds available after transmit confirmation. Both attempts restored the original checked settings and verified a fresh MQTT PUBACK. Neither is an RF qualification pass, and no original packet was resent.

A separate, predeclared minimum-gap-v1 measurement keeps the seeded packet order and minimum gaps, allowing slow frequency changes to stretch the cadence. It uses 40 fresh unique packets, 20 per profile, each sent once. The entire transmission phase completed in 114.162 seconds with zero retries or duplicate deliveries. Its changed cadence must not be treated as a passing replacement for the failed absolute-slot attempt or directly compared as the same loss-rate experiment.

V4 awake, radio2 scanning Sent once Received Primary 20 20 Secondary 20 12

Both radios recorded zero SDK sleep calls, returns and errors. Both captures had zero overflow, SPI errors and context errors. All 32 shared packet-end checks agreed with the clock model. The timing model uses causal clock-query brackets, a conditional awake ESP32-S3 rate bound of 10 ppm per radio, and an explicit conditional 2 ms PHY allowance. Its full uncertainty envelope is retained; the predeclared 50 ms quality limit never narrows that envelope.

Three of the eight secondary losses have a positive ownership classification: the entire possible preamble interval fell inside a confirmed valid primary packet reception from ambient traffic, rather than one of the owned test packets. The single transceiver could not listen on the secondary frequency during that reception. Five losses remain unresolved. Three overlap a read/rearm window, which is only a timing association; the other two have no qualifying cause in this capture. These results do not establish desense, noise, a new scanner defect, or the cause of historical missing packets.

The strict RF result remains failed. The successful fixed-frequency controls and the new timing evidence establish frequency sharing as a cause of some losses while CPU sleep is disabled. Five losses still lack a qualifying cause. A longer preamble, different scan period, or dropping a valid primary reception is not silently applied as a general fix.

The completed capture receipt is radio-trace-minimum-gap-v1-completed.private.json (SHA-256 a2f34dc973692c9567ac7854db59446c590ab1c176375b06076fe6944ea7440a). It retains a cleanup failure: restoring the V4's automatic radio2 permission exceeded the controller's combined 15-second budget after a busy rejection. The primary temporary lease had already become inactive; automatic permission remained unrestored. Fresh MQTT PUBACK recovery passed. A separate guarded operation restored that permission with one setter and no RF, checked channel semantics and the PIN through a fresh handshake, and preserved the original failed receipt. Its receipt SHA-256 is 432b1104156e2b6c4802efa5af6089d1b2ccc6bcfe70ea7acb1596fcc3cfa89f. A subsequent read-only baseline verified both radios against all checked original settings and credential digests, with no transmissions or preference writes (SHA-256 f32cb54600142b2011fd9bcf51a186e95f41b70e590fabbf5e06516bb6506dc7).

"},{"location":"test-results/esp32_ble_radio2_sleep_2026-10-21/#final-handback-and-limits","title":"Final handback and limits","text":"

V3 and V4 now run normal Full Companion 9b70827d0, with the diagnostic trace compiled out. The peer remains on ebb59d48. Both application-only returns passed complete readback, partition-table and OTA-metadata preservation, and checked configuration/credential preservation. The normal application SHA-256 values are 39e15c4df674993ef1a3a809867a381a01b182af3e920ff208f28bef16178619 (V3) and dd714ad4e9f57f811647af889c8eab7e99a434f81da9541a4398624049b6361f (V4). Return receipts have SHA-256 28a5e075ee9154c2e67b1d4bc1fbec375d45abc1a41f40ac4797dc4a23bd9528 (V3) and 5894b59b1d3344cf88d3804a05022271c86f7b4aad29e5307be0f1af475237b9 (V4). A read-only post-boot observation confirms V4 MQTT running with its saved broker connected and zero core error flags.

The first V3 return invocation stopped before any ROM operation or firmware write because the existing diagnostic install used the same source-commit attempt marker. That failed receipt is retained (SHA-256 972e888b0d5c425676b2411ae6f7ca04cbd1b5e126fd1557667c474d0617c741). The normal return uses a separate node/source/application-hash marker while retaining exclusive creation and all original update checks; the diagnostic marker is unchanged. Later source commits only repair a host fixture and update this report. Unchanged RF tests were not repeated for the normal return.

The completed earlier V3 campaign and V4 scanning controls, including the a5293fa50 awake run, restored their checked radio, RX power-saving, Bluetooth, WiFi, MQTT, logging, diagnostics and USB-watchdog settings, including credential digests, original radio2 automatic permission and peer power/channel state. Fresh MQTT PUBACK increases verified recovery. The fixed-tuple cleanup power failure is retained, with the later peer recovery verified separately. Full overlap restoration is verified. The ordinary run's original cleanup failure is retained, with its separate automatic-permission recovery verified. No nRF52 board, hub power or other firmware was changed in this task.

The Bluetooth hardware checks pass. SDK light-sleep calls are observed without SDK errors. Strict sleeping two-frequency packet qualification remains open. The later V2 cache repair passes its existing RAM reserve gate. These results do not qualify every ESP32 board or the full thirteen-radio fleet, and do not measure electrical power savings. Unchanged Bluetooth and quiet-wait checks were not repeated for padding, version, documentation or fixture-only changes.

Private artifacts, receipts and traces are retained under /home/mesh/hwtest/esp32-ble-radio2-sleep-cd4ea053-20261021/ and /home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/. See power-saving diagnostics for the controls and the limits of interpreting wait counters as energy savings.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/","title":"Esp32 full power ap 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#esp32-full-idle-power-and-compact-ap-validation-2026-10-06","title":"ESP32 Full idle power and compact AP validation \u2014 2026-10-06","text":"

Tested firmware source: 219e936ef82d16539111f03bc7982dd5654412df, after pulling upstream through 5a7c07a6c. Version: v1.17.1.9-halo-keymind-cascade-dev-powerap3-219e936e. PlatformIO commands ran one at a time in a native WSL checkout. A later upstream test-only commit f8e04f090 was merged without changing the tested firmware sources; its 14 USB serial hygiene tests also passed.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#corrections","title":"Corrections","text":"

Fresh expanded ESP32 Full profiles default USB packet logging off. Existing saved logging and power-saving preferences remain authoritative. Repeater and Room Server automatic setup with no saved SSID suspends unused network bridges and their automatic retries. When the setup window ends, idle cleanup releases the actual WiFi driver before sleep becomes eligible. Failed shutdown remains a sleep blocker, and active/stopping WebConfig, MQTT, ESP-NOW, and OTA retain their ownership. OTA resumes only bridges that were actually running beforehand.

Native USB Companion idle handling now checks the actual WiFi and Bluetooth drivers, GPS UART, pending work, logging, host presence, and host-loss grace. An enabled Bluetooth controller still blocks manual light sleep. This does not change the deliberate raw-UART console or primary ESP-NOW sleep constraints.

Compact browser OTA and Companion setup use the shared scan-before-AP startup. They require an AP-start event and correct live SDK configuration, protocol, channel, and IP before reporting success. Scanner cancellation and stale Arduino versus SDK state have explicit regressions. Browser OTA preserves inherited LAN/ESP-NOW ownership, and repeated OTA start preserves joined clients on a healthy AP. WebConfig setup disconnects STA to isolate its open setup API while preserving an explicitly running ESP-NOW owner.

Physical testing caught an additional regression in the first candidate: manual WiFi setup suspended explicitly enabled ESP-NOW. The final implementation limits that suspension to automatic boot setup. Manual starts and master radio off/on now preserve the requested WiFi/ESP-NOW subset. The regression test executes the real infrastructure backend and wireless controller for both roles; reverting the manual wrapper to automatic suspension makes it fail.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#software-checks","title":"Software checks","text":"

All 98 focused host tests in 20 suites passed. They include extracted production startup, idle cleanup, sleep guards, USB host grace, saved preferences, OTA bridge restoration, AP fault injection, and real wireless controller subset restoration. The new suites are included in the unit-test workflow. A separate shared AP fixture compiles Arduino 2/3 and open/password setup variants. Negative controls demonstrate rejection of stale live SDK AP state and revived idle bridges.

All eight release builds passed linked capability, internal RAM, and app-slot checks:

Target/profile MCU/core App bytes App slot bytes Heltec V4 Repeater Full ESP32-S3 / Arduino 2.0.17 2,100,584 6,553,600 Heltec V4 compact LoRa OTA ESP32-S3 / Arduino 2.0.17 1,283,304 1,310,720 portable ceiling Heltec V4 Room Server Full ESP32-S3 / Arduino 2.0.17 2,035,864 6,553,600 Heltec V4.3 Companion Full FEM off ESP32-S3 / Arduino 2.0.17 2,362,280 6,553,600 Heltec V4 Sensor Full ESP32-S3 / Arduino 2.0.17 1,493,976 6,553,600 Heltec V2 Repeater Full classic ESP32 / Arduino 2.0.17 1,675,432 3,342,336 Xiao C3 Repeater Full ESP32-C3 / Arduino 2.0.17 1,826,728 2,031,616 Heltec RC32 without display Repeater Full ESP32-S3 / Arduino 3.3.11 2,277,304 6,553,600

Full V4 SHA-256: ceb901afaba5f8012c49094122e30612dbf08518ebeffa51d8f92375cb52bc02; running body hash 8D26967FCC1A4984; target ID E792A051. Compact V4 SHA-256: 7b00131b795b236ce42cf62a824fb5e30a3f934034a819bf5d7a5d71a55983c9; running body hash EF44A3B4FE567589; target ID 4D015420.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#physical-checks","title":"Physical checks","text":"

Mercerwood's Heltec V4.3 OLED was identified through its exact USB serial, board, Repeater role, hardware family, running image hash/size, target ID, and expanded two-slot partition layout. Only application slots were updated; partition table, bootloader, identity, and ACL were preserved.

Boundary Result Automatic boot with no SSID, temporarily saved WebUI/ESP-NOW enabled PASS: fresh boot, working discovered/joined setup AP and cached scan results; unused ESP-NOW stayed suspended. Temporary saved flags were restored before manual cases. Explicit ESP-NOW off/on, then manual WiFi on PASS: ESP-NOW remained on through WiFi startup, AP connection, and portal stop. Master 2.4 GHz off/on PASS: individual on rejected while master off; both independently requested WiFi and ESP-NOW restored afterward. Full AP after WiFi toggle, ESP-NOW toggle, master restore, and OTA stop PASS: fresh beacon, association, HTTP 200, setup UI/API, cached scan results, and requested ESP-NOW ownership. Full OTA AP after master off and repeated after stop PASS: fresh beacon, association, HTTP 200, usable update page. Compact explicit AP, restart after stop, and implicit start ota PASS in all three cycles: fresh beacon, association, HTTP 200, lightweight raw uploader. Full \u2192 compact \u2192 Full application uploads PASS: each exact qualified image/target and application slot switch verified; final Full body 8D26967FCC1A4984. Restoration PASS: original saved preferences, public identity, ACL, runtime radio flags, Pi WiFi, and wired route verified; temporary profiles removed, serial released, no cleanup errors.

The compact direct-build alias displays env:? because the optional target-name catalog has no entry for its ID. The harness stopped on that display after upload, then resumed only after exact qualified body hash, size, version, hardware, role, target ID, and slots were independently proven. Unknown bodies, wrong IDs, and wrong named environments still reject. That guard stop is retained in the sanitized evidence; it was not counted as a firmware pass.

The final node runs the new Full image with its original settings. It still has saved power saving off and USB logging enabled. USB is attached and no current meter is available, so actual sleep/current is unmeasured. The automatic setup window expiration and sleep eligibility are covered by production-code host tests; the physical check did not wait through the entire setup window.

Sanitized build, host-test, and physical evidence: esp32_full_power_ap_2026-10-06.safe.json. Actual ESP-NOW peer packet exchange, the reported iPhone client, saved-network LAN recovery, and other boards' physical current/AP behavior were not qualified by this transaction.

"},{"location":"test-results/esp32_full_power_ap_2026-10-06/#board-scope-and-limits","title":"Board scope and limits","text":"

The fixes use shared ESP32 code: Repeater/Room lifecycle, CommonCLI Full defaults, ESP32Board browser OTA/sleep, and Companion setup. Other ESP32 boards inherit the applicable fixes when rebuilt through their matching release profile. No normal ESP32 board sleep override or alternate production AP path was found. Standalone partition migration/legacy seed utilities retain separate AP code. nRF52, RP2040, and STM32 do not use these ESP32 changes.

Build and host-test coverage does not establish each board's battery current. Raw UART consoles, active BLE/controller, GPS UART, Ethernet/serial Companion interfaces, native USB host grace, primary ESP-NOW, and intentionally enabled logging can still inhibit manual sleep. Saved power-saving off also remains off after updating. There is no claim of 11\u201314 mA consumption from this validation.

Future AP acceptance must verify fresh beacon discovery, association, working pages/API, scan results, owner preservation, and cleanup on physical hardware. CLI success and mocked SDK state alone are insufficient.

"},{"location":"test-results/esp32_management_password_2026-10-19/","title":"Esp32 management password 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/esp32_management_password_2026-10-19/#esp32-management-password-hardware-check-2026-10-19","title":"ESP32 management password hardware check, 2026-10-19","text":"

PASS on one physical XIAO S3 WIO Full Companion at Mercerwood. The same admin password worked for the setup and OTA WiFi hotspots, WebConfig login, and authenticated WiFi firmware upload at source 036fb477. Password changes through the production browser, automatic reauthentication, and persistence after reboot and OTA passed. The original firmware/data and the Pi's networking were restored and verified. This does not qualify every ESP32 board, firmware role, migration path, or the release.

A sanitized result record preserves the source/artifact hashes and check outcomes without credentials or device IDs.

"},{"location":"test-results/esp32_management_password_2026-10-19/#candidate-and-recovery-baseline","title":"Candidate and recovery baseline","text":"Item Recorded value Physical board XIAO S3 WIO Firmware target Xiao_S3_WIO_companion_radio_full Source 036fb47717774a1fb13e824175462ad4442d00c7 Candidate version v1.18.0.1-halo-keymind-cascade-dev-036fb477 Application BIN size 2,271,096 bytes Application BIN SHA-256 2419a991b7421b8444b0f39e4e941a60f9d4e5bcfb3a187ca77a0a2d41d31dcf Pretrial full-flash backup size 8,388,608 bytes Pretrial full-flash backup SHA-256 9517ff0caa61ae5ae2be95d4dfd2ab28a58e979eb1c7607b857055b9fb5f1445

The pretrial full-flash image and raw hardware evidence remain private. The independent restoration readback matched this full-flash hash before runtime handoff. Candidate application and full-flash backup hashes identify different artifacts.

"},{"location":"test-results/esp32_management_password_2026-10-19/#physical-results","title":"Physical results","text":"Check Observed result Eight-character printable ASCII password WPA-protected setup AP was usable with the admin password. Fifteen-character ASCII password containing spaces and punctuation, saved through the API Protected AP operation passed with the changed credential. Five-character password Setup AP became open; configuration still required the admin HTTP login. Return to a twelve-character printable ASCII password Protected AP operation passed again. Session rotation Old HTTP cookies were rejected; the originating operation retained its scoped save receipt. Warm reboot persistence The new password remained usable after an actual Binary Companion reboot. Production browser password form Login, exactly one password save, scoped receipt, automatic reauthentication, and actual protected-AP rejoin passed with the new password. OTA hotspot and HTTP authentication The same current admin password joined the protected OTA AP and authorized HTTP Basic as username admin. Anonymous, wrong and previous credentials returned HTTP 401 on /, /update and /log. Empty OTA requests Anonymous POST returned HTTP 401; authenticated empty POST returned HTTP 400 before image installation. Real WiFi application upload One raw application upload sent exactly 2,271,096 pinned bytes; HTTP 200 and the exact install/reboot response were received. Post-OTA USB Candidate full version and public identity matched; 350 contacts, 40 channel slots and zero core error flags were retained. Post-OTA password persistence The browser-saved password joined the protected setup AP and authenticated WebConfig after OTA; the previous password returned HTTP 401. Alternate OTA slot CRC-selected slot changed from original app0 at 0x10000 to app1 at 0x340000; its application bytes matched the candidate SHA-256.

These are real-board association, HTTP API and production browser password results. Chromium used the normal home-page loader and the decoded loader/UI bodies matched the candidate's embedded hashes; native fetch and production handlers were unchanged. One new-password login was observed, and the browser was closed after success. This does not establish browser firmware-upload completion behavior. The reboot was a software warm reboot; it does not establish power-removal persistence or a battery-only cold start.

The expected policy is documented in WiFi setup, Full Companion, and the password command. Password classes are recorded here without exposing the test secrets, network names, BSSIDs, cookies, or private device identity.

"},{"location":"test-results/esp32_management_password_2026-10-19/#original-device-and-host-state-restored","title":"Original device and host state restored","text":"

The original full app0 and app1 partitions, NVS and SPIFFS were restored, with complete readback verification after every write. Original OTA metadata was restored last. Bootloader, partition table and coredump bytes were unchanged. An independent 8,388,608-byte readback then matched every byte and the SHA-256 of the fresh pretrial backup, before the firmware was started again.

The separate normal-boot check passed on the original 652289ee build: complete SELF_INFO and DEVICE_INFO records, full CLI version, all 350 raw contact records, and canonical contents of all 40 channel slots matched the baseline. Core error flags were zero and the USB session closed normally. Whole-flash equality is a preboot observation; normal firmware may write flash after boot.

The Pi's original WLAN profile was restored by its recorded UUID. All 12 owned temporary profiles were removed. The original Ethernet connection, default routes and selected Internet route remained unchanged. No factory reset or bootloader/partition-table/coredump write was used.

The same candidate version was used for the initial USB installation and the WiFi upload. Installation is established by the success response and the independent alternate-slot application readback, rather than by a version string alone. The authenticated /log request returned HTTP 404 because no packet log was present; unauthenticated access was rejected before that check.

"},{"location":"test-results/esp32_management_password_2026-10-19/#retained-failed-samples-and-limits","title":"Retained failed samples and limits","text":"

Early attempts did not count as passes. An incorrect identity offset in the test manifest was rejected before HTTP mutation. The first browser attempts hit a startup deadline or requested the versioned UI asset without its required version token; the completed browser run used the normal production loader. A separate initial HTTP connection failed before browser startup; a fresh AP scan and guarded rejoin restored access.

A previous WiFi key did not associate within the 20-second wait, but NetworkManager reported a timeout rather than the test's expected wrong-key error. That sample establishes only bounded non-association. The HTTP old-key rejections and successful new-key associations above are separate observations. This campaign does not establish uninterrupted WiFi operation or cold-boot behavior.

A scripted raw upload does not by itself qualify the unchanged browser uploader. The broader ESP32 browser OTA release gates remain separate, as do two-source recovery-AP login throttling, infrastructure roles, partition migration, and other hardware families.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/","title":"Espnow bridge diagnostics 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#esp-now-bridge-diagnostics-22-october-2026","title":"ESP-NOW bridge diagnostics, 22 October 2026","text":"

Status: host tests and both Full repeater builds PASS. No radio was flashed or contacted in this work. The reported V4/V4 R8 TFT bridge delivery failure remains physically unverified; matching saved settings and espnow.running on do not establish peer receipt.

Clean firmware source 400878207 contains the diagnostics. Both canonical builds used the Full profile, Cascade runtime defaults, USA Cascade radio preset and version v1.18.0.1-halo-keymind-cascade-dev-40087820. The safe build receipt records source, artifact hashes and linked memory measurements. Later documentation-only changes do not alter these firmware artifacts.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#investigation-and-changes","title":"Investigation and changes","text":"

Both V4 variants use the same ESP-NOW bridge implementation. No display-specific forwarding difference was found. bridge.source rx is a valid choice: it exports LoRa receptions. If both boards directly hear the same LoRa message, normal mesh duplicate suppression can discard a later bridged copy. This is a condition to measure, not an established cause of the reported failure.

Normal builds previously compiled out the bridge's verbose debug path. Runtime USB logging/debug can now produce bounded ESP-NOW summaries without a special build flag. Status exposes the configured and actual 2.4 GHz channel, SDK state, queue depths and historical error stage/code. Separate counter pages locate RX decode/admission, TX queue/start/completion and filter/duplicate decisions. No secret is printed.

The local-only espnow test command sends a single bounded diagnostic probe. The peer consumes it before mesh allocation or LoRa forwarding. It has no echo or retry and does not modify saved settings. The probe checks 2.4 GHz reachability only; it is unauthenticated and does not validate the wrapped secret or normal packet format. Confirm receipt using the peer's increased rx count and matching rxid, then test the other direction. SDK send success alone is not application receipt; see the Espressif ESP-NOW documentation.

Use the diagnostic procedure and CLI reference for the counters and exact command grammar. The USB summary requires both usb.logging on and usb.debug on; record and restore both original values after a short capture. Counter pages and probes need neither logging gate.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#build-results","title":"Build results","text":"Canonical target Selected Full environment Available internal bytes Required heap bytes Result heltec_v4_repeater heltec_v4_repeater_observer_mqtt 202,576 192,656 PASS heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt 200,344 190,608 PASS

The largest linked internal regions are 169,808 and 167,576 bytes respectively, against a required contiguous allocation of 21,184 bytes. These are linked capacity estimates, not runtime free-heap, fragmentation or maximum-load measurements. Application and merged packages, memory gates, OTA packaging and linked capability verification pass for both targets. Both capability receipts report the Full profile with no feature reductions.

An initial attempt at source 14682de00 compiled but was rejected by the immutable-input packaging guard because generation changed the embedded web page header. That header was committed in 400878207; both qualifying builds then passed from clean source. The rejected attempt is retained privately and is excluded from the safe receipt.

"},{"location":"test-results/espnow_bridge_diagnostics_2026-10-22/#verification-and-limits","title":"Verification and limits","text":"

The host validation passed for:

Callback work remains bounded and does not print or allocate. Counter snapshots and resets use the existing RX/TX locks. Diagnostics are guarded by ESP-NOW bridge support, including the CLI callbacks, so unrelated firmware roles do not acquire the new callback interface.

No physical WiFi coexistence, MQTT coexistence, LoRa delivery, probe exchange or USB debug capture was performed. Actual pair qualification still requires the new readings and fresh LoRa traffic in both directions. Stop/restart can discard pending queue work without classifying delivery; lifetime counters are not a complete end-to-end delivery ledger.

"},{"location":"test-results/firmware_variant_consolidation/","title":"Firmware variant consolidation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/firmware_variant_consolidation/#firmware-variant-consolidation-validation","title":"Firmware variant consolidation validation","text":"

All ten packaged profiles passed. Firmware source: b31d942b231360386fdfb92d0561f36095fdf011. Version: v1.18.0.1-variants-b31d942b. PlatformIO builds ran one at a time. The packages were built locally; physical testing of these combinations remains pending.

"},{"location":"test-results/firmware_variant_consolidation/#qualified-behavior","title":"Qualified behavior","text":"

Future release selection retires an alternate choice only after its exact successor is qualified. Existing published binaries, filenames and update identities retain their original meaning. Switching between different target identities requires the documented local installation or an exact compatible update route.

"},{"location":"test-results/firmware_variant_consolidation/#packaged-applications","title":"Packaged applications","text":"Profile Application bytes RAM admission margin (bytes) Application SHA-256 prefix Pico W combined Companion 902,956 58,540 246b07c54f5c V4 R8 TFT Full Companion 2,287,528 66,528 19170cc8866b V4 R8 TFT Full Repeater 2,297,368 5,736 0837183ff083 V4 R8 TFT Full Room Server 2,302,648 7,784 28ffdb3bd325 V4 R8 TFT Full Sensor 1,590,952 131,472 064c4c98d2b6 V4 OLED Full Repeater 2,283,496 5,904 73ec156914e8 V4 TFT Full Companion 2,281,768 68,176 85c2dcaa00e8 Muzi Base Uno Full Companion 580,640 32,704 0d9fbe41f3ed Muzi Base Duo Full Companion 571,792 32,784 d5970c20227f T-Echo Lite Full Companion 595,896 28,928 91495097a524

The machine-readable evidence contains the full application SHA-256 values, hashes for every offered BIN/merged BIN/UF2/DFU ZIP, exact requested and packaged targets, source version, hardware/update identity where present, detection markers and RAM values. The application hash covers the ESP32 BIN, nRF52 DFU application's BIN, or decoded RP2040 flash application; container hashes are recorded separately.

RAM margins are linked capacity after the complete modeled heap requirement and transient reserve. They are admission results; runtime load and physical current were not measured.

The verifier checked every memory manifest's file bindings and RAM admission. It read the promised marker and exact firmware/source version from each uploadable application, compared ESP32 application bytes with their merged install, compared nRF52 DFU application bytes with decoded UF2, and required Pico W's decoded UF2 to match its BIN. UF2 headers, unused sector bytes and alignment padding cannot provide application capability evidence. Observer-named ESP32 packages retain their actual target and embedded observer version in the JSON.

"},{"location":"test-results/firmware_variant_consolidation/#software-and-browser-checks","title":"Software and browser checks","text":"Check Result Native R8 rotation, Muzi accessories and T-Echo Lite detection PASS, 11 methods across three suites; production paths, sanitizers and negative controls RP2040 application admission and existing capability checker PASS, 11 RP2040 methods and 18 existing methods; malformed UF2, wrong family/address/layout, BIN mismatch and metadata-only proof rejected Consolidation policy, release controls and legacy compatibility PASS, focused Python suites; packaged marker/config/source gating, paired sensor policies and unchanged historical downloads Picker JavaScript PASS, five suites covering consolidation, Full defaults, explicit Reduced links, historical metadata and runtime directions WebConfig source checks PASS, 13 methods WebConfig browser behavior PASS, 25 methods in the real Chromium headless browser, including saved rotation load/change Firmware picker browser behavior PASS, 14 DOM checks across Full default/Advanced interaction, explicit Reduced links and stale metadata Complete build-profile selection suite PASS Actual package controls and picker replay PASS, 10 qualified profiles; R8 rotation commands offered for all four roles Physical boards/accessories, sleep current and radio/transport load Not performed

The production controls generator and picker used release metadata assembled from these exact local files. Public firmware gains the new behavior when a matching qualified image is published. Full sensor support is the ordinary picker default; Advanced retains Reduced images and explicitly linked Reduced selections. Release policy keeps both Full and Reduced downloads and their exact update endpoints.

"},{"location":"test-results/firmware_variant_consolidation/#physical-acceptance-still-pending","title":"Physical acceptance still pending","text":"

These checks are required before describing the hardware combinations as physically qualified.

The earlier optional-screen record covers separate RC32 and Tracker smoke packages; those are not part of this ten-profile matrix.

"},{"location":"test-results/four_fixed_tx_dwell_validation/","title":"Four fixed tx dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#four-fixed-transmitters-longer-dwell-repeats","title":"Four fixed transmitters: longer dwell repeats","text":"

Same physical MercerMesh fixture and installed firmware as the completed 5.1-symbol run: RAK4631, Heltec T096, MeshTower V2 and T1000-E fixed transmitters; Heltec V4 RX. SF10 / BW125 kHz / CR4/5, four channels spaced 1 MHz apart, 32-symbol preamble, normal SX1262 RX gain, -9 dBm chip TX setting, zero added settling and one frequency write per hop. No firmware was rebuilt or reflashed for these repeats.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#61-symbol-run-user-stopped-incomplete-sample","title":"6.1-symbol run: user-stopped, incomplete sample","text":"

Started 2026-09-14 22:44:47 UTC. Dwell was 49,972 us per channel. All four stationary controls passed 100/100 again.

The user requested stopping this run and proceeding to 7.7 symbols. The collector was interrupted only after verifying its PID, command line and owner. At the last saved checkpoint, 342/348 scored scan packets were received (98.28%), with six timeouts:

TX / channel Scored attempts Received Timeouts RAK4631 / 0 87 87 0 Heltec T096 / 1 87 86 1 MeshTower V2 / 2 87 85 2 T1000-E / 3 87 84 3

This is not a completed 400-packet run or a validated full-run timing capture. An interrupted in-flight packet can exist beyond the last saved checkpoint; it is not scored as another success or failure. No final switch timing was collected before reboot. The raw capture remains incomplete, and the separate user-stop record documents why. All five radios were then independently verified idle, and ModemManager was restored. Gateway services stayed active; the V4 memory-soak service stayed inactive.

Partial raw capture SHA-256: ad199124930da19fe56c78a4ea8280a6d94b95ef408589b19a9e165155dbf7fd.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#77-symbol-run","title":"7.7-symbol run","text":"

Launched 2026-09-14 22:58:14 UTC; collector ran from 22:58:15 to 23:11:45 UTC with 63,079 us visits. All four fixed-channel controls passed 100/100. The full scan completed with 395/400 received (98.75%), five timeouts and zero wrong-channel or invalid payloads. Thus 7.7 symbols did not achieve a 400/400 pass.

TX / channel Scanning result Timeouts Mean received RSSI (dBm) Mean received SNR (dB) RAK4631 / 0 100/100 0 -21.05 +8.14 Heltec T096 / 1 99/100 1 -4.35 +8.98 MeshTower V2 / 2 97/100 3 -44.86 +8.63 T1000-E / 3 99/100 1 -18.56 +8.26

All 800 baseline/scan TX sequences were unique, all transmit calls succeeded, and every TX packet reported zero frequency and modulation configuration writes. There were zero packet-response transport replays. All four final TX lifetime checks reported 200 packets and a prepared, non-failed transmitter.

The five timeouts occurred at scan attempts 158 (T1000-E), 184 (T096), and 200, 293, 314 (MeshTower). Each reported RX mode 1 and zero device errors. Final RX failure, RX-mode-error, cache-error and receive-error counters were zero.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#measured-switch-and-sweep-timing","title":"Measured switch and sweep timing","text":"Hop population Count Minimum (us) Mean (us) Maximum (us) All single-pass switches 5,046 438 524.146 804 Cached modulation; no 0x8B 4,651 438 502.147 686 Post-reception modulation refresh 395 760 783.180 804

Exactly 5,046 RF-frequency commands and 5,046 optimized RX resumes were counted, with one retune pass and zero second passes/detours. The 395 modulation writes match the 395 post-reception cache refreshes. This is the same cache behavior as the earlier run, not double-setting the radio. The slightly higher overall mean than the 5.1 run partly reflects a larger fraction of refresh hops.

Across 860 measured idle cycles, the four-channel sweep averaged 254.351 ms, minimum 254.208 ms and maximum 255.482 ms. Relative to the 262.144 ms programmed preamble, that leaves 7.793 ms at the mean and 6.662 ms at the maximum. Nominal dwell was 63.079 ms; measured idle dwell averaged 63.084 ms. Packet-held visits include payload/guard time and are not comparable to idle cycles.

The 7.7 run received three more packets than the earlier 5.1 run (395 versus 392 out of 400). This small, sequential comparison does not establish a reliable improvement or a lossless dwell threshold. The 6.1 result is partial and must not be treated as an equal-size completed comparison.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#evidence-and-cleanup","title":"Evidence and cleanup","text":"

Unlike the original 5.1 run's overly strict host assertion, this collector completed normally with complete: true and exit code 0. Independent offline validation also passed. The same collector SHA-256 was used for both repeats: cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f. The copied deployment/manifest files are provenance for reused firmware, not records of a new flash operation. Host options and acknowledged dwell match.

Raw capture SHA-256: 5e3f9a80af1384c3072303428b75c8550ac42d2ac95bd1234bf92cb0aea625e8.

The cleanup wrapper retained three errors from its reboot phase: the T096 primary USB interface was temporarily missing/ambiguous, and attempts to open the MeshTower/T1000-E ports returned permission denied. Its subsequent final readbacks nevertheless succeeded for all five radios: each TX was unprepared, non-failed, packet count zero and autonomous TX disabled; RX scanning was inactive with zero channels. No further radio access or permission changes were attempted after those results returned. The wrapper's verified_idle flag remains false because it retains any earlier cleanup error, and the outer operation returned nonzero for that reason. These warnings happened after the complete RF sample; they are not counted as RF packet losses or silently removed from the record.

ModemManager was restored active; gateway services remained active. The V4 memory-soak service remains inactive with test firmware installed. No firmware restore, commit, push or pull was performed. Previous raw captures are unchanged.

"},{"location":"test-results/four_fixed_tx_dwell_validation/#dwell-budget","title":"Dwell budget","text":"

One chirp at SF10/125 lasts 8.192 ms. For the experiment's nominal budget of one complete four-channel idle sweep inside 32 programmed preamble symbols:

maximum dwell symbols = 32 / 4 - switching_ms / 8.192.

The 5.1-symbol run's mean switch (0.513980 ms) gives about 7.937 symbols; its maximum switch (0.801 ms) gives about 7.902 symbols, before other loop overhead. These are timing ceilings, not proven lossless dwell limits or a complete acquisition model. Near the ceiling there is little timing margin. At 7.7 symbols, four nominal dwell-plus-average-switch intervals total about 254.4 ms, versus a 262.144 ms programmed preamble.

The host keeps the same seed and arrival-randomization rule, spanning about two nominal scan cycles; absolute arrival pauses therefore scale with dwell. The fixed-channel controls, sample counts, switching policy and firmware stay unchanged. As before, external PA/LNA paths and unrelated RF traffic mean this is a finite bench comparison, not a calibrated sensitivity or interference test.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/","title":"Four fixed tx single pass validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#four-fixed-transmitters-fast-single-pass-receiver","title":"Four fixed transmitters / fast single-pass receiver","text":""},{"location":"test-results/four_fixed_tx_single_pass_validation/#result","title":"Result","text":"

MercerMesh Pi bench run, started 2026-09-14 22:18:49 UTC (15:18:49 PDT): four physically separate, fixed-channel transmitters and the Heltec V4 receiver. The full stationary controls passed 400/400. The subsequent four-channel scan received 392/400 correctly (98%), with 8 timeouts (2%) and zero wrong-channel payloads. No RF retries were used.

TX / assigned channel Frequency (MHz) Fixed-RX control Scanning RX Scan mean RSSI (dBm) Scan mean SNR (dB) RAK4631 / 0 909.5 100/100 96/100 -20.69 +8.63 Heltec T096 / 1 910.5 100/100 98/100 -3.84 +8.45 Heltec MeshTower V2 / 2 911.5 100/100 100/100 -45.48 +8.41 Seeed T1000-E (LR1110) / 3 912.5 100/100 98/100 -18.27 +8.51

RSSI/SNR describe received packets only. Each transmitter sent 200 unique packets across its baseline and scan. All 800 transmit calls returned success, with zero frequency or modulation commands during those packet calls. There were zero recovered/replayed USB responses in the packet records.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#switching-timing-and-command-evidence","title":"Switching timing and command evidence","text":"Hop population Count Minimum (us) Mean (us) Maximum (us) All single-pass switches 7,206 429 513.980 801 Cached modulation; no 0x8B 6,814 429 498.602 686 Modulation cache refresh 392 763 781.291 801

The receiver recorded exactly 7,206 frequency commands and 7,206 optimized RX resumes: one frequency write and one resume per hop. Second-pass count was zero. Duplicate frequency writes, frequency/CR detours and added settling were disabled. Warm XOSC, bulk 8 MHz SPI and cached modulation were enabled.

The average switch was 0.514 ms, but not every switch was below 0.6 ms. After each received packet, ordinary RX staging invalidates the modulation cache; the next hop refreshes it. Subsequent unchanged hops omit 0x8B. The 392 refreshes match the 392 received packets. The source path is RadioLibWrapper::recvRaw() -> startRecv() / startReceiveMode() -> PhysicalLayer::startReceive() -> CustomSX1262::stageMode() -> SX1262ProfileSwitchState::invalidate(). No production cache-invalidation behavior was changed for this test.

Mean switch-plus-settling-instrumentation time was 515.698 us, maximum 803 us. The settling measurement averaged 1.718 us of timer/measurement overhead; the configured settling delay was zero. The idle four-channel sweep averaged 169.135 ms (maximum 170.470 ms), versus 262.144 ms for the 32-symbol preamble. Packet-held visits include reception/guard time and are not idle sweep timings. The deferred counter counts guard-loop deferrals, not lost packets.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#method-and-fixture","title":"Method and fixture","text":" Role Hardware serial / MAC Pi USB topology TX0 RAK4631 9AB3B64C641BA927 1-1.3.1 TX1 Heltec T096 651F8E496197F882 1-1.2.3 TX2 Heltec MeshTower V2 9352162A72082314 1-1.2.2 TX3 Seeed T1000-E LR1110 34A9141999729D5D 1-1.2.1 RX Heltec V4 44:1B:F6:69:CF:98 1-1.3.3

Board selection checked serial plus topology, not transient tty names or CDC interface count. The RAK3401 gateway, serial 0B81C9C68D8D01B4, was excluded. The earlier local XIAO radios were not modified during this run.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#remaining-losses-and-interpretation","title":"Remaining losses and interpretation","text":"

Timeouts occurred at overall scan attempts 10, 14, 55, 65, 145, 299, 304 and 384. Thus they were not confined to startup. Each timeout reported RX mode 1 and zero device errors; TX calls returned success and still reported no configuration writes. Final RX failure, RX-mode, cache and receive-error counters were all zero.

The wrong-channel behavior did not reproduce in this configuration. The remaining misses do not require a transmitter that retunes between packets. However, this is not a controlled A/B against the earlier XIAO setup: the RX board, TX boards/chips, antennas and geometry changed. These results do not prove that the earlier transmitter was faulty or establish the cause of the eight remaining losses.

Several received signals were very strong, particularly the T096 at about -4 dBm. The weakest link, MeshTower V2 at about -45 dBm, passed 100/100 while scanning. That makes a controlled spacing/attenuation comparison worth considering, but this sample does not prove receiver overload. The observed 98% is a finite bench result, not a reliability guarantee or sensitivity test.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#capture-integrity-and-post-run-validation","title":"Capture integrity and post-run validation","text":"

All 400 scan probes finished and final radio telemetry was returned. The original host checker then rejected that telemetry because it incorrectly required zero modulation writes over the entire run, including post-packet RX staging. Its raw capture therefore remains complete: false, with final status preserved verbatim inside its error string. The normal sender_final collector step did not run. The raw failure record was not rewritten.

Source review established the cache-refresh behavior above. The host checker now validates modulation writes against the measured refresh-hop count, checks that cached plus refresh hops equal total hops, and still requires single frequency writes, single fast resumes, zero added delay and the other policies. It also saves final telemetry before checking it, so future failures retain that structured record.

Separate offline validation parsed the saved telemetry, checked the full 400-probe scan and all four 100-probe controls, verified 800 unique sequences, and checked every transmitter's packet counter from 1 through 200 and its zero-retune acknowledgments. A capture regression additionally checks that each TX/RX response ID matches its probe and every accepted payload/channel matches the assigned channel. No radio was rerun to replace the failed host assertion. The derived summary explicitly reports original_collector_complete: false and post_run_validation_passed: true.

Evidence files:

Raw capture SHA-256: 42de3ac0c335b2c59f35c1cab217a01ef242c9f2e7b0010c8000a908a59232e6. Derived summary SHA-256: 316bd551eb7306dfc576f4b88ad23e82cb1d701481d1e022067c961806d31bfe.

"},{"location":"test-results/four_fixed_tx_single_pass_validation/#build-deployment-and-final-state","title":"Build, deployment and final state","text":"

Built from the dirty keymindCascade worktree based on 2b5a6f209835f0f67ed73fee78d36447a4566ce6. This is not a claim that HEAD alone reproduces the images. The manifest records image and HIL-source hashes. All five PlatformIO builds succeeded and ran sequentially.

After the host-checker correction, 25 focused unit/regression tests passed, including the preserved-capture check. git diff --check passed; its only warning concerned a pre-existing CRLF conversion in MyMesh.cpp.

RadioLib was pinned to 187ef24791c3d844939b2be13a68bd890bd04e4c (7.7.1). The nRF framework was d5413016 (1.10701.0); the ESP build used platform 6.11.0 / Arduino 2.0.17. The first three nRF targets retained S140 6.1.1 (FWID 0xB6, app base 0x26000); T1000-E retained S140 7.3.0 (FWID 0x123, app base 0x27000). nRF DFU packages were application-only, with their matching SoftDevice requirements intact. No nRF bootloader/SoftDevice replacement or cross-board override was used. Runtime FWID and application base were verified.

V4 received the normal ESP bootloader, partition table, boot_app0 and HIL app at 0x0, 0x8000, 0xE000 and 0x10000, respectively, with esptool hash checks. No whole-flash bulk erase was issued. No backup was requested for these authorized bench deployments.

The initial bundle's collector/deployer hashes are not the final scripts used: the actual run used profile_four_tx_v2.py with SHA-256 b762e412636076902174c76b71961cc8ab3059c2b6de3dd3b4123bafcb06545c (startup-greeting fix, original strict final assertion), and deployment used profile_four_tx_deploy_v3.py with SHA-256 8a660788e0215e6ac1aece0a5d033521f9f376c9e51c00e760e918ebfd72cca1. The current local collector includes the later validation correction; firmware images were not changed after the run. Remote evidence remains under /home/mikec/hwtest/runs/four-tx-sf10-20260914.

All five radios were subsequently rebooted and independently verified idle. All TXs reported unprepared, packet count zero, no failure and autonomous TX disabled; V4 reported scanning inactive and zero hops after reboot. HIL firmware remains installed. ModemManager was restored active, gateway services were left active, and the V4 memory-soak service remains inactive because its firmware is now the bench receiver. No Pi USB hub power cycle was performed.

No commit, push or pull was performed for this test. Unrelated worktree changes were preserved.

"},{"location":"test-results/full_usb_logging_validation/","title":"Full usb logging validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/full_usb_logging_validation/#full-usb-logging-repair-11716","title":"Full USB logging repair - 1.17.1.6","text":"

Audit date: 2026-09-13. Release firmware source: 306feebe6d648a247f925894f876be9d747fc8d2.

This is historical qualification of the 1.17.1.6 images, not an all-board runtime test of later firmware. Current source separates the saved USB output master (usb.logging) from opt-in diagnostic verbosity (usb.debug, default off), and adds full ASCII RAW records to Full Companion. See current USB logging controls for defaults, legacy preference handling, capture-loss behavior and dedicated CDC/control-port restrictions. These source changes do not imply new hardware validation of the published images listed below.

The original Station G2 Full Repeater and Full Room Server images omitted USB packet logging. Both inherited -UMESH_PACKET_LOGGING from their ordinary MQTT observer recipes. Full added -DMESH_PACKET_LOGGING=1, but PlatformIO's ProcessFlags moves undefines after defines in the compiler command. The resulting image compiled successfully with its packet logger removed.

The previous release gate checked OTA and other features without requiring USB logging in Full infrastructure. An old passing manifest could also be reused by --resume without the new logging evidence.

"},{"location":"test-results/full_usb_logging_validation/#published-image-audit","title":"Published-image audit","text":"

Each downloaded/cached artifact was matched to GitHub's published SHA-256. The check inspected application bytes, including the application inside nRF52 DFU ZIPs. It required the packet-log formatter and USB logging controls.

Release group Application images Missing USB packet logging ESP32 Full infrastructure/utility 157 2 ESP32 Full Companion 52 0 nRF52 Full Companion 43 0 Total 252 2

Affected targets:

V4/R8 Full builds and Station G2's Full ESP-NOW bridge passed. This audit does not count ordinary MQTT profiles that intentionally omit USB logging as broken. Artifact inspection proves compiled support, not hardware operation on every board.

"},{"location":"test-results/full_usb_logging_validation/#fix-and-regression-coverage","title":"Fix and regression coverage","text":"

Focused verification (after installing PlatformIO and its tool-scons package):

python3 -B test/test_full_build_flag_dedup.py\npython3 -B test/test_full_logging_compile.py\npython3 -B test/test_firmware_capabilities.py\npython3 -B test/test_full_logging_resume.py\nbash test/test_build_profiles.sh\npython3 -B test/test_cascade_release_package.py\n

Both Station G2 profiles were rebuilt through build.sh --full with the original observer recipes, including their inherited logging undefines. They passed the new application checks, runtime RAM budgets, and both OTA slot size checks. The new checker rejects both original published images.

On the Bellevue Station G2, the corrected Full repeater implementation also passed serial CLI and passive traffic checks: four RX and two TX packet log lines were captured. Its active temporary profile retained preamble 120 and the original schedule end time; the Pi relay services were restored.

Corrected application SHA-256:

Repeater:    81a0a2926d4e567c069d0b10f316047881bcedce062afaa42119dddb2baadf23\nRoom Server: 7ee81f1cb65fc274bd363397c3c80ce9006d575efae50fe952c8ca246613a18c\n
"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/","title":"Hardware validation bluetooth stealth 2026 09 07","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#xiao-bluetooth-stealth-hardware-validation-2026-09-07","title":"XIAO Bluetooth stealth hardware validation - 2026-09-07","text":"

Historical command syntax: the initial run used stealth as a MAC mode. Current firmware uses the independent bluetooth.stealth on|off flag instead and does not migrate that development mode. See current commands and the independent-flag follow-up below. Each observation applies only to the exact artifact listed in its section.

This is a hardware observation report, not an all-board or phone qualification. The test host was the MercerWoodMesh Raspberry Pi Zero 2 W, accessed remotely over Tailscale. Tests used its built-in Bluetooth adapter, not USB resets or hub power switching. Local timestamps below use America/Los_Angeles.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#exact-target-and-firmware","title":"Exact target and firmware","text":"

The application was installed with Nordic Legacy BLE DFU. The tool verified the exact ZIP hash, bootloader advertisement and live DIS model XIAO nRF52840 before transmitting 616,584 application bytes. Target image validation and target-initiated activation disconnect passed. The transfer reported about 2,223 bytes/second (about 277 seconds for payload transmission). Authenticated MeshCore device queries subsequently returned protocol 14, board Seeed Xiao-nrf52, and the expected unique test-version prefix.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#observed-results","title":"Observed results","text":"Check Result and evidence First-pair discovery set bluetooth.mac stealth saved a new random-static address; reboot returned that exact address with the normal name and Nordic UART advertisement. First authenticated pairing The Pi received a passkey request and completed pairing. BlueZ reported both Paired and Bonded. MITM-protected Nordic UART notifications and real command/reply exchanges worked at MTU 247. Pairing transition keeps the session alive The initial connection returned stealth; bonded-peer-only advertising, remained usable for five more seconds, and answered another command without disconnecting or pairing again. Advertising after disconnect A public-address passive scan captured ADV_DIRECT_IND from the exact target, with advertising data length zero. The capture contained no undirected advertisement or scan response from that address. Bonded reconnect Subsequent direct connections by saved BlueZ device identity succeeded without a new passkey, including live version, core-statistics and Bluetooth-mode replies. Warm reboot persistence Two commanded reboots retained the address and saved bond. Directed advertising was captured after reboot, followed by authenticated reconnect. Device uptime decreased from 61 seconds before the second reboot to 35 seconds at the next query; core error flags were zero. Slow advertising interval A successful bonded reconnect occurred about 61 seconds after reboot, beyond the 30-second fast-advertising interval. Reopen pairing Setting stealth again from an existing bonded connection generated a new address, and the next boot allowed a fresh first pairing. Restore default set bluetooth.mac default plus reboot restored E5:C3:A8:B0:60:66 and normal discovery. Fresh PIN pairing, version/core-statistics queries and repeated factory address (default) replies passed. The bench target was left in this discoverable mode.

The newly completed pairing reaches Bluefruit's secured callback before its bonded flag is necessarily ready. Recording the completed pairing in the pair-complete callback as well, waiting for the deferred bond write, and deferring identity-list reconfiguration until disconnect are covered by this fresh-pair hardware run. Static source contracts also check those paths.

The binary reboot command can reset the target before BlueZ receives its ATT write acknowledgement. One initial harness run reported Unlikely Error after successful pre-reboot commands. Later checks require an observed disconnect, a live post-reboot query and decreased device uptime; they do not treat the write exception or tool exit code alone as a successful reboot.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#discovery-and-dfu-cautions-found-during-testing","title":"Discovery and DFU cautions found during testing","text":"

An ordinary BlueZ discovery scan used a temporary random local address and did not show the directed target. A passive scan using the Pi's public identity did show it. Reconnection must use the saved peer/device identity; absence from an ordinary scan is not proof that the node stopped working. Conversely, BlueZ can retain a cached name after pairing, so a displayed name is not proof that the name is still being advertised. The packet-type and zero-length-payload capture is the relevant evidence here.

The existing DFU handoff helper assumed the application and bootloader shared one address. That assumption is false with a custom application address: the authenticated handoff worked, but the helper timed out looking for DFU at the application address. A fresh exact-address scan found the known factory-address XIAO_DFU instead. An old factory-address host bond also caused the first DFU service-discovery attempt to disconnect; clearing only that old lab record allowed DFU. No bootloader code was changed for this test.

An earlier application-address host pairing record was also removed before its saved stealth state was known. A direct, known-address connection from the same Pi identity allowed fresh PIN pairing and protected commands again. This does not prove that an arbitrary replacement phone can rediscover or recover a stealth node after forgetting its bond.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#remaining-hardware-gates","title":"Remaining hardware gates","text":""},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#repeatable-regression-procedure","title":"Repeatable regression procedure","text":"
  1. Record the exact board, application version, ZIP hash, local/remote BLE identities and original Bluetooth setting. Keep a working recovery path.
  2. Set the desired custom or random MAC, enable bluetooth.stealth on, reboot, and capture normal discovery before pairing.
  3. Pair once. Require a stored bond and successful protected MeshCore commands, then keep the original session open and issue another command.
  4. Disconnect. Capture the actual advertising type and payload with an appropriate receiving identity; do not use cached scan names as evidence.
  5. Reconnect by the saved peer without another PIN and exchange commands.
  6. For custom and saved-random addresses, repeat bluetooth.stealth on, reboot twice, and verify unchanged identity/bond, decreased uptime and live commands. Include a reconnect after the fast-advertising period ends.
  7. Separately remove all target power and repeat the persistence checks.
  8. Test bluetooth.stealth off, then on, as a pairing reset, and off as the exit from stealth. Verify live discovery, pairing, commands and an unchanged MAC policy. Changing the address to default is a separate step.
  9. With each rotating MAC policy, verify that its rotation trigger still works, stealth stays enabled, and the new address permits fresh pairing.
  10. Repeat relevant gates for a phone with address privacy and each BLE backend.

Raw VM-side logs, the exact artifact and bounded test helper are in /home/mesh/mercer-stealth-validation.jUDW0v/. Raw Bluetooth captures are local diagnostics and should not be published without privacy review.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#mercer-crash-capture","title":"Mercer crash capture","text":"

During this run the Pi used its existing dwc_otg.speed=1 full-speed USB configuration. Kernel and health logs were streamed to this VM. No new kernel errors or Pi lockup were observed during these tests; that does not establish the cause of the earlier lockup.

The live test streams and all temporary scans were stopped at approximately 22:56 PDT. Journald remained active, the restore service remained enabled, and the test XIAO was disconnected in default mode. Disk journals occupied about 4.9 MiB at the final check.

At 22:35 PDT, temporary persistent journaling was enabled with a 16 MiB configured disk-use limit, 2 MiB journal files, one-day retention and a 30-second normal sync interval. Recent buffered messages can still be lost in a sudden failure. The original Storage=volatile configuration is intact.

mercer-crash-capture-restore.service is enabled for the next Pi boot. It checks the armed boot ID, removes only the unchanged managed override, switches journald back to RAM and leaves the old disk journals for collection. Its same-boot no-op guard and unit syntax passed. The next-boot restoration has not yet been exercised; no Pi crash or reboot was deliberately induced.

"},{"location":"test-results/hardware_validation_bluetooth_stealth_2026-09-07/#independent-flag-follow-up-dev-stflag","title":"Independent-flag follow-up - dev-stflag","text":"

The same XIAO and Pi were used later on 2026-09-07 to test the separate bluetooth.stealth on|off flag, with no development-mode migration. This run used Xiao_nrf52_companion_radio_ble, target radio preset, default profile, and firmware dev-stflag-4b400ea5 with the uncommitted flag changes.

Check Observed result Default and rejected input Initial mode was factory address with stealth off. set bluetooth.mac stealth and set bluetooth.stealth maybe returned errors, leaving both settings unchanged. Custom plus stealth D2:27:5A:40:06:99 advertised normally before first pairing. Fresh PIN pairing, stored bond, protected commands, and a five-second held session passed with stealth on. Repeated on and warm reboot Repeating set bluetooth.stealth on retained the paired state. After reboot, the same custom address accepted protected commands without another PIN. Uptime decreased from 84 to 67 seconds. Actual advertising after reboot A public-address passive capture showed ADV_DIRECT_IND for the custom address, data length zero, and no undirected advertisement or scan response from that address. The scan was explicitly disabled afterward. Off preserves custom address set ble.stealth off and reboot restored normal discovery at the same custom address. A live connection returned stealth off and the unchanged custom policy. Saved random plus stealth F2:2E:E5:65:18:5E passed first pairing and held commands. After reboot, it retained its address, saved-random policy and stealth bond; protected reconnect needed no PIN. Uptime decreased from 55 to 30 seconds. Random-after-connect plus stealth Selecting this policy kept stealth on and reopened pairing at C7:FD:29:44:4F:3E. Pairing produced both bonded-peer-only status and an armed next-boot rotation. Reboot changed the address to F5:F3:89:B7:B5:26, where normal discovery, fresh pairing and protected commands passed with stealth still on. Random-every-boot plus stealth The first boot used F9:B2:03:DD:CC:43, completed fresh pairing, and answered commands with stealth on. The next commanded reboot advertised F8:C7:C0:96:1E:00; fresh PIN pairing and protected commands passed again with the rotating policy and stealth on. Restore the bench target Setting the MAC to default kept stealth on until the separate set bluetooth.stealth off. After reboot, factory-address discovery, fresh PIN pairing and protected commands confirmed default MAC and stealth off. The obsolete factory-address host bond was replaced during this final pairing.

These checks used actual encrypted BLE connections at MTU 247, not scan sightings alone. Two first-pair harness attempts stopped before connecting because their unpaired BlueZ discovery object no longer existed. A fresh bounded scan immediately followed by pairing was used for retries; this was not a firmware GATT command failure. Reboot-write/disconnect races were accepted only with an observed disconnect and subsequent live device checks.

Local automated verification passed 39 native cases, 32 source/persistence/UI contracts, and 11 real-browser cases. Supported-wrapper builds passed for XIAO nRF52, XIAO C3, M5Stack Unit C6L and XIAO S3 WIO Full Companion, covering nRF52, ESP32 NimBLE and ESP32 Bluedroid integration. The contracts and browser suite are included in the unit-test workflow; a browser must be available for the optional browser suite to execute instead of skip.

Exact artifacts, build logs, command logs and the private packet capture are in /home/mesh/bluetooth-stealth-flag.ApLB8o/ on the VM. The remaining hardware gates above still apply; these results do not qualify every board or phone.

At 23:42 PDT the XIAO was disconnected in normal discoverable mode. The Pi retained the same boot ID throughout, remained responsive, and had no new kernel messages in the streamed log. Journald was active, the next-boot volatile-log restoration service was still enabled, and disk journals used about 5.9 MiB. No USB reset, Pi reboot, or target battery removal was used.

"},{"location":"test-results/hardware_validation_checklist/","title":"Hardware validation checklist","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_checklist/#hardware-validation-checklist","title":"Hardware validation checklist","text":"

This file records dated hardware observations and open validation tasks. Historical defaults, versions, and test frequencies below describe the tested revision, not necessarily the current release. For current ASCII USB defaults and runtime switches, use feature switches by role.

Use this checklist for release-candidate hardware runs. A check is complete only when its log identifies the physical device, firmware artifact, artifact hash, command result, and cold/warm boot outcome. Do not infer success from a tool's exit code when the tool has a documented false-success mode.

"},{"location":"test-results/hardware_validation_checklist/#known-failure-follow-up-2026-10-2021-utc","title":"Known-failure follow-up, 2026-10-20/21 UTC","text":"

The follow-up report binds builds, host tests and hardware observations to their actual sources. It preserves the earlier failures and the accepted fleet results.

The independent eight-board nRF52 test-bootloader deployment and original-app handoffs are complete with documented USB concurrency and RAK secondary-data preservation limits. This is separate from final-source Companion qualification. The nRF52 testing hold is released; MeshPocket remains excluded.

The current V4 differs from the physical board in the 18 October failure below. Its follow-up cannot establish that earlier USB disappearance's cause or erase the failed historical measurement. Remaining release gates keep their latest documented status.

"},{"location":"test-results/hardware_validation_checklist/#mercerwood-candidate-campaign-2026-10-18-utc","title":"Mercerwood candidate campaign, 2026-10-18 UTC","text":"

The current hardware campaign records actual candidate flashes, identity-bound USB/BLE and warm-contact checks, live Room authentication/browser results, and shipping Morse failures. It retains the original populated-board stack failures and the d52e4e93 repeated-read/browser retests that passed. The matching-profile trials established LoRa reception and paced live-session mailbox operation; durable replay, final-role testing and other release gates remain open. These observations do not constitute release approval.

The ec9ebc09 Full V4 logging-off native-console trial 03 passed 600.444 seconds with zero idle writes, complete replies/reopen and zero core error flags. The passed Room logging-on trial does not qualify Full logging-on.

Complete CI runs 39110179910 at 5fd33a230 and 39109551854 at 7a5a7993c are green. The native-console/TTY cleanup follow-up passed 35 offline checks. Both run 39115389840 at 76220d5d7 and run 39116894845 at 7b41d2893 completed successfully with all eleven jobs green. Those CI passes do not qualify the failed hardware case.

"},{"location":"test-results/hardware_validation_checklist/#pr1721-follow-up-qualification-2026-10-18-utc","title":"PR17\u201321 follow-up qualification, 2026-10-18 UTC","text":"

The merged source checkpoint is 0a24279a3. This audit adds host regressions, operator documentation, and a C6 GPIO capability fix; it is not a hardware qualification. No device was flashed or contacted during the audit.

Completed host coverage includes guest replay-store capacity and live-client eviction; per-address/shared WebConfig limits and room-password windows across millis() rollover; existing WebConfig sessions during login throttling; migration credential precedence and failed-identity upload denial; generated package instructions; and cached/uncached partial-contact recovery. The real Chromium room and WebConfig fixtures use mocked HTTP responses, so they do not prove the actual network/firmware interaction below.

The merge was compiled for Heltec_v3_room_server, heltec_v4_companion_radio_usb, esp32_s3_4mb_partition_migrator, and all four small STM32 USB Companions at release-version length. Follow-up builds cover Xiao_rp2040_companion_radio_usb and Arduino 3's LilyGo_Tlora_C6_room_server_. The C6 build initially exposed unavailable global deep-sleep GPIO hold APIs; the helper now follows the SDK's per-pad capability, with a fixture that omits the unavailable declarations. The CI cross-platform-smoke job retains both RP2040 and C6 build checks.

Additional migration builds passed for esp32_8mb_partition_migrator, esp32_c3_4mb_partition_migrator, heltec_v4_partition_migrator_lora_repeater, heltec_v4_partition_expander, and esp32_s3_4mb_partition_migrator_usb, including their application-size and runtime-RAM gates. This covers representative chip families and Wi-Fi, identity-only LoRa, Expander and USB code paths, rather than every board alias.

"},{"location":"test-results/hardware_validation_checklist/#physical-acceptance-checks-and-subsequent-campaign-results","title":"Physical acceptance checks and subsequent campaign results","text":"

Record the exact clean source revision, firmware SHA-256, device identity, observed result, and cold/warm boot outcome for each check. Record password classes and success/failure, never the passwords themselves. Checked entries below are later, candidate-specific campaign results; they do not change the host-only scope of the earlier 0a24279a3 audit.

The later d52e4e93 campaign completes the real-board browser throttle-error/retry/draft and reproduced repeated-read cases above. An executed configuration-restore-to-AP credential handoff remains open. The host AP fixture supplies pre/post-restore files; it does not run an entire migration. The current hardware campaign remains in progress and is not release qualification.

"},{"location":"test-results/hardware_validation_checklist/#esp32-web-console-and-browser-ota-release-gates","title":"ESP32 web console and browser OTA release gates","text":"

The 2026-10-05 V4/XIAO checkpoint records reproduced browser OTA failures, fixes, physical results, and remaining USB qualification limits. It is not a release approval.

The 19 October XIAO S3 WIO password campaign records Full Companion AP/password transitions, HTTP session rotation, production browser password saves, warm-reboot persistence and a real authenticated raw WiFi upload at 036fb477. Post-OTA password persistence, alternate-slot image readback, original device state and host networking all passed. This scoped result does not close the browser-upload gate below, two-source recovery-AP throttling, migration checks, or other board/role qualification.

Host tests cannot prove USB enumeration, WiFi discovery, or an HTTP upload on the physical board. Before qualifying an ESP32 release, record the following on each available hardware family. Mark unavailable hardware as untested; a successful build or simulated peripheral is not a hardware pass.

  1. Run the pinned, unmodified official web console, recording its source revision and hash. With power saving enabled, leave it idle for at least ten minutes without transmitted commands or keepalives, then require a complete CRLF-delimited command reply. Repeat with USB logging enabled and disabled. Close and reopen the console and verify that uptime did not reset.
  2. Flash through the official website's reset/handoff sequence. Record the delay until the first complete CLI reply and reconnect without unplugging or issuing another reset. A boot banner is not a command reply.
  3. Exercise start ota from WiFi off, connected LAN, WebConfig LAN, and WebConfig setup AP states. Require the reply to identify the reachable URL, required network, and WebConfig shutdown when applicable. Also exercise start ota ap while the LAN uploader is already running.
  4. Discover and connect to the actual board's hotspot. Measure its WiFi MAC; do not infer it from the USB serial number. Pin that measured identity before any firmware upload. Fetch the returned URL repeatedly across delayed WebConfig cleanup and background bridge retry intervals.
  5. With ESP-NOW configured on, exercise OTA across background bridge retry intervals. For builds that pause bridges during upload, require that pause to last until stop ota, then verify that the configured bridges resume.
  6. Upload a board-compatible, hash-verified application through the HTTP uploader's unchanged browser page. A scripted HTTP POST alone does not exercise the page's upload format or completion handling. Require the server's completion response, normal reboot, expected application version, and working USB console without an extra reset or unplug. Use a distinct target version so a reboot without an update cannot pass. Select the upload protocol from the compiled implementation: AsyncElegantOTA uses multipart data and an MD5 field; LightweightOTA uses a raw binary request. Confirm identity and settings were preserved, then restore temporary device settings and the test host's network connection.

Keep the host regressions (test_official_console_acceptance.py, test_official_web_console.py, test_wifi_ota_start.py, test_wifi_ota_lifecycle.py, and test_async_web_lifecycle.py) alongside these gates. They cover parsing, sleep policy, startup errors, network ownership, and deferred cleanup. A Started reply alone does not satisfy the physical OTA gate.

Record USB line states and reset reasons during these checks. An opener that briefly asserts the hardware reset combination invalidates a no-reset test. Keep its failed sample, correct the opener, and repeat under a new run name. Report startup readiness, uninterrupted idle, and close/reopen as separate results; a later successful command must not erase an earlier timeout.

"},{"location":"test-results/hardware_validation_checklist/#bluetooth-stealth-qualification-2026-09-07","title":"Bluetooth stealth qualification - 2026-09-07","text":"

The XIAO stealth report records exact artifacts for the initial implementation and independent-flag follow-up. The flag run covers custom/saved-random pairing and bonded reboot reconnect, idempotent on, off preserving the MAC, directed advertising, and both rotating policies with stealth still enabled. It also gives the repeatable hardware regression procedure. True battery-disconnected cold boot, phone address-privacy behavior, a second unbonded central, and ESP32 hardware remain separate uncompleted gates; do not infer them from the XIAO results.

"},{"location":"test-results/hardware_validation_checklist/#current-marathon-ledger-through-2026-09-04","title":"Current marathon ledger (through 2026-09-04)","text":"Hardware Stable identity Current state Next blocking check Seeed XIAO nRF52840 B35E71C1C3726CE7 OTAFIX candidate 0x02040405 passed exact serial install and corrected Legacy DIS model gates; the patched Bluetooth repeater-updater application was restored over BLE and returned as exact 2886:8044 USB Use the updater for an identity-gated Bluetooth transfer to another repeater, then prove current-build cold persistence Seeed Tracker T1000-E 34A9141999729D5D OTAFIX candidate 0x02040405 and the uf2reset-fixed Full Companion are installed. The real text command returned exact 2886:0057 boot USB, the same fixed app was restored, and refreshed bonded BLE services pass as MeshCore-09848C15 Restore the protected Repeater identity when Companion qualification is complete; cold boot, then force LR1110 reset RAK3401 0B81C9C68D8D01B4; FICR 8D8D01B4 0B81C9C6 OTAFIX test version 0x02040403 passes bidirectional application UF2, exact SWD application/bootloader readback, unchanged UICR, and a 74-block direct V4 LoRa apply from exact 3caf9dcf to current HEAD 9fd580c8. Post-apply USB and LoRa identity/hash checks pass and the temporary 0 dBm bench setting is restored to 22 dBm Add exact RF packet counters, then repeat the supported-bandwidth and controlled/passive/mixed routed-hop matrix Heltec T096 651F8E496197F882 OTAFIX candidate 0x02040405 and the uf2reset-fixed no-footer Full Companion are installed. The real text command returned exact boot USB, and final USB terminal/Binary mode, dual CDC, version, help, and BLE identity pass Perform a privileged or physical host-driven USB bus reset without resetting the MCU; complete the deferred multi-click/long-press physical matrix Heltec MeshTower V2 with SD 9352162A72082314 OTAFIX candidate 0x02040405 passed serial combined install, the historical cached DIS handles, BLE application restore, and exact application USB return. Card format/cooldown/forced-format/raw-erase/remount and the live SD apply contract passed earlier Signed interrupted-download resume, corruption/signature rejection, then full/delta apply Heltec V4 USB MAC 44:1B:F6:6A:E8:44; BLE 44:1B:F6:6A:E8:45 USB/BLE/Wi-Fi and all three TCP services pass on hardware. The final fresh-NTP-gated Full Companion image builds with 4.31 MB app space free Flash the exact final NTP build, prove NTP-before-TLS success/failure ordering, then OTA seeding SenseCAP Indicator LoRa CH340 plus USB MAC D8:3B:DA:75:23:AC; BLE D8:3B:DA:75:23:AD The current dark-layout application is SHA-256 81bebdc07b6a8349c1c975cb5a0e30c5f6019d35bcac641e5f3d4df951f7406b; identity-gated flash, USB ASCII/Binary switching, runtime logging, and separate configured-Wi-Fi, BLE, and TCP checks pass. Fresh-NTP HTTPS recovery, strict Range resume, STAGEV2 install, and RP USB readback passed on earlier exact artifacts; the final coordinator source still needs its blocked-UDP/123 hardware gate Physical display-wrap and four-mode render/heap checks, exact-final blocked-NTP recovery gate, LoRa/TempRadio, and non-empty OTA seeding

The current source checkpoint passes 1,132/1,132 native cases, 68/68 host broker/endpoint cases, all 318 LoRa-OTA chain/route/bundle cases, and 41/41 MOTA format/signature/delta cases under the documented pipx detools interpreter. Indicator geometry, dark-theme, and font-recovery contracts pass 4/4, 6/6, and 25/25. A serialized real Indicator ESP32-S3 Wi-Fi/USB profile also compiles at 95,944 bytes RAM and 1,589,433 bytes flash. The direct system-Python MOTA invocation predictably lacks detools; that environment boundary is not a firmware failure and must not be worked around by installing the package globally.

The RAK failure log is /home/mikec/hwtest/runs/rak3401-syncidle-clean-tSp7PLgA/result.txt on the Mercerwood Pi. The tested UF2 SHA-256 was 650c9fda456685cdf99c1f2996c60f1f551cc9216d2b746a22c8cb78464b0ff7. cp returned success in 95 ms, but sync -f failed with EIO after 157,188 ms. Post-failure SWD readback proved that all seven ordinary sectors whose bytes differed between the old and intended builds still contained the old values, and the settings page was still erased. The only available pre-copy SWD dump was taken several hours earlier, however, so it cannot prove that zero unchanged blocks were written during this attempt or establish when the stale S140-v7 tail in the first application page appeared. The next run must capture an immediate pre-copy dump as well as the post-copy dump. Serial DFU recovery then completed in 35,698 ms with the exact Device programmed. marker. All 1,830 UF2 payload blocks matched SWD readback after recovery, and the application passed a subsequent real power-removal boot.

The corrected 2.4.4-preview.2 candidate was installed through identity-gated SWD from combined-image SHA-256 9b1654baccd2a920f68651893e31d79170400d1a7982a3f854ae640f1d3513a8. An immediate pre-copy dump closed the earlier evidence gap. A deliberately old UF2 then copied and flushed successfully in 16,568 ms, and the exact formerly failing UF2 above copied and flushed successfully in 16,601 ms. After each direction, all 1,830 payload blocks matched a full SWD readback with zero header or payload mismatches. The return update changed 123 bytes across five expected application pages, UICR remained byte-identical, and a subsequent downstream USB power removal plus Pi reboot returned the RAK as the same stable identity. It reported the intended 3caf9dcf application, board, node name, USA radio tuple, and 22 dBm setting after that cold boot. The new bootloader's CLI source string is truncated before the injected preview number, so the independently hashed SWD artifact is the authoritative candidate identity for this run.

The later TinyUSB lifecycle/generation candidate identifies itself as test version 0x02040403. Its exact 40,960-byte bootloader readback SHA-256 is f609eb18754070c54f127325b1cb48547baf5c6b1c695d36fd0c8e0874713c13. With that candidate installed, a full CURRENT.UF2 read completed before each write. The 936,960-byte bf092b24 application UF2 (SHA-256 12477a9734cf12eb920361bd21024e39bf303953fcf79d0bf720e00847e2ae92) copied and flushed in 28,203 ms and returned to application mode four seconds later. The exact 3caf9dcf application UF2 (SHA-256 650c9fda456685cdf99c1f2996c60f1f551cc9216d2b746a22c8cb78464b0ff7) then copied and flushed in 26,479 ms and also returned in four seconds. Both boots retained RAK3401-OTA-BENCH and 910.525/62.5/SF7/CR5.

Identity-gated SWD then read the entire 468,480-byte padded UF2 application range. Its SHA-256 exactly matched the converted source on both sides at b2e37a7681027ca91d1b2bd8e39b55395d9e91d4863aed17131e50c1ba1d2227. The bootloader still matched the installed 0x02040403 image byte-for-byte, and UICR still matched the pre-test image at d029ea630c2f632a1b690cb52b2a96a28b6f870c656c3e1bd74ba3a571c53b9c. The authoritative logs are under /home/mikec/hwtest/runs/rak3401-uf2-02040403-20260830-170829 and /home/mikec/hwtest/runs/rak3401-uf2-return-02040403-sfHiGEqA on Mercerwood.

The subsequent direct LoRa qualification used the V4 as controller/seeder and the RAK's exact bf092b24 body 91CF4EC2882F23D6 as the base. Its mandatory fixed three-minute rehearsal passed on 909.95/250/SF5/CR5, including natural expiry and exact normal-radio recovery. The 2,132-byte, two-block in-place delta reached 2/2 and ready in six seconds; install was accepted, and the new body appeared at the 60-second reboot probe. The automation took 514 seconds including rehearsal, clock/radio safety checks, and post-install verification. The final body was 27A223DDFC3A8F47, version 1.17.1.5 at 3caf9dcf, and the bootloader reported blrc:B8, which is OTAFIX's successful application apply result rather than an error. Exact target TX power was wrapped from 22 dBm to 0 dBm for the co-located transfer and restored to 22 dBm afterward; the source RXPS configuration and both normal radio tuples were also restored.

Identity-gated SWD then read the live application at 0x26000. All 468,384 bytes matched the intended release image exactly (SHA-256 b496e3f4419c9f771564ed3c74b3407822f45acdf60cbd6c4625a54b181ca084), and the independently recomputed EndF hash was 27A223DDFC3A8F47. The target ID was 2FA509C1, hardware ID was RAK_3401, and version word decoded to 1.17.1.5. The 40,024-byte bootloader readback also matched the exact installed ffb1580/test-version-0x02040402 artifact byte-for-byte; UICR was unchanged. After SWD reset-halt/resume, USB again reported the expected application, identity, name, radio, 22 dBm, no TempRadio lease, and blrc:00 (the retained success diagnostic is intentionally cleared by the next normal boot).

The current 0x02040403 bootloader candidate then passed a second direct V4 LoRa qualification using a materially larger application change. The unsigned in-place package was 75,614 bytes with 75,108 payload bytes and 74 transfer blocks. Its mandatory fixed three-minute rehearsal passed, including natural TempRadio expiry and exact normal-channel recovery; one lost read-only ota status reply was reconciled by its bounded exact retry. The transfer reached 74/74 and READY in 122 seconds, the explicit unsigned install was accepted, and reboot produced exact HEAD 9fd580c8 with body 744BC4443891ED10. The complete safety-wrapped run took 589.487 seconds, including rehearsal, recovery, and final verification. The first postboot ver reply was lost and its exact read-only retry passed. Independent USB checks then confirmed target 2FA509C1, hardware RAK_3401, unchanged identity/name/radio/RXPS/watchdog state, bootloader CRC 6683C066, ABI 3, apply result blrc:B8, and no active TempRadio/download/seeder/key state. After the separate host-recovery exercise, the near-field TX setting was restored from 0 dBm to its exact original 22 dBm and read back over an authenticated LoRa session. The sanitized authoritative log is /home/mikec/hwtest/runs/rak3401-02040403-head-vzWOXVxT/qualification-redacted.log with SHA-256 3e53dcaa5922c7c18a7890963b8fc092311d2db01caa5603640f43f1482802f6.

A serialized VM build of the six connected-board qualification profiles then completed 6/6 in 136.122 seconds: XIAO nRF52 BLE Companion, T1000-E BLE Companion, RAK3401 LoRa-OTA Repeater, SenseCAP Indicator LoRa USB/Wi-Fi Companion, MeshTower V2 SD LoRa-OTA Repeater, and the consolidated Heltec V4 V4.2/V4.3 Full Companion. The earlier V4 selector typo was rejected before any compile began and is not a firmware build failure.

After the fresh-NTP download gate, shared SNTP coordinator, MQTT-to-MeshCore RTC handoff, and daily refresh policy were added, the complete native matrix passed 1,102/1,102 cases in 93.953 seconds. The scheduler is host-tested at 24-hours-minus-one-millisecond, the exact 24-hour boundary, normal and wrapped millis() values, a short retry that overrides the daily cadence, the zero deadline sentinel, and a Wi-Fi event latch. A reconnect after a successful sample preserves the existing deadline, so successful samples are requested after boot and then only once per day. All current Python and shell contract suites passed, and the expanded LoRa-OTA automation suite passed 317/317 cases.

Serialized real builds also passed after that change. The final event-safe implementation uses an atomic GOT_IP latch so even a short initial connection before the 10-second MQTT status sample cannot skip the required boot NTP request. The final Heltec V4 MQTT Observer application is 1,853,304 bytes with SHA-256 1938ae88fcdbe601cf8698df44b548656079562d899801886b808af913e5f9ac. The consolidated Full V4 image used 2,250,801/6,553,600 flash bytes and left 4,302,799 bytes of app-image space. Its 2,251,272-byte application image has SHA-256 05131eac29164be46d1bcceeb4f03455f5ea8d61c8d4f529b77d8adfe25fdb6a; the 2,316,808-byte merged image has SHA-256 c4c1fc639b6d0087c795a81aca79c07eba56dc4e071eb374dc70da0bec81815e. The focused daily-NTP policy suites passed all 42 cases after the latch was added, and the Full capability manifest verified all ten expected features. The reduced RAK3401 LoRa-OTA Repeater also compiled with the verified WisBlock I2C aliases. At that source state its link used 471,344/815,104 flash bytes; the reduced external-sensor contract retains INA219/INA226/INA260/INA3221 as the voltage/current entries in the optional environmental-telemetry table. These are not the image's only I2C consumers. The image separately retains its SSD1306 display, auto-discovered I2C RTCs, RAK12500 I2C GPS, and RAK12501/L76K UART GPS paths. The configured INA3221 and RAK12500 both default to I2C address 0x42; those two devices cannot coexist at those addresses. The supported combined arrangement leaves RAK12500 at 0x42, straps INA3221 A0 to SCL for 0x43, and uses a build with -DTELEM_INA3221_ADDRESS=0x43. The nRF52840 die-temperature and ADC battery-voltage paths are also retained. The 472,227-byte DFU ZIP SHA-256 is fdd369005a61635756efa9e38b217376a952f334b0864f927a46799b65264fa9, and the matching 943,104-byte UF2 SHA-256 is 2155632fde730e0ae2e4a72ce64f89910861f6c5105a4bd7ac96aaa538a33dca. These VM results do not replace the remaining exact-final hardware test with UDP/123 blocked.

The later serialized RAK3401 contract build, after the non-blocking nRF52 USB logging change, used 471,576 bytes before its 56-byte EndF trailer. Its EndF body hash is 026749684bf6ee8f, target ID is 2FA509C1, hardware ID is RAK_3401, and version word is 0x01110105. The 472,435-byte DFU ZIP has SHA-256 cf0880b26f2d6bd8157f8af76a6718fe1f998de8ea7df8cd952e734448201fb9; the 943,616-byte UF2 has SHA-256 f893a8cebf309af12a4a6f5483acb28f94158e18632f660e011658c846e8ca7f. The linked ELF contains GPS, SSD1306, RTC, and the four INA families, while an exact symbol audit finds none of AHT10/20, BME280, BMP280, SHTC3, SHT4X, LPS22HB, MLX90614, VL53L0X, BME680/BSEC, BMP085, or RAK12035. The complete native matrix passed 1,112/1,112 cases at that intermediate source state. These are VM gates only; flash the exact artifacts and repeat the physical peripheral and blocked-clock checks before marking any later source state hardware-qualified.

Subsequent source hardening changed RAK peripheral discovery: the shared 3V3_S rail remains enabled, UART GPS requires a complete checksum-valid NMEA sentence, an exact INA3221 identity blocks a conflicting u-blox probe, and a successful I2C GPS claim suppresses a sensor only on the same bus and address. The hashes and sizes above describe their recorded intermediate artifacts; they are not current-HEAD evidence and must not be reused for the hardened source.

The immutable font endpoint was re-probed only after the VM reported NTPSynchronized=yes. GitHub returned HTTP 206 for bytes 0-65535, an exact 65,536-byte match to the checked-in font (range SHA-256 8b0dd308eb01469d1f9e732fba260720a80cc86f6cb5385c28e8a16c76dcfbf3), with total length 1,302,608 and strong ETag \"39ff0cfbe37e36905507697d042869c05374c3202dd96d5a9f98812153f324c1\". This validates the current strict Range assumptions against the live service; the exact-final Indicator still needs the negative hardware run proving that blocked UDP/123 prevents every TLS request and RP2040 staging write.

Mercerwood subsequently recovered without a target write. Both its Tailscale and LAN addresses again accepted SSH, Bluetooth was idle, and the exact T1000-E stable serial 34A9141999729D5D was present as application USB 239a:8029. The RAK3401 simultaneously uses the same VID/PID, which confirms that every continuation must resolve the stable serial/by-id identity rather than VID/PID. The T1000-E remained in the exact bonded Full Companion application; no stale DFU, scanner, meshcli, or HCI-capture process was running.

The post-reboot seven-TTY inventory was also resolved without assuming that one board owned adjacent device names. ttyUSB0 is the SenseCAP Indicator's ESP32-S3 CH340 UART and ttyACM5 is its RP2040; their sibling physical USB paths and the Indicator's live TCP board reply agree. The separately attached ttyACM4 is the MeshTower V2 native HT-n5262 endpoint and returned the qualified MeshTower build over its own console. The remaining ACM endpoints independently identify the RAK3401, XIAO nRF52840, Heltec V4, and T1000-E.

ModemManager had restarted after the Pi reboot and could probe otherwise idle radio consoles. Six serial-bearing boards now use stable-serial-only ID_MM_DEVICE_IGNORE rules. Because the Indicator CH340 has no stable serial, its separate rule requires the exact current physical port chain and exact 1a86:7523 identity; there is no broad CH340, product, or VID/PID-only rule. A controlled ModemManager restart reported zero modems, displaced no TTY owner, caused no USB disconnect/re-enumeration, and left all seven endpoints ignored. The same mctomqtt process retained sole ownership of the RAK, while a subsequent Indicator TCP query still returned the expected board and build. Reversible root-only backups were retained on the Pi before each rule update.

"},{"location":"test-results/hardware_validation_checklist/#run-wide-gates","title":"Run-wide gates","text":""},{"location":"test-results/hardware_validation_checklist/#compute-placement-gate","title":"Compute-placement gate","text":""},{"location":"test-results/hardware_validation_checklist/#lora-controlled-host-recovery-gate","title":"LoRa-controlled host recovery gate","text":"

The sanitized host-recovery evidence bundle is under out/host-actions-live-20260831/. Its SHA256SUMS manifest has SHA-256 5acff71ed98f483bb769e8c99c577bf358d0491355ac72855ee7093cd658bf21, and sha256sum -c verifies every deployed-hash, pre/post-state, radio, OTA, network, and reboot record in that manifest.

"},{"location":"test-results/hardware_validation_checklist/#mercerwood-pi-usb-power-cycle-gate","title":"Mercerwood Pi USB power-cycle gate","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-xiao-nrf52840","title":"Seeed XIAO nRF52840","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-tracker-t1000-e","title":"Seeed Tracker T1000-E","text":""},{"location":"test-results/hardware_validation_checklist/#rak3401","title":"RAK3401","text":""},{"location":"test-results/hardware_validation_checklist/#rak4631-reduced-profiles","title":"RAK4631 reduced profiles","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-t096","title":"Heltec T096","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-meshtower-v2-with-sd","title":"Heltec MeshTower V2 with SD","text":""},{"location":"test-results/hardware_validation_checklist/#heltec-v4","title":"Heltec V4","text":""},{"location":"test-results/hardware_validation_checklist/#seeed-sensecap-indicator-lora","title":"Seeed SenseCAP Indicator LoRa","text":""},{"location":"test-results/hardware_validation_checklist/#cross-radio-lora-ota-acceptance","title":"Cross-radio LoRa OTA acceptance","text":""},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/","title":"Hardware validation esp32 web console ota 2026 10 05","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#esp32-web-console-and-browser-ota-2026-10-05","title":"ESP32 web console and browser OTA - 2026-10-05","text":"

This is a local qualification checkpoint, not a release approval. GitHub writes and publication of 1.17.1.9 remain on hold. Firmware source for the final runs is 7821329851f69b6b945f061bc4d15212d43c3198.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#coverage-gap-and-reproduced-failures","title":"Coverage gap and reproduced failures","text":"

Earlier component and simulated-peripheral tests did not establish the complete V4 browser flow. In particular, accepting a Started reply did not prove that a normal WiFi client could discover the hotspot or connect to its HTTP server. The actual embedded uploader also had to receive its completion response before the radio restarted. Those boundaries needed physical qualification.

The physical V4 runs found three failures:

Host regressions also cover background ESP-NOW retries undoing an OTA pause, WebConfig teardown retaining OTA's WiFi, upload ownership and interruption, failed binds, and complete CRLF replies through the stock web console client. Negative controls fail with the old AP protocol, TCP binding, callback reboot, USB sleep guard, or line delimiter behavior.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#final-physical-observations","title":"Final physical observations","text":"Check Result and boundary V4 actual browser upload PASS. Chromium ran the unchanged embedded AsyncElegantOTA page. Multipart upload returned HTTP 200 OK; normal reboot loaded a distinct target version. USB answered afterward without another reset or unplug. V4 OTA startup PASS for WiFi off -> OTA, WebConfig setup AP -> OTA, ESP-NOW coexistence, WebConfig LAN -> OTA, and an active LAN uploader -> OTA AP. The returned URLs served real pages across deferred cleanup. V4 USB idle PASS for 600 seconds with no transmitted commands or keepalives, power saving on, USB logging on, and DTR asserted. Subsequent query and close/reopen passed; uptime advanced from 111 to 717 seconds. V4 DTR deasserted PASS for immediate queries, 30 seconds idle, and reopen using an opener that established RTS low before deasserting DTR. No reset or startup delay was added. XIAO S3 WIO Companion Restored from the temporary Repeater test role using an application-only flash. Partition signature and identity were preserved. All 60 read-only replies met the two-second deadline, including 10 held query pairs and 10 reopen pairs; maximum latency was 22 ms.

The V4 console runs above use the unchanged official SerialConsole through Node Web Streams and a physical Linux serial transport on Mercerwood. They do not exercise Chromium's OS Web Serial backend on that remote V4. Earlier physical XIAO browser tests are separate evidence, not a substitute for it. The client is pinned to meshcore-dev/flasher.meshcore.io revision 45a5257bf792b11a83f4c51c18d4f312addfb677, with lib/console.js SHA-256 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9.

The first disposable LAN association failed. A separate run with a fresh SSID, explicit WPA2/RSN CCMP configuration, AP readiness, and credential readback passed on the same firmware. The first failure is retained; these observations do not identify which test-network difference caused it.

Two DTR-deasserted first-query runs also failed. Their original pyserial opener briefly presented the USB hardware reset combination; the boot log recorded USB_UART_CHIP_RESET. Correcting the opener's line-state order passed without a reset or settle delay. These are retained test-fixture failures, not evidence that the user's intermittent console hang has been fixed.

During the 600-second idle run, dispatcher flags changed from 0 to 8. The radio remained in RX with no receive errors, zero received packets, and a configured five-minute receive-silence watchdog. This is consistent with the existing radio recovery policy on a quiet channel; uptime proves the MCU did not reboot. It is not an error-free RF stress qualification.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#artifacts-and-host-checks","title":"Artifacts and host checks","text":"

V4 application installed by the successful browser run:

XIAO application restored after console testing:

Both firmware builds passed. On this source, focused host checks passed: OTA startup 7, OTA lifecycle 15, async web lifecycle 2, official web console 3, WebConfig 40, and V4 partition migrator 4. PlatformIO was run serially.

Private raw logs and safe reports are retained under /tmp/meshcore-hil-20261004/web-console-investigation, including the failed runs. The index 782-evidence-index-safe.json records evidence hashes. Temporary V4 settings and the Pi network are restored after testing. The G2, its recovery ACLs, and its running MQTT services were not changed by this run.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#remaining-limits","title":"Remaining limits","text":"

The subsequent current Full V4 native browser qualification adds actual Pi Chromium Web Serial, the unchanged setup GUI's five-second deadline, and measured fresh-flash and power-saving console idle results. The older source and boundaries above remain historical evidence. Neither report qualifies every host OS, physical USB adapter, cold power cycle, ESP32 board, firmware profile, or radio traffic load. USB-attached console checks do not establish CPU sleep, measured current or button wake. The subsequent V4 button-wake profile correction passed host/build validation and another native GUI fresh-flash plus 180-second open/closed console check, as recorded in that linked report. On 2026-10-06 the user separately confirmed V4 button screen wake; its firmware revision and CPU sleep state were not specified. The operator USB tests included no actual button press or sleep/current measurement.

"},{"location":"test-results/hardware_validation_esp32_web_console_ota_2026-10-05/#release-candidate-follow-up-recorded-2026-10-07","title":"Release candidate follow-up recorded 2026-10-07","text":"

The following source-scoped observations were originally recorded in the 1.17.1.9 candidate release notes. They are retained here as historical test results, including failed attempts and their separate recoveries. Build and publication status in this snapshot does not override the final release notes. Older passing images do not qualify a later firmware revision or turn an earlier failure into a pass. Latest-source V4 hardware was unavailable for a repeat; that is a physical coverage limitation, not a failed latest-source test.

The earlier c5 GitHub unit workflow passed all four jobs. Host regressions exercise production OTA callbacks, the official configuration GUI and app protocol behavior, including MD5 ordering, rejected second files, incomplete POSTs, cleanup and subsequent valid uploads. Sanitizer fixtures cover these OTA lifecycle cases. These checks use MeshCore's actual-code fixtures; MeshBench is not integrated.

For 1e421ff4, the coupled production WebConfig credential interception, NVS commit, CLI routing and MQTT consumer fixture passed four tests, including fresh portal-only setup, legacy fallback, reconnects, raw-PSK boundaries, failed and interrupted writes, and mutations that restore the old credential lookup. The broader WiFi and MQTT host suites passed 62 and 14 cases, respectively, with some overlap. The WebConfig host suite passed 53 cases, including the embedded page in an isolated Chromium headless browser. Seven infrastructure power-lifecycle cases and six portal-ownership cases passed. Five V4 setup-page cases include actual button/session handling under both signed and unsigned char. The new tests are explicitly included in the unit workflow. The current GitHub unit workflow at documentation and host-test commit 4c5f58f4 passed all four jobs; its firmware source remains 1e421ff4. The completed local release passed the strict package checks below; V4 OTA and G2/V4 portal-to-MQTT hardware checks remain pending.

The completed local all-board release has 463 qualified recipes and 926 firmware files. Its complete checksum inventory, capability and memory checks passed independent read-only revalidation. All 94 exact ESP32 board/role migration ZIPs and their combined archive passed validation. The 114 nRF52 infrastructure targets each have a qualified Full/Reduced pair with matching actual DFU identity, firmware and flash layout. These results qualify local packages from 1e421ff4; they do not qualify the pending physical upgrade paths or publish the release. The newer bd4cbf281 source protects ordinary setup/OTA APs from the SDK's shared long-range WiFi flag, defers conflicting ESP-NOW starts and keeps long-range protocol writes out of saved WiFi settings. It reports an active ESP-NOW conflict before starting an AP. The subsequent 8c4e09275 source also defers MQTT station maintenance and Full Companion station recovery while an AP owns WiFi. Final builds and hardware qualification of this candidate remain pending. Final public assets and catalogs still require their separate inventory and digest checks.

The physical G2 and V4 tests used the unchanged embedded browser uploader and the normal host console, retaining the stock 5-second first-command deadline. The earlier c5fe8a0e applications passed AP uploads with HTTP 200/OK, automatic restart, and preserved identities, ACLs and saved settings. The G2's initial upload completed in 19.053 seconds; the V4's completed in 16.392 seconds. First ordinary replies took 1.385 and 1.010 seconds, respectively, without a cable replug or reset rescue.

On the G2, a full valid first file followed by a second file returned HTTP 409 without an observed MCU restart. A full-file request with an incomplete closing multipart boundary also left uptime continuous. A valid upload then passed through that same running listener in 18.767 seconds. Full-image wrong-MD5 requests had already returned HTTP 400 without restart on parent 70bf4bdf. The tests do not claim that rejected requests leave the inactive flash slot untouched. A fresh 385-second sample of unmodified Cisien meshcoretomqtt had all four brokers connected, zero publish failures and no service restarts; both the original and recovery Admin ACLs were preserved. The earlier G2 LAN upload stall is treated as a local slow-WiFi limitation, not a release blocker. LAN upload was not qualified by these AP tests.

The new 1e421ff4 G2 Full Observer application also passed the unchanged native AP uploader: HTTP 200/OK in 18.437 seconds, firmware-owned restart, and first ordinary ver reply in 1.304 seconds. Live EndF body identity, the exact G2 target and source were verified, with the original 15 settings, both Admin ACLs, services and the Pi's exact SlowFi connection preserved. This qualifies that AP update, not fresh WebConfig provisioning into the on-device MQTT consumer; that check and the new-source V4 checks remain pending.

On parent 70bf4bdf, both G2 and V4 passed four 600-second quiet intervals: port open and closed with logging off, then open and closed with USB logging, all with power saving enabled. Uptime remained continuous, with no host keepalive writes during the intervals. The V4 reported radio-watchdog event bit 8; this was not an MCU reset or an all-zero-error result. Its USB-logging open interval received 26 bytes. The V4 also passed the first unchanged native setup-GUI connection immediately after a fresh flash, with the first clock reply in 73.3 ms. These quiet and fresh-flash checks were not repeated on c5fe8a0e, whose change was confined to OTA completion behavior. The newer canonical-WiFi source 1e421ff4 has the narrower G2 results below.

On 1e421ff4, the G2 passed one 600.022-second open-port quiet interval with USB logging and power saving enabled, with no host writes and continuous uptime. The following 600-second closed-port interval ended in a stock-browser reopen failure at the test helper's 5-second acquisition gate, before ver was sent. The host then had VMIN=1/VTIME=0; the ordinary exclusive serial client still replied in 21 ms, with continuous uptime and unchanged core error flags. This is an unresolved browser/native-port acquisition result, not a qualified closed-port pass or evidence of an MCU crash. A cleanup-overlay bug in that test helper was also found and fixed for the instrumented repeat; the earlier failed receipt is retained.

The instrumented G2 repeat on the same 1e421ff4 source passed both 600-second open and closed quiet intervals with USB logging and power saving enabled, continuous uptime and no host keepalive writes. Original settings and the host were restored. The maximum stock Enter-to-reply time was 3.584 seconds. The second native port open took 5.532 seconds; click-to-connected took 8.201 seconds. That acquisition time is separate from the unchanged 5-second command deadline, and the helper's connected wait started after its click RPC returned. This repeat does not reclassify the earlier failure or establish its cause. The separate logging-off run also passed both 600-second open and closed quiet intervals with power saving enabled, 85 successful queries, continuous uptime and restored original settings. Together the completed runs qualify all four quiet intervals on this G2 and source.

The subsequent 100-cycle acceptance attempt stopped after 32 completed cycles. All 86 stock commands passed, with a maximum Enter-to-reply time of 450 ms and no observed MCU reset. The failure was an automation timeout while clicking the stock close-overlay button; this is separate from the stock command deadline. At the failed-trial snapshot, the UI had marked itself disconnected while native close had not started. Native close subsequently fulfilled in 105.5 ms, and the serial port and page closed. The cause of the earlier disconnect or click delay remains unproven. The failed receipt is retained; 100-cycle qualification and follow-up diagnosis remain pending.

A separate Linux Chromium 139 comparison isolated an inherited host TTY setting: VMIN=0/VTIME=0 produced device-lost stream termination, while changing only VMIN to 1 gave a stock ver reply in 154.1 ms without reset or replug. That finding does not establish the cause of the later reopen failure above. See the Linux Web Serial report for primary browser-source evidence and the bounded host preflight.

The physical RAK3401 Companion on 70bf4bdf passed 21 malformed-request cases with 217 request/reply frames. Its Full Repeater passed the official native setup GUI and 600-second open/closed quiet intervals with power saving on, continuous uptime and zero core error flags. It was returned to the then-current c5fe8a0e Full Companion; the first stock SDK reply took 25.9 ms without an initial retry or settling delay. Original identity, contacts, channels, custom variables, RX power-saving settings and actual packet-stream mode were restored. The planned reboot needed to restore stream mode was separate from the stability measurements. An initial SWD programming failure was recovered and verified; this was not a first-attempt programming pass. These tests did not qualify RAK3401 BLE DFU, LoRa OTA or bootloader updates.

The actual published 9053038f 1.17.1.8 Full V4 installed newer applications over both AP and a controlled shared WiFi LAN, but its old page did not receive HTTP completion. Check ver before retrying a stalled old page. These trials used already expanded application slots; actual phone-hotspot uploads remain untested. The requested legacy small-slot G2 trial now has a verified existing 16 MiB backup and genuinely programmed and verified the official 1.14.1 stock components and small-slot table while preserving NVS. Its first stock USB ver request reached the five-second host command limit; a later read-only probe replied without reset. A separately recorded stock setup restored and verified the original identity, 22 common settings and two Admin ACLs after normal reboots. Password setters were confirmed and the guest password persisted; stock has no admin-password getter. The final start ota returned its expected URL, but the test's AP scan timed out. A subsequent completed native scan found no MeshCore-OTA AP at any BSSID. A later read-only NVS audit identified the test starting-state problem: earlier preconditioning had saved the WiFi long-range protocol flag, which stock inherited. Its long-range AP beacons cannot be received by the ordinary WiFi clients used here. This does not establish a stock AP startup bug. A separately recorded correction changed only the fresh live NVS protocol flag and its entry CRC, with exact readback and unchanged stock components and small-slot table. After stock identity, common settings, ACL and guest/ private-identity checks, a completed fresh scan found the exact G2 OTA AP. This establishes the genuine stock WiFi-only starting boundary. The earlier failed receipts remain failed. The first native migration controller then failed on a missing host iw executable before any HTTP or image-upload step. It may already have associated with the AP; that outcome was not verified. Its return to SlowFi and owned guard cleanup passed. A separately reviewed retry retained the firmware/controller inputs, separately pinned the old runtime log and checked host executables before using fresh guards. It associated with the exact stock AP and observed one native bridge-image POST with the exact bytes, SHA256 and MD5, but the old embedded page did not complete its HTTP response before the uploader deadline. Browser closure, return to SlowFi and guard cleanup passed; that transaction remains failed. A later scan found the exact migration AP. A separately recorded stage2 continuation then verified the actual expanded-layout-ready page and uploaded the exact 1e421ff4 ordinary Full application through the unchanged native page, with matching MD5 and HTTP 200/OK in 14.526 seconds. That upload phase also passed its network return and guard cleanup. The subsequent controlled Pi AP phase failed on a host FileNotFoundError before any final HTTP request. The host's scheduled WiFi refresh also interrupted that AP phase. Its later guard-file cleanup refused an unproven network state; a separate read-only qualification and owned-file cleanup then verified the original network, dead controller and removal of all prior guard profiles and files. A new proof-only attempt stopped before AP activation because reloading services moved the host hold timer's relative deadline. Its failed receipt is retained, and the held host script was restored. The next proof used a fixed absolute hold deadline, but stopped before AP activation when a host-file permission check rejected the actual private service file; its failed receipt and host restoration are preserved. A third proof verified and ran the isolated Pi AP with the independent return timer armed, then reached the five-minute client/DHCP discovery deadline without a verified G2 client or any HTTP request. It returned to SlowFi, removed its owned network resources and restored the host's scheduled WiFi recovery script. Final running-image/layout/identity/settings HTTP verification remains unproven; the successful firmware upload does not establish that final verification. After that completed failed WiFi check, separate USB maintenance installed the 8c4e09275 ordinary Full Observer application. Full application readback and the normal running version matched, while the bootloader, partition table, NVS, OTA selection and checked filesystem region stayed unchanged. This maintenance does not qualify the WiFi-only upgrade. The saved-settings check found RX boosted gain off instead of its original on; the other 21 common settings, identity, passwords and two Admin ACLs matched. A separate checked CLI repair restored gain on and verified all 22 settings. The first extras restoration then missed the five-second CLI deadline while changing bridge source. A later read-only probe found continuous uptime, zero core error flags and the requested source value; it does not establish the missing command acknowledgment or its save duration. After checking that state, a separate restoration completed with the original WiFi pair, saved extras, all 22 common settings, identity, passwords and both Admin ACLs verified. The MQTT worker was running with valid effective configuration; broker connectivity was not tested. Original TTY settings, all three host services and SlowFi were restored, and the exact temporary host executable alias was removed. The earlier timeout and WiFi proof failure remain recorded. The loader correction is included in 88c851080. Its G2 ordinary Full Observer application was independently read back in full after installation, and normal boot matched the exact new runtime and EndF footer. The checked lower 64 KiB, OTA selection and first 64 KiB of the filesystem stayed unchanged. A separate read-only post-boot check verified the original identity, passwords, two Admin ACLs, all 22 common settings, seven supported bridge fields and saved extras. Core error flags were zero; the MQTT worker was running with valid configuration, without a broker-connectivity claim. Original TTY settings, all three host services and SlowFi were restored. This USB maintenance does not qualify the failed WiFi-only final verification. The fresh all-board build and V4 hardware checks remain pending. These are separate outcomes; the initial failed transactions remain failed. The earlier c5 AP upload success does not turn those old HTTP-completion failures into passes. The upgrade guide keeps the old and new updater behavior separate.

Original host services, temporary network changes and owned test resources were restored after the historical completed hardware checks. The current new-source V4 native AP OTA trial has not yet produced a verified result; its return to SlowFi and original host-service state also remain unverified. The legacy G2 check established the stock starting boundary with its original identity, common settings and ACLs verified, then completed the expansion and Full application upload over WiFi. It has not yet qualified the final running image through HTTP. Post-verdict 88c851080 G2 firmware readback, normal boot, original-settings acceptance and host restoration passed separately over USB. The all-board build and current V4 hardware qualification remain pending. These checks keep publication pending. Build qualification does not establish physical compatibility on every board. Quiet USB trials do not measure MCU sleep/current draw, stack high-water, sustained log backpressure or long-term fleet reliability. Linux native-browser tests required host TTY preparation; they do not qualify every browser or operating system. Hardware CDC has finite buffers, and closing a Linux TTY does not necessarily produce a hardware session reset. See the historical V4 console report and USB backpressure results for their exact source revisions and measured scope.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/","title":"Hardware validation esp32 web console startup 2026 10 05","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#esp32-native-browser-console-startup-2026-10-05","title":"ESP32 native browser console startup - 2026-10-05","text":"

This is a local validation report, not a release approval. No GitHub upload, website deployment, or release publication is part of these results. The GUI time-sync connection timeout was reproduced on native USB. After the boot filesystem correction, the unchanged GUI's first fresh-flash connection passed on both the Heltec V4 and XIAO ESP32-S3. The final V4 600-second idle run also passed with power saving on, logging off, and no transmitted command during idle. Earlier terminal runs establish different boundaries and are recorded separately. The V4 core error flags changed during idle and are retained below; this is not a claim of error-free radio operation.

A later user screenshot identifies the affected surface as the Repeater / Room server USB setup GUI at config.meshcore.io, with Cannot connect: Command timeout: time <epoch>. The initial physical observations used the terminal at flasher.meshcore.io/console. Native tests of the actual GUI were added after identifying that mismatch and are recorded separately below.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#native-test-boundary","title":"Native test boundary","text":"

The physical target was a Heltec V4 ESP32-S3 Repeater attached to Mercerwood. Chromium 154.0.8037.92 ran on the Pi and used its actual OS Web Serial backend, native SerialPort objects, and native readable/writable streams. Playwright drove the full Vue website and recorded the UI and loader lifecycle. This is additional coverage beyond the physical Linux serial/Node Web Streams tests in the earlier OTA report.

The baseline firmware source was 7821329851f69b6b945f061bc4d15212d43c3198. The unchanged website was pinned to upstream commit 45a5257bf792b11a83f4c51c18d4f312addfb677. Source hashes were checked before flashing, including these production files:

File SHA-256 lib/console.js 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9 js/flash.js e9762a29f6a91f8e33736e02ff41a81cd49e7b96fe253a5d71cba7daefd46d5b lib/esp32.js 2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6

Only the unavailable headless permission chooser was replaced with selection of the single, already granted V4. Before each application-only flash, native ROM reads checked the 4096-byte partition table and OTA metadata. The table hash was 9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60; valid OTA sequences 3 and 4 selected app1 at 0x650000. The UI's application address was changed to that verified active slot. Full erase was disabled. The actual stock loader write, native reset sequence, and disconnect remained unchanged. No filesystem erase or additional backup was needed.

In the initial terminal experiments, the first command was sent after native open resolved with a 30-second deadline. The actual setup GUI kept its stock 5000 ms command deadline. No arbitrary startup sleep, extra reset, unplug/replug, keepalive, or automatic command retry was added to turn a failure into a pass. A first failure would remain a failure even if a separately recorded reopen recovered. USB-open readiness and firmware reply latency are separate measurements.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#completed-physical-observations","title":"Completed physical observations","text":"Check Observation Baseline fresh application flash, same native port handle PASS. The first ver reply took 9398 ms. Subsequent power-saving and core-stat queries passed. No unplug or additional reset was needed. Progress 100 preceded flashEsp32 completion by 604 ms. Baseline power saving on, logging output off, 600 seconds idle PASS with no transmitted command or keepalive during the idle interval. The post-idle ver reply took 5544 ms. Uptime advanced from 119 to 741 seconds, so the MCU did not reboot. Power saving remained on and logging remained off; temporary settings were restored afterward. Stock website Close clicked immediately at the success heading, live site PASS after reload; first ver took 1558 ms. The heading appeared 61.8 ms after progress 100, while both native streams were still locked. The original document continued through reset and disconnect; disconnect finished 987.3 ms after progress 100. Same Close test with identical pinned HTML served locally at the official origin PASS after reload; first ver took 1418 ms. The heading appeared 33.5 ms after progress 100 with both streams locked. Reset/disconnect still completed before navigation replaced the original document. The served index, app, and console hashes matched the unchanged pinned source. Actual browser tab closed at the stock success heading, new tab in the same profile PASS; first ver took 1575 ms. The tab-close request was emitted 126.5 ms after progress 100 with both streams locked. Reset and disconnect had not completed before that request. This used normal Page.close(run_before_unload=False), not the website Close/reload handler, browser shutdown, or a renderer kill. Local fixed terminal UI 452e6e7, final startup firmware 163364e52, actual tab close and 600 seconds idle PASS. Completion was exposed only after reset/disconnect, with native readable/writable streams already absent. First ver took 1408 ms; the post-idle reply took 1064 ms with power saving on and logging output off. Core-stat queries and preference restoration passed. This is terminal qualification, not GUI qualification.

The stock completion display is demonstrably premature: progress 100 does not mean the loader has finished its tail, reset, and port release. However, both site Close tests and the actual tab-close test passed on this physical V4. The mock's canceled-tail ordering is not evidence that the physical device was left permanently hung. In particular, the tab-close request time is not the same as the time Chromium finished closing the target.

The qualified fresh-flash application SHA-256 was 912c1c5d80497de04a4ebd5d3a81b9275181ab2bb3bfb7f22e74c692ef0c5d8a. The three completion/close experiments used the 2,093,336-byte application ebee70b05528b21aaaeddf8bc80f2762b7ad767ff5b6f29eadf8e2a78e90859d. Those terminal-only baseline passes did not establish a failing-baseline / passing-fix comparison for the affected GUI. The later unchanged-GUI comparison below provides that first-connect evidence.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#firmware-startup-correction-and-synthetic-coverage","title":"Firmware startup correction and synthetic coverage","text":"

The startup changes are local firmware commits:

The startup predicate is captured when the native reset interrupt occurs, before delayed framework delivery can reclassify an initial enumeration as an active-session reset. Boot diagnostics and early RX do not end startup. The first application command-service loop ends startup; resets afterward retain the existing quarantine, queue purge, and session cleanup. Early command RX is retained until that loop can service it.

Delaying hardware USB data attachment through application setup is not a safe way to hide this startup boundary from the stock flasher. After progress 100, the unchanged loader still needs the native interface for RTS/reset and port cleanup. Withdrawing that interface during its tail can break the handoff. The selected correction keeps the transport available and retains first RX until command service is ready instead of forcing another physical reconnect.

Synthetic tests execute the pinned real driver ISR, production owner predicate, event handling, and session cleanup against host peripheral/RTOS stubs. They cover early and delayed enumeration, boot TX, boot RX, first-command retention, and a real reset after application readiness. Negative controls remove the startup gate, remove capture-time classification, arm on boot traffic, or leave startup suppression active forever. The removed protections cause the expected failures. Driver source pins, mode/version scope, unchanged SDK files, and idempotent build-local transforms are also checked.

The startup synthetic suites and V4/C6 builds passed. The earlier V4 full Repeater build at 163364e52 passed its linked capability and memory checks. Its application hash is 41ed8598aa2fb1b7aa5d320157249dc03a6e543051257c8b1d7d34d8e4c26df6. The 3.x transforms only add startup reset classification; they do not import the 2.0.17 TX/PHY backport or claim its guarantees. Unreviewed framework versions keep their existing driver. C6 ISR tests and compilation do not substitute for physical C6 qualification.

Five sequential qualified builds passed at source base 631f2ad97, with the local test version v1.17.1.9-hil-bootfs-631f2ad9-631f2ad9. Their generated capability/memory reports and application artifacts were retained privately. This test version is not a published release.

Requested build Resolved environment and scope Full heltec_v4_repeater heltec_v4_repeater_observer_mqtt, PASS Full Xiao_S3_WIO_repeater Xiao_S3_WIO_repeater_observer_mqtt, PASS Full heltec_v4_room_server heltec_v4_room_server_observer_mqtt, PASS Ordinary Xiao_C6_repeater_ Complete supported profile with pinned Arduino-ESP32 3.1.3, PASS Explicit Full XIAO S3 Companion Xiao_S3_WIO_companion_radio_full, PASS

The two physical unchanged-GUI first-connect passes below used the S3 Repeater images. The Room server and C6 results in this table qualify compilation and packaged capabilities, not physical GUI operation on those targets.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#local-website-correction-and-browser-tests","title":"Local website correction and browser tests","text":"

Website commit 452e6e7 is local in a separate flasher checkout; the pinned baseline source remains unchanged and the website has not been deployed. Its three production changes are limited to index.html, js/app.js, and lib/console.js:

All 16 full Vue/Chromium lifecycle tests passed, including five negative controls. They run the production UI, console, Web Streams, and flash/reset wrappers with mocked USB peripherals and loader data transfer. The immediate Close test uses a DOM MutationObserver so locator delay cannot miss the early completion race. Coverage includes delayed/failed open, nonfatal RX recovery, fatal RX, rejected writes, cancelable and uncancelable pending writes, slow reader cancellation, close errors, and flash-tail failure. The five controls restore progress-only completion, early editable input, missing RX recovery, missing writer cleanup, and the old fixed 50 ms close delay. Each fails for the intended missing protection. These tests establish client lifecycle behavior; they do not reproduce the user's physical hang.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#actual-setup-gui-handshake-and-regression-comparison","title":"Actual setup GUI handshake and regression comparison","text":"

The live setup GUI matches upstream meshcore-dev/config.meshcore.io commit aff42ad8ef2b332f24d644ff6a783f8272a88ff2. The initial path is GUI Connect -> native port open at 115200 -> held reader/writer -> time <epoch> -> getData. There is no explicit DTR/RTS call on GUI open. The command is ASCII followed by CR only. Its 5000 ms timer is armed before awaiting native writer.write, so both write completion and response parsing must fit that deadline. The firmware Repeater parser accepts CR and ignores LF; CR-only is a supported input path.

GUI source SHA-256 index.html efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d src/gui.js 919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800 lib/serial-cli.js fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870

The actual GUI page is preloaded before a separate tab runs the unchanged stock flash backend. After that backend returns, the GUI Connect button is clicked without reloading the GUI, inserting a settle delay, or changing its timeout. Each official origin is granted only the single intended native device. Raw GUI debug output and app.device are not retained: getData reads private keys and passwords. Lifecycle observations record command categories, byte counts, locks, timeouts, and safe numeric statistics without secret values.

Completed GUI configuration reads mean the unchanged getData flow returned and the GUI became connected. They do not exhaustively validate every field: the stock GUI can catch optional private-key, region, and ACL errors or skip an unsupported variable. The final XIAO run recorded no command-error events and completed its radio, region/default, and ACL commands as well as initial time.

Actual GUI run First attempt and separate recovery V4 163364e52, fresh flash FAIL. Initial time timed out at 5022 ms, and explicit stock cli.disconnect followed by GUI reconnect timed out at 5007.3 ms. Writes themselves completed in 4.7 and 29.3 ms. Only 147 boot RX bytes arrived; no command echo or reply marker was observed. Both locks remained held and GUI connected remained false. Same V4 source, separate settled no-flash control PASS. Initial time completed in 50.2 ms and the full GUI configuration read completed. This was a separate control, not a successful first fresh-flash attempt. V4 cooperative GPS source c8c5f87ab, fresh flash FAIL. Initial time timed out at 5024 ms. A separately recorded clean reopen completed time in 3336.2 ms and the full GUI read passed. Recovery does not change the first-attempt result. Local XIAO ESP32-S3 Repeater, fresh flash FAIL. Initial time timed out at 5001 ms. Separate clean reopen completed time in 3983.4 ms and the full GUI read passed. Native local Chromium and the target's own verified partition/active slot were used. Same local XIAO, separate settled no-flash GUI control with 600 seconds idle PASS. The initial GUI time completed in 9.9 ms and the full configuration read passed. With power saving on and logging off, no native writes occurred during the 600-second idle interval. Uptime advanced from 177 to 777 seconds, errors stayed at 0, and a visible GUI disconnect/reconnect reread passed. Original preferences were restored. This does not change the failed fresh-flash first attempt. V4 normal, untraced firmware 631f2ad97, fresh flash and 600 seconds idle PASS. Initial time completed in 420.7 ms and the full unchanged GUI read flow completed. With power saving on and logging off, zero native writes occurred during 600 seconds idle and uptime advanced from 18 to 618 seconds. Core err_flags changed from 0 to 8, setting the radio-watchdog bit. Visible GUI Disconnect/Connect completed a full reread, original preferences were restored, and final awaited Disconnect left port/reader/writer absent. XIAO normal, untraced firmware 631f2ad97, fresh flash PASS. Initial time completed in 3110.6 ms and the full unchanged GUI read passed. Core statistics reported uptime 4 seconds, errors 0, and queue length 1. No diagnostic reopen, extra reset, unplug/replug or timeout change was used. Final visible Disconnect completed with port, reader and writer absent.

The native V4 failure is not just a pending write: both writes completed quickly. The second attempt began about 10.48 seconds after the stock flash backend had returned and still failed on 163364e52. A five-second GPS wait alone therefore does not explain every observation. Cooperative GPS discovery removes that real startup block, but its first GUI attempt still failed physically. That remaining gap required the direct startup-phase diagnosis below. The short GUI deadline and the terminal's 30-second test deadline are material differences in coverage.

The final driver preserved the original command order and native calls. Source checks, permission selection, and ROM preflight occurred before application flashing; no settle sleep or additional USB operation was inserted after the stock flash backend returned. Automation and native-open latency are separate from the time command's elapsed response measurement: the final XIAO time began about 71 ms after backend return, while the V4 time began about 3586 ms later. Their 3110.6 ms and 420.7 ms reply times begin at command entry. Passing these cases does not claim that native port open or first command always occurs at zero elapsed time after a flash.

The final V4 application SHA-256 was ffb0a00b97b382038db758954a38af464efe6b00bb88784936f9cc7c4b54db39. Its final run recorded no client command-error events and no diagnostic reopen. The final XIAO application SHA-256 was b00ffbd0b7154f29741c3d93110705b33d3d34dcfa392ba7bd0e8e03d8cac9ce. Its verified partition table hash was f4e3b6cfe370c81dca210d57f3c463f924a539842a19fff6eda4615278ee97a2, with parsed active app0 at 0x10000; the image fit that slot. Native Chromium 140.0.7339.186 ran locally on the VM. The V4 used the Pi's native Chromium.

In the stock GUI, a failed handshake alerts without disconnecting. The visible connected state stays false, while the native reader and writer remain owned. Retrying Connect can therefore encounter its own still-open port. The explicit cli.disconnect/reopen in these experiments is separately labeled diagnostic recovery, not an automatic retry added to the user's first handshake.

The hardware-free full Vue reproducer also shows the exact time alert when the mock initial write is delayed 9400 ms. The stock 5000 ms deadline fails while the native-style writer is pending; a separate runtime 15000 ms deadline control completes the same time command. That control is an ordering proof, not an unchanged-client fix or a reproduction of the native cause.

New CI coverage in test/test_official_config_gui.py executes the full SHA-pinned stock SerialCLI and the exact GUI connect/disconnect functions in Node. Actual production GPS init elapsed time controls command admission, and the production Repeater command pump/CommonCLI time branch generate byte-fragmented replies. The actual boot filesystem inventory and production recovery/presence paths also supply a deterministic metadata-cost workload. Its absent native scans cost 129 ms each plus 1000 ms setup settle: one inventory scan yields 1129 ms, while the same workload without inventory performs 40 scans and yields 6160 ms. These are modeled readiness costs, not predicted full physical boot times. All six tests passed in local commit b259b1055. The four negative controls restore blocking GPS, omit inventory, replace the CRLF reply with LF, or regress CR command recognition. Each fails at the stock client-visible 5000 ms initial-time boundary. getData is a callback witness there; full Vue fields, enumeration, and other physical setup latency still require the native tests. No complete third-party bundle is vendored.

The diagnostic V4 phase run retained its first time failure at 5026.1 ms and separate recovery at 2136.8 ms. Recovery core statistics reported uptime 16 seconds, errors 0, and queue length 1. The firmware's own millisecond phase timestamps locate setup work before the first CLI service:

Measured startup phase MCU interval and elapsed time Sensors 2815 -> 2829 ms, 14 ms Dispatcher 2829 -> 3714 ms, 885 ms Preferences 3714 -> 8261 ms, 4547 ms Management state 8261 -> 9941 ms, 1680 ms ACL 9941 -> 11104 ms, 1163 ms Regions 11104 -> 11362 ms, 258 ms Filters 11362 -> 12781 ms, 1419 ms First CLI entry, ready admission, and available RX All at 12796 ms

No blocked CLI admission was reported before that first readiness; the first blocked-gate marker was later, at 14147 ms. The measured setup duration, rather than a new reset requirement or the GPS block alone, was the baseline first-time deadline problem. These trace intervals identify where to investigate; they do not by themselves prove a particular filesystem or CPU cause, or qualify an faster startup path. Qualification of that correction came from the subsequent unchanged-GUI first passes, rather than from trace intervals alone.

A qualified second run of the same granular trace application recorded the inner preference and management phases. It retained the initial time failure at 5024.1 ms and a separate recovery at 1787 ms; recovery core statistics were uptime 17 seconds, errors 0, and queue length 1. Its first CLI service was at 12805 ms. The earlier trace does not contain these inner markers.

Qualified inner phase MCU interval and elapsed time Radio profile lookup 3714 -> 4232 ms, 518 ms OTA speed lookup 4233 -> 4492 ms, 259 ms Display settings lookup 4492 -> 5010 ms, 518 ms Common preference recovery 5010 -> 6571 ms, 1561 ms Existing common preference image, open through completed load 6572 -> 6576 ms, 4 ms MQTT preference recovery/read 6577 -> 8266 ms, 1689 ms Management route lookup 8267 -> 9043 ms, 776 ms Absent management reporter probes 9043 -> 9561 ms, 518 ms

These measurements separate slow missing-path metadata scans from the quick read of an existing preference image. No management reporter was allocated. The correction is a boot-only validated absence inventory, so known absent files can avoid repeated scans while existing-file reads, recovery, and mutation invalidation keep their native behavior. A failed or incomplete inventory falls back to native probes; existing-file errors remain delegated. Known absence is witnessed by the completed enumeration, so those cached negative lookups intentionally perform no later per-path metadata operation. The actual inventory negative controls and both normal-firmware unchanged-GUI first fresh-flash passes qualify the tested startup boundary.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#final-target-state","title":"Final target state","text":"

Final V4 readback reported uptime 718 seconds, active radio state 1(RX), zero received packets, one sent packet, physical RX errors 0, err_flags 8, and no outbound packet. This confirms active RX after the idle test without identifying the exact past watchdog trigger. Power saving was restored off, logging to USB, USB debug off, and MQTT and WebUI off/inactive. A manual get bridge probe was unsupported and is retained as such, not counted as a pass; the correct get bridge.enabled and get bridge.running readbacks both returned off.

The local XIAO was restored to the latest tested Full Companion image at source 631f2ad97, application SHA-256 cf77ff2824f8359ab5e690424e5d6181911f401f0fe7b9a827550f13c0610e2c. Native ROM preflight checked the target, partition table and OTA CRC selection of app0. The filesystem and partition table were preserved.

Settled USB verification passed 60 requests with zero missed 2-second deadlines and maximum reply latency 7.152 ms. It covered ten held app/core pairs, ten reopen pairs, and ten seconds idle. The identity hash matched the recorded original, settings hashes remained stable, no input flush was used, and all serial handles closed. This restore check includes the helper's existing 250 ms open-handshake wait; it is not the fresh GUI's 5000 ms startup proof.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#qualification-and-limits","title":"Qualification and limits","text":"

The affected first-connect timeout has a failing baseline and passing corrected normal-firmware result on both tested S3 boards. Diagnostic HIL_BOOT tracing was off in those passing images. The GUI timer and reset/reconnect behavior remained unchanged.

The final V4 fresh-flash 600-second idle test completed its uptime checks, configuration reread, preference restoration, and awaited final disconnect. The JSON field stats-core.errors is the err_flags bitmask serialized by StatsFormatHelper.h, not an event counter. Its change from 0 to 8 sets ERR_EVENT_RADIO_WATCHDOG (bit 3 in Dispatcher.h). It does not mean eight errors. USB remained responsive, but this evidence alone does not identify which radio-watchdog trigger path set the flag. The XIAO Full Companion restoration is complete.

The local XIAO unchanged-GUI 600-second no-transmit idle control completed successfully with power saving on and logging off. Its successful initial settled handshake, uptime/error checks, full GUI reread, and preference restoration are separate from the failed first fresh-flash handshake. The already completed fixed-terminal 600-second run is also separate evidence.

These results do not qualify every host OS, USB adapter, cold power cycle, firmware role, high logging volume, or physical ESP32 board. A matching initial GUI timeout was reproduced and the tested first fresh-flash boundary now passes; idle results apply only to their measured configurations and intervals.

"},{"location":"test-results/hardware_validation_esp32_web_console_startup_2026-10-05/#evidence-retention","title":"Evidence retention","text":"

The completed sanitized reports are retained privately under /tmp/meshcore-hil-20261004/web-console-investigation. This document contains no credentials, device identities, or raw console logs. Completed native reports and their SHA-256 hashes are:

Safe report SHA-256 pi-chromium-baseline-fresh-flash-qualified-safe.json d01b18ce6a467f704605832d7a8adc1653999718f7cb4e23d11a9dd9d7bbc414 pi-chromium-baseline-power-idle-600-safe.json 100d609d7a448ee223a86ec4ac6b2e14362f86d44ee2670b407815eff7c1b66d pi-chromium-stock-ui-immediate-close-safe.json 32bcc0a67ea7716e2a0a0f60bf9081c4d2800bac3a8fe61f3595ee5d34b077c0 pi-chromium-stock-ui-fast-reload-close-qualified-safe.json b44e5d4885bb598596d021342692292c55c3f6f7a426c532845742caf8cb7e18 pi-chromium-stock-ui-tab-close-safe.json 13557189ba0b68bb28837cc88b108d2df1536e583b96199027edeef307a7cadc pi-chromium-fixed-ui-final-firmware-idle-600-safe.json e1b3f14e5ae6d0762fc44ab4dc28b4b41a231415cae54b4cc5332f75bd46d926 pi-chromium-config-gui-baseline-fresh-flash-qualified-safe.json 7abf519696c55d5af14d59c6781c644d7016d36f13d7c05833c23ce82b16f041 pi-chromium-config-gui-baseline-settled-control-safe.json da85499cebfddac700696cdfdf9ad6d96970932801af469552a2d91899c3a637 pi-chromium-config-gui-gps-fixed-fresh-flash-safe.json 06c5525d66134cbbb460bf9e4e37e7ec74275559f1727f52cef889f074829e40 local-chromium-xiao-config-gui-fresh-flash-safe.json aa6c945f3d2c40b0db3e368a905ba38ab1a4d966f0d15b4d5848fccc88ff4f2b local-chromium-xiao-config-gui-settled-idle-600-safe.json 3e7b996f13978574c77819ab80132d5c4452f8c7490ebb55333c124887f903bb pi-chromium-config-gui-startup-trace-fresh-flash-safe.json 8062dfcdb202a2320c430e9f013f9510392f090a0883dc8fadbc296c92a46eed pi-chromium-config-gui-startup-granular-qualified-fresh-flash-safe.json 10b269c68ef1e56a6104376d6c0b4f671bbe230ec005425a09cc7805953e07b0 local-chromium-xiao-config-gui-bootfs-fixed-fresh-flash-safe.json b94a53a4d017e95688c0231474c0aa0c67bc091b5738e0100173441d108db262 pi-chromium-config-gui-bootfs-fixed-fresh-flash-idle-600-safe.json 8419f294aeeed087a43c1f062a109da2770f2dcfa79c52ac8aae72d82250cb05 bootfs-xiao-companion-restoration/flash-safe.json ae72ae2e3645735e44d46cb4d4a21920b7f669edb5571151e3a28ebafb89848c bootfs-xiao-companion-restoration/strict-USB-safe.json a8c56c8b7030ca5b5d18eed55f012e7227c9d234427e9e7df7e261b9cabb4dfa v4-bootfs-final-readback-safe.jsonl 6186871dfed0f0a62bc35dc004c8eb971a5cd4ab3b81c0e11ab2aa9db00afc38 v4-bootfs-final-bridge-readback-safe.jsonl b26b9d104cd853208aff685f528659d2b1aa35cd2c802c1dd31c8288a04700ae

The host reproductions are browser-console-lifecycle-mock-safe.json and browser-flash-ui-early-completion-safe.json. Their mocked peripheral and write-tail boundaries are recorded separately from native physical evidence. The GUI timing control is config-gui-investigation/gui-startup-timeout-mock-safe.json. Earlier GUI safe reports used an overly strict observational time_reply_ok equality check for plain OK; production returns OK - clock set: .... That metadata flag was corrected to accept the prefix and never controlled first-connect PASS/FAIL.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/","title":"Hardware validation heltec v4 native console 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#heltec-v4-full-native-browser-console-2026-10-06","title":"Heltec V4 Full native browser console - 2026-10-06","text":"

The normal Full Heltec V4 Repeater from source c88885a315208908e050c57fc0ec0a71ff10bd6f passed fresh-flash connection, 600 seconds with the console open, and 600 seconds with it closed through the unchanged config.meshcore.io setup GUI. Power saving was on and logging output was off during those idle intervals. A separate fresh-flash and 600-second open-console trial also passed with USB logging on. Both trials restored the original settings and closed their streams. Final operator readback and owned browser/host-service cleanup also passed. Release 1.17.1.9 remains held and unpublished.

The subsequent button-profile correction at ebf2f082 passed host/build checks and another native fresh-flash GUI connection with 180-second open/closed console intervals. That updated-source checkpoint is recorded separately below; it does not extend the older source's 600-second measurements to the new image.

On 2026-10-06, the user separately confirmed that a Heltec V4 button press wakes the screen. This is a user-reported manual confirmation; its firmware revision and CPU sleep state were not specified. It does not supply a current measurement or a V4-R8 manual result.

These results extend the earlier startup investigation to the current normal Full image and native hardware CDC. They are physical console stability checks, not a measurement of sleep current, radio traffic capacity, or every ESP32/host combination.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#image-and-build-gates","title":"Image and build gates","text":"

The requested target was heltec_v4_repeater, Full profile. Its resolved heltec_v4_repeater_observer_mqtt feature base retains the normal repeater update identity. Runtime readback had to match the complete version v1.17.1.9-halo-keymind-cascade-dev-c88885a3 (Build: 06-Oct-2026). The source includes the OTA upload correction in d2983762 and WiFi ownership correction in 95913c53. The GitHub unit workflow for c88885a3 passed; this does not replace the physical checks below or the final release matrix.

Artifact Bytes SHA-256 Full application 2,097,528 1f10f44149b3f56edbe0ecb2a2bab9b51da651a6d047e2c2c1555691468d2b4c Merged image 2,163,064 3264d7a1a5713755b5dd10ad1c81bb0588e4a072eeaa126b86307a42be77bb5b Matching ELF - 0545eef46da30a86f86531199a91e887167d463c5981f193c0455cd75d633538

The package hash, linked capability, and RAM gates passed, with 20,880 bytes of margin above the required heap budget. The linked application includes HWCDC, its retained-PHY handoff and startup owner hook; no TinyUSB USBCDC class is linked. Full ESP-NOW, WiFi/LoRa OTA, and USB logging capability checks passed. These are build checks; this run did not exercise those radio/OTA features.

Both application slots are 0x640000 bytes, at 0x10000 and 0x650000. The padded partition table SHA-256 is 9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60. The merged image contains that table and the exact audited application at 0x10000. The application fits either slot with 4,456,072 bytes of margin. The merged artifact was audited, but the physical tests wrote the application only; they do not qualify a factory erase or merged-image installation.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#native-browser-boundary","title":"Native browser boundary","text":"

Chromium 154.0.8037.92 ran on the Pi with native OS Web Serial ports and streams. The actual Vue GUI and stock SerialCLI were loaded before flashing. The stock DOM Connect flow after the stock flasher returned sent the normal plain time <epoch> request. Its unchanged 5,000 ms timeout covers both native write completion and the framed response. No additional reset, cable action, post-flash settle, command retry, or keepalive was inserted to turn that connection into a pass.

Website source SHA-256 GUI index.html efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d GUI src/gui.js 919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800 GUI lib/serial-cli.js fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870 Flasher js/flash.js e9762a29f6a91f8e33736e02ff41a81cd49e7b96fe253a5d71cba7daefd46d5b Flasher lib/esp32.js 2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6

Automation selected only the single preauthorized native device. Before each write, actual ROM reads checked the partition table and valid OTA sequence/CRC, then directed the application write to the verified active slot (app1 at 0x650000). The stock loader transfer, reset and disconnect remained in place; full erase was disabled. Phase instrumentation recorded existing calls without adding hardware operations. This qualifies that stock flash backend and native GUI connection, not the complete firmware-picker selection/erase flow.

Original public identity, the ACL response, and 15 checked saved preferences were compared with the original private reference after flashing and cleanup. Only the numeric channel-busy diagnostic counter was normalized in preference comparison. No raw identities, credentials or console replies are published.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#physical-results","title":"Physical results","text":"Check Result Fresh flash, then first GUI connection with logging off PASS. Plain time clock-set acknowledgement in 1,614.4 ms; complete GUI bootstrap and exact version/reference checks passed. Power saving on, logging off, console open PASS. 600.093 seconds with zero native command writes or keepalives. Uptime advanced 18 -> 619 seconds; the first post-idle core reply took 35.9 ms. A full DOM Disconnect/Connect reread acknowledged time in 23.6 ms. Power saving on, logging off, console closed PASS. 600.100 seconds with streams closed and zero writes. Uptime advanced 627 -> 1232 seconds. The first DOM reconnect time response took 33.2 ms; it was the recognized framed clock-cannot-go-backwards rejection, not a clock-set acknowledgement. Full GUI bootstrap and version checks still passed. Separate fresh flash, power saving on, USB logging PASS. Fresh plain time acknowledgement in 696.1 ms, then 600.101 seconds open with zero writes or keepalives. Uptime advanced 18 -> 618 seconds; first post-idle core reply took 40.4 ms. Full DOM reconnect time acknowledgement took 24.8 ms. Both trial cleanups PASS. Original power saving off, logging output USB, all 15 checked preferences, public identity and ACL response were restored. The GUI port, reader and writer closed.

Uptime gates account for the surrounding query/GUI bootstrap time. None of the three quiet intervals contains a native write event. Both trials recorded zero native command errors and no GUI modem-signal calls.

Radio observations stayed in active RX with zero physical RX errors. The core error bitmask changed from 0 to 8, with zero received packets and one sent packet. This is the configured receive-silence watchdog warning, consistent with the quiet-channel recovery policy; it is not eight errors or a CPU reset. Neither trial establishes error-free RF operation or a loaded radio soak.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#retained-setup-failures-and-host-qualification","title":"Retained setup failures and host qualification","text":"

Two preceding browser attempts remain failed and unqualified. Attempt 2 timed out before metadata completion or any GUI command was recorded; its exact flasher substage was not instrumented. Attempt 4 timed out in the stock loader connection/reset loop after 674 USB JTAG reset entries. It did not reach loader main return, application writeFlash, or a GUI command. Neither failure exercised the GUI's five-second command timer, so they are not counted as firmware console timeout failures or silently converted into passing trials.

Before the passing logging-off attempt, the host TTY was normalized from VMIN=0, VTIME=0 to VMIN=1, VTIME=0, with zero explicit modem-signal calls in that preparation. No firmware or website code changed. That host setup is part of the qualified configuration; the evidence does not isolate the original ROM synchronization failure's cause. The second logging trial started without another external CLI preflight or TTY change.

The pinned stock flasher also calls connect(resetMode, romBaudrate) from detectChip, although the second connect argument is a retry count with a default of seven. The supplied value is 115,200. This explains the excessive retry budget after failed synchronization, not why synchronization first failed. It is an external flasher issue; no website patch was required for the passing GUI tests and no website deployment is claimed.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#evidence-and-limits","title":"Evidence and limits","text":"

Final read-only operator verification again matched the exact version, original 15 checked preferences, public identity and ACL response; uptime was 686 seconds and flags remained 8. The owned Chromium profile and tunnel were closed, the serial port had no owner, and original host service states were restored: ModemManager and mctomqtt active, mesh-logger inactive. No Pi network configuration changed. Restored service state does not establish MQTT delivery.

The compact safe audit contains artifact/helper/report hashes and selected outcome fields. Completed sanitized reports and phase observations are retained privately; raw reference settings remain private. Failed attempts are retained separately from the qualified results.

The USB cable remained attached throughout these trials. The native-host sleep guard keeps CPU command service available while the USB host is connected, including when its serial port is closed. Power saving on is therefore not proof of CPU light-sleep entry, measured current, or an operator button test. At this source, V4 lacks the momentary-button wake profile flag. Its subsequent software/build and console validation is recorded below. The operator had no physical button access in this setup. The user's separate screen-wake confirmation does not establish this historical image's CPU sleep state.

These checks qualify this historical c88885a3 Full application, native Pi Chromium setup, protected active-slot application flash, and measured intervals. They do not qualify every host OS, board/role, adapter, cold power cycle, factory erase, heavy logging volume, measured power consumption, or RF traffic load. GPIO/reset behavior outside the unchanged stock flasher's reset sequence was not separately qualified. The release remains unpublished; the final same-source all-board build and release packaging are separate gates.

"},{"location":"test-results/hardware_validation_heltec_v4_native_console_2026-10-06/#updated-button-profile-checkpoint-ebf2f082","title":"Updated button-profile checkpoint: ebf2f082","text":"

Source ebf2f0823ef8413987d31ccb2d044a2090e88469 enables the existing momentary-button wake flag in the shared V4 and V4-R8 profiles. Seven ESP32 USB/sleep tests and two display-power tests passed. Configuration assertions cover 71 firmware profiles, excluding four deliberately standalone partition migrators. Six host cases execute actual board sleep/USB methods, real family button constructors and implementation, role polling guards, UITask button fragments and DisplayPowerPolicy. Three controls must fail if the profile flag, gesture polling guard or display wake is removed. These are synthetic GPIO/sleep results, not a physical button press.

Updated artifact Bytes SHA-256 Full application 2,097,560 aebfd06f5e35ad636f52f329bd051deb9dfeb633361dc77396c0ee9d4456e7c1 Merged image 2,163,096 08750bd38f9f71ea37b8739a8b3c26befa91829df6d5754ad9cffbf61a796a90 Matching ELF - c2ec6d54c32c4b3cb331f9216607480e851933cf4865cc399be9409fdf587467

The normal Full target, table, two slots, capabilities and RAM gates passed, with the same 20,880-byte required-heap margin. Linked instructions now contain the GPIO0 pending-press check, active-low wake and button cleanup, plus the gesture polling guard, while retaining radio IRQ14 wake. The HWCDC PHY handoff and startup hook remain linked; no TinyUSB USBCDC class is linked. This confirms the profile flag reached the application rather than only its source recipe.

The same pinned stock native GUI/flasher and unchanged 5,000 ms deadline were used. Before this flash, host-only TTY preparation again changed VMIN/VTIME from 0/0 to 1/0 with zero explicit modem-signal calls. The app-only ROM preflight again selected the protected active slot. Exact runtime readback was v1.17.1.9-halo-keymind-cascade-dev-ebf2f082 (Build: 06-Oct-2026).

Updated-source check Result First stock DOM GUI Connect after fresh flash PASS. Plain time clock-set acknowledgement in 2,036.4 ms; full GUI bootstrap, exact version and original reference checks passed. No extra reset, replug, settle or retry. Power saving on, USB logging, console open PASS. 180.061 seconds with zero native writes or keepalives; uptime 17 -> 198 seconds. First post-idle core reply took 39.7 ms; full DOM reconnect time acknowledgement took 45.0 ms. Power saving on, USB logging, console closed PASS. 180.100 seconds with streams closed and zero writes; uptime 205 -> 391 seconds. First DOM reconnect time response took 26.7 ms and was the recognized clock-cannot-go-backwards rejection, not an acknowledgement. Full GUI bootstrap passed. Cleanup and operator readback PASS. Original power saving off, USB logging, all 15 checked preferences, public identity and ACL response were restored. Streams closed; independent operator readback again matched the exact updated version and original digests at uptime 448 seconds.

There were no native command errors or GUI modem-signal calls. Radio remained in active RX with zero physical RX errors; flags changed 0 -> 8, received packets stayed zero and one packet was sent. This remains a receive-silence watchdog warning, not a CPU crash or loaded-radio qualification. The owned browser and tunnel were closed, the serial port had no owner, original host service states were restored, and no Pi network configuration changed.

The compact safe audit records this as updated_source_checkpoint, separate from the older 600-second trials. The updated source qualifies these 180-second console intervals. The operator did not physically press the button; the user's manual screen-wake confirmation is recorded above without assigning it a firmware SHA or CPU sleep state. CPU sleep and current draw were not measured in these USB-attached tests. GitHub unit run 37426156162 for this new source is still in progress at this checkpoint. Release 1.17.1.9 remains held and unpublished.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/","title":"Hardware validation heltec v4 scan first ap 2026 10 06","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#heltec-v4-scan-first-ap-validation-2026-10-06","title":"Heltec V4 scan-first AP validation - 2026-10-06","text":"

The expanded-partition Full repeater is the recommended image for these V4s. Minimal remains a compatibility profile for the smaller portable OTA slot; expanded nodes do not need a second minimal variant.

Source base: 8d31b3932de614ac38d238f1a01010bc5f362fe8, with the working changes described below. git pull --ff-only reported already up to date. The tested version is v1.17.1.9-halo-keymind-cascade-dev-v4fix8-8d31b393. PlatformIO commands ran sequentially in a native WSL checkout.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#failures-and-corrections","title":"Failures and corrections","text":"

The original latest-source check reproduced a WebConfig AP that reported startup but could not be discovered by a normal WiFi client. Earlier simulated tests and CLI status had not proved beacon discovery, association, or the full browser flow. Those were material coverage gaps against the reported problem.

WebConfig now removes the previous AP while preserving a shared STA/ESP-NOW driver, scans in STA-only mode, and retains the results for the first picker request. Startup verifies live SDK SSID, visibility, authentication, channel, b/g/n protocol, AP-start event, IP, and absence of a STA association before reporting success. Startup failures remain bounded by the five-second console deadline. A failed scan is canceled before AP startup; ordinary picker requests then return an empty result and permit manual SSID entry. Only explicit Rescan starts another scan on that setup AP.

The pinned Arduino 2.0.17 scanner used an uninitialized wifi_scan_config_t. Its active path left passive dwell and home-channel dwell fields indeterminate. A source/version-checked middleware now zero-initializes the structure in a private build copy. The shared PlatformIO framework remains unchanged. Before this correction, channel-zero scans were still running after 3.6 seconds and were canceled. Afterward, repeated all-channel scans completed in approximately 2.4 seconds and supplied cached results. This hardware comparison supports the uninitialized configuration as the cause of that timeout.

The standard ESP32 minimal LoRa OTA profile uses the existing compact Ed25519, SHA-512, and target-name implementations to fit its portable slot. Full and ordinary standard profiles retain their existing implementations.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#builds-and-host-regressions","title":"Builds and host regressions","text":"Check Result Full repeater app PASS, 2,098,744 bytes in each 6,553,600-byte A/B slot; 10 capability markers and internal memory gate passed. Full SHA-256 d534c9d79796886fb84690c784ae985bfef6d808d5e7879409f139e9463f126d Full running body hash 8C8A7ADBDA25501E Minimal app PASS, 1,281,576 bytes; 29,144 bytes spare in the 1,310,720-byte portable slot; 6 capability markers and memory gate passed. Minimal SHA-256 4f0d55aa3c0c85e76c50a2ba60738294515552dafa5e9496d6a33d73f1072e27 Minimal running body hash 060106D2CA3CA669 Focused host suite PASS, 65 Python tests, including four compiled AP fixture variants: Arduino 2/3 x open/password AP. Build profile tests PASS, test/test_build_profiles.sh.

New AP fixtures exercise stale independent SDK state despite successful facade returns, setter/readback failures, delayed AP events, shared ESP-NOW ownership, LAN promotion, scan timeout/cancellation, cached picker results, explicit rescans, and failed-scan manual-entry behavior. Worst modeled startup failure is 4.59 seconds. A very slow scan can consume the retry budget, so a transient first AP failure in that case returns a bounded failure rather than retrying.

Five new middleware tests execute the actual patched scanNetworks method against an SDK capture. A poisoned-local negative control fails with the old uninitialized source. Tests cover caller parameters, active/passive defaults, already-running scans, private-copy isolation, and source/version guards. Eleven existing crypto and target-name tests verify signature compatibility and preservation of all target names/IDs.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#physical-checks","title":"Physical checks","text":"

Mercerwood's Heltec V4.3 OLED repeater was identified by its exact USB serial. Its expanded partition geometry was checked before each application-only OTA upload. No partition table, bootloader, identity, or ACL was replaced.

Boundary Result WebConfig, ESP-NOW off PASS: fresh discovery, association, gzip loader, full UI, setup API, cached all-channel results, explicit rescan. Startup reply approximately 2.70 seconds. WebConfig, ESP-NOW on PASS: discovery, association, full UI/API, cached shared-channel results, explicit rescan. Startup reply approximately 0.44 seconds. 2.4 GHz off/on PASS: master off rejects individual WiFi-on; master on restores WiFi/ESP-NOW; subsequent WebConfig AP works. OTA after master off and repeated startup PASS: discovery, association, and update page. Subsequent WebConfig AP also works. Actual browser JavaScript PASS: the unmodified setup wizard opened populated networks automatically; Rescan repopulated them; manual unsaved SSID entry and picker close/reopen worked. Automatic startup after reboot PASS with WebUI temporarily enabled and no saved SSID: AP discovered/joined and setup API returned cached scan results before any manual start command. Original disabled WebUI setting restored. Minimal WiFi OTA PASS for three start/stop cycles, completed raw scans with fresh beacon age, exact-BSSID association, and the lightweight uploader page. Its raw uploader installed the supplied Full app and rebooted into its exact hash.

Full's seven-case manual run completed without an unexpected CPU reboot. Minimal cycles also completed without an unexpected reboot. The final node is back on the Full hash above. All original saved preferences, public identity, ACL, and runtime radio states were compared after restoration. The Pi's original WiFi profile and wired route are restored, temporary profiles are removed, serial ownership is released, and the original service states are unchanged.

The minimal profile omits manual WiFi/ESP-NOW group controls; their unavailable responses are recorded rather than counted as master-off recovery. Full carries that recovery qualification.

Two test-runner errors are retained in the raw evidence: an early minimal run could not find iw outside sudo's path; the corrected run then incorrectly expected automatic AP startup while the saved WebUI master was disabled. Its five minimal/AP/upload checks and Full/settings restoration passed. A separate enabled fresh-boot run correctly qualified automatic startup. An earlier update and verification overlap was also corrected by a lock covering the entire Pi test and cleanup transaction. These failures were not firmware acceptance passes.

Sanitized physical evidence is in heltec_v4_scan_first_ap_2026-10-06.safe.json. Private credentials, scanned network names, and saved secret values are excluded.

"},{"location":"test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/#remaining-boundaries","title":"Remaining boundaries","text":"

Future acceptance must include fresh discovery, association, usable UI/API, first cached picker results, explicit rescan, and cleanup on actual hardware; CLI success and simulated state alone are insufficient.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/","title":"Hardware validation known failures 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#known-failure-follow-up-20-21-october-2026-utc","title":"Known-failure follow-up, 20-21 October 2026 UTC","text":"

Status: V4 USB PASS; Morse false letters are an accepted limitation. Final-source CI remains in progress. This scoped follow-up preserves failed measurements and the operator's limited Morse acceptance. It does not grant broader release approval. The current V4 is a different physical board from the 18 October campaign.

Final source: 0b207a1ec3a32eb0a8ad6b8643db211de4921cd4. Safe evidence summary records source bindings, evidence digests and explicit pending fields.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#builds-and-host-verification","title":"Builds and host verification","text":"Check Source Result RAK4631 Full 0b207a1ec PASS: 712432/712704 application bytes, 272 bytes free T096 Full, FEM on 0b207a1ec Shipping build/package PASS V4.3 Full, FEM off 0b207a1ec Shipping build/package PASS: 2559496 application bytes Focused Morse host suite 0b207a1ec 111 methods PASS Targeted USB host suite 7278eacb9, included in final source 45 methods PASS, including 12 HWCDC TX backport suite methods with actual ISR scenarios Earlier unchanged RadioProfile suite fc72943f3 17 methods PASS; not rerun for final source

RAK Full retains all 13 feature capabilities plus profile.full; its build used INA260 1.5.4 and MLX90614 2.1.7. Builds and host tests do not qualify RF behavior or physical USB stability. In CI run 39523154379, all 11 non-main jobs are reported green; the main host job is PENDING. A completed workflow pass is not yet claimed.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#v4-application-and-native-usb","title":"V4 application and native USB","text":"

The earlier 4773ac4219666d0f5b4c2bff5eb72aa053014662 application passed full readback with unchanged partition table and OTA metadata. An initial private adapter's ordinary hard reset left native S3 in forced download mode despite correct readback. The pinned flasher's masked forced-download clear and watchdog reset restored the application. Its adapter guidance was clarified in meshfirmware commit 4ce4f44; the flasher already had the correct sequence.

The second logging-on native-console trial on 4773ac42 passed 600.1086 seconds with zero idle command writes. The observed idle phase started at 2026-10-21 00:13:03.667 UTC. First reply/reopen took 124/21.8 ms; uptime advanced 378 -> 979 -> 981 seconds. Identity, settings restoration, strict binary handback and cleanup passed, with zero core errors. However, watchdog recoveries rose from 0 to 2; the last reason was client inactivity (0x08). This is a transport PASS, not a clean watchdog qualification. The earlier browser startup failure and its successful restoration remain recorded separately.

Fix 7278eacb9131d613da719fee814fefcdad3adf23, included in final source, requires 100 ms of continuous presence loss before invalidating the valid client lease and recognizes the host-backed physical reader. It preserves bus-reset, stall and true-unplug recovery. The finding does not conclusively attribute both earlier events to that path.

Ordinary shipping 0b207a1ec was restored successfully at 2026-10-21 01:21:32 UTC. All 2559496 application bytes matched SHA256 04eed20ee0f964fef2a5a0f74f4aafe4b11e0a71b011ff757efc96c03a2cfb14; partition table and OTA metadata stayed unchanged. Runtime source/identity, absence of the raw diagnostic marker, strict binary handback and clean core state passed. This was an application-only update.

Final logging-on native-console case 3 on this shipping image: PASS. The idle lasted 600.0913 seconds with zero command writes; post-idle reply and reopen took 105.3/20.5 ms. Uptime advanced 172 -> 773 -> 775 seconds, with zero core errors. Watchdog recoveries stayed 2 -> 2 and boot count 0 -> 0. The watchdog's boot field counts reset authorizations, not all physical boots; the continuous uptime separately establishes no MCU restart during this case. Identity, original preferences, strict binary handback, TTY restoration and owned browser/stream cleanup passed. The wrapper finished at 2026-10-21 01:34:33 UTC without errors. This qualifies the current board and artifact for this finite logging-on idle/reopen case; it does not establish the cause of the different 18 October board's USB disappearance.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#morse-measurements-and-accepted-limitation","title":"Morse measurements and accepted limitation","text":"

These shipping controls use automatic detection without HIL threshold overrides.

No intentional TX, 30 seconds RAK4631 T096 Unchanged 08650f64 FAIL: 3 false characters FAIL: 1 false character Adaptive 4773ac42 PASS: 0 characters in this control UNQUALIFIED: 6 retained characters and command timeout during external-actor contention

The contended T096 observation is not a clean detector pass/failure attribution. The RAK control alone does not qualify complete messages or other board families.

An unchanged 08650f64 V3 transmitted to a shipping 4773ac42 V4. The quiet control came first: FAIL, 26 false characters in 300.820761 seconds. Three messages at 25/60 WPM, two repeats, then produced 4/12 exact passes and 8 failures. Both preferences and strict binary handbacks passed; the wrapper reported no errors. This one-way campaign does not replace the original two-way 24-message matrix.

The operator accepted false letters for now and stopped further detector/gate work. That accepts the false-letter limitation; it does not establish complete message accuracy or turn the failed measurements into passes. Further Morse experiments are not a mandatory gate in this follow-up. The receive setting minimum is 15 WPM, default 25; TX at 1-14 WPM remains available. Hand-timing tolerance remains, so setting validation is not an absolute physical speed cutoff.

Separate raw diagnostic captures used production RF settings and automatic squelch. Capture integrity, both preference restorations and both strict binary handbacks passed. These instrumented captures do not qualify shipping accuracy. The ordinary shipping image is restored; raw lab details remain private.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#external-nrf52-bootloader-handback","title":"External nRF52 bootloader handback","text":"

The operator reported completion of their separate bootloader testing. Its deployment record was also read: COMPLETE_WITH_KNOWN_LIMITATIONS, eight live INFO/original-application handoff proofs and gateway service handback PASS. It uses unpublished test bootloader TEST_0x02040B7E, source b07255d4cdc601f1313e4cbe020caad3b5544821, not a latest-stable release. The record reports no publication or push by that deployment.

Limits remain: scheduled concurrent USB control failed with one EPIPE and 31 successful requests; 11 unique bulk halt-clear cycles passed. RAK secondary-data preservation is uncertain after earlier manual scratch overlap. Identity and seven settings match, without a complete immediately prior contact/channel baseline. These records do not qualify final-source 0b207a1ec applications. The nRF52 hold is released; no new nRF52 campaign was started. MeshPocket remains excluded from all further testing in this session.

"},{"location":"test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/#remaining-scope","title":"Remaining scope","text":"

Current CI completion and final release artifact-matrix binding remain pending. The scoped V4 logging-on hardware case is complete. The 20 October fleet qualification retains its accepted USB/BLE/RF/contact coverage at its actual sources and conditions. This report neither reopens those closures nor extends them to unmeasured artifacts. Historical failures in the 18 October report remain visible. Private device identities, addresses and raw responses are omitted.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 18","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#mercerwood-11801-hardware-campaign-2026-10-18-utc","title":"Mercerwood 1.18.0.1 hardware campaign, 2026-10-18 UTC","text":"

This campaign is in progress. It is not release approval. Results below bind specific images and physical devices; successful compilation and older bench results do not qualify unexecuted hardware cases.

After the operator reset the V4, its exact USB by-id and physical-path endpoints were present at 15:09 UTC. A fresh Full ec9ebc09 binary identity snapshot passed. Authoritative original-data restoration and fresh binary/62-field comparisons subsequently passed. The owned host diagnostic bond was removed and original services were active. Full logging-on remains failed/unqualified; the staged MQTT/WSS soak has not launched while upstream changes are integrated.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#firmware-and-preservation","title":"Firmware and preservation","text":"

The initial eight-image Full matrix built successfully from clean source 652289ee823bbed22918fe584f9d49ccb2ec230f, with release-length version v1.18.0.1-halo-keymind-cascade-dev-652289ee: V4 Room Server, V4.3 Companion FEM-off, RAK4631, T096 FEM-on, MeshPocket, T1000-E, SenseCAP Indicator LoRa, and Xiao S3 WIO. Room fixes were separately built and readback-verified at d3e05abce308b52261e86c9297029b4bce3cc4a1 and d52e4e93475e0960328131e2e65cc6b992aa4dbb. The diagnostic-enabled Full V4.3 FEM-off and Indicator LoRa images built successfully at ec9ebc0914d61a02fb21f23444768ec5f847e7ec. The later MeshTower V2 SD Full build and transfer are recorded separately below.

Before ESP32 application changes, complete private flash backups were made through the pinned real flasher's identity/reset/finish path. Application-only writes checked silicon MAC, USB identity, capacity, active OTA slot, image integrity and full application readback. Partition tables and OTA metadata were verified unchanged. Original NVS/filesystem restoration is a separate operation; an application-only flash does not restore settings changed by tests. The Indicator retains its existing noncanonical 8 MiB partition table.

nRF52 updates used application-only DFU. The RAK3401 service radio and Indicator RP2040 display coprocessor were excluded. No root USB hub power operation or concurrent PlatformIO process was used.

The flashed 652289ee artifacts below were built from source 652289ee823bbed22918fe584f9d49ccb2ec230f. USB identity labels are the first 12 hexadecimal characters of SHA256 of the UTF-8 USB serial as reported; the serial-less CH340 Indicator was bound privately by its physical USB path and public device identity. These hashes bind the executed images, not unexecuted release qualification. ESP32 hashes cover application-only BINs; nRF52 hashes cover the application DFU ZIPs actually transferred.

Target Source USB serial hash Artifact SHA256 heltec_v4_room_server 652289ee8 af0a529cb0cc Application BIN 4e370dd285813ce330aae13a9a8891795145d3bbb7adef84ff4dbb19926cb677 RAK_4631_companion_radio_full 652289ee8 b11fbd38286f Application DFU ZIP 64940ae543491831a20da2176e50e44a1519917bd42c4615601ddc0c7bf82364 Heltec_t096_companion_radio_full_femon 652289ee8 ab7bc0653271 Application DFU ZIP 0a44dbc7a39fb65007713aa61c004b8bb790035c1d6c343ba150a0a77f855dfa Mesh_pocket_companion_radio_full 652289ee8 c5ba0587de70 Application DFU ZIP eb13c05521f2545719db809c5367ed31de22e9aeadc0c0992366a6ab770570c5 t1000e_companion_radio_full 652289ee8 5244f0b64696 Application DFU ZIP 7a6003afa50720e2c97cfbb625692143f66b4e7de063de63106fecd5ff4fc5fb SenseCapIndicator-LoRa_companion_radio_full 652289ee8 No USB serial Application BIN 6cb1405fb2a6c3f4889506f184751d7576ad23159c303f84c054e4ad6b124b03 Xiao_S3_WIO_companion_radio_full 652289ee8 b00e0958f2ff Application BIN 59c62ff6d985aa42740b6c26dcc30efe50c04b747d94d566e4ca4a8cd1e42e47 Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors 652289ee8 030f24857801 Application DFU ZIP 0d20f4486023a13427eb09087da70aacac5562848371a919631ab6a76f7888d1

The same V4's subsequent Room application hashes were 54962d21088f1c87c3956620181a3499102e074d2e28791003d1b0479f64c7af for d3e05abc and 7753f5f4161a6631d7f3abe44a99d70d5b892a94ad3cb69d313d54e44c608c87 for d52e4e93. Their archived exact ELF hashes are respectively 57d5b38fcc454d1ccc05763d3d8c6fa047904c42ca761be3a0393f14e799d1eb and 5db0fb151ee388ffab66ccce11b0d137907ff6450530f7812d35383c8c6aa785.

After the Room campaign, the V4.3 Full FEM-off ec9ebc09 application passed identity-bound application-only flashing and complete readback. Its 2,545,080-byte BIN SHA256 is cb1e167a222e7a8d0ca253f7e3272561a29ba6753f4173eb89c79405b2bdad2f. The separate restoration then wrote only the original NVS and SPIFFS partitions from the pinned pre-campaign full backup. Both partitions matched their original bytes on readback; the active application, partition table and OTA metadata were independently verified unchanged before runtime handoff.

A fresh Full snapshot matched all original SELF_INFO bytes, complete contact records and all 40 canonical channels. Returned DEVICE_INFO settings matched after excluding only the runtime active PIN and independently verified new build/version metadata. There were no new core error flags. This qualifies the original data readback and those returned settings, not every unexposed preference or battery-backed cold boot. At that restoration checkpoint, the Full runtime had WiFi off, an unset SSID, MQTT enabled but not running, and unavailable ESP-NOW; temporary test changes require their own restoration. Later native-console tests enabled power saving and USB logging and disabled MQTT. Those flags were unrestored at 09:22 UTC, then passed identity-bound BLE restoration and readback at 09:41:23: power saving off, USB logging off and MQTT on, with WiFi off and MQTT not running. After USB returned, both original NVS/SPIFFS partitions again matched the backup on readback; the active application, table and OTA metadata were unchanged. Fresh binary and all 62 returned CLI fields matched the original baseline. The diagnostic's owned host BLE bond was removed after restoring the original device data. This establishes those readbacks and returned settings, without extending the earlier cold-boot or unexposed-runtime qualification.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#executed-companion-checks","title":"Executed Companion checks","text":"Physical hardware Candidate USB queries/reopens Owned contact add/update/delete Add/delete survive warm reboots BLE bonded hold/reconnect RAK4631 Full 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions T096 Full FEM-on 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions MeshPocket Full 652289ee PASS: 100/10 PASS PASS PASS: two 10-second sessions T1000-E Full 652289ee PASS: 100/10 PASS PASS FAIL/unqualified: four initial attempts; see 19 October follow-up below Xiao S3 WIO Full 652289ee PASS: 100/10 Not attempted: 350 occupied contacts Not tested Not tested SenseCAP Indicator LoRa Full 652289ee Identity/readback and USB WiFi/BLE mode cycle PASS Not tested Not tested Not tested

Warm tests used a confirmed-absent random contact, verified complete readback, performed the shipping guarded reboot, verified persistence, removed only the recognized owned record, then rebooted and verified original state. They do not prove cold boot. Display boards legitimately regenerate the active BLE PIN when the saved PIN selects that policy; comparisons exclude only those four runtime bytes and retain the private raw frames. Original contacts, canonical channels, public identity, name and the other returned SELF_INFO/DEVICE_INFO settings fields are checked. This comparison does not inspect every persisted preference.

The Xiao retained all 350 contact keys and all 40 channels. A known contact's advertisement and modification timestamps advanced during live mesh traffic; this was not a contact edit by the runner. No existing Xiao contact was removed to make room for a test record.

The Indicator LoRa Full mode cycle passed on 2026-10-18 UTC. USB checks verified the exact board, complete candidate version and public identity, then proved warm reboots into WiFi and BLE modes with the expected selected, boot and live service states. The original BLE selector was restored and another warm reboot verified. All 40 channels, the empty contact baseline, returned binary settings, saved radio/RXPS/radio2 configuration and queried Bluetooth preferences matched the original state, with no new core error flags. The runner opened seven USB sessions and issued 510 framed requests; it issued no RF message commands or partition/RP2040 writes. These checks do not qualify an external TCP or bonded BLE session, font download/retention, fresh NTP, visible rendering or cold boot.

The ESP-NOW Full build at ec9ebc091 failed the mandatory internal RAM gate: 233,688 bytes available versus 251,168 required, a 17,480-byte shortfall. Its native 480px WiFi canvas policy remains unchanged. Both ESP-NOW by WiFi/BLE combinations remain unqualified; the normal Reduced ESP-NOW USB image lacks those secondary transports and cannot substitute for their Full acceptance.

T1000-E BLE initially resolves services and returns identity/core data, and one attempt returned all five read-only getters. Later writes/reads fail with backend service-discovery, ATT 0x0e, or timeout errors. USB remains healthy. The probe's final disconnected event is cleanup, not proof of which side terminated a connection. These observations do not establish a root cause.

A fourth existing-bond trial captured HCI traffic. It completed encryption and service discovery at MTU 247 and validated the complete version and public key. The get name ATT Write Request (0x12) received its Write Response (0x13), then no reply notification appeared. About 2.01 seconds later the controller reported Connection Timeout (0x08), matching the negotiated two-second supervision timeout and preceding the client's eight-second reply deadline. No host HCI Disconnect command appeared in the capture. This trial therefore lost the link before client cleanup. BLE uptime was 2720 seconds and later USB uptime was 2993 seconds, consistent with the 272.70-second host interval; both reported zero core error flags. This excludes a full MCU reboot in that observed window, without identifying a firmware or RF cause. The post-trial BlueZ state was disconnected and both owned processes closed. Negotiated link parameters were a 30 ms interval, latency four and 2000 ms supervision timeout; no PHY/data-length-change event appeared in the saved capture.

A separate diagnostic build enabled the existing RAM trace and disabled debug printing, retaining the same source, version and connection parameters. Its application DFU ZIP SHA256 is 713765d9145db2aec03059a87280a065d4ae68e1a23ee4dd56a4d16f37b4f8c1; the exact ELF SHA256 is 18442324b87deef9af658d2997b668ee10912929af4be9bacf9c473bc70d2169. This trial failed during GATT setup before any Companion request was sent. The MCU initiated its documented no-app-data recovery exactly 15 seconds after security completed. All 14 recorded stack dispatches exited, with a maximum 2.93 ms dispatch and 103 ms main-loop gap. All 62 trace records were retained, with no ring wrap or recorded contention drops. HCI recorded the peripheral termination and no host Disconnect command. This is a different failure stage from the shipping post-getter supervision timeout; concurrent host Chromium startup is a possible, unproven confounder. It does not identify a production firmware fix. No old post-USB core metrics were reused for this trial.

The original shipping application was then restored through application-only DFU. Identity, complete contact records, all 40 channels and all returned device settings matched the immediately preceding baseline, including the active PIN; core error flags were zero. An explicit read confirmed trace support absent. The diagnostic image does not count as shipping BLE qualification.

GPS was enabled for a bounded 180-second local-only T1000-E telemetry window. No location appeared. The exact GPS switch was restored, sync cadence was unchanged, and packet TX counters did not advance. Outdoor acquisition, fresh-fix age, stale/no-fix behavior and sleep current remain unqualified.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#t1000-e-ble-follow-up-2026-10-19-utc","title":"T1000-E BLE follow-up, 2026-10-19 UTC","text":"

The unchanged Full 652289ee application passed a fresh five-second bonded session at 07:00 UTC and three consecutive 30-second held/reconnect sessions at 07:04\u201307:07 UTC. Each session checked the board, complete firmware version, USB-associated public identity, five read-only getters and advancing uptime. The existing bond remained, host cleanup completed, and a subsequent USB snapshot reported zero core error flags. The earlier timeout was not reproduced.

Source review separately found a use-after-free in the pinned Bluefruit dispatcher: disconnect handling deleted its BLEConnection, then role routing read getRole() through the freed pointer. Commit dc17f507dd0657cfd37dfe73e7634d6b81166f64 caches the role before deletion and refreshes it when a CONNECTED event allocates a replacement connection. The production-source ASan/UBSan regression reproduces the original memory error and passes the corrected peripheral and central paths, callback ordering, handle replacement and repeated reconnects. All 36 nRF52 BLE host tests passed. This removes the disconnect dispatcher's memory-safety defect; it does not establish the cause of the earlier on-air supervision timeout.

The shipping Full T1000-E and RAK4631 targets built successfully at that commit, including their capability and runtime-RAM gates. The T1000-E application DFU ZIP SHA256 is aa02aadad102201b30489efc80a044f98af3b9303630b4832ee21c65c3a93b40; its exact ELF SHA256 is 6076d25e4c4ecadea8552c1bb56a97865b3369c1062d4a040e90d33368f9d214. The candidate booted through identity-bound application-only DFU and passed three 30-second held/reconnect application sessions at 07:13\u201307:15 UTC, with the same identity, getter and uptime checks. Before and after the BLE trial, USB snapshots matched the pre-flash public identity, complete contact record, all 40 channels and returned SELF_INFO/DEVICE_INFO settings, excluding only the independently verified build/version metadata. Core error flags were zero. The existing bond remained and owned probe/capture processes closed.

These successful application sessions include connection retries; they are not evidence of uniformly reliable connection establishment. The baseline capture contains six 0x3e disconnects before encryption, and the candidate contains two, each about 154\u2013179 ms after a connection-complete event. All three completed sessions in each trial encrypted successfully. Baseline sessions two and three, and candidate session one, ended with 0x22 about 1.97 seconds after an explicit host HCI Disconnect request. No 0x22 occurred during the completed getter/hold traffic; the other completed sessions ended with 0x16. Both captures recorded zero drops and no remaining target handle. Neither held trial reproduced the earlier 0x08 supervision timeout. Longer soak, phone-specific testing, establishment retries and host-requested disconnect completion remain open; these captures do not assign their cause to the peripheral or host controller.

The exact original 652289ee application ZIP was restored through application-only DFU. A fresh five-second bonded session also passed after restoration. USB snapshots before and after that session matched the original complete version, all SELF_INFO/DEVICE_INFO bytes, full contact record and all 40 channels, including the active PIN; core error flags were zero. BlueZ remained paired, bonded and disconnected, and the Bluetooth service remained active. No pairing or preference setters were used; DFU transferred only the application. Raw HCI and snapshots remain private; these comparisons do not inspect every unexposed preference.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#meshtower-v2-sd","title":"MeshTower V2 SD","text":"

The MeshTower V2 SD Full image from 652289ee8 passed identity-bound, application-only USB DFU and initial SD mount/list/space checks. All 16 returned getter checks matched the original journals, including public identity, radio/power/logging settings, OTAFIX2.4.11-preview.2, and InternalFS geometry at 0xED000+28 KiB. Ten consecutive SD space/list/core triplets completed all 30 replies: uptime advanced from 119 to 125 seconds, core error flags and queue length remained zero, and the original directory metadata and reported 32.0 KiB used/959.6 MiB free stayed unchanged. The directory contained the same single 4-byte /mota/cache.off marker; no file contents were exported. Original firmware had error flags 2 before the update; the new boot is evaluated separately. These checks qualify returned values and read-only SD access only. They do not qualify card-absent fallback, cold initialization, interruption or power-loss recovery, LoRa OTA transfer/install/serving, or a bootloader update. No SD format/erase or deliberate LoRa OTA transmission was performed.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#room-authentication-and-browser","title":"Room authentication and browser","text":"

On the original 652289ee Room image, actual LAN requests from two separately bound source addresses passed per-address failure isolation, shared-budget exhaustion/refill, existing-session access, no token refund on success, and post-window recovery. The Room password checks passed duplicate-password suppression, five-unique-failure admission, the global 30-second window, nonempty admin/guest rejection during lockout, and continued public blank read-only access. Public guest wire role is 0; role 1 denotes explicit ACL read-only permission. An earlier runner used the wrong expectation; its failed sample is retained separately.

The complete two-source-address LAN suite was repeated on d52e4e93 and passed all WebConfig and Room cases in 161.67 seconds, with no temporary lockout left. These network checks use guarded timing windows, not exact millisecond boundary measurements.

The actual saved-network recovery AP passed anonymous protected-config denial, wrong-password rejection, existing-admin login/config read and logout. Status required needs_setup=false and needs_password=false, distinguishing recovery from initial password setup. The Pi used an owned temporary WLAN profile while Ethernet management/default route stayed unchanged, then restored its original WLAN profile and deleted only the temporary profile.

The real Room browser also passed throttle-error polling, Connect retry and draft preservation over this AP. The first AP browser attempt was an inconclusive harness failure: the portal's favicon redirect was outside the strict request allowlist. A narrow setup-mode redirect-chain check fixed that observer; the second run passed with native fetch unchanged and zero compose/write requests. Its unused icon follow-up failed outside the SSH-forwarded origin and is reported separately from completed API response bodies. The AP BSSID was measured, and the observed AP was open while protected settings still required the saved admin password. AP two-source throttle testing remains unexecuted.

An actual Room LoRa trial verified a temporary matching 910.525 MHz/62.5 kHz/SF7/CR5 profile before transmission. Shared guest-password login returned non-admin RW2 permission, denied admin-only ver, and returned the board index. The next mail settings request had local send admission but no decoded reply before its 35-second deadline; no delivered reply was discarded by the runner's type/prefix matching. This is an incomplete RF request/reply trial, not a demonstrated mailbox authorization fault. Source inspection confirms active sessions may read their own settings. Source contacts/returned settings, logout and the Room's original saved/temporary radio state were restored.

Post-trial counters showed 18 received packets (14 flood/four direct), six transmits, one receive error and no outbound packet, with continuous MCU uptime. This establishes reception on the temporary matching profile. It does not identify which packet was lost or qualify mailbox mutation/retry behavior.

A separate admin trial with an explicit two-second gap after each peer request passed login, exact version/board reads, complete mailbox settings, self-send, body readback, acknowledgement and both exact request replays. It required an empty own inbox and provisioned only its unchanged closed policy. The first replay returned the original message ID; replay after acknowledgement also returned that ID without re-enqueueing. Final reads verified an empty queue and the original mode, delivery and complete allow list. Source contacts/returned settings, logout and the Room radio lease were restored. This qualifies the explicitly paced live-session sequence, not unpaced reliability or durable replay across reboot/cache eviction. Owner/revision/retired-receipt metadata persisted until the separate original-filesystem restoration described above; policy preservation alone does not mean byte-identical filesystem contents. The earlier incomplete trial remains a failed sample.

Actual Chromium/native-fetch testing exposed a loop-task stack overflow while loading board.index. The saved core matches the 652 Room ELF and shows processRoomRequest -> loadRoomBoard -> recover -> filePresence -> snprintf reaching the stack-bottom watchpoint. Browser 202/429 response bodies also were not consumed under their request deadline.

Commit d3e05abc separates operation scratch frames and drains response bodies under a bounded deadline. Actual browser runs with an empty board and with an owned 2048-byte UTF-8 article both passed login, automatic throttle-error polling, production Connect retry, preserved unsent drafts and unchanged boot/browser identity. Every completed 202 response in these runs was drained.

A subsequent populated-board stress run completed five whole article reads, then triggered another panic during the sixth. Its fresh core positively matches d3e05abc, reaches the loop stack bottom, and reconstructs the populated index/filesystem/flash-cache/context-switch chain. The initial fix therefore does not qualify repeated populated-board operations. Further stack reduction and live retesting were required.

Commit d52e4e93 clears index state in place, recovers directly into the output, uses a checked heap allocation for web index metadata, and separates read/write scratch frames. Host fault and allocation-failure checks pass, including preserving an existing 2 KiB article on allocation failure. Stack-frame analysis shows a substantial reduction, but its estimated margin is not a hardware pass. The original owned article was recovered through exact title/version/content checks into a new private intent after flashing this candidate. Twenty complete live index/article reads passed with unchanged content, boot identity and continuous uptime. This qualifies the reproduced repeated-read case; original-filesystem restoration was verified separately after the Full handoff.

The populated-board Chromium check also passed on d52e4e93: exact served HTML, real admin form login, visible automatic throttle-error polling, Connect retry, and unchanged unsent draft and Room boot identity. It used unmodified native fetch, observed zero compose/write requests, and closed its owned browser.

The later CLI preflight showed sticky error bit 8 (ERR_EVENT_RADIO_WATCHDOG), versus zero shortly after the d52e4e93 boot. At observation the radio reported RX, zero received packets/errors, one direct transmit, no outbound packet, and noise floor -73 dBm. There was no MCU uptime regression. The HTTP/content pass does not establish absence of radio recovery events. Source inspection shows this Full Room build enables the five-minute successful-RX-silence watchdog even when MQTT is off. Its flag is set before the soft re-arm outcome is known. With zero received packets, that policy is consistent with the observed transition; it is not proof that HTTP caused a fault. Here, with recv=0, last_rx=0s ago is the formatter's zero-timestamp fallback, not recent-RX evidence.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#native-official-usb-console","title":"Native official USB console","text":"

The unmodified official console.js from commit 45a5257bf792b11a83f4c51c18d4f312addfb677, SHA256 107d3ad867915aea8b2454c324327397a0f9e2f0469656046054011bc64475f9, ran through Chromium's actual Linux Web Serial on the d52e4e93 V4 Room image. Power saving was on; WebConfig WiFi, MQTT and ESP-NOW were off.

With USB logging off, 600.35 seconds of uninterrupted idle transmitted zero commands/keepalives. The first complete CRLF reply arrived in 303.2 ms; after close/reopen, the reply arrived in 35.7 ms. MCU uptime advanced continuously and the preexisting sticky error flags remained 8. Native streams, owned Chromium and the original TTY settings were all closed/restored. This is a console lifecycle pass, not a current-consumption measurement.

With USB logging on, the same image also passed 600.38 seconds with zero commands/keepalives. The first reply took 164.5 ms and the reopened reply took 25.2 ms. Uptime remained continuous, sticky flags stayed 8, and all streams, owned Chromium and original TTY settings closed/restored. Power saving and USB logging were then restored to their original off values. Earlier startup/framing harness failures remain separate failed samples and do not qualify idle. These passes bind the Room image; final Full Companion remains a separate role-specific qualification.

The ec9ebc09 Full Companion image subsequently passed logging-off trial 03: 600.444 seconds, zero idle writes, a 275 ms first reply and an 18.1 ms reopened reply. Uptime advanced from 1284 to 1885 to 1888 seconds; error flags stayed zero. Both closes confirmed return to binary mode, and streams, owned browser and TTY settings closed/restored.

Full logging-on trial 01 started at 09:02:25 UTC with baseline uptime 2076 seconds and zero core error flags. Its nominal 600-second idle began around 09:03:30 UTC. At 09:11:13 the kernel reported the target hub port as disabled by hub (EMI?), re-enabling...; the parenthetical is the kernel's tentative wording, not a demonstrated EMI cause. Target USB path 1-1.2.1.2 disconnected at 09:11:14, followed by repeated descriptor/address errors -71. The kernel attempted a port power cycle at 09:11:15 and reported inability to enumerate at 09:11:16. Both the bound by-id and physical-path endpoints remained absent at 09:22.

The run ended at 09:13:42 with FileNotFoundError while TTY cleanup reopened the old path. That cleanup error masked the primary trial failure, which cannot be reconstructed from the old safe report. No post-idle reply, reopen check or qualified idle interval was obtained. USB recovery was still in progress at the 09:22 checkpoint; no firmware, reset or physical-fault cause was proven. The passed Room logging-on case does not qualify this Full image.

A subsequent identity-guarded, reset-only request to the exact parent hub's port 2 returned success. Port status changed from 0x101 to 0x103 after two seconds, with zero change bits: connected, powered and then enabled. The target still did not enumerate; the sibling T096 was unchanged. No hub power change or broader hub reset followed. A physical V4 RESET was requested from the operator. ModemManager was restored to active at 09:24:34 UTC; all four checked host services were active at that checkpoint.

The first read-only BLE diagnostic passed exact board, complete ec9ebc09 version and public-key checks. At approximately 09:35 UTC, core uptime advanced from 3959 to 3961 seconds with zero error flags, versus the pre-idle 2076 seconds. This establishes continued MCU uptime through the USB disappearance rather than an MCU reset in that interval; it does not establish a working USB connection.

The live USB watchdog reported Auto, stage 2, two recovery attempts and zero reboots. Its persisted last event was re-enumeration, sequence 2, uptime 2532 seconds, reason 0x08 (CLIENT_INACTIVE). Its RTC epoch is advisory and does not date this incident reliably. Offline extraction of the exact original SPIFFS active namespace found no usb_wdg, usb_wdg.bak or usb_wdg.tmp files. The journal therefore was not imported by the original-data restoration. Policy stage is volatile and starts at zero, so the evidence establishes two current-boot USB recovery attempts. These are requested actions, not proof of successful repair or proof that they caused the observed kernel errors. The later sticky TX-stall observation is separate from the event's earlier reason.

Source tracing confirms actual single-TTY ASCII stats-core renews the 900-second logging-client lease; framed USB/BLE stats and CLI getters do not. Natural expiry after the fresh pre-idle ASCII poll cannot explain a fault starting roughly 97 seconds later. A bus-reset/session-generation change, observed host/reader loss or logging-off can clear that lease earlier. No pre-failure evidence selects one of these paths. This is watchdog-progress evidence, without a firmware or physical-fault cause assignment.

BLE flag-restoration trial 01 passed at 09:41:23 UTC. Each of the three original flag values was acknowledged and read back immediately and again at the end; WiFi off and MQTT not running also matched. Uptime advanced from 4325 to 4333 seconds with zero error flags, and owned BLE cleanup passed. This restores those flags, not the whole saved-state image or the new diagnostic bond.

Guarded warm-restart trial 01 then passed exact identity and original-flag gates, durably recorded intent, sent the shipping 0x13 reboot request and observed disconnect. Pre-request uptime was 4556 seconds with zero error flags; owned BLE cleanup passed. USB was still absent at 09:45:07 and the kernel had no new entries.

The post-restart read-only trial 01 passed exact identity with uptime 34\u219236 seconds and zero core error flags, proving the deliberate warm reboot. It did not pair or create another bond. Persisted power saving off, logging off, MQTT on, WiFi off and MQTT not running all matched, and every owned cleanup check passed. The last watchdog journal was unchanged: re-enumeration, reason 0x08, uptime 2532 seconds and sequence 2 refer to the previous boot, not a new event.

By 09:48 UTC all four checked host services, including ModemManager, were active; USB was still absent. No additional software or hub recovery is planned. The operator was asked to unplug and reconnect only the V4 USB cable; physical reconnection and verified USB recovery were pending at that checkpoint. Battery-backed USB reconnection does not qualify cold boot. The new diagnostic BLE bond was retained while authoritative restoration awaited USB return.

After the operator reset the V4, its exact USB by-id and physical-path endpoints were verified present at 15:09 UTC. A fresh identity-bound binary snapshot passed the Full ec9ebc09 board/version/public-identity gates: uptime advanced 481\u2192482 seconds, core error flags and queue length were zero, contacts were empty and all 40 canonical channel records were read. That snapshot did not verify the live application flash hash. The Pi had been up approximately seven minutes at session start; neither that observation nor battery-backed USB return verifies a V4 cold boot.

The authoritative original NVS/SPIFFS restoration then passed complete readbacks, with the active ec9 application, partition table and OTA metadata unchanged. A fresh binary snapshot passed with uptime 85\u219286 seconds and zero core errors; comparison matched original identity, complete contacts and all 40 channels. All 62 returned CLI fields and the original power/logging/MQTT flags matched. Only the owned diagnostic host BLE bond was removed; all four original checked services, including ModemManager, were active afterward. USB return and restored data do not qualify the failed Full logging-on case. No cause or firmware fix is established, and WSS remains unstarted while upstream changes are integrated.

For Full, tools/hil/official_console_acceptance.py uses explicit --terminal-mode companion. It sends the shipping terminal-start control through the unmodified official module on each open and confirms terminal-stop on each logging-off close. Those four setup writes are counted separately from the zero writes during idle. Full's labeled logging-status reply is checked against the exact live mode. An earlier attempt omitted this handshake and timed out before idle; another failed during owned Chromium startup before serial opened. Both remain separate failed harness samples.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#shipping-morse-results","title":"Shipping Morse results","text":"

The unchanged Full RAK4631/MeshPocket images were tested at 902.1 MHz with requested chip TX power at most -9 dBm, three different messages, 25/60 WPM, both directions and two repeats. No HIL threshold, timing or sampling override was enabled. All 24 decoded messages mismatched. Thirty-second no-transmitter controls produced 20 characters on RAK and four on Pocket.

Wanted peaks across all 24 trials ranged from -50.5 to -46.0 dBm; the 25 WPM subset ranged from -50.5 to -47.5 dBm. Maximum observed valid sample gaps were 2-3 ms. Quiet RSSI excursions reached -110.5 dBm on RAK and -92.5 dBm on Pocket, above their roughly -126/-125 dBm floors. This supports an automatic-detection qualification failure, without identifying external RF interference versus receiver-generated excursions. The reported bad counter is consecutive read failures, not a cumulative guarantee of no SPI errors. All decoded capture lengths matched their firmware headers and reached the end marker, excluding host paging truncation in these trials. Source inspection shows automatic squelch can admit sustained quiet excursions and corrupt mark or gap boundaries; RSSI extrema alone do not establish those pulse durations.

A separate five-minute Pocket quiet capture expired automatically after 300.40 seconds, retained seven false characters, and returned the same capture on repeated firmware paging. Lifecycle/retrieval passed; silence detection failed. Original settings were read back after both campaigns. Historical successful HIL fixed-threshold trials do not qualify the shipping detector.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#host-checks-and-ci","title":"Host checks and CI","text":"

The production Room fix passed the complete workflow at d52e4e93 (run 39107275083). A later tooling run exposed a host fixture that invoked real fuser against its fake device path. The stricter ownership guard correctly refused that diagnostic error. Commit 7a5a7993 makes the no-owner fixture deterministic and retains real refusal tests for busy/error cases; its complete workflow passed (run 39109551854). The later AP browser-observer workflow also passed at 5fd33a230 (run 39110179910); both that run and the preceding 7a5a7993 run completed with all jobs green. The native-console cleanup follow-up preserves the primary JS/transport failure separately from browser/TTY cleanup failures, reports missing or replaced endpoints, and cannot qualify a cleanup failure. The native-console and Linux TTY suites passed 35 offline checks. The follow-up workflow at 76220d5d7 completed successfully with all eleven jobs green (run 39115389840). The later recovery-report workflow at 7b41d2893 also completed successfully with all eleven jobs green (run 39116894845). Those CI passes do not rerun or qualify the failed hardware case. Documentation route/link checks also passed in the CI-pinned renderer environment.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/#remaining-release-gates","title":"Remaining release gates","text":"

Private raw snapshots, flash images, credentials, BLE PINs, inbox traffic and cores remain outside the repository in the dated campaign directories.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/#mercerwood-full-firmware-testing-2026-10-19","title":"Mercerwood Full firmware testing, 2026-10-19","text":"

Later Companion results are recorded in the qualification follow-up. The records below retain the earlier campaign snapshot.

Source: 4e4f27a25a720c52ca0f5381b5e5834d75532cc6 Version: v1.18.0.1-halo-keymind-cascade-dev-4e4f27a2

Builds: 13/13 targets, 15/15 profiles PASS. The two Reduced OTA profiles are compile-only; repeater hardware status uses Full OTA installation proof. Expected radios: 13. The earlier 10-second inventory observed 12 distinct USB identities. Tag was observed separately after its later recovery; no fresh fleet-wide count is claimed. USB enumeration does not establish physical presence or absence. Build success does not mean all 13 radios passed hardware testing.

Board Build Flash USB BLE RF Bootloader Heltec V4.3 PASS PASS PASS PASS unknown n/a RAK 3401 PASS PASS runtime only PASS n/a n/a C2 baseline PASS LilyGo T-Deck PASS PASS PASS PASS PASS n/a RAK WisMesh Tag PASS PASS PASS PASS PASS C2 baseline PASS Heltec MeshPocket PASS PASS PASS PASS PASS C2 baseline PASS SenseCAP Indicator PASS PASS PASS PASS PASS n/a RAK 4631 PASS PASS PASS PASS PASS C2 baseline PASS Heltec V3 PASS HEAD runtime PASS PASS PASS PASS n/a Seeed XIAO ESP32-S3 PASS PASS PASS PASS unknown n/a Seeed XIAO nRF52 PASS transfer PASS; runtime unqualified unknown unknown unknown C2 baseline PASS Heltec T096 PASS PASS PASS PASS PASS C2 baseline PASS Seeed T1000-E PASS PASS PASS FAIL PASS C2 baseline PASS Heltec MeshTower SD PASS PASS runtime only PASS n/a n/a C2 baseline PASS

Repeater rows cover Full OTA application/runtime verification only. Their runtime-only USB entries do not claim Companion 100/10, BLE or RF qualification. C2 bootloader entries describe source-qualified installation evidence, including the later Tag recovery, separately from application results. All 8 required profiles now have installation evidence; this is not an installed-flash CRC readback.

Tag recovery is a later, separate result. Its earlier non-enumeration remains in the JSON history. Before recovery, USB descriptor error -110 persisted across cable and port changes, while T1000-E enumerated on Tag's former connection. These observations did not establish a root cause.

A physically correlated application-only Legacy BLE transfer was accepted without writing the bootloader or SoftDevice; that phase did not establish the original USB or FICR identity. A subsequent USB read matched the original serial and clean normal Full 4e runtime. Exact C2 bootloader and matching normal Full application USB DFU then both completed, with the public UF2 version TEST_0x02040BFF verified. Package manifest CRC DE20EBCB was validated before transfer; no live installed-manifest CRC measurement is claimed.

Tag then passed 100 persistent USB query cycles, 10 reopen checks, owned-contact cleanup and warm reboot, and radio2 warning/suggestion trials. Its three 30-second BLE sessions matched the USB public identity and full version, retained clean core flags and ended disconnected. One zero-hop DEV datagram passed in each direction with the RAK hub; owned channels were cleared and radio leases restored.

The V3's first 15-second discovery trial failed. Its separate 25-second scan trial is PASS; the earlier failure remains part of the record.

T1000-E's initial batch BLE trial failed. The explicit later retry is FAIL. The initial failure remains part of the record.

Seeed XIAO nRF52 completed the normal application transfer, but normal runtime was not verified within the required 75-second window. The failed recovery phase and original batch outcomes remain in the JSON trial history. Bootloader baseline evidence is reported separately.

A later XIAO recovery attempt observed runtime CDC windows of 27-151 ms. One guarded serial open was attempted, but its completion was uncertain, no explicit DTR-low operation was reached, and exact DFU was not observed during the following 45 seconds. The startup diagnostic was not installed. After the user moved the USB plug, a 20-second inventory and separate 180-second sysfs watch did not observe the original XIAO identity; the new connection repeatedly reported USB descriptor/address error -71. The other 12 USB identities were present in a separate snapshot, and the Pi reported throttled=0x0. These observations do not identify a cable, power, firmware or bootloader root cause. A separate 25-second BLE scan found no XIAO DFU advertisement. Recovery requires a stable connection to the original device before another firmware transfer.

Source hardening prevents fresh XIAO identity generation from reading an unavailable radio and defers saved-radio settings retries until hardware is available. Eight focused host tests and the XIAO Full and legacy BLE builds passed. The new Full package retains all verified capabilities and is staged on the Pi, but has not been installed. These software checks do not resolve the USB enumeration cause or change the hardware qualification counts above.

The measured V3 92-contact refresh median was 3.347882 s at 32.560 kbit/s, versus 6.191396 s at 17.606 kbit/s for the earlier firmware. These are protocol throughput measurements on one V3/Linux connection, not BLE PHY rates or phone/full-app refresh claims. Measured results.

The historical inventory observed 12 distinct USB identities during a 10-second read-only window, including the cycling XIAO nRF52. This does not mean 12 stable ports. It preceded the separate later Tag recovery and has not been replaced by a fresh fleet-wide inventory.

This is a partial campaign report: 10/11 Companion USB qualifications, 9/11 BLE qualifications and 7 two-way RF links (8 boards including the RAK hub) passed. The full campaign is incomplete.

USB qualification used 100 persistent query cycles and 10 reopen checks, owned-contact CRUD and warm reboot, plus RX-only radio2 overlap-warning and three-decimal frequency-suggestion trials. BLE passes required three 30-second sessions. RF passes required exact two-way zero-hop DEV datagrams and owned-channel/radio-lease cleanup; they do not qualify text delivery, range, cold boot, phone interoperability or every firmware feature.

RF setup attempts with no completed delivery are retained separately; they do not count as failed deliveries.

MeshPocket previously reported error flag 8. Receive silence is a hypothesis, not a confirmed radio fault. A controlled warm reboot with flushed command delivery returned clean core flags, and its later two-way RAK RF test passed. The earlier unconfirmed reboot attempt and incomplete RF setup remain part of the history.

The T1000-E retry discovered the USB-bound advertisement, but pairing failed with a D-Bus error before any completed sessions. The NUS public key was not verified. Its initial batch discovery failure remains retained; BLE qualification is unresolved.

The retained pairing reason was Page Timeout.

V4.3 history retains 4 non-ASCII RF setup aborts and one separate failed USB setup diagnosis. An intervening guarded classification completed a clean normal HEAD handshake and closed its port; it did not qualify RF or run a transition drain.

The final read-only gateway check verified the active service held the RAK 3401 primary CDC0 interface.

The unit-test workflow passed for the measured firmware source 4e4f27a2: GitHub run.

Machine-readable results.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/","title":"Hardware validation mercerwood 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/#mercerwood-bench-qualification-12-active-radios-20-october-2026","title":"Mercerwood bench qualification: 12 active radios, 20 October 2026","text":"

21 October follow-up: the user lifted MeshPocket's bootloader-work exclusion. Its fresh Full Companion USB, BLE, contact persistence/sync and three two-way LoRa rounds passed in the MeshPocket qualification. The exclusion and source-bound measurements below describe the earlier 20 October campaign; the other twelve boards were not retested for this update.

Status: PASS. 12/12 active boards have complete accepted bench coverage; 0 have a latest required failure and 0 remain incomplete. The original inventory has thirteen radios: eleven Full Companions and two Full OTA repeaters. The active target is twelve radios: ten Companions and two repeaters. MeshPocket was operator-excluded at this campaign's close for separate bootloader work.

All thirteen 0865 candidate builds passed. Companion updates target 08650f644b680a5dfaacb95142e3cef8529da9d2; the table records each board's actual installed application. Both repeaters retain actual c46defaefd642973cbbdb473815d56b5b261cb2e (C46). Test-only commit 190064f4b5fa97e75dcfca7876568b757ba43b31 passed all twelve CI jobs. Build manifest SHA-256: 1dbf13ae8a3419f2c8e12d0432c37fa990d5bcfbb5649ab4d1d4ed4cec21a468. Seven active nRF52 boards retain the source 88d39510140ce6894cf8e74b93cbd28d7387fa21 bootloader baseline; Companion application-only updates transferred no bootloader or SoftDevice data.

That bootloader baseline describes this campaign's measurements. A subsequent independent deployment restored the original applications after installing an unpublished test bootloader on eight nRF52 boards. Its remaining USB stress and RAK data-preservation limits are recorded in the known-failure follow-up. This does not repeat or extend the source-bound qualifications below.

Board Installed source Required USB / BLE / RF / contacts Overall Heltec V4.3, FEM off 0865 USB/BLE/contacts PASS; RF retained ABF PASS RAK 4631 0865 USB/BLE/RF/contacts PASS PASS Seeed XIAO nRF52 0865 USB/BLE/RF/contacts PASS PASS Seeed T1000-E 0865 USB/BLE/RF/contacts PASS PASS Heltec V3 0865 USB/BLE/RF retained ABF; new contacts PASS PASS RAK WisMesh Tag 0865 USB/BLE/RF retained ABF; new contacts PASS PASS Heltec T096, FEM on 0865 USB/BLE/RF retained ABF; new contacts PASS PASS Heltec MeshPocket Current state unverified Historical PASS as of 22:28 UTC only; excluded from active target Operator-excluded LilyGo T-Deck 0865 USB/RF/contacts PASS; BLE PASS with WiFi quiet PASS Seeed XIAO ESP32-S3 0865 USB/RF/contacts PASS; BLE PASS with WiFi quiet PASS SenseCAP Indicator 0865 USB/BLE/RF/contacts PASS PASS Heltec MeshTower V2 SD C46 USB10/3, warm restart and RF3 PASS; SD PASS (read-only metadata) PASS RAK 3401 MQTT gateway C46 USB10/3 retained C46; new warm/RF3 PASS; service PASS PASS

The safe evidence summary records each axis's actual tested source, artifact, measurement interval, conditions and evidence digest. V3, Tag and T096 retain valid ABF USB/BLE/RF measurements; V4 retains ABF RF. Pocket's earlier ABF and 0865 results appear only in its excluded historical record; its current installed application and bootloader are unverified outside this report for operator bootloader work. The later Pocket tests are recorded separately in the follow-up linked above. ABF is abf7ba5d5c3958580d52d6954d816c08a035a552. For active boards, changed applications, current identities and new 64/32 contact trials are separate measurements. Reviewed source assurance 577aee5162c7553f58bf13b550560da50bbf73860ab37febe3674a5ae73c01aa covers the unchanged behavior; retained results keep their original source, artifact and time. Binary equality and remeasurement are not implied.

Companion USB checks cover 100 query cycles, ten reopens, owned CRUD, persistence through warm restart and applicable radio2 checks. BLE covers three authenticated thirty-second sessions, exact USB-bound identity and device information, clean core state and strict controller lifecycle. Two-way RF verifies exact zero-hop DEV datagrams, counters and owned channel/radio-lease cleanup. Each contact trial verifies 64 owned full-stream records and a 32-record timestamp subset, then removes them and proves absence after reopen while preserving unrelated records. Repeater coverage uses ten query cycles, three reopens, verified warm restart and three nonce-bound RF rounds. Gateway retains its earlier C46 query/reopen baseline; its new held warm restart and RF rounds keep separate intervals and evidence digests. SD coverage is read-only metadata, not media integrity.

T1000-E's eleven physical USB-priority phases passed. These qualify the USB-priority policy, not simultaneous active USB/BLE. The first current cold-host-release strict failure remains recorded before one later explicit unchanged-firmware retest passed. XIAO nRF52's original unaligned QSPI failure was corrected by shared buffer-alignment fix 714cfc4546ac43c220d06abff9adb128f58d16dd; its earlier strict BLE 0x3E failure also remains recorded before the later clean same-bond result.

T-Deck and XIAO ESP32-S3 later passed strict three-session BLE with USB connected and host WiFi quiet; earlier extra-0x3E failures remain in the history. WiFi was checked at trial boundaries, not continuously. T-Deck's initial two WiFi-on failures predate its current boot, so the observations do not isolate WiFi from boot/current-PIN/state differences. Its diagnostic wrapper retained a restoration-check FAIL; later read-only records separately verified handback. XIAO's original wrapper passed handback. Separate exact 64/32 contact trials, secure controller proofs and cleanups later passed with normal WiFi restored. WiFi association at 2437 MHz was observed before and after those trials. No WiFi root cause or firmware fix is established.

V4's earlier contact setup refusal preceded a later bounded-transition trial and exact cleanup. Tower's CAD2 history and Pocket's pre-DFU core8 refusal remain recorded before their separate warm preparations; preparations are not qualification passes. Tower's cumulative receive/read-error count does not prove CRC failure. Gateway's earlier warm failure had 43 complete responsive snapshots with advancing uptime, not a proven hang. Its later CAD2 baseline refusal came after the service was stopped. A separate one-request warm preparation preceded the qualification's own warm restart and RF3; the exact service/configuration owner then returned. Latest accepted results do not relabel those failed attempts.

These finite checks do not establish RF range, phone interoperability, long-duration reliability or every firmware feature. Earlier source-bound results remain in the fleet follow-up, diagnosis follow-up, and initial USB-priority report.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/","title":"Hardware validation ota 1 18 0 1 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#ota-hardware-qualification-20-21-october-2026-utc","title":"OTA hardware qualification, 20-21 October 2026 UTC","text":"

Status: COMPLETED for the listed OTA cases. Earlier failures and unresolved observations remain recorded below; this is not a claim that every board or transport has passed all release qualification. The operator authorized erasing these test nodes without backups. MeshPocket remains excluded. No nRF52 bootloader or SoftDevice payload is part of this campaign. Temporary ESP32 bridge role changes use the board's merged image, including its ESP32 bootloader and partitions, under the authorized erase.

The initial hardware OTA results use artifacts pinned to 8b5660b00b88d8abaae094859bc1232f7ce886fd. That commit changes documentation only relative to firmware source 0b207a1ec3a32eb0a8ad6b8643db211de4921cd4. Separate OTA-check version strings produce distinct image hashes for controlled update tests. The common numeric version is 0x01120001; same-version updates are explicitly authorized for these tests. They are not claimed as numeric version upgrades. Later source, CPU-sleep and one-hop results identify their own commits below.

Device and role Transport Result Heltec V4.3 Full Companion Production browser WiFi OTA PASS: one upload installed the exact intended image; postboot identity, version, core, radio and original flags verified RAK4631 unified repeater Bluetooth application DFU; QSPI full LoRa OTA; two-frequency bridged one-hop LoRa delta Transfers and eventual postchecks PASS, including one-hop installation; initial USB version response remains under investigation Seeed XIAO nRF52 repeater Bluetooth application DFU; external QSPI full LoRa OTA Transfers and eventual postchecks PASS; initial USB version response remains under investigation RAK3401 unified repeater and gateway Bluetooth application DFU; internal LoRa delta; gateway restoration PASS: Bluetooth update and repeater service, internal LoRa delta apply, normal-radio restoration and gateway service handback MeshTower SD repeater Signed SD-backed LoRa delta and full update PASS: signed SD delta and full-image transfer, installation and LoRa postboot checks"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#v4-wifi-ota","title":"V4 WiFi OTA","text":"

The unchanged production /update page selected the ordinary shipping image and started one native browser upload. Fresh USB CLI afterward reported the full intended dev-8b5660b0 version and EndF body hash B93CB6D7775BC072. The 2559496-byte image SHA256 is 0b37ec73fa39558d13e48213fe6c4ae365800c37811584bd93ea81aba1c2a8d7. The independent postboot/restoration phase passed with zero core errors, unchanged normal radio/TX, original runtime flags, and strict Binary handback. Its private summary SHA256 is 75d0ccaad83de2ce6b928a5545a48c84c03fcaf29a1a84cd4ceb6f3a16e1444a.

The browser observer reported a CDP command error after the upload action. It did not retain an HTTP status/result-body proof. This is an observability failure recorded separately from the installed-image proof; no HTTP 200 claim is made. The unchanged firmware was not uploaded again solely to repeat that measurement. An earlier preparation attempt used an obsolete WiFi status parser and timed out; its verified restoration passed before the corrected parser was used. Neither preparation attempt wrote firmware.

A known test admin password and the actual protected lab WiFi fixture were intentionally configured and retained. Secret values and network/device addresses are omitted. The Pi's network settings were unchanged.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#rak3401-internal-lora-delta-and-gateway-handback","title":"RAK3401 internal LoRa delta and gateway handback","text":"

The physical internal-flash delta completed all 9/9 blocks in 23 seconds. The 80-second post-apply probe verified the exact intended ordinary shipping EndF body hash 4748CB6ABFB3EBF4. Normal-radio LoRa communication passed after installation, and the source's original receive-power-saving setting was restored. This application-only update transferred no bootloader or SoftDevice payload.

The first gateway restoration guard failed when the initial ver command returned Unknown; its other getters, intended image hash and zero core errors were recorded separately. A second independent guard passed the complete verification and restored the original gateway service. Exact serial-port ownership and unchanged original service configuration were verified. The first failed guard remains a failed workflow record, not a gateway pass.

The later application-only LoRa delta to commit 2bd4cb4d1 completed all 55/55 blocks in 103 seconds. An on-air install acknowledgement verified the unsigned internal-flash delta, and on-air postboot reads proved exact body 1A3C9EB603C581C3 after 80 seconds plus the full dev-2bd4cb4d version. The receiving RAK used LoRa for setup, progress, installation and postboot proof. No receiver USB command drove this update. Normal radio restoration passed. Lost postboot status/version replies required bounded getter retries; their failed attempts remain in the private log. A subsequent independent USB diagnostic snapshot verified the same latest image, original saved preferences and zero core errors before the CPU sleep measurement.

That first latest-image CPU measurement FAILED. Over 90.013 seconds, the nonhalting MEM-AP capture recorded 8013 CPU states, with one sleeping state (about 0.01% weighted sampled time). There was no halt, lockup or reset in the capture. Firmware diagnostics reported zero sleep-wait calls and zero eligible time; the surrounding diagnostic window was blocked primarily by usb.recovery. The saved settings were CPU power saving on, USB logging on, USB debug off, the system watchdog on and the USB logging watchdog in Auto. The gateway service and UART were closed during the capture.

The watchdog's Auto policy still retained a previously confirmed reader and an initial recovery stage after the serial client closed. The shared awake hint treated that waiting policy as work requiring the CPU to remain awake. A focused nRF52 fix and physical retest are recorded below. Logging, watchdog settings and the failed result have not been disabled or cleared to obtain a sleep pass. CPU debug enable was already set and stayed unchanged; the explicit firmware sleep veto is recorded independently of that limit. Subsequent authenticated LoRa reads returned the exact same latest body and boot uptime, with zero core errors and both receive/transmit counters advancing by three. Radio communication remained functional after the failed idle measurement; this is not a CPU-sleep pass.

Commit bd4524318 allows the nRF52 interrupt wait while USB watchdog policy timers run. Actual transport recovery still vetoes sleep; logging, Auto/On mode and the existing recovery/reset deadlines remain enabled. Other platforms retain their prior sleep guard. The regression failed with the old predicate and passed with the fix across the four role guards. All 31 relevant regression methods passed. Blame places the earlier shared awake predicate in 3add4695cc and 18f7edcf82; it is present in the older 8b5660b0 image as well as 2bd4cb4d. The rebuilt image passed physical confirmation below.

The fixed RAK3401 image was installed using another application-only LoRa delta: 20/20 blocks in 71 seconds. The install reply was lost, so the install command was not replayed. Exact on-air postboot body 15B81DD21FB8A906 and the full dev-bd452431 version proved the outcome after 30 seconds. Normal radio restoration passed. Independent USB diagnostics afterward verified the same image, unchanged saved preferences and zero core errors.

The fixed CPU retest PASSED. A 90.013-second nonhalting MEM-AP capture recorded 7989 states, including 6275 sleeping states: about 78.6% weighted sampled time asleep. There was no halt, lockup, step or reset during the capture. Existing CPU debug enable stayed unchanged. This measures sampled CPU state, not electrical current or exact sleep residency. The surrounding firmware diagnostic window recorded 163516 sleep-wait calls and returns, zero errors, zero pending-recovery vetoes and zero time blocked by usb.recovery. Logging, CPU power saving, the system watchdog and the Auto USB logging watchdog remained enabled. The capture occurred before the existing recovery deadline; waiting for the watchdog to expire did not produce the pass.

After the capture, authenticated LoRa reads proved the same installed body and boot, zero core errors and advancing receive/transmit counters (+4/+3). The original diagnostic-enable setting was restored, and the original gateway service resumed with its unchanged configuration and exact serial port ownership verified. The additional diagnostic leases were released.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#rak4631-and-xiao-nrf52-full-qspi-lora-updates","title":"RAK4631 and XIAO nRF52 full QSPI LoRa updates","text":"

The RAK4631 downloaded all 325/325 blocks in 375 seconds. Its install response explicitly verified a full image on QSPI, and the post-apply probe found the intended ordinary shipping EndF body hash B0886153CE824448 after 30 seconds. Normal radio settings were restored to 910.5250244 MHz, 62.5 kHz bandwidth, SF7 and CR5 with TX at 2 dBm. The repeater role was retained. Fresh USB postchecks reported zero core errors and identified the W25Q16 QSPI device as JEDEC EF4015, with 2 MiB capacity. This qualifies external QSPI apply; an internal-only RAK4631 hardware result is not claimed.

The XIAO nRF52 downloaded all 280/280 blocks in 258 seconds. Its install response explicitly verified a full image on QSPI, and the post-apply probe found the intended ordinary shipping EndF body hash 725FDE69F8C0B8EA after 30 seconds. Normal radio settings were restored to the same frequency, bandwidth, SF and CR with TX at 22 dBm, and the repeater role was retained. Fresh USB postchecks reported zero core errors. The external Puya QSPI device reported JEDEC 856015 and 2 MiB capacity.

After each application update, the first independent USB ver command returned Unknown; a second independent read passed. The gateway showed the same observation. All failed first-attempt records are retained. This unresolved startup or CLI bootstrap observation is under investigation; it is not treated as missing command support or a first-attempt pass. The successful image, role, settings and core checks above describe the later independent verification.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#two-frequency-esp-now-bridged-one-hop-lora-update","title":"Two-frequency ESP-NOW bridged one-hop LoRa update","text":"

The physical one-hop test PASSED. The V4 source ran 2bd4cb4d1 and the RAK4631 received an application-only delta to bd4524318. Its setup, discovery, download, progress checks, install command, postboot proof and settings restoration all used LoRa. The receiving RAK's USB and Bluetooth interfaces were never opened for this case.

The measured path was V4 at 909.5 MHz to a V3 bridge, ESP-NOW to a XIAO ESP32 bridge, then LoRa at 908.5 MHz to the RAK4631. Both LoRa segments used 500 kHz bandwidth, SF7 and CR5. The two bridges copied received packets before appending their own hop, with one-hop limits. Disabling the far bridge's ESP-NOW link made the target's full-key query time out; re-enabling and verifying the same configuration restored that query. This counterfactual confirms that the two-frequency bridge was required.

The ordinary file-data session populated the source catalog and its automatic discovery burst. No separate announcement command or control channel was used. The first asynchronous catalog query requested discovery; the next query found the intended manifest. Download completed 62/62 blocks. The one install command received an on-air acknowledgement verifying the unsigned QSPI delta. Subsequent normal-channel LoRa reads proved exact installed body D2073EC88FF4E459, the full dev-bd452431 version and zero core errors. Original target policies, receive power saving, output path and the source's saved contact path were restored. The package SHA256 is f0a7319570394e23f06c9a901838be3cbaf5ae520d2743607828977e5fa3a0e9.

A passive source USB capture wrote zero bytes to the radio and recorded 47 matching update requests with one hop through the measured near bridge. This is source-side packet evidence, not an independent RF capture of the whole chain. The separate frequencies, bridge counterfactual and on-air install/postboot proofs establish the complete case. The integrated private receipt SHA256 is c6d6b2e2d3aefec128303b0ef5e2d8a0be52c38f03bc17ecf3032f4118489cc6.

Preparation failures remain recorded separately: a historical channel-busy flag prompted one documented LoRa restart, and several bridge receive-power configuration attempts returned a generic busy/unsupported rejection before a fresh configuration check and acknowledged attempt succeeded. No cause for those rejections or the historical busy flag is claimed fixed.

Both temporary ESP32 bridges were returned to Full Companion images built from ebb59d482. Exact runtime bodies were E297CF6E342C8332 (V3) and 4EEB12667E5E554B (XIAO ESP32), with matching Binary/CLI identity and zero core errors. The V3's first postflash Binary handshake timed out; a later independent read passed without a reset or another flash. Its role reply required a narrow correction to the bench parser's nested prompt handling. The XIAO's positive-readiness check also rejected a valid prompt-prefixed acknowledgement before sending any Binary request. After correcting that check, its first Binary handshake and complete CLI checks passed without another flash, reset or settings change. Original failed workflow receipts remain retained. These bench corrections are not claimed as firmware fixes.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#findings-and-fixes","title":"Findings and fixes","text":"

An authenticated nRF52 application handoff does not preserve a custom peripheral Bluetooth address. The retained bond belongs to the central peer; the bootloader can return at the peripheral's factory address. The first RAK attempt looked only at the custom application address and sent no firmware. A subsequent fresh authenticated handoff found the expected bootloader at its previously USB-bound factory address and passed live model, manufacturer, firmware and DFU revision checks before starting data transfer.

The host LoRa updater forced old fixed in-place workspace limits despite the images carrying hash-bound layout records. MeshCore commit 8e96b4d3b passes an explicit workspace only when requested, allowing motatool to size and validate the recorded application/staging layout. Legacy images retain the tool's conservative fallback. The focused file passed 318 host tests, including real-tool coverage for larger internal images, external layouts, explicit bounds rejection and legacy rejection. Package preparation alone does not qualify hardware apply.

The first RAK3401 LoRa attempt stopped during preflight because deployed adaptive-storage images omitted the bootloader ABI and codec mask from ota self. It changed neither firmware nor temporary radio settings. Commit 4b2f37013 adds those actual fields before the adaptive storage diagnostics. For existing images, the host runner verifies the known adaptive reporting contract with the installed bootloader's complete read-only status, including its board/name/target identity, CRC status and storage profile. That contract guarantees application codec subset 0x5; storage flags are not treated as codecs. Bootloader packages still require directly reported capability fields.

The updater host suite passed 365 tests with zero skips, and all seven firmware diagnostic/policy host tests passed. The affected RAK3401 canonical Full/Reduced build pair passed, reporting 619832/567640 bytes of flash use. An earlier build compiled successfully but its packaging guard rejected concurrent host-source changes; the second build completed packaging from a frozen workspace. The modified adaptive branch is preprocessed out of source-only Full Companion builds. These build results do not change the 8b5660b0 hardware-artifact binding above.

Commit 4c898ecbc recognizes native DEFLATE logical-block progress in the source's passive status output. It uses that output only as a progress signal, not as proof that the receiver has completed or installed an image. The host updater suite passed 369 tests with zero skips after this change.

Commit 720b9728c adds cold-idle RAK host-preflight preparation. The preparation may initialize optional staging media, but does not itself download firmware, install an image or change saved preferences. The host updater suite passed 379 tests with zero skips. The targeted live cold-idle RAK4631 check subsequently passed: one USB restart established the unchanged, idle application with unprobed storage, then authenticated LoRa reads observed that state and the host performed exactly one explicit storage-preparation query. The result was external QSPI with ABI 3 and codec subset 0x5, with unchanged body B0886153CE824448 and no firmware download or bootloader write. The first host attempt lost a version reply before preparing storage; a fresh read-only snapshot and bounded getter retries completed the check without another restart. Both attempt records are retained.

Commit 093fd98a3 retries lost remote clock reads within the existing bounded operation budget. Validation passed 671 tests, including nine new clock-read regressions; one optional PyPI integration test was skipped. Canonical Full and Reduced RAK4631 and RAK3401 pairs, plus Full V3 and XIAO S3 WIO images, built successfully from this commit. The ESP32 images were subsequently installed as the temporary one-hop bridges. Build success alone does not qualify an update or replace the earlier hardware results.

Repeater Bluetooth provides an update service; Companion terminal/sync functionality is not claimed for repeater builds. The RAK4631 reports a 2 MiB W25Q16 QSPI device and completed its full QSPI LoRa update as recorded above. RAK3401 live diagnostics report no external NOR and 252 KiB internal staging capacity, matching the completed internal delta case.

Commit 2bd4cb4d1 automatically restarts the existing four-beacon discovery burst when a complete, changed update catalog becomes available through the source's ordinary file-data session. It requires no separate announcement command or control channel. USB or WiFi can feed files into the source radio; receiving radios still discover and download them over LoRa. All 136 affected native test methods passed, including cold catalog discovery and a negative control that removes the production readiness trigger. Canonical V4 Full and RAK3401 and RAK4631 Full/Reduced builds passed. This fixes source discovery, and does not establish a fix for the separate Tower manifest timeout.

The V4 source subsequently accepted one application-only WiFi upload of this commit. Authenticated GET and POST both returned HTTP 200, followed by exact USB version/body proof (B6E8C347E1C5FA69) and unchanged saved preferences and receive-power-saving configuration. This deploys the source firmware; it is not a receiver LoRa qualification result. The initial preparation rejected a valid start acknowledgement with a WebConfig-status suffix before any upload; the corrected continuation did not replay the start command.

During RAK4631 discovery, source receive power saving was on and discovery timed out. After disabling that setting under a restoration intent, a fresh advert was received. This sequence records an observation; it does not establish a receive-power-saving firmware defect. Source receive power saving stayed off under the owned restoration intent while the remaining LoRa cases ran. Exact final source restoration is recorded below.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#meshtower-signed-sd-updates-and-remaining-failures","title":"MeshTower signed SD updates and remaining failures","text":"

Before the MeshTower OTA cases, its unchanged earlier application had a latched core error bitmask 0x02 while still receiving. This bit records a generic channel-busy wait exceeding four seconds. It is a historical flag, not a count of two errors or evidence that the radio is currently stuck. Diagnostics were retained. One software restart cleared the bitmask to zero, observed at 39 seconds uptime. Its normal radio settings and 0 dBm TX were unchanged, and the earlier application's EndF body remained 60EE4D4C61CFCA58. A later historical channel-busy flag was recorded again after that restart. No underlying channel-busy defect is claimed fixed, and a low ADC battery reading is not identified as the cause.

The MeshTower temporary-channel rehearsal at SF5 and 500 kHz bandwidth failed three full-public-key proof attempts before any firmware transfer. Natural lease expiry and return to the normal channel were independently verified. A retry at SF7 and 500 kHz bandwidth passed full-public-key, status and ota self proofs on the temporary channel. The SF5 preflight failure remains unresolved; no firmware was transferred in that attempt.

The signed MeshTower delta update then completed all 41/41 blocks in 206 seconds using SF7 and 500 kHz bandwidth. The install acknowledgement verified a trusted signature and a delta image on SD. The post-apply probe verified EndF body hash F1909AD4C46915FD after 70 seconds and a fresh OTA-check version built from 8b5660b0. Return to the normal radio was verified.

The first signed full-image attempt stopped before TempRadio when the host observed a zero clock epoch; it transferred no firmware. The bounded clock-read retry fix above addresses the host's handling of lost reads. A second full-image attempt at SF7 and 500 kHz bandwidth passed the full-key and status proofs, but all three ota self proof attempts failed. Safe temporary-lease expiry and return to the normal radio were verified. No firmware was transferred in either of those full-image attempts; full apply and postchecks were still pending at that point.

A local USB snapshot afterward confirmed body F1909AD4C46915FD, 1133 seconds uptime, core error bitmask 0x02 and an empty queue. Radio diagnostics reported RSSI -60 dBm, SNR 11.25 dB and noise -103 dBm. These values do not establish why the remote self proofs failed. A separate USB diagnostic attempt failed on open with error 71 before any getters. The Pi's DWC USB controller then re-enumerated the same Tower while application uptime remained at 1133 seconds. This is a separate host transport failure, without evidence of an application reboot or a basis for a brownout claim.

A read-only comparison of the earlier C46 and pinned 8b5660b0 handlers did not support the proposed clock or access-control explanation for the rehearsal failure. The relevant handlers were the same; no clock or ACL cause is claimed from this review.

A later local-control diagnostic made the signed full catalog visible, but both matching pull attempts remained at manifest timeout, 0/0 blocks. No install was requested. This local-control attempt is not a wireless-only qualification pass. Its temporary radio and saved receive-power-saving settings were restored and its USB port closed. One restoration command was explicitly rejected with a misleading \"unsupported\" response; source review shows that this response also covers a busy or failed reconfiguration. A fresh identity/settings check followed by one new, acknowledged restoration attempt proved the original configuration. No particular runtime guard is identified as the cause of that rejection.

A later signed-full retry used the ordinary production file-data session and source catalog readiness, with every receiver command on LoRa. Its first attempt stopped before new data because an old failed receive session required an explicit replacement choice. The one-minute temporary-channel cleanup command lost its acknowledgement. The updater then proved identity on the temporary channel and restored the source, but did not prove that the receiver had returned to its normal channel. The next attempt could not reach the receiver on the normal channel. These are failed workflow records.

A bounded, LoRa-only recovery found the exact Tower still running body F1909AD4C46915FD on the temporary channel. One command restored its normal radio; its acknowledgement was lost and the command was not replayed. Subsequent authenticated normal-channel reads proved the full identity, unchanged body, inactive temporary mode, normal radio and 0 dBm TX. No target USB, Bluetooth, reset or flash operation was used. A historical core bitmask 0x06 was retained: channel-busy timeout and radio outside receive for over eight seconds. It is a bitmask, not a count of six errors. The recovery receipt SHA256 is bc44880091e0024966fb6b2d9fd77b21556a9899e9c9995cf772dc14ae1bd57e.

After that recovery, a fresh ordinary updater attempt passed its temporary channel rehearsal and natural return, explicitly replaced only the old failed receive session, and downloaded the signed full image 279/279 blocks in 831 seconds. The install acknowledgement verified a trusted signer and a full image on SD. Normal-channel LoRa postboot reads proved body 718365A6E08823C7, bootloader apply OK and the full dev-ebb59d48 version. The first matching body response arrived during the 60-second postboot probe. A lost optional statistics reply required a bounded getter retry. The 570682-byte package SHA256 is 4f92525f7500ad74b7f1895a62c4626a02a049b1a3978906ae8eea8617393bde. The production runner exited successfully; its private result SHA256 is de1bb4c4fa7af7fe627c00d96c94d1f7244141556c29271754379ad8f7fd7ca9. All receiver setup, download, install and postboot commands used LoRa; its USB and Bluetooth interfaces were never opened for this attempt. The source was fed through its ordinary file-data session, with no extra announcement or separate receiver control channel.

An independent, authenticated LoRa postcheck then verified the new full body and version, repeater role, normal radio/TX, inactive temporary mode, zero core error bitmask and an empty transmit queue. It removed only the owned fourth test signing key and verified that the original three key prefixes remained in their original order. The CLI exposes prefixes, so this is not an independent full-byte readback of those three keys. The postcheck repeated the identity, radio and core checks after removal and closed its controller. Lost getter/status replies used bounded read-only retries; all failed attempts remain recorded. Its private summary SHA256 is 02964ab9a2f511f96055ef6c99c59d32bb091d21dbf5c61dbe9de5ac00555659.

The passive source-only capture recorded 405398 bytes and wrote zero USB bytes. Its SHA256 is 6558971dea7e9cdc221e3e196d34f5ae00427300932c766e3fb7798d3332d194. The capture was stopped and its serial port closed before the source's original logging-off setting was restored and verified. This capture is source-side evidence; the receiver's installed-image and runtime proofs above came from LoRa.

The host cleanup defect is separate from the radio transfer: temporary-channel identity alone cannot establish that a lost relative-lease command executed. Cleanup must retain its recovery obligation until a full-key proof and inactive temporary mode are observed on the normal channel. The same requirement applies independently to controlled relay participants.

Commit c5c67f8f7 fixes that host ownership defect. It persists a private recovery record for each full-key-bound target and controlled relay before arming, records a one-shot shortening attempt before sending, and requires each participant's full identity plus inactive temporary mode on the normal channel before retiring its obligation. A shared Full Companion source must also report its actual temporary mode inactive; its saved Binary radio settings alone are insufficient. Lost relative-lease acknowledgements are not replayed. Unresolved records survive interruption without passwords, and manual read-only recovery is documented in the LoRa OTA automation guide. The regression cases cover delivered and undelivered lost acknowledgements, independent relay failures, mismatched keys, active temporary profiles, interruptions, existing recovery records, resolved-password redaction and the explicit no-install staging path. These are host changes; accepted unchanged firmware was not flashed again to validate them.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#final-source-restoration-and-handback","title":"Final source restoration and handback","text":"

After all transfers and the Tower signing-key cleanup, two fresh USB cleanup attempts received an explicit Error: radio busy; retry while restoring the source's original RXPS setting. Both attempts stopped and retained their raw failed records. This firmware rejects that command before updating or saving RXPS preferences; a missing or ambiguous reply would not permit the same continuation. Fresh read-only diagnostics showed normal receive operation, zero core errors, an empty queue and RXPS still off. They did not identify which radio reconfiguration guard rejected those particular commands.

A bounded continuation used the production updater's varying retry cadence for this exact explicit busy response, opening a fresh console after each rejection. Its second attempt received the exact positive acknowledgement and exact original configuration readback: on, level 8, preamble 16, 39657 microseconds receive and 15975 microseconds sleep. It did not retry a lost or malformed acknowledgement, reset the radio or change firmware. The continuation receipt SHA256 is 4253c91dbf2eca91e7b2716367e27e95a7dd1b952008b9ef6bb036fd673d3c62.

A separate final cleanup skipped the already restored RXPS setting, restored all five original saved flags, and verified normal radio/TX, idle detached OTA file service, original hop settings and strict USB Binary handback. It disabled the test WiFi runtime before restoring MQTT. The completed private summary SHA256 is 1c07fc8301f883eb3a33201924ca77ce4d2d02573b9a27a785f84547a06a8165. The hash-bound completed receipt retired the host's RXPS restoration intent. Owned test tunnels, target leases and the cold-idle hold were closed; the root hardware session had no remaining FLOCK descriptors before it exited. The already restored RAK3401 gateway service was left running.

"},{"location":"test-results/hardware_validation_ota_1_18_0_1_2026-10-20/#limits-and-completion","title":"Limits and completion","text":"

The listed physical OTA cases and their restoration are complete: V4 WiFi, RAK3401 internal delta, RAK4631 and XIAO nRF52 external QSPI full images, Tower signed SD delta/full, and the two-frequency ESP-NOW bridged one-hop update. Latest RAK3401 CPU sleep, post-idle LoRa and gateway handback passed. Each case records its exact firmware commit and body; these results do not qualify all 13 radios against every later commit.

Receiver setup, progress, installation and post-install verification in the new radio campaign used LoRa. Receiver USB commands did not substitute for failed radio communication. Earlier local-control diagnostics remain explicitly identified. The RAK3401's pre-fix historical channel-busy bitmask 0x02 was retained without clearing statistics or claiming a sleep result from that snapshot.

The first-command USB bootstrap observation, Tower SF5 preflight failure, earlier full-image self-proof failures, separate USB open failure and historical radio-busy flags remain unexplained. Later full-image transfer and postchecks passed under the stated conditions; this does not establish their underlying causes. Source restoration's explicit busy replies were handled with bounded retries; no radio guard defect is claimed fixed by that action. Private raw responses, keys, PINs and device identities stay outside the repo. Earlier accepted Companion tests are not repeated without a relevant change.

"},{"location":"test-results/heltec_t114_display_detection/","title":"Heltec t114 display detection","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_t114_display_detection/#heltec-t114-automatic-screen-detection","title":"Heltec T114 automatic screen detection","text":"

The canonical T114 firmware now includes the optional ST7789 screen driver and checks for the stock screen at boot. Screenless boards use the same firmware. Legacy without_display build names remain compatibility aliases; automatic release selection uses the canonical targets.

"},{"location":"test-results/heltec_t114_display_detection/#hardware-basis","title":"Hardware basis","text":"

Heltec's stock panel specification identifies the controller as ST7789V2 and describes SDA as an SPI input/output pin. The existing T114 wiring supports register reads over this shared data line; a separate MISO wire is unnecessary.

The Sitronix controller specification documents the read-ID command and serial turnaround on pages 58\u201364 and 167, and reset recovery on pages 48\u201349. Its ID bytes are programmable, so detection does not require a single guessed factory ID.

"},{"location":"test-results/heltec_t114_display_detection/#expected-behavior","title":"Expected behavior","text":""},{"location":"test-results/heltec_t114_display_detection/#validation","title":"Validation","text":"Check Result Native probe, GPIO, headless and wake regressions PASS, 4 methods with ASan/UBSan; late-turnaround, reset-delay and floating-line negative controls Resolved recipe and compatibility-alias checks PASS, 5 methods; actual PlatformIO configuration also confirms all 5 legacy aliases match their canonical targets Companion headless buttons and related navigation, reader, pairing, startup and deadline suites PASS, 55 methods, including native headless fault regression and 4 input/reader configurations Picker and release-selection regressions PASS, 78 Python methods and 3 Node suites; old assets, scoped previews, newest-role defaults and unavailable legacy choices covered Display allocation accounting and firmware RAM gates PASS, 46 methods, including production allocation tracking across 3 panel sizes and buffer configurations T114 firmware and package checks PASS, all 8 published profiles; UF2/DFU application bytes, hashes, version, hardware/OTA identity, flash limits, display capability and RAM admission checked Hosted documentation links PASS, 7 methods Physical T114 with and without a screen Not performed

The T114 1.18.0.1 preview1 packages firmware source 794f5f7b8c508b708ee8fc7db7bff066aa3d1a0a. Its seven display-capable profiles contain the verified automatic-detection marker; the eighth is the headless KISS utility. The published qualification report records each profile's flash range, retained update identity and RAM allowance. Repeater Full has the smallest RAM margin at 10,200 bytes after the display allocation and normal transient margin. KISS has no embedded EndF version or update identity; its package provenance, UF2/DFU application equality, flash bounds, hashes and RAM gate are checked.

The release-bound picker check offers those eight profiles under the single canonical T114 hardware choice, with seven automatic-display profiles and one headless KISS profile. It preserves all 111 other hardware profiles from the current main release and retains historical T114 files as exact downloads.

Native tests simulate controller replies and GPIO state; they do not establish physical detection reliability. Before calling the hardware qualified, test cold USB and battery boots with a stock screen and with no screen, then verify screen sleep/wake, Bluetooth/USB connections, and normal radio receive/transmit.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/","title":"Heltec v2 ble memory 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#heltec-v2-ble-ram-reserve-repair-22-october-2026","title":"Heltec V2 BLE RAM reserve repair, 22 October 2026","text":"

Status: build, memory and host tests PASS. Physical V2 operation was not tested in this work. The earlier RAM failure remains recorded below.

Clean source 1d4016f48 was built through the canonical builder as Heltec_v2_companion_radio_ble, using the auto feature profile, Cascade runtime defaults, USA Cascadia radio preset and release-length version v1.18.0.1-halo-keymind-cascade-dev-1d4016f4. The safe receipt binds the source, linked measurements and packaged artifact hashes.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#change-and-retained-behavior","title":"Change and retained behavior","text":"

Only V2 BLE explicitly enables the existing MESH_CONTACT_CACHE policy. Its Heltec_v2_companion_radio_ble_ps alias inherits the same setting. Other V2 profiles and the default cache policies of other boards are unchanged.

All 160 contacts, eight channels and 128 offline frames remain available. The cache keeps 16 paths and 16 shared secrets resident, rather than embedding a complete path and shared secret in every contact object. Other paths remain accessible through checked reads of existing contact records; cold ESP32 secrets are recalculated without secret-file I/O. These resident-cache limits do not reduce the contact capacity. Cold access can add filesystem reads or key-calculation work.

The Bluedroid Bluetooth host, PIN authentication, bond storage, dependencies, radio/display setup, contact protocol and 152-byte contact-file records remain unchanged. No pairing migration or repartitioning is introduced. The packaged capability verification passes with an empty reductions list.

The existing 8,192-byte static DRAM reserve remains mandatory. The runtime model additionally reserves 6,144 bytes for this explicit ESP32 cache opt-in: 4,096 bytes for retained reader buffering plus 2,048 bytes for reader objects, allocator metadata and related overhead. This is above the unchanged common filesystem and transient allowances; it is not a claim of zero heap cost. Automatic Full profiles keep their existing accounting policy.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#linked-results","title":"Linked results","text":"Check Baseline 76437c6bf Fixed 1d4016f48 Static DRAM region 124,580 bytes 124,580 bytes Occupied static span 122,964 bytes 112,140 bytes Static DRAM free 1,616 bytes 12,440 bytes Required static reserve 8,192 bytes 8,192 bytes Margin above reserve -6,576 bytes, FAIL 4,248 bytes, PASS

The fixed image saves 10,824 static bytes. Its runtime model passes with 181,400 internal bytes available versus 141,104 required, including the extra reader allowance. The modeled margin is 40,296 bytes; these are linked capacity estimates, not running free-heap or fragmentation measurements.

The application occupies 1,810,568 of 3,342,336 app-slot bytes. The partition table is byte-identical to the baseline. Both firmware and merged packages, capability verification and unchanged application-size gates pass. The failed baseline ELF/map and successful candidate/final artifacts are retained privately.

"},{"location":"test-results/heltec_v2_ble_memory_2026-10-22/#verification-and-limits","title":"Verification and limits","text":"

All 74 host test methods passed:

The unit workflow now builds this exact classic ESP32 target at release-version length and enforces both memory gates. The inherited power-saving alias is checked by the recipe contract; an identical feature configuration was not built a second time. No firmware feature or capacity was removed to obtain the pass, and no memory gate was lowered.

No radio was flashed or contacted. Physical V2 boot, pairing/reconnect, populated contact sync, filesystem latency and sleep remain outside this build/host result. It closes the documented V2 RAM build failure, not those hardware qualification claims.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/","title":"Heltec v3 full memory 2026 10 19","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#heltec-v3-full-memory-qualification-2026-10-19","title":"Heltec V3 Full memory qualification, 2026-10-19","text":"

Canonical V3 Full now passes its flash and runtime-RAM gates with 6,032 bytes of modeled RAM margin, retaining its existing features and capacities. Physical testing is pending: no V3 is currently exposed to the VM, and no board was flashed during this work.

Measured source: eb9259f2a97113b1d482ca9610661fb24c1c8e98. The machine-readable evidence records package hashes, allocation components, capabilities and linked ARM checks.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#what-changed","title":"What changed","text":"

V3 Full uses the existing NimBLE-Arduino 2.5.1 Companion adapter. It preserves the authenticated NUS service, PIN, address/stealth controls and frame queues. The canonical profile retains one active BLE session, 15 saved phones and 30 CCCD records for NUS TX and Service Changed subscriptions. This does not reuse the smaller default bond store from the optional trial configuration.

Bluetooth upgrade step: existing Bluedroid bonds are not migrated. Forget the previous pairing in the phone's Bluetooth settings and pair again after installing this image. The PIN setting remains available as before.

Non-PSRAM ESP32 Full also uses a bounded external-source Merkle cache. It keeps 32-leaf groups and their roots, then rereads the selected group to generate the same wire proof. It supports the same 4,096 source blocks, folder capture, raw/host-encoded/device-encoded serving, getter requests and transport protocols. A proof step issues at most one metadata transaction; jobs survive partial progress and radio backpressure.

The complete workspace is 1,004 bytes in the 32-bit Xtensa layout, with a production sizeof assertion enforcing a 1,536-byte bound. Its output block and allocation allowance remain separate. The legacy 16 KiB source-leaf allocation is absent on this path. PSRAM, nRF52 and other unqualified paths retain their existing implementation, as do explicit larger custom source limits.

The RAM check accounts for this smaller real allocation. The 16 KiB transient margin, Bluetooth/WiFi allowances, MQTT admission, stacks and minimum floors remain unchanged. There is no new OTA queue loan or reduction in offline frames.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#measured-image","title":"Measured image","text":"

Both comparisons use the canonical release-length version, Cascade profile and USA Cascade preset. The baseline is 31e693ec2; it already failed admission before the shared contact-memory changes.

Measurement Baseline Final Internal capacity after linking 177,144 189,616 Modeled allocation requirement 198,432 183,584 Modeled margin -21,288 6,032 Application bytes Not admitted 2,114,744 Application-slot capacity \u2014 3,342,336 Application-slot margin \u2014 1,227,592

The existing V3 Full recipe retains 100 contacts, 40 channels, 256 offline frames and one MQTT runtime slot. These limits were not reduced by this fix. All 24 linked/package feature checks pass, alongside the Full profile marker. USB, Bluetooth, simultaneous WiFi, direct MQTT, WebConfig, logging and OTA remain in the image. Modeled capacity is not a running free-heap or load measurement.

Final ELF SHA-256: 3a56805b2d0216026c3710e149db43dedb677684e47ad08ebfe748094c1de55b. Application SHA-256: c4f276095626560abc4c6875216c147d2fae292bf5c514e5029ef7d03454d3ce. Every file bound by the packaged memory manifest was hash-verified.

"},{"location":"test-results/heltec_v3_full_memory_2026-10-19/#verification","title":"Verification","text":"

PlatformIO commands ran one at a time in a clean checkout. Reproduce the V3 package with:

bash build.sh build-firmware Heltec_v3_companion_radio_full --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota\n

Hardware acceptance still requires the physical V3: preserve its flash and settings, then check boot, live heap under simultaneous transports, contacts, Bluetooth pairing/reconnect, MQTT, WiFi administration, OTA and persistence. These software results do not establish that hardware acceptance.

"},{"location":"test-results/heltec_v3_preview3_ram/","title":"Heltec v3 preview3 ram","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/heltec_v3_preview3_ram/#full-companion-ram-audit-and-heltec-v3-preview-3","title":"Full Companion RAM audit and Heltec V3 preview 3","text":"

Date: 2026-10-02. Baseline: 0ccac453 plus the early startup-screen changes included in this revision. All 13 selected ordinary Full builds passed the existing linked runtime RAM guard. This is a targeted audit, not a claim that every Companion has been rebuilt or hardware-qualified.

"},{"location":"test-results/heltec_v3_preview3_ram/#builds-needing-the-most-attention","title":"Builds needing the most attention","text":"Hardware Available bytes Reserved bytes Margin bytes Generic ESP-NOW 143,872 142,336 1,536 Tracker V2 (FEM on) 180,000 177,746 2,254 Heltec V3 185,032 182,016 3,016 XIAO ESP32-C3 146,048 142,336 3,712 Heltec CT62 146,240 142,336 3,904 Wireless Tracker 184,304 178,946 5,358 Heltec T096 (FEM on) 84,152 75,298 8,854 Heltec T1 84,488 75,298 9,190 LilyGo TLora V2.1.1.6 185,304 173,840 11,464 Heltec V2 187,176 173,840 13,336 TBeam SX1276 194,160 169,744 24,416 TBeam SX1262 196,112 169,744 26,368 Heltec T114 84,336 46,880 37,456

The six smallest margins belong to non-PSRAM ESP32 Full Companions. They pass their existing release capacities but have little room for further growth. Heltec T096 and T1 are the next constrained screen-equipped nRF52 Full builds. T114 Full passes; the failed older T114 BLE-only recipe is not a Full failure.

Values come from the firmware RAM guard in the retained local out/companion-ram-*-before.log build logs. Available bytes exclude statically reserved memory and unavailable internal regions; the reservation covers startup allocations and a transient allowance. The margin is not measured live free heap or a prediction of time to failure. No safety budget was lowered.

"},{"location":"test-results/heltec_v3_preview3_ram/#v3-solution-under-test","title":"V3 solution under test","text":"

The opt-in platformio.heltec-v3-preview3.ini profile uses the existing NimBLE-Arduino adapter, pinned to version 2.5.1, instead of Bluedroid. It retains the current ordinary V3 Full capacities: 100 contacts, 40 channels, and 256 offline frames, including during mOTA. It adds no new queue-sharing or storage changes and does not enable the larger 350-contact capacity trial.

V3 image Available internal bytes Reserved bytes Margin bytes Published preview 2 185,040 182,016 3,024 Preview 3 200,152 182,016 18,136

Compared with published preview 2, preview 3 recovers 15,112 bytes of internal RAM without removing Full features. Its bootloader and partition-table bytes match preview 2. The normal release matrix is unchanged.

Bluetooth still provides the Companion server, encrypted PIN pairing, bonding, MAC policy and stealth controls. Existing Bluedroid bonds are not migrated: forget the old device in the phone's Bluetooth settings and pair again after installation. This does not require erasing contacts or identity.

The preview also contains the early Starting... screen and animated key generation display. The saved Off and pairing-only display modes remain respected. Normal UI timeout starts after startup. Fresh installs still default to Bluetooth on and infrastructure WiFi off; saved WiFi-on settings remain on. Use set wifi.enabled 1 to enable WiFi.

"},{"location":"test-results/heltec_v3_preview3_ram/#verification-and-remaining-work","title":"Verification and remaining work","text":"

The real embedded build passes the unchanged RAM guard and fits both existing WiFi OTA slots. Capability checks inspect the linked ELF, packaged application and WebConfig gzip asset. The package includes ELF-bound RAM proof and hashes.

Adapter regressions exercise actual NimBLE transport code under sanitizers, including identity/bond failures, encrypted PIN authentication, stealth, notifications, MTU gating and frame retry. Startup-screen, display policy, Companion preferences, USB defaults, identity and frame-queue regressions are also checked. Embedded compilation uses the actual pinned NimBLE library.

This is still a hardware-test preview. V3 first-boot screen, phone pairing and reconnection, contact sync, WiFi/BLE coexistence, mOTA sending, and sustained load need physical verification. A build passing RAM checks does not establish that all reported V3 boot symptoms are fixed. Do not make this stack the default on every board before those checks.

Reproduce with one PlatformIO process at a time:

pio run -c platformio.heltec-v3-preview3.ini \\\n  -e Heltec_v3_companion_radio_full_preview3 -t mergebin\npython3 -B test/test_heltec_v3_preview_profile.py\npython3 -B test/test_nimble_companion.py\npython3 -B test/test_startup_screen.py\npython3 -B test/test_firmware_ram.py\n
"},{"location":"test-results/home_text_spacing/","title":"Home text spacing","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/home_text_spacing/#home-screen-text-spacing","title":"Home-screen text spacing","text":"

On non-Indicator companions, a visible Bluetooth PIN or connection status owns a separate bottom-aligned block. It replaces the ordinary inbox instruction, clock and WiFi rows while displayed. The PIN stays below the inbox title; the label and value use measured font heights, including the MeshPocket's larger e-paper font. An unusually short viewport may omit the label, never overlap the title or clip the PIN. Indicator's dedicated pairing layout is unchanged.

The repeater home page also stacks measured font-height rows instead of fixed ten-pixel offsets. Long names and status lines are ellipsized horizontally.

Run python3 -m unittest discover -s test -p test_home_text_spacing.py -v on a host with a C++ compiler. This renders the actual companion and repeater home branches, recording every text rectangle and rejecting overlaps or clipping. It covers 128x64 OLED, 160x80 T096, 250x122 MeshPocket and larger viewports, PIN/connected/disabled/prompt states, WiFi variants, long strings and a sweep of font heights. It also checks the static reader hint. The separate native display and T096 tests exercise the driver drawing/measurement methods.

Hardware acceptance: check that the full six-digit PIN is near the bottom, with no text over it; connect/disconnect Bluetooth and verify no stale digits; then check the repeater's five rows and the V4's non-blinking reader footer.

"},{"location":"test-results/mbedtls-tls-footprint/","title":"Mbedtls tls footprint","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mbedtls-tls-footprint/#shrinking-the-per-connection-tls-footprint-on-non-psram-observers","title":"Shrinking the per-connection TLS footprint on non-PSRAM observers","text":""},{"location":"test-results/mbedtls-tls-footprint/#why","title":"Why","text":"

On a non-PSRAM Heltec V3 running two WSS/JWT broker slots, the largest allocatable block in internal DRAM walks down in ~16 KiB steps at every TLS reconnect while total free heap stays flat. Measured on hardware over 50 reconnect cycles: 62,452 -> 16,372 bytes, permanently.

The step size is not a coincidence. framework-arduinoespressif32 3.20017 (Arduino 2.0.17, IDF 4.4) builds mbedTLS with the symmetric buffer configuration:

CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN 16384      # sizes BOTH the in and out record buffers\nCONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE 1\n# CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN       -- not defined\n# CONFIG_MBEDTLS_DYNAMIC_BUFFER               -- not defined\n# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH   -- not defined\n

Read from packages/framework-arduinoespressif32/tools/sdk/esp32s3/qio_qspi/include/sdkconfig.h. Note the separate framework-arduinoespressif32-libs package in ~/.platformio belongs to the esp32c6 env (pioarduino, IDF 5.3) and is not what this env links against -- though it happens to carry identical mbedTLS values.

So each broker slot costs 2 x 16 KiB = 32 KiB of contiguous internal DRAM in record buffers alone, and two slots cost 64 KiB on a board with roughly 80 KiB free. Every reconnect frees and re-allocates those buffers, and anything that lands in the vacated hole in between prevents them from going back, which is the ratchet.

Confirmed by two independent observations: losing a whole TLS session returned exactly 16,384 bytes of contiguity on one device and exactly 32,768 on another, and per-connection teardown frees ~41.6-44.7 KB total.

"},{"location":"test-results/mbedtls-tls-footprint/#what-the-firmware-could-already-do-and-its-limit","title":"What the firmware could already do, and its limit","text":"

softDisconnect() (branch perf/mqtt-renewal-no-stop, commit 6c51e468) stops the JWT renewal bounce from destroying and recreating the esp-mqtt task, keeping its 6 KiB stack out of the hole. Measured: the staircase arrests after 2 steps at 36,852 through cycle 16, where the baseline took 4 steps and settled at 16,372 by cycle 11 -- about 20 KB better.

That is as far as the application layer reaches. MQTT 3.1.1 has no re-authentication packet, so presenting a fresh JWT requires a transport reconnect; mbedTLS's internal allocation order during the handshake is not controllable from the application. The remaining cost is the record buffers themselves.

"},{"location":"test-results/mbedtls-tls-footprint/#the-changes","title":"The changes","text":"

All three are compile-time in mbedTLS, and the Arduino framework ships precompiled .a archives (tools/sdk/esp32s3/lib/libmbedtls.a), so a project-level -D cannot change them. A custom framework build is required.

Setting From To Saving per connection CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN unset y enables the two below CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN 16384 (implied) 16384 none -- keep it CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN 16384 (implied) 4096 ~12 KiB CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE 1 n ~4 KiB

Roughly 16 KiB per connection, 32 KiB across two slots.

"},{"location":"test-results/mbedtls-tls-footprint/#why-inbound-stays-at-16-kib","title":"Why inbound stays at 16 KiB","text":"

A peer may legitimately send a 16 KiB TLS record. Lowering the inbound limit only works if both brokers negotiate the max-fragment-length extension or provably never send larger, and getting it wrong produces invalid-record and handshake failures rather than a clean error. Outbound is ours to choose: this firmware's MQTT and WebSocket frames are far below 4 KiB (MAX_TRANS_UNIT-bounded packets plus small JSON), so 4 KiB is comfortable.

"},{"location":"test-results/mbedtls-tls-footprint/#risk-on-the-peer-certificate","title":"Risk on the peer certificate","text":"

Dropping KEEP_PEER_CERTIFICATE means mbedtls_ssl_get_peer_cert() returns NULL after the handshake. Chain validation still happens -- only retention of the parsed leaf changes. This firmware verifies against a CA (GTS_ROOT_R4 / the bundle) and never inspects the peer certificate or a fingerprint, so it should be safe. It does change mbedtls_ssl_session layout, which is exactly why the whole framework must be rebuilt together rather than swapping a single archive in.

"},{"location":"test-results/mbedtls-tls-footprint/#build-procedure","title":"Build procedure","text":""},{"location":"test-results/mbedtls-tls-footprint/#do-not-use-esp32-arduino-lib-builder-for-this","title":"Do not use esp32-arduino-lib-builder for this","text":"

release/v4.4 is the branch matching Arduino 2.0.x, but its update-components.sh clones every dependency at master, so it no longer resolves. Five successive failures, each a different repo: the arduino branch name it passes to -A does not exist; jq is absent from the IDF image and its absence makes build.sh's target loop a silent no-op that still exits 0; esp_littlefs and esp32-camera master require IDF >=5.0/>=5.1; esp32-camera later needs an esp_jpeg version the 4.4 registry cannot satisfy; and tinyusb's source layout no longer matches arduino_tinyusb/CMakeLists.txt. Pinning each one in turn just surfaces the next.

"},{"location":"test-results/mbedtls-tls-footprint/#rebuild-only-the-mbedtls-archives","title":"Rebuild only the mbedTLS archives","text":"

More rigorous anyway, because it reuses the shipped sdkconfig verbatim -- so the archives differ from stock only by the intended change, with no arduino-version or DYNAMIC_BUFFER drift.

This is ABI-safe for the content-length change specifically: ssl.h declares in_buf and out_buf as unsigned char *, allocated in ssl_setup(), and no public struct embeds a CONTENT_LEN-sized array. The other precompiled archives (esp-tls, esp_http_client, esp-mqtt) therefore remain compatible. It is not safe for CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE, which changes mbedtls_ssl_session layout -- that one needs everything rebuilt together, so it is excluded here.

  1. Minimal IDF project whose only component requirement is mbedtls.
  2. sdkconfig.defaults = the shipped packages/framework-arduinoespressif32/tools/sdk/esp32s3/sdkconfig, with CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384 replaced by the three asymmetric lines. Verify the diff is exactly 5 lines before building.
  3. docker run --rm -v $PWD:/project -w /project espressif/idf:v4.4.7 idf.py -DIDF_TARGET=esp32s3 build
  4. Confirm build/config/sdkconfig.h carries OUT_CONTENT_LEN 4096.
  5. Stage the four archives under the framework's names -- note the rename:
built framework name stock size rebuilt esp-idf/mbedtls/libmbedtls.a libmbedtls.a 113,914 113,338 esp-idf/mbedtls/mbedtls/library/libmbedtls.a libmbedtls_2.a 1,252,150 1,245,718 .../libmbedcrypto.a libmbedcrypto.a 4,302,698 4,259,458 .../libmbedx509.a libmbedx509.a 676,590 673,318

All within ~1% of stock, which is a good check that only the config differs.

"},{"location":"test-results/mbedtls-tls-footprint/#wire-it-in-with-l-never-platform_packages","title":"Wire it in with -L, never platform_packages","text":"
PLATFORMIO_BUILD_FLAGS=\"-L/path/to/staged/archives\" pio run -e Heltec_v3_repeater_observer_mqtt\n

Verify with grep -oE \"[^ ]*libmbed[a-z0-9_]*\\.a\" .pio/build/<env>/firmware.map | sort -u -- every path must be the staged directory.

Do not point platform_packages at a file:// copy of the framework. PlatformIO installs it over the shared ~/.platformio/packages/framework-arduinoespressif32, silently changing mbedTLS for every other ESP32 env and project on the machine. It does this even when the copy's package.json version differs -- verified twice here, and both times the fix was rm -rf the package and pio pkg install to re-download stock. A prepended library search path keeps the change scoped to one env, because the linker takes each archive member from the first archive that satisfies an undefined symbol.

"},{"location":"test-results/mbedtls-tls-footprint/#how-the-archives-are-distributed","title":"How the archives are distributed","text":"

They are not committed: ~6 MB per architecture, and they have to be rebuilt for every platformio/espressif32 bump, so committing them would grow history permanently and go stale silently. Instead they are published as a release asset and fetched on demand:

scripts/fetch_mbedtls_4k.sh esp32s3            # download + verify against the manifest\nMESHCORE_REDUCED_TLS=1 pio run -e Heltec_v3_repeater_observer_mqtt\n

The opt-in path is deliberately loud, because both ways this can go wrong produce a firmware that looks correct and silently lacks the change:

failure what happens without a guard guard directory missing or partial linker ignores an unusable -L and resolves mbedTLS from the framework pre-build: hard error archives stale after a platform bump links the wrong build pre-build: sha256 vs manifest -L present but outranked framework archives win, flag is inert post-link: firmware.map must resolve every libmbed*.a to .mbedtls-4k/

That last one is the reason the post-link check exists rather than trusting the flag: a build flag reaching the compiler proves nothing about what got linked.

"},{"location":"test-results/mbedtls-tls-footprint/#how-to-verify-it-worked","title":"How to verify it worked","text":"
  1. strings/grep the new sdkconfig.h for the four settings.
  2. Build and check the RAM figure; static usage should be unchanged (these are heap buffers).
  3. On hardware, get mqtt.stats at boot with two slots connected: the largest free block should start roughly 24-32 KiB higher than the current 62-67 KiB.
  4. Soak across reconnect cycles and compare the floor against the two recorded runs: baseline settled 16,372 (cycle 11); softDisconnect holds 36,852 (cycle 16).
"},{"location":"test-results/mbedtls-tls-footprint/#prior-art-in-this-investigation","title":"Prior art in this investigation","text":"

.scratch/mqtt-non-psram-heap-staircase-analysis-2026-08-05.md (untracked -- .scratch/ is globally gitignored) holds the full allocation inventory. ~/mqtt-soak/STATE.md holds the soak evidence, including two retracted hypotheses worth not repeating: the perf commits were not the cause, and waev does not cap connections per IP.

"},{"location":"test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/","title":"Mesh pocket qualification 1 18 0 1 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/#meshpocket-companion-qualification-21-october-2026","title":"MeshPocket Companion qualification, 21 October 2026","text":"

Status: PASS for the automated Companion bench checks below. The user's request to test MeshPocket lifted its earlier bootloader-work exclusion. The board now runs Full Companion built from 21a0f318da26bdb1fa5257471c3d7a68a0684337, version v1.18.0.1-halo-keymind-cascade-dev-21a0f318, protocol 14. Its measured application body is 54411895235BE9C4; application ZIP SHA256 is 7841c4e7e8bbb8695ba0e796fa811efa41112367641c2790f22cc9085ba00da6. No firmware change was needed for these checks.

Check Measured result Build and application update Full capability/memory checks passed; identity-bound USB application-only DFU succeeded; exact version and EndF body verified after boot USB 100 query cycles, 10 reopens, owned contact CRUD and persistence through one verified warm restart passed; no framing errors or core error flags Bluetooth Three authenticated sessions held for 30.269 / 30.271 / 30.271 seconds; saved bond, exact USB identity/version and three clean controller lifecycles; no bond deletion Full contact refresh All 64 owned records verified in a 65-record stream, including the original contact; request to END: 2.491665 seconds Incremental refresh Exact 32 owned timestamp-selected records in a 33-record stream; request to END: 1.305048 seconds Contact cleanup All 64 synthetic contacts removed; zero remained after reopen; original contact fingerprint unchanged Two-way LoRa Three rounds, six exact direct nonce-bound datagrams, six send commands; every round passed Handback Owned channels removed; both radios' original modem settings, TX power and saved routes restored; ports closed; final Pocket core errors and outbound queue both zero

Bluetooth passed while the USB data cable remained connected. Each contact refresh used the existing bond and a separately verified clean controller lifecycle. USB persistence covers one owned contact across a warm restart; the 64-record fixture separately covers a seven-second save allowance and exact reopen readback. Neither is a cold-power persistence measurement.

The RF partner was the already restored XIAO ESP32-S3 Full Companion, retained at ebb59d48292de4a573250cf4c6a2ed7cf5f7ef69, body 4EEB12667E5E554B. Both identities and actual versions were verified before the three rounds. The temporary test tuple was 910.525 MHz / BW62.5 / SF7 / CR5, preamble 32. Exact source, destination, nonce, zero-hop route and physical packet counters were checked. This is an explicitly mixed-source pair, not a claim that every radio runs the new Pocket artifact. The RAK3401 gateway was not touched.

The existing bootloader and SoftDevice were preserved: the verified DFU ZIP contained only application BIN/DAT components. Its exact installed bootloader revision remains unverified because this Full Companion returns an unknown command for get bootloader.ver. Successful application DFU and subsequent runtime checks do not establish the independent bootloader's revision.

The first updater attempt failed before its DFU touch because the newer flasher's pinned USB helpers depend on startup globals absent from the definitions-only bench adapter. The compatible, previously reviewed updater then installed the app successfully. The adapter now validates and privately stages both bundled helpers and resolves them locally without running flasher startup. Fourteen adapter tests and five application-ZIP verifier tests passed; shell syntax and diff checks passed. This tooling-only fix did not trigger another firmware installation or repeated hardware campaign. The preinstall 0865 application's core flag 8 and the failed updater attempt remain in the private evidence; no causal firmware fix is inferred from the clean new boot.

The safe receipt binds every measured axis to its source, artifact, interval and evidence digest. Earlier Pocket results and the temporary exclusion remain historical in the 20 October fleet report. The other twelve boards retain their accepted source-bound measurements; unchanged hardware tests were not repeated.

These finite checks do not establish RF range, long-duration reliability, phone interoperability, battery current or physical cold starts. Screen/page rendering awaits operator observation. MeshPocket has no native WiFi. Bluetooth DFU is build-verified but a physical wireless update was not run in this task. This Companion can seed LoRa firmware transfers; local LoRa firmware installation is disabled and was not claimed tested.

"},{"location":"test-results/mixed_scan_validation/","title":"Mixed scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mixed_scan_validation/#mixed-bandwidth-two-channel-reception-test","title":"Mixed-bandwidth two-channel reception test","text":"

V4 transmitter on MercerWoodMesh; XIAO ESP32-S3/Wio SX1262 receiver. Channel 0: 909.5 MHz, SF7/62.5 kHz, preamble 32. Channel 1: 910.5 MHz, 500 kHz, SF and preamble below. Both use CR4/5, explicit header, CRC and 64-byte test payloads. Fast RX-preamble scanning uses four symbols of the active profile per visit, not four simultaneous channels or hardware CAD.

Each setting starts with a five-packet-per-channel continuous-RX baseline. Scanning sends 50 packets per channel in shuffled order with random host delays. Only CRC-valid, payload-validated, sequence-matched packets count. Any scan loss escalates the 500 kHz preamble by 16, capped at 128. SF9 and SF8 start at 32; SF7 starts at 48. An additional SF8/40 test was requested after SF8/48; its firmware adds acceptance of preamble 40 without changing radio timing or scanning behavior.

500 kHz SF Preamble Mode SF7/62.5 received 500 kHz received Mean scan cycle SF9 32 Fixed baseline 5/5 5/5 \u2014 SF9 32 Scan 50/50 50/50 14.676 ms SF8 32 Fixed baseline 5/5 5/5 \u2014 SF8 32 Scan 50/50 49/50 12.628 ms SF8 48 Fixed baseline 5/5 5/5 \u2014 SF8 48 Scan 50/50 50/50 12.629 ms SF8 40 Fixed baseline 5/5 5/5 \u2014 SF8 40 Scan 50/50 49/50 12.630 ms SF7 48 Fixed baseline 5/5 5/5 \u2014 SF7 48 Scan 50/50 50/50 11.603 ms

These are strong-signal, sequential-transmission lab results. Successful reception of 50 packets is not proof of zero loss over months. The receiver remains on one channel through an entire detected packet, so overlapping traffic on the other channel can be lost. Scan-cycle measurements exclude time occupied by packet reception; USB command handling can add timing outliers.

Machine-readable counts, timings and firmware/source hashes: mixed_scan_results.json.

After testing, both boards were restored to their original B soak firmware using app-only flashes. Version and normal radio settings were verified on both boards; Wi-Fi was connected and MQTT reported custom (ok). Both persistent soak loggers were restarted in phase B-post-CAD-soak, marking the intentional uptime reset.

"},{"location":"test-results/mixed_sf_bw_validation/","title":"Mixed sf bw validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mixed_sf_bw_validation/#equal-symbol-time-mixed-sfbw-four-channel-test","title":"Equal-symbol-time mixed SF/BW, four-channel test","text":"

2026-09-14. Promising, but no strict 400/400 pass. With no added settling, the mixed scanner received 61 consecutive packets on the correct channels, then timed out on packet 62. The near-ceiling +22.4 ms setting received four packets and timed out on the fifth. There were no wrong-channel payloads in either run. Both stopped on the first miss without retries.

"},{"location":"test-results/mixed_sf_bw_validation/#configuration","title":"Configuration","text":"Channel Center (MHz) SF BW (kHz) Symbol time (ms) 0 909.5 9 62.5 8.192 1 910.5 10 125 8.192 2 911.5 11 250 8.192 3 912.5 12 500 8.192

These preserve the preceding SF10/125 symbol duration exactly, rather than approximately: symbol_us = 2^SF * 1000 / BW_kHz. The supported SF/BW values and symbol-rate equation are specified in Semtech's SX1261/2 datasheet, sections 6.1.1.1\u20132. Equal symbol durations do not imply equal payload airtime or sensitivity.

The lab scanner stages four channels through the existing two production profile slots; this does not change the public two-profile protocol.

"},{"location":"test-results/mixed_sf_bw_validation/#positive-controls","title":"Positive controls","text":"

Before scanning, each profile received ten fixed-frequency probes after full normal initialization. All four passed 10/10, forty total. Zero frequency commands occurred after each setup; no stationary receiver hardware failure. These are fresh ten-packet controls, not claims of 100/100 on each new profile.

Both devices acknowledge the same exact modulation table through mixinfo. Transmitter results verify SF, fractional BW, center, sequence and packet size. RX records verify center-command word and SF/BW at packet read. Full expected payload identity and CRC remain required. Mode/cache errors are treated as fixture failures rather than ordinary RF misses.

"},{"location":"test-results/mixed_sf_bw_validation/#timing-controls-and-settling-budget","title":"Timing controls and settling budget","text":"

Six-second RX-only window at zero added delay: 141 hops, mean 637.922 us, maximum 828 us. The largest 100 us delay step with a 500 us per-hop reserve is floor((262144/4 - 41780 - 828 - 500)/100)*100 = 22400 us.

The +22,400 us RX-only verification window measured 87 hops, mean 638.241 us, maximum 833 us. Nineteen complete idle cycles measured mean 259,292.789 us, maximum 259,310 us, below the 262,144 us preamble. Every hop had two observed frequency commands and one modulation command, with no mode/cache/RX errors.

This is an empirical near-ceiling budget, not a guarantee of acquisition for every arrival phase. Longer packet holds and software jitter can defeat the idle-cycle calculation. RX is active throughout added settling.

"},{"location":"test-results/mixed_sf_bw_validation/#four-channel-results","title":"Four-channel results","text":"Added settling Strict received / attempted before first miss Failed TX profile Mean switch (us) Max switch (us) Max idle cycle (us) 22,400 us 4 / 5 Channel 2, SF11/250 660.951 892 259391 0 us 61 / 62 Channel 0, SF9/62.5 674.819 899 169798

Both failures were timeouts, with TX rc=0, RX mode=1, device errors=0. Both runs had zero mode/cache/receive errors and zero off-channel payloads. The +22.4 ms run exercised 163 hops, 326 frequency commands and 163 modulation commands. Zero-delay exercised 718 hops, 1,436 frequency commands and 718 modulation commands. Optimized RX resume counts equaled hop counts.

No-delay per-channel counts at stop:

Channel Received Attempted Good-packet mean RSSI (dBm) Good-packet mean SNR (dB) 0 15 16 -34.00 11.15 1 16 16 -33.69 8.24 2 15 15 -34.33 5.78 3 15 15 -33.27 3.47

The receiver stayed longer on some visits: maximum held visit 582,993 us with settling, 580,577 us without. Held-visit statistics include successful packet reception, so these maxima alone do not prove false off-channel preamble holds in this experiment. No IRQ trace was collected for the two failures; their precise mechanism is not established.

The longer success sequence and absence of wrong-channel payloads are encouraging relative to the earlier uniform-SF/BW tests, but do not establish a statistically measured improvement or zero-loss reliability. Each setting was censored at first failure; do not present 61/62 as an unbiased measured packet-delivery rate or either run as a completed 400-packet test. The random arrival method is seeded, not RF-phase synchronized across configurations.

"},{"location":"test-results/mixed_sf_bw_validation/#implementation-verification-and-final-state","title":"Implementation, verification and final state","text":"

HIL-only ProfileMixedChannels.h, basemixed, mixtx, scanmixed and profile_switch_mixed.py add the bounded experiment. Uniform modes preserve their defaults. Post-hop checks use the actual per-channel modulation; the uniform collector and settling evidence checks reject mixed-mode results. Receive ownership, packet guards, BUSY and command-error handling remain intact.

17 native profile tests and 48 HIL host tests passed. Tests cover fractional 62.5 kHz, equal symbol duration, table identity, normal uniform mapping, required modulation writes, TX replay, and wrong-policy rejection. Both firmware builds and normal uploads succeeded. The nRF52 ZIP is application- only, SoftDevice ID 0x0123; no bootloader or SoftDevice replacement. RAK COM29 was not opened. Production settings/source were not changed this turn.

Both boards rebooted idle after the second failed setting, with test firmware retained. No autonomous TX, background collectors or restoration. Temporary PlatformIO overlay removed. No commit or push was requested. Existing unrelated working-tree changes remain intact. git diff --check passed apart from unrelated pre-existing CRLF warnings.

"},{"location":"test-results/mixed_sf_bw_validation/#provenance","title":"Provenance","text":"

Raw capture: tools/hil/profile_switch_mixed_equal_symbols_results.json. It includes the four baselines, two timing controls and two strict scans, with complete:true, stopped:both_delay_controls_failed, and no fixture or cleanup errors. Earlier captures were not rewritten.

RX build reported 94,844 bytes RAM / 367,537 bytes flash. TX reported 18,220 / 95,908 bytes. Source is keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus existing uncommitted work and these HIL changes; this is not a clean-release comparison.

"},{"location":"test-results/morse_rx_hil_2026-10-12/","title":"Morse rx hil 2026 10 12","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#morse-receive-hardware-checkpoint-12-october-2026","title":"Morse receive hardware checkpoint - 12 October 2026","text":"

Status: incomplete hardware validation. The release default remains 25 WPM / 902.100 MHz and the public limit remains 60 WPM. Experimental 255-WPM builds are not a validated release limit.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#implementation-and-host-verification","title":"Implementation and host verification","text":""},{"location":"test-results/morse_rx_hil_2026-10-12/#hardware-identities-and-isolation","title":"Hardware identities and isolation","text":"

The Mercerwood Pi was reached at 100.78.195.38. Only identity-verified application DFU packages were written through the existing mcfirmware.sh path. No bootloader or SoftDevice was replaced, no services were stopped, and the Pi was not rebooted by this test.

Board USB chip serial RAK4631 9AB3B64C641BA927 Heltec T096 651F8E496197F882 Heltec MeshPocket 93DA83ADF7B2092B

The RAK3401 service radio and MeshTower were left alone.

Early tests put CW at 909.5 MHz and packet profiles elsewhere. This was corrected: the requested CW default is 902.100 MHz, separate from the LoRa lab control tuple 909.5 MHz / 500 kHz / SF5 / CR5. Results from the early layout are not proof of packet-spectrum isolation.

set radio only saves Companion primary settings. The test runners now use set tempradio and poll the exact active tuple; saved or pending settings do not satisfy the check. The USB-only HIL recipes explicitly include the existing temporary-radio capability. Names and saved primary settings are not changed by the corrected runners.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#observations-not-a-new-maximum","title":"Observations, not a new maximum","text":""},{"location":"test-results/morse_rx_hil_2026-10-12/#remote-acknowledgement-and-retained-capture-evidence","title":"Remote acknowledgement and retained-capture evidence","text":"

A 30-second LoRa-controlled test physically demonstrated:

  1. A six-minute request was rejected in 0.682 seconds.
  2. The valid request was acknowledged in 0.684 seconds before RX started.
  3. The timed receiver returned to LoRa after 30.889 seconds as observed by the host.
  4. All 34 retained characters read locally were returned byte-for-byte in the remote LoRa read.

The decoded RF text was not equal to the transmitted message, so this run failed overall. It used the early shared CW/control frequency and is not a clean decoder validation. The full five-minute and multiple-page remote tests remain outstanding.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#why-hardware-work-stopped","title":"Why hardware work stopped","text":"

At approximately 22:43 Pacific the RAK USB connection disappeared during the zero-gate isolated-frequency sweep. It subsequently reported v1.17.1.10-halo-keymind-cascade-preview3-9ce2a076, with get morse unknown, instead of the just-verified Morse application. Another concurrent job had flashed the shared RAK. This is not evidence of a Morse firmware crash. The affected sweep is incomplete and must not be counted as a completed speed test. Hardware writes were stopped for user coordination.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#ready-to-resume-experiment","title":"Ready-to-resume experiment","text":"

An isolated HIL-only MESH_MORSE_HIL_FAST_OFF_DEBOUNCE=1 experiment keeps ON confirmation at two ms and permits OFF confirmation at one ms for fast nominal dots. Default/production behavior, capture size and low-speed timing are unchanged. Synthetic tests pass; physical results are still pending.

The following application-only packages were compiled and staged on the Pi but not installed:

Package SHA-256 RAK_4631_companion_radio_usb-morse-fastoff.zip 0340b6135c36c7d6cb61be45ac28555ead5a2f6529feb3702466b284d2abd204 Mesh_pocket_companion_radio_usb-morse-fastoff.zip e0d94389310244655136564a1dbd55d5e4d4d18a17bbae6e1c7ae018f5bc7952

Pi run directory: /home/mikec/hwtest/runs/morse-20261012.3Lp6jA. Local raw evidence is retained in the ignored out/morse-rx/ directory; the strict test runners are in tools/hil/morse_bench.py and tools/hil/morse_remote.py.

Before resuming, coordinate exclusive use of those three radios, verify the installed images and exact USB identities, and run the isolated timing A/B, repeated mixed-message speed boundary tests, and the five-minute multipage LoRa capture/recovery test. Only then choose and verify any production cap or tuning changes. No commit or push was performed for this checkpoint.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#follow-up-13-october-2026","title":"Follow-up: 13 October 2026","text":"

The original checkpoint above is historical. Its implementation and Reader / RS232 integration were subsequently pushed as 0be4c7488 and 396304822.

RF investigation and repeated standard/uneven-timing speed qualification remain in progress. Default 25 WPM, public ceiling 60 WPM, radio bandwidth, and shipping RF filtering have not been changed. Packet control stays at 909.5 MHz / 500 kHz / SF5 / CR5; CW remains separate at 902.100 MHz by default. Raw results remain in out/morse-rx/ and the Pi run directory above.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#controlled-speed-and-human-timing-follow-up","title":"Controlled speed and human-timing follow-up","text":"

These are conditional laboratory results, not a promise of error-free reception on every board or with automatic noise detection. CW stayed at 902.100 MHz, packet control at 909.5 MHz / 500 kHz / SF5 / CR5, and requested chip TX power at -9 dBm. The Pocket / RAK pair used a volatile HIL-only -80 dBm receive floor against strong received tones. Shipping images do not contain this receive-floor setting. Earlier automatic-threshold quiet controls produced false characters on the Pocket and T096; that remains a separate qualification limitation.

The original 2.25 / 5.5-dot letter / word boundaries failed ordinary uneven timing even at 25 WPM. With seeded +/-20% phase jitter, gradual -10% duration drift, and no extra letter or word spacing, the first sweep passed only 2/6 messages at each of 25, 60 and 120 WPM. A short, fully retained RF trace decoded VW MN DOG as VW MNDOG in all six directions/rate combinations. All 20 expected marks remained present; the second word gap measured 246 / 246 ms at 25 WPM, 100 / 102 ms at 60, and 54 / 52 ms at 120. The old word thresholds were 264, 110 and 55 ms respectively.

The receiver now uses the ordinary 2 / 5-dot gap midpoints, without changing mark classification, RX allocation, RF filtering or the public speed limit. The same tight human-timing profile (seed 6789), with two repetitions of all three mixed alphabet/digit, word and punctuation messages in both directions, then passed strictly:

Requested WPM Pocket to RAK RAK to Pocket Total exact messages 25 6/6 6/6 12/12 60 6/6 6/6 12/12 120 6/6 6/6 12/12

Only leading/trailing capture whitespace is removed in the physical harness; extra internal spaces, noise characters, missing letters and ? fail the test. There were no cleanup errors. This establishes zero observed errors in those 36 messages under the stated laboratory conditions, not universal 100% reliability or a 120-WPM release qualification.

The production-equivalent RF comparison uses cold-XOSC TX, 3400-us PA ramp, RSSI window 0, two-ms sampling and ordinary OFF debounce. Compile guards and actual PlatformIO/SCons flag tests prevent faster experimental settings or a different receiver clock from being mislabeled as shipping RF behavior. The lab maximum override and diagnostics still make these HIL applications, not release images.

Requested WPM is not measured RF pace. Four strict 13-word PARIS trials per rate transmitted 643 dot units, omitting the unsent final seven-unit word gap:

Requested WPM Host-observed duration PARIS-equivalent pace 60 14.354-14.365 seconds 53.7 WPM 120 7.937-7.940 seconds 97.2 WPM

The pace includes CLI response and polling overhead; it is not an oscilloscope measurement of RF edges. It also does not substitute for varied-text testing.

Latest focused verification passed 129/129 test methods. The actual HIL scheduler / decoder matrix has 175 exact results plus one explicitly unsupported 240-WPM / two-ms-sampling / 50%-extended-spacing case. The separate human envelope matrix has 84 exact results plus 14 explicitly unsupported doubled-character-spacing cases. Unsupported transcripts remain checked and visible; they are not normalized or counted as exact-decoding passes. Former-threshold negative controls reproduce the tight-gap failure. The production receiver remains 4168 bytes, and compact STM32 still links at 228868/229376 flash bytes with its unchanged feature boundary.

Evidence filenames in the directories above:

The tested application-only gap-correction packages were:

Package SHA-256 RAK_4631_companion_radio_usb-prod-rf-gap-20261013.zip 26cc93db4f6a602e24a35828cca79efaef002b13b18192fe763eb5426515ec6b Mesh_pocket_companion_radio_usb-prod-rf-gap-20261013.zip 9d1f76c1cc42e1d30ca5daa3d30fe30232d426d72b80278cac67d2d98c21a178

The T096's earlier image and measurements do not qualify its speed under this new gap correction. No LR1110 or SX127x physical speed limit is established by the SX1262 pair. Default 25 WPM and the public 60-WPM ceiling remain unchanged.

The next faster production-RF timing buckets did not meet the zero-error criterion, even with the same HIL receive floor:

Requested WPM Standard timing Tight human timing, two repeats 130 5/6 11/12 150 2/6 6/12 170 0/6 Not repeated in this corrected sweep 200 0/6 Not repeated in this corrected sweep

At 130 and 150 some failures affected the initial short mark; 150 also had later body errors. The first RF mark currently begins before the command reply and the remaining foreground-loop work. That is a possible delay window, not a measured explanation of all these errors. No unproved transmitter or mark-classifier adjustment was made to improve the reported score.

Additional boundary evidence:

A second human-style pattern, using +/-20% jitter, gradual +10% duration drift (slowing down), no extra gaps and seed 12345, passed another 6/6 mixed messages at each of 25, 60 and 120 WPM in both directions. Across the corrected speeding-up and slowing-down profiles, that is 18/18 messages at each rate, 54/54 overall, under the same controlled receive threshold.

The subsequent 30-second automatic-threshold, no-intentional-TX control was not clean overall: the RAK captured E E, while the Pocket was empty. This is evidence against an unconditional production zero-error claim; it is not a diagnosis of the source of the quiet RSSI excursions. The current shipping speed ceiling remains unchanged.

Short, fully retained two-second initial-window traces sharpen the initial-mark finding. At 130 WPM, the failed first E measured 18 ms, exactly twice the nominal nine-ms unit; a successful first E measured 16 ms. At 150, the corresponding failed/successful widths were 16/14 ms against an eight-ms unit. Those widths were also common among later dots, so these recordings do not show an unusually long first mark. The later learned units were 11/10 ms, making comparable later dots classify correctly. Two-ms sample phase and the initial untrained threshold can therefore matter near this boundary. The trace has no transmitter deadline timestamps and does not prove that CLI work caused the errors. These partial diagnostic captures are not complete-message qualification failures or passes.

"},{"location":"test-results/morse_rx_hil_2026-10-12/#faster-experimental-rf-comparison","title":"Faster experimental RF comparison","text":"

The corrected fast SX1262 HIL applications use warm-XOSC TX, an 800-us PA ramp, RSSI window 2, one-ms sampling and the experimental short OFF-confirmation minimum. These are not shipping RF settings. The same -80 dBm HIL receive floor and exact-message checks were used on the same Pocket / RAK pair.

Requested WPM Standard timing, two repeats Tight human timing 120 Previously all-pass; not repeated in this boundary sweep 6/6 150 Not tested in this boundary sweep 5/6 200 12/12 3/6 255 10/12 Not tested in this boundary sweep

The tight human profile is again +/-20% jitter, -10% duration drift, no added letter/word spacing and seed 6789. At 150, DOG became DKG in one direction. At 200, three messages had real body or punctuation errors. The shortest timing bucket therefore does not qualify as zero-error, and neither does the higher uneven-keying speed. Integer rounding maps requested 219-255 WPM to the same five-ms dot, so 255 is not 255 measured WPM.

The fast steady-PARIS pace check gives:

Requested WPM Host-observed duration PARIS-equivalent pace Exact PARIS messages 120 6.868 seconds 112.3 WPM 2/2 200 4.273-4.277 seconds 180.5 WPM 2/2 255 3.669 seconds 210.3 WPM 1/2

The failed 255 PARIS capture contained PANIS in place of one PARIS. These rates include the same host/CLI overhead as the production-equivalent pace measurement; pace is calculated only for the 643-unit PARIS payload, not by applying that reference count to the other test messages.

The strongest observed all-pass points are therefore 200 requested WPM for machine timing on the fast HIL pair, and 120 requested WPM for the tested uneven timing. Their steady-PARIS references are approximately 181 and 112 WPM respectively. With production-equivalent RF timing, the tested uneven-timing reference is approximately 97 WPM. None of these conditional limits resolves the automatic-threshold quiet-control failure or qualifies another board/radio family. Shipping defaults remain unchanged.

Fast HIL package SHA-256 RAK_4631_companion_radio_usb-fast-gap-20261013.zip 7722ca7a0008c660217535c19e9dd17d5923fc90510f9850b2ce188a6d753951 Mesh_pocket_companion_radio_usb-fast-gap-20261013.zip 8d153c544f19e01d45e178105b704840f855a53cb8fda2044ba102cb19b85521

Final cleanup confirmed both selected radios had Morse stopped, configured 25 WPM / 902.100 MHz, default HIL timing and automatic squelch restored. Their saved primary settings still held an earlier 919.5-MHz lab tuple; those two test devices were explicitly changed to 909.5 / 500 / SF5 / CR5, and that tuple was also confirmed live. No service radio was changed. The pair remains on the identified fast HIL applications, not shipping images.

"},{"location":"test-results/mqtt_tls_v4_regression/","title":"Mqtt tls v4 regression","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/mqtt_tls_v4_regression/#esp32-mqtt-tls-connection-regression","title":"ESP32 MQTT TLS connection regression","text":""},{"location":"test-results/mqtt_tls_v4_regression/#report-and-diagnosis","title":"Report and diagnosis","text":"

The Heltec V4 report contains two separate failures:

The firmware configured a 2,500 ms network timeout to bound synchronous publishes. ESP-MQTT also uses that setting for connection establishment, including TLS and the WebSocket upgrade. It was not a write-only setting.

The shipped ESP32-S3 AES driver allocates DMA-capable internal bounce buffers for unaligned external-memory records, up to 1,600 bytes per buffer. Ample PSRAM or an aggregate free-heap reading therefore does not establish that an AES operation can succeed. The supplied log identifies an allocation failure in this driver. The same allocation error was reproduced on the lab V4 with the older firmware and five enabled WSS presets.

The shipped Arduino 2.0.17/IDF 4.4.7 TLS allocator was checked in its archives: TLS allocations use the configurable mbedtls_calloc function, so the existing PSRAM redirect takes effect. Retaining a PsychicMqttClient does not retain its TLS records: ESP-IDF frees/recreates the TLS session on reconnect.

References: ESP-IDF 4.4.7 AES implementation and ESP-TLS errors.

"},{"location":"test-results/mqtt_tls_v4_regression/#firmware-changes","title":"Firmware changes","text":"

These changes apply to the shared ESP32 MQTT bridge, including repeater, room-server and companion images that enable it.

Existing slot limits, certificate verification, authentication and backoff remain in effect. Memory pressure defers additional slots instead of consuming memory needed by existing connections and the radio.

"},{"location":"test-results/mqtt_tls_v4_regression/#validation-and-limits","title":"Validation and limits","text":"

The regression tests execute the production optimizer, adapter setters, connection admission, TLS allocator fallback, scheduler and diagnostics with controlled SDK calls and heap readings. They cover IDF 4/5 config layouts, PSRAM and internal-only modes, fragmented heap, exhausted PSRAM, delayed connection callbacks, retry timing and bounded replies. They do not execute an ESP32 AES peripheral or qualify physical WiFi behavior.

All 46 focused Python MQTT tests and 33 memory-budget tests passed after the final reconnect/buffer changes. The 1,794-test native core suite passed after the initial TLS/admission fix, before the subsequent MQTT-only refinements. The focused MQTT tests are also connected to the GitHub unit-test workflow. The new memory tests include a linked-ELF boundary that the previous WiFi-omitted policy incorrectly accepted.

Full MQTT repeater and room-server builds with USB packet logging passed on 10 October 2026 at revision 937f8610c. The V4 Full Companion build passed at d11bf3615. The RC32 build at the same revision also passed using Arduino 3.3.11 and ESP-IDF 5.5.5, exercising the newer SDK in an actual firmware build. The only intervening change corrected the host-side WiFi memory accounting. Their build logs report:

Build Application flash bytes Available internal heap bytes Required heap bytes Largest internal region bytes Heltec V4 2,182,265 205,080 198,432 172,312 Heltec V3 2,125,377 211,296 209,184 178,528 Station G2 2,017,953 201,280 200,480 168,512 Heltec V3 room server 2,104,057 201,992 196,016 169,224 Heltec RC32 without display, IDF 5 2,287,033 195,688 194,336 162,920 Heltec V4 Full Companion 2,418,405 233,880 194,336 201,112

Application flash is the PlatformIO size-check reading. Internal heap is linked capacity before dynamic allocation, not free heap measured after boot. These RAM gates passed the startup policy; they do not qualify maximum TLS/WSS/outbox load. The default MQTT startup allowance remains 24,576 bytes, with runtime admission for additional connections. See firmware memory checks for the separate maximum-slot estimate and qualification limits.

Two additional Full builds compiled and linked the shared MQTT code but were held by the unchanged RAM safety gates:

Full build held Available internal bytes Required bytes Shortfall bytes T-Beam SX1276 repeater, original ESP32 179,736 192,560 12,824 Heltec V3 Companion 182,264 198,432 16,168

Both already enabled WiFi through other build flags, so the corrected MQTT-only WiFi detection did not increase either requirement. Their capacity shortfalls require separate memory work before those Full artifacts can be qualified; neither gate was lowered and neither failed artifact was published. The shared firmware fixes still apply to their MQTT code. The original ESP32 compile check and IDF 4/5 tests cover the non-V4 source paths; physical testing in this report is limited to the V4.

Live TLS 1.2 certificate chains from 12 audited preset endpoints were verified with the exact CA PEMs extracted from MQTTPresets.h using a native host build of the pinned mbedTLS 2.28.7 submodule from ESP-IDF 4.4.7, commit 2b8e772fc1cb0732cda3bae7d1e9d6f4cfaf63d9. Hostname verification was enabled. All 11 GTS Root R4 endpoints and the ISRG Root X1 Cascadia endpoint returned verification result 0 and flags 0x00000000. Wrong-hostname and unrelated-anchor negative checks were rejected. WAEV also passed with the server root omitted from its supplied chain.

The existing cross-signed GTS Root R4 PEM is a valid trust anchor for that pinned mbedTLS verifier. Ordinary OpenSSL root-only verification rejected it, while partial-chain verification with the same PEM passed all 11 GTS endpoints. This host-probe policy difference does not establish a firmware CA failure or require a CA replacement for the reported AES allocation errors. The verifier used the native default mbedTLS configuration; this establishes pinned X.509 chain and trust-anchor behavior, not an ESP32 hardware handshake.

"},{"location":"test-results/mqtt_tls_v4_regression/#physical-v4-results","title":"Physical V4 results","text":"

MercerWoodMesh's Heltec V4 was tested over its native USB console using the existing SlowFi network. The Pi's WiFi configuration was not changed. A full 16 MiB private flash backup was made before testing. Test credentials and raw console logs are not included in the public results.

Check Result Older firmware c5fe8a0e, WAEV and Cascadia Both connected for the 150-second trial; accepted writes increased with no outbox backlog. The older firmware has no PUBACK counter, so accepted writes alone do not establish broker delivery. Older firmware, five WSS presets Reproduced esp-aes: Failed to allocate memory, 0x8018 and 0x8006. All five slots eventually disconnected. This is a failing baseline. Final firmware 937f8610c, five presets enabled WAEV, Cascadia and Analyzer US connected within 46 seconds. Analyzer EU and MeshMapper waited without allocating SDK clients because DMA memory was below the cold-start admission threshold. No AES allocation, TLS timeout/write error or CPU reset was observed during this trial. Final firmware, power saving on, 180.005 seconds without console commands All three connected brokers stayed healthy. Their pub_ack counters increased from 2/1/1 to 5/5/5. Uptime advanced from 57 to 258 seconds including surrounding queries. DMA heap stayed at 22,776 free / 22,516 largest bytes, with no outbox backlog, drops, AES allocation error, TLS timeout/write error or CPU reset. Cooperative MQTT stop The stop request completed and get mqtt.stopping returned off. The radio console remained responsive.

The three-connection measurements included about 22.7 KiB free DMA-capable heap with a 22.0 KiB largest block. Outbox activity briefly consumed additional memory. Three brokers were qualified in these conditions; this does not qualify five simultaneous WSS connections or a long-duration loaded-radio soak. Additional enabled slots wait for headroom rather than exhausting the memory needed by active TLS connections.

The final power-saving trial recorded core error bitmask 0 before and after the idle interval; bitmask 8 appeared after the subsequent MQTT stop. An earlier fixed-firmware trial also recorded bitmask 8. This is the configured radio watchdog flag on a quiet channel, also observed in earlier V4 console qualification; it is not eight MQTT errors or evidence of a CPU reset. This run does not claim zero radio watchdog events.

The attempted SSID interruption returned \"WiFi SSID saved; reboot to apply\" and did not disconnect the station. It is therefore not counted as a physical WiFi-loss recovery test. Regression tests exercise the production WiFi-loss handler and connection scheduler with retained SDK clients, including the restart memory allowance and serialized reconnect attempts.

A Pi-wide USB hub reset interrupted the older five-broker baseline. Several radios disconnected together and subsequently reappeared. That interruption is retained as a host-lab failure, not attributed to a V4 firmware crash.

After the final live check, the original NVS and filesystem backup regions were restored and their writes verified, preserving the final firmware. Saved configuration replies matched, and a separate get public.key check verified that the public-key hash was unchanged. Comparison removes console echo/prompt framing and treats the old blank get wifi.ssid response as the new (not configured) response. MQTT and power saving were both returned to their original off state. No release assets were uploaded as part of this test.

"},{"location":"test-results/mqtt_tls_v4_regression/#long-running-mqtt-recovery","title":"Long-running MQTT recovery","text":"

The 14 October 2026 follow-up report says the preview initially connects, but three reporting sites show it offline after roughly three hours while WiFi and the console still show connected. The earlier 180-second trial does not qualify this longer-running workload.

Code inspection found recovery gaps, not a verified cause of that hardware failure. The adapter clears its connection flag on an MQTT error, but the bridge previously left its separate flag set until a disconnect callback. Its five-second cache refresh only reread that same flag. A lost terminal callback could also leave a connection attempt pending indefinitely, blocking the other brokers' handshake and renewal requests. Accepted status enqueues were treated as successful publication without using the existing PUBACK counter to detect stalled delivery.

Recovery now runs on the bridge's owning task. An errored or timed-out SDK client must be proven stopped before its pending-handshake reservation is released. The client object and credentials are retained, and the existing admission checks and reconnect backoff still apply. No WiFi reset, node reboot, extra periodic heartbeat, or broker credential change is required. An idle connection with no outstanding QoS 1 publication is not timed out merely for being quiet.

Both the pending-handshake deadline and the outstanding-PUBACK deadline are 120 seconds. This leaves a generous margin over the SDK's 10-second network timeout and the configured 15-second QoS 1 retransmission interval. A failed SDK stop retains its recovery reservation instead of permitting another overlapping TLS handshake.

CLI and WebConfig slot status show recover during reserved cleanup. get mqttN.diag retains pub_ack and adds a bounded recovery count/reason (rec) and pending acknowledgment age (ack_wait) when they fit. A delayed connected callback cannot cancel cleanup after an SDK stop failed.

The delivery watch starts with the first accepted QoS 1 enqueue and observes real broker acknowledgments, including the on-connect status publication. Further enqueues cannot postpone that oldest outstanding deadline. Timeouts are serviced on the next owning-task pass; they are not a guarantee of wall-clock recovery if the worker is blocked inside an SDK call. This change does not establish or repair an indefinite SDK mutex deadlock.

All 251 native MQTT test cases and 79 MQTT Python host tests passed. The new tests execute the production callbacks, publishing, maintenance, recovery and admission methods with controllable SDK calls and clocks. They include three brokers running beyond three simulated hours, missing PUBACKs and callbacks, silent synchronous write failures, failed SDK stops, late connected events, WiFi loss, suspension/foreground wake, backoff, memory admission and timer rollover. Removing the new error-callback recovery signal makes its regression test fail, establishing that it detects the previous behavior. AP ownership fixtures also passed with address/undefined-behavior sanitizers. The six pure transport-health tests are included in the 79-test total and in CI.

The exact Full heltec_v4_repeater_observer_mqtt image compiled and linked against Arduino 2.0.17 / ESP-IDF 4.4.7. This local test image used baseline dce323e37 with the MQTT repair applied; it predates the subsequently pulled beacon changes. Its application size was 2,237,677 bytes, within the 6,553,600-byte application slot. The linked runtime RAM gate reported 204,752 internal bytes available, 198,432 required and a 171,984-byte largest region. These are build-time capacity/admission checks, not free heap measured with three live brokers or qualification of maximum simultaneous load.

No new physical three-hour V4 soak or reporter-side retest has been performed as part of this repair. These tests establish the repaired recovery behavior, not the cause of the reporter's hardware failure or website-side availability.

"},{"location":"test-results/nimble_companion_trial/","title":"Nimble companion trial","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nimble_companion_trial/#esp32-s3-nimble-full-companion-trial","title":"ESP32-S3 NimBLE Full Companion trial","text":"

Canonical Heltec V3 Full now uses the same pinned NimBLE backend, preserving its existing 15 saved phones and both notification subscriptions per phone. Its Full memory qualification is separate from the optional trials below. Switching from Bluedroid requires forgetting the old phone pairing and pairing again; stored bonds are not migrated.

These optional builds use NimBLE-Arduino 2.5.1 with the existing ESP32 Arduino 2.0.17 toolchain. They are hardware qualification builds, outside the normal release matrix and firmware picker.

See the 2026-09-08 hardware results for measured RAM, MAC-policy checks, WiFi updates, and remaining qualification.

The contact-cache guide adds six optional 350-contact trials for ESP32 boards without PSRAM, including Heltec V3. Their qualification results include linked RAM margins, V4/RAK hardware tests and the mOTA queue policy.

Hardware Trial environment Contacts Channels Offline queue Heltec V4.2/V4.3 OLED, FEM on heltec_v4_2_v4_3_companion_radio_full_femon_nimble 350 40 512 XIAO ESP32-S3 with WIO SX1262 Xiao_S3_WIO_companion_radio_full_nimble 350 40 256

The XIAO profile uses WIO radio pins CS 41, DIO1 39, BUSY 40 and RESET 42. It is not the generic XIAO profile for separately wired radio modules. Both boards retain their parent Full Companion features, USB mOTA sender, WiFi update slots, and PSRAM-backed offline queue. The V4 also retains direct MQTT.

The small-screen message layout uses 6-pixel capitals on 128x64 panels and 5-pixel capitals on very tiny panels. It includes a compact channel/sender line and full message previews. Font selection is automatic.

"},{"location":"test-results/nimble_companion_trial/#build","title":"Build","text":"

Enable the optional configuration in the ignored platformio.local.ini:

[platformio]\nextra_configs =\n  variants/*/platformio.ini\n  platformio.nimble.ini\n

If that file already contains local settings, merge this list with them. Run one PlatformIO/build.sh command at a time. Both commands below use USA Cascade:

OUTPUT_DIR=.releases/nimble-v4 bash build.sh build-firmware \\\n  heltec_v4_2_v4_3_companion_radio_full_femon_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n\nOUTPUT_DIR=.releases/nimble-xiao bash build.sh build-firmware \\\n  Xiao_S3_WIO_companion_radio_full_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n

Use the merged image at address 0 for a USB installation. The V4 uses 16 MB flash; the XIAO uses 8 MB. Both use DIO flash mode. Use the application-only .bin for an existing, matching WiFi OTA layout. Remove the optional config entry when finished to return to the ordinary release matrix.

"},{"location":"test-results/nimble_companion_trial/#bluetooth-identity-and-pairing","title":"Bluetooth identity and pairing","text":"

All existing MAC settings remain available from the ASCII USB terminal and the app's authenticated local CLI. A setting takes effect after reboot.

Command Behavior get bluetooth.mac Show the saved MAC policy. set bluetooth.mac C2:17:15:04:00:01 Use this custom random-static address. set bluetooth.mac random Generate one address and retain it across boots. set bluetooth.mac random-every-boot Generate a new address on each boot. set bluetooth.mac random-after-connect Rotate on the next boot after an authenticated connection; otherwise retain the address. set bluetooth.mac default Restore the factory Bluetooth address. set bluetooth.stealth on Pair once, then accept the saved bonded peer. set bluetooth.stealth off Restore ordinary discovery without changing the MAC policy. get bluetooth.stealth Show pairing or bonded-peer-only state.

The ble.mac and ble.stealth aliases also work. A literal custom address must be a valid BLE random-static address (first byte C0-FF). Forget the old entry in the phone's Bluetooth settings and pair again after an address change or switching Bluetooth libraries. Existing Bluedroid bonds are not migrated.

Stealth does not disable the selected rotation policy. If rotation changes the address on reboot, first pairing opens again for the new identity.

Pairing requires encryption, PIN authentication and a stored bond. NimBLE's controller byte order is converted explicitly, so a custom address appears over the air in the order entered. Failed identity setup stops advertising and retries initialization instead of advertising the wrong address.

"},{"location":"test-results/nimble_companion_trial/#v4-wifi-setup-qr","title":"V4 WiFi setup QR","text":"

The active WiFi setup page displays a compact QR code with a one-pixel white border. The normal open setup network fits a 44 x 44 pixel code, with 2 x 2 pixel modules. The SSID, portal address and HOLD STOP hint remain beside it. Short-click to leave the page; hold the button on this page to start or stop the setup AP. From USB, start webconfig ap starts the same setup session.

When the setup AP is inactive and the node has joined WiFi, this page shows WIFI CONNECTED, IP <address>, SETUP AP INACTIVE, then HOLD START AP from top to bottom. The address appears once assigned and clears when the station disconnects. While the setup AP is active, the QR page shows the setup portal's address instead.

"},{"location":"test-results/nimble_companion_trial/#regression-checks","title":"Regression checks","text":"
python3 -B test/test_nimble_companion.py\npython3 -B test/test_bluetooth_mac_contract.py\npython3 -B test/test_bluetooth_pairing_ui.py\npython3 -B test/test_heltec_v4_wifi_setup_page.py\npython3 -B test/test_firmware_ram.py\npython3 -B test/test_esp32_dram.py\npython3 -B test/test_shared_mota_queue.py\npython3 -B test/test_companion_mqtt_stack.py\npio test -e native -f test_companion_node_prefs \\\n  -f test_ble_tx_stall_watchdog -f test_companion_frame_queue \\\n  -f test_display_driver\n

The adapter test compiles the actual transport under address/undefined-behavior sanitizers with API doubles. It injects allocation, identity and bond failures; checks PIN authentication and stealth; and exercises 176-byte frames, MTU and notification subscription gating, retry and duplicate prevention. Embedded builds compile against the pinned real library. RAM reports retain the existing wireless budget; linked headroom is not a measurement of live free heap. The MQTT save callback also has a compiler-enforced stack-frame regression check: nested preference copies previously overflowed the V4 loop task when saving MQTT settings through WebConfig, despite sufficient free heap.

Hardware qualification must additionally verify pairing and reconnection, advertised addresses for every MAC policy, contact synchronization, WiFi/BLE coexistence, and uptime under load. BlueZ results do not establish iOS or Android camera/pairing compatibility.

"},{"location":"test-results/nimble_companion_trial_results/","title":"Nimble companion trial results","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nimble_companion_trial_results/#esp32-s3-nimble-trial-results-2026-09-08","title":"ESP32-S3 NimBLE trial results, 2026-09-08","text":"

These are local hardware trial results for USA Cascade Full Companions on a Heltec V4.3 OLED and a Seeed XIAO ESP32-S3 with WIO SX1262. The V4.2 shares the profile but was not physically tested. Build and command instructions are in the trial guide.

The final source revision is 9dbab431, using NimBLE-Arduino 2.5.1 and the existing Arduino-ESP32 2.0.17 toolchain. These optional profiles are outside the ordinary release matrix. This trial does not change GitHub release assets or the firmware picker.

"},{"location":"test-results/nimble_companion_trial_results/#capacity","title":"Capacity","text":"Measurement Heltec V4 XIAO S3 WIO Contacts / channels 350 / 40 350 / 40 PSRAM-backed offline queue 512 256 Application image, bytes 1,774,600 1,510,536 OTA application slot, bytes 6,553,600 3,342,336 Linked internal capacity before runtime allocation, bytes 263,944 265,576 Required runtime RAM budget, bytes 173,056 148,480 Headroom beyond that budget, bytes 90,888 117,096 Live free heap with Bluetooth and 350 test contacts, bytes 142,644 147,444 Minimum free heap during that contact test, bytes 138,024 142,788

The live contact-test readings are from revision 7f1bd8b1. Revision 9dbab431 retains the same Bluetooth adapter, contact allocation and queue sizes, and fixes the MQTT save stack use described below. Linked capacity is not live free heap; the firmware memory reports label that distinction.

A matching V4 build using Bluedroid had 248,840 bytes of linked internal capacity. NimBLE recovers 15,104 bytes at link time. The matching Bluedroid application was 2,169,848 bytes. There was no matching physical Bluedroid heap measurement, so these figures do not establish a live heap saving.

"},{"location":"test-results/nimble_companion_trial_results/#bluetooth-hardware-checks","title":"Bluetooth hardware checks","text":"

Both boards passed the following cases with BlueZ on the Mercerwood Pi:

The complete MAC suite ran on 7f1bd8b1. Final-image connection and update checks also verify the unchanged adapter after the MQTT stack fix. Synthetic contacts were removed after testing; factory MAC mode and stealth off were restored.

"},{"location":"test-results/nimble_companion_trial_results/#mqtt-save-stack-overflow","title":"MQTT save stack overflow","text":"

The V4 test reproduced an actual reset while saving MQTT settings through WebConfig. Serial output identified Stack canary watchpoint triggered (loopTask). onConfigBatchEnd() kept a second 2.8 KB preference copy while the loader and NVS code also used stack space. Free heap was not the limiting resource.

The loader already updates its destination only after validation, so the callback now passes the live preference object directly. The actual Xtensa callback frame shrank from 2,928 bytes to 32 bytes. The regression test compiles the real callback with a 512-byte frame ceiling and demonstrates that the previous nested-copy implementation fails the check.

On the corrected 9dbab431 image, the 11-setting WebConfig batch completed with all entries accepted and no reboot. The V4 connected to a local Mosquitto broker, published MQTT traffic, and kept its authenticated Bluetooth session alive through a 30-second load interval with continuous uptime and zero reported error flags. Free heap measured 120,684 then 122,140 bytes; the minimum was 118,332 bytes and the largest free block was 110,580 bytes. Disabling the test MQTT slot also completed successfully without reboot.

"},{"location":"test-results/nimble_companion_trial_results/#wifi-updates-and-qr","title":"WiFi updates and QR","text":"

Both boards completed real application-image uploads over WiFi with Bluetooth connected, followed by reboot and reconnection using the saved bond. Repeated HTTP page requests and Bluetooth status requests ran together before upload. The image hash was checked before transmission, and the running firmware version was checked after reboot.

The V4 WiFi setup page again contains a QR code. The default open setup SSID fits a 44 x 44 pixel code: 2 x 2 pixel modules and an exact one-pixel white border. The native test rendered the actual display/QRCode code and decoded the result with ZXing. SSID, portal address and the hold-to-stop hint fit beside it. Physical phone-camera scanning has not been verified.

"},{"location":"test-results/nimble_companion_trial_results/#observed-limits","title":"Observed limits","text":"

One XIAO warm reboot left its USB endpoint unresponsive while Bluetooth still answered. The user's physical unplug/replug restored USB; the later full MAC suite completed its commanded reboots successfully. The cause of that one USB failure is unconfirmed.

The XIAO initially failed to associate with the saved WiFi network. Reapplying the SSID scheduled a fresh connection, after which it joined at -53 dBm and completed WiFi OTA. No antenna change was needed for that recovery.

This was functional hardware testing, not a long-duration soak. iOS/Android app pairing, phone camera scanning, multiple TLS MQTT brokers, and an end-to-end LoRa mOTA transfer were not qualified in this trial. The XIAO parent profile does not include direct MQTT; the V4 does.

"},{"location":"test-results/nimble_companion_trial_results/#final-application-hashes","title":"Final application hashes","text":"Hardware SHA-256 V4 e6a1522ced4ed38ddd00e7cdf8393857224adbb622888a86370c0f7a3ee28d88 XIAO S3 WIO ecf7afe6222e3897556a04e022515f9dcfcb825172dae9eaaa72eb92396dd004"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/","title":"Nrf52 bluetooth dfu handoff","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#nrf52-bluetooth-dfu-handoff","title":"nRF52 Bluetooth DFU handoff","text":""},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#confirmed-application-defect","title":"Confirmed application defect","text":"

The released RAK4631 application RAK_4631_repeater-v1.17.1.7-halo-keymind-cascade-dev-2d03e098 contains an unsafe transition from the running application into the Bluetooth DFU bootloader. This transition belongs to the application's Bluefruit BLEDfu service. Replacing the bootloader does not replace that application code.

The original callback changes CONTROL to select the main stack pointer (MSP) while its compiler-generated C frame still belongs to the FreeRTOS process stack pointer (PSP). The compiled callback then restores its C frame using MSP. With the observed stack state, that restore reads outside nRF52840 SRAM before reaching the bootloader entry point. A compiler/emulator regression reproduces the fault.

This defect explains why a Bluetooth flasher can reach \"enabling bootloader\" and then lose the device before the application transfer starts. It is consistent with the reported solid blue LED and later application restart. The exact reset cause and timing in the user's report remain unrecorded. The standard application enables a 60-second watchdog; Android can also report a separate 30-second GATT connection timeout. See the physical comparison below rather than attributing every timeout to the watchdog.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#application-fix","title":"Application fix","text":"

scripts/nrf52_ble_dfu_fix.py patches a private build copy of the pinned framework's BLEDfu.cpp. It preserves the legacy B1 DFU entry marker, retained peer/bond record, and SoftDevice vector-table handoff. It leaves PlatformIO's shared framework cache unchanged.

The callback now enters the naked, nonreturning assembly helper mesh_nrf52_dfu_jump. The helper loads the bootloader vectors, sets its MSP, selects MSP, clears inherited interrupt masks, and branches directly to the bootloader. No compiler-generated C stack access follows the stack switch. The supported handler-mode path constructs an explicit exception-return frame. The callback also clears SysTick, pending FreeRTOS system exceptions, and both external interrupt banks before the handoff.

Every environment inheriting nrf52_base uses the private patch and the compiled-image check in scripts/check_nrf52_ble_dfu_handoff.py. The check requires the qualified helper instructions, rejects a CONTROL write in the C callback, and verifies that the callback branches to the helper. An unrecognized framework source or an unsafe compiled handoff fails the build check.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#verification-completed","title":"Verification completed","text":"

The CI regression requires both the ARM compiler and Cortex-M emulator; missing dependencies must not silently turn qualification into skipped tests.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#installing-the-fix-on-an-affected-application","title":"Installing the fix on an affected application","text":"

An already installed affected application may fail before it can enter Bluetooth DFU. Install an application containing this fix once through USB UF2, or force the board into hardware DFU and use the matching application package. Use the board's supported recovery entry procedure. Installing a new bootloader does not replace the unsafe callback in the application. The in-place correction to OTAFIX recovery 2.4.11 adds a narrow fault fallback for this legacy callback; the originally published 2.4.11 lacks it. When the forwarded HardFault occurs with the B1 Bluetooth DFU request marker, it sets A8 and resets directly into BLE DFU without touching the broken stack. Other faults keep their existing halt behavior. The unchanged 1.17.1.7 app was physically observed reaching DFU through this fallback. This supplies an alternative to USB application installation when the corrected bootloader has already been installed; complete application transfer is recorded below.

The recovery bootloader is a temporary bridge for bootloader migration. Complete migration to the normal physical-board bootloader according to the recovery release instructions. An R_ bootloader version is not itself evidence that Bluetooth DFU is disabled.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#separate-setup-page-and-settings-report","title":"Separate setup-page and settings report","text":"

The reporter's first application install produced setup-page errors, while the same application worked after an erase UF2 and reinstall. Application UF2 installs normally preserve the application's filesystem. The standard MeshCore erase firmware formats both InternalFS and ExtraFS, which changes stored identity and preferences.

That result is consistent with a retained-state problem, but it does not identify which setting, file, or client state failed. The original state was lost after the erase, and the exact setup-page error was not recorded. The Bluetooth handoff regression does not prove the cause of that earlier setup failure.

The old app also appends ,preamble=32 (auto) to get radio on this test device. Current firmware restores the four-field radio reply expected by the official setup app (commit ca23d38e). Its compiled CLI settings regression checks this compatibility contract. This is another concrete older-application compatibility issue, although the reporter's original setup-page error was not captured and cannot be attributed conclusively.

Current firmware already includes truncated preference checks, bounded strings, non-finite radio-value sanitization, durable identity retries, and filesystem startup recovery. The 7 passing settings tests qualify those existing protections. This Bluetooth fix adds no automatic wipe and does not require an erase as its installation step.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#hardware-testing","title":"Hardware testing","text":""},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-recovery-baseline","title":"Published recovery baseline","text":"

On 2026-10-01, RAK4631 USB serial 9AB3B64C641BA927 ran the exact reported v1.17.1.7-halo-keymind-cascade-dev-2d03e098 application and published recovery R_0x02040BFF (2.4.11). Nordic nRF Connect 4.24.3 on a second LG phone running Android 5.1.1 attempted the fixed application ZIP. It failed before payload transfer: the graph stayed at zero, the initial opcode disconnected, repeated GATT 133 reconnect errors followed, and the device later answered ver with the unchanged application. No RAK DFU advertisement was seen in a later scan. USB data remained connected for this baseline.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#historical-lab-only-nordic-fallback-with-the-unchanged-old-application-pass","title":"Historical lab-only Nordic fallback with the unchanged old application: PASS","text":"

The lab-only recovery qualification image uses packed test version 0x02040CFF, production-length text R_v0.11.0-OTAFIX2.4.12, and bootloader CRC 5B8F3E90. It is a development qualification image, not the tagged production artifact. This correction creates no 2.4.12 release. This physical result keeps its tested version and does not qualify the replacement 2.4.11 release assets. The combined serial DFU ZIP installed it, then USB UF2 restored the unchanged reported 1.17.1.7 application. Both identities were checked before retrying.

At 16:57:51 local time Nordic requested the same application ZIP. The bootloader advertised 4631_DFU at the original C3:C6:A8:FC:AD:A6 address about 11.7 seconds later. Nordic reconnected automatically, transferred the 531,708-byte application image from the 532,510-byte ZIP at about 2.2 kB/s, and activated the application at 17:02:19. Afterward USB CLI reported v1.17.1.8-ble-dfu-test (Build: 01-Oct-2026) and OTAFIX2.4.12. USB data was connected throughout this test.

No erase firmware was used. Seven queried values retained identical digests: name, TX power, latitude, longitude, frequency, airtime factor, and public identity. The radio tuple also stayed 869.6179809,62.5,8,5; its old digest exactly matches the new tuple plus the old preamble suffix. The differing raw radio reply is the intended setup-client compatibility fix, not lost settings. These are checks of the queried fields, not a dump of the entire filesystem.

The older application lacks a hash-bound EndF trailer. The installed recovery bootloader safely refused its USB bootloader updater while that app was valid, because it could not prove the staging scratch range was unused. This guard is unchanged. The combined serial/Bluetooth DFU ZIP supplies the supported installation path; permissive image identity policy does not disable flash bounds or live-application staging safety.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#corrected-2411-through-meshcore-open-with-the-unchanged-old-application-pass","title":"Corrected 2.4.11 through MeshCore Open with the unchanged old application: PASS","text":"

The second LG phone's Android legacy Open build 9.5.5/code 26 reached the Bluetooth updater, but Enable radio updater rejected a selected route's hash width before sending its command. Open commit 03b0d707 preserves each supported route's own width and rejects malformed routes before sending. Its 81 focused tests passed, including 13 new tests through the real route preparation and command path.

The corrected API 21/ARMv7 Open build 9.5.5/code 27 was installed with the phone's pairing data retained. Its SHA-256 is 5a3a2f83299f1d4e27f242680335494d0ac49db5f8fa3e2ee8b71902f1924d33. The second phone disappeared from the VM's USB inventory during USB re-enumeration before a completed Open transfer could be recorded. The automatic USB configuration watcher was stopped. Testing continued on the first LG phone, also running Android 5.1.1, using that same Open build.

The RAK4631 ran the corrected 2.4.11 qualification image with packed version 0x02040BFF and bootloader manifest CRC C2A4B64B. The exact published old 1.17.1.7 application was restored before this test. USB data was disabled at the target; USB power remained connected. Forced flood authentication through the companion and the temporary radio tuple 910.525,62.5,7,5 enabled the repeater's updater, whose reply identified the correct target Bluetooth address.

On 2026-10-02, Open's native Nordic DFU SDK 2.11.0 initially reported GATT 133 and disconnect status 8 while writing opcode 1. Its automatic retry reached bootloader revision 0800, negotiated MTU 247, and transferred all 531,708 bytes in 68,651 ms. Validation returned status 1 and Open sent Activate/Reset, logged at 03:27:16 America/Los_Angeles. Final CLI queries confirmed v1.17.1.8-ble-dfu-test (Build: 01-Oct-2026), bootloader OTAFIX 2.4.11, the restored saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. The raw radio reply differs only by the removal of the old preamble suffix. USB data was restored for verification. No erase firmware was used. This qualifies the tested application/bootloader/phone combination, not every board or a battery-only install. These measurements used a qualification image; the production archive inventory identifies release assets by their source tag, source commit, manifest CRC, and checksums.

The repair uses the existing non-latest prerelease R_0.11.0-OTAFIX2.4.11, preserving that original distribution tag and its 110 asset names. Corrected production binaries use the source-only tag R_v0.11.0-OTAFIX2.4.11. The normal latest release remains 2.4.10.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#corrected-2411-through-nordic-with-the-unchanged-old-application-pass","title":"Corrected 2.4.11 through Nordic with the unchanged old application: PASS","text":"

On 2026-10-02, the first LG Android 5.1.1 phone ran Nordic nRF Connect 4.24.3/code 114 against the same corrected 2.4.11 qualification image, packed version 0x02040BFF and manifest CRC C2A4B64B. The exact published old 1.17.1.7 application was restored before the test. Local CLI start ota returned the correct target Bluetooth address. USB data was disabled during the application transfer while USB power remained connected.

Nordic displayed upload progress at 2%, 20%, 44%, 67%, and 88%, at about 1.2 kB/s, then disconnected and the application restarted. This phone provided no Nordic INFO log; the recorded evidence is UI progress plus final CLI verification, not an exact transfer duration or a captured 100% frame.

Final CLI queries confirmed v1.17.1.8-ble-dfu-test, bootloader OTAFIX 2.4.11, all seven unchanged settings digests, and the saved radio tuple 869.6179809,62.5,8,5. The raw radio reply differs only by the removal of the old preamble suffix. No erase firmware was used. This qualifies the tested application/bootloader/phone combination, not every board or battery-only operation. The earlier lab-only 2.4.12 Nordic pass remains separate. Production release assets must pass the clean-tag build and package validation before upload; these physical tests do not bypass those gates.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#earlier-restored-node-handoff-and-exit-check","title":"Earlier restored-node handoff and exit check","text":"

The RAK4631 was returned to the published normal 2.4.10 auto bootloader and the fixed v1.17.1.8-ble-dfu-test application. The ordinary application has no EndF marker, so its bootloader UF2 replacement was safely refused; the combined normal DFU ZIP and application UF2 supplied the working path. No erase firmware was used.

A supplemental Linux Bluetooth check reached the application's DFU service revision 0100, requested handoff, and reached 4631_DFU revision 0800 at the same Bluetooth address. It then requested a return to the application. After subscribing to control notifications on the bootloader connection, the exit request returned the node to the application. Final CLI queries confirmed the normal 2.4.10 bootloader, fixed application, saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. USB data was restored. This checks the restored node's Bluetooth entry and exit; it is not an Open transfer.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-recovery-correction-verification-pass","title":"Published recovery correction verification: PASS","text":"

On 2026-10-02, bootloader recovery workflow run 36997468388 completed all 37 jobs: preparation, 35 board builds, and release packaging/publication. The corrected production source is tag R_v0.11.0-OTAFIX2.4.11, commit de88c5fcae72bb00992ac9db7fd144fec150835e.

The existing recovery 2.4.11 prerelease retains all 110 filenames and its original distribution tag object b3784854e72b1d3d68e6bef04c1890f251256f59, which still points to d6b3edf7d8b58c15187ccae54ea212cfa806d01c. The release remains non-latest; normal latest remains OTAFIX 2.4.10. All downloaded digests, archive checksums, provenance, 35 profile manifest CRCs, and HEX/DFU ZIP/UF2 reconstructions passed. Six included profiles remain explicitly pending hardware qualification, and thinknode_m8 remains excluded. The published recovery bundle SHA-256 is 803dffcfed20331205d1fbe581593865785a4e53988efd20d777e5fe19b05b3d.

The physical Open and Nordic tests used RAK4631 board qualification CRC C2A4B64B, raw SHA-256 239c23fabcee8478475b3bc3e1c2fc94daa79f386c5fd7ccf980c689220eda7a. The published RAK4631 board production image has CRC AA30573D, raw SHA-256 13188cc150682a23bdee1ed7d2340a4ff0e6701b6d18dc347b75803c9ae53ae2. Both have packed version 0x02040BFF; their complete raw binaries differ.

The production/qualification comparison found exactly eight differing data bytes out of 40,960: four in the CURRENT.UF2 FAT create/update time fields and four in the resulting whole-image manifest CRC. All remaining 40,952 bytes match, including executable instructions, vectors, CF2, version, and policy guards. The production timestamp matches the clean source commit, and the production DFU ZIP, published HEX, and workflow artifact agree. This verifies the published implementation against the tested one while keeping the physical test tied to its actual qualification artifact.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#final-test-node-restoration","title":"Final test-node restoration","text":"

After both corrected 2.4.11 phone tests and publication verification, the RAK4631 was restored to the published normal 2.4.10 bootloader and the fixed v1.17.1.8-ble-dfu-test application. Final CLI verification confirmed those versions, the saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. USB data was re-enabled. No erase firmware was used.

"},{"location":"test-results/nrf52-bluetooth-dfu-handoff/#published-production-bootloader-and-following-application-ble-test-pass","title":"Published production bootloader and following application BLE test: PASS","text":"

On 2026-10-02 an attached LG LGL51AL/Android 5.1.1 phone running nRF Connect 4.24.3 installed the published repaired RAK4631 board recovery 2.4.11 ZIP over Bluetooth. This uses the production package rather than the earlier qualification image. The package SHA-256 is 3ec67bba63dff7ed44dc1680d0f65b0b371ffd9505f5ed6540ec7de8e4999771.

The phone recorded validation success and Activate and Reset. Read-only UF2 information confirmed recovery 2.4.11. With host USB configuration blocked before the replacement bootloader appeared and VBUS retained, recovery subsequently advertised 4631_DFU at C3:C6:A8:FC:AD:A7. The phone found it in a fresh scan and read firmware revision R_0x02040BFF. The previous application connection used A6; the cached tab's reconnect to A6 logged GATT 133 after successful activation, while connecting to the new A7 DFU advertisement worked.

The same phone then installed the 531,708-byte fixed MeshCore application over Bluetooth. Validation succeeded and Activate and Reset was logged at 13:51:27 America/Los_Angeles. CLI verification confirmed v1.17.1.8-ble-dfu-test, bootloader OTAFIX 2.4.11, the saved radio tuple 869.6179809,62.5,8,5, and all seven unchanged settings digests. No erase firmware was used. The normal 2.4.10 bootloader and the original fixed application were restored afterward; final identities and settings matched the initial baseline.

The full hardware report records the host USB configuration guard, its failed trailing audit after a separate hub reset, restored hub access and policies, package identities, and evidence. This is one RAK4631/profile/phone test, not an all-board, physical charge-only-cable, battery-only, or LoRa transfer qualification.

"},{"location":"test-results/nrf52-usb-ready-fix/","title":"Nrf52 usb ready fix","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52-usb-ready-fix/#nrf52-usb-ready-hang","title":"nRF52 USB READY hang","text":"

The RAK3401 on Mercer was found stuck in the framework's TinyUSB power handler: USBD was enabled and attached, but its write-one-to-clear READY event was already consumed. The duplicate-event guard also required HFCLK to be running. If that guard missed the completed initialization, the following unbounded READY wait could stop application startup. A bootloader update alone cannot replace this USB driver in an already installed MeshCore application.

The wait and guard were inherited through the pinned Adafruit nRF52 Arduino framework, not introduced by the recent OTAFIX upstream merge. TinyUSB's wait dates back at least to 2019; the guard was added in upstream commit 7d9efd0697. OTAFIX's August 24 HFCLK backport addressed a later clock wait, leaving this earlier READY wait uncovered. The exact live interrupt ordering is not known; the stalled program counter and consumed event were read directly over SWD.

All nRF52 environments inherit pre:scripts/nrf52_usb_power_fix.py. It compiles a build-local replacement for the matching framework driver, without changing PlatformIO's shared SDK or affecting ESP32 builds. A changed/unrecognized framework implementation stops the build for review. The backport:

The poll budget is an iteration limit, not a promised elapsed-time timeout. If the clock/peripheral never becomes ready, the caller returns instead of freezing the application. A later power event can retry; permanent electrical failure can still require reconnecting USB. This does not establish or fix the separate Pi hub/controller fault.

Run python3 -B test/test_nrf52_usb_power.py -v. The tests compile the actual patched handler from the pinned-framework fixture, model W1C event semantics, and cover duplicate/nested callbacks, delayed/missing clocks and READY, removal, retry, and detached USB. Restoring the inherited READY prefix must reproduce the infinite wait under a subprocess deadline. Additional tests check SDK isolation, idempotence, fail-closed patching, the startup DETECTED/READY order, and all nRF52 environment hooks. The harness is shared with OTAFIX's TinyUSB fork at test/otafix/nrf5x_power_test.py; keep the two copies in sync when extending it.

"},{"location":"test-results/nrf52_companion_storage_recovery/","title":"Nrf52 companion storage recovery","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/nrf52_companion_storage_recovery/#nrf52-companion-automatic-extrafs-recovery","title":"nRF52 Companion automatic ExtraFS recovery","text":"

Companion builds using internal ExtraFS reserve 100 KiB at 0xD4000..0xED000. Identity and preferences remain in the separate 28 KiB primary store at 0xED000..0xF4000. This applies to internal-ExtraFS Companion builds, including Full, not repeaters or external QSPI storage.

At startup the firmware:

  1. Mounts primary storage without erasing nonblank media. Only proven-blank primary storage may be initialized automatically.
  2. Checks the exact ExtraFS address, size, block geometry, and application linker reservation, then attempts a non-formatting secondary mount.
  3. Validates primary filesystem metadata before any destructive secondary recovery. An unavailable primary store blocks recovery and identity replacement.
  4. Keeps an already mounted, metadata-valid ExtraFS unchanged. If its initial mount or validation failed, unmounts and retries once without formatting.
  5. If the retry remains unusable, automatically formats only the reserved 100 KiB region, then remounts and validates it. There is one format attempt per boot; failure keeps the contact/channel write quarantine active.
  6. Runs the existing verified migration from primary into ExtraFS. Primary contact/channel sources are retired only after every destination is copied, read back, and the migration transaction is committed. Identity and preferences stay on primary.

No terminal repair command, phone intervention, or bootloader update is needed for this recovery path when a compatible application/layout is already used. After successful recovery, the Companion storage API reports 100 KiB total rather than the 28 KiB primary fallback.

"},{"location":"test-results/nrf52_companion_storage_recovery/#data-loss-boundary","title":"Data-loss boundary","text":"

Automatic rebuilding prioritizes a usable contact/channel store over retaining an unreadable secondary filesystem. Data stored only in that damaged region can be lost. Surviving primary files are migrated, but may not contain the latest secondary-only changes. An API backup remains advisable before flashing. Primary storage is never formatted by this recovery path.

This is mount/metadata recovery, not forensic file recovery. An invalid migration journal, unreadable individual contact page, primary-source error, full filesystem, or failed migration cleanup does not on its own trigger a format of a mountable, metadata-valid secondary store. Those cases retain the existing validation, retry, and incomplete-load protections.

"},{"location":"test-results/nrf52_companion_storage_recovery/#regression-coverage","title":"Regression coverage","text":"

test/test_internal_secondary_fs_repair exercises healthy/no-format startup, successful remount, failed remount/traversal followed by one repair, and format/remount/validation failure. test/test_nrf52_extrafs_contract.py checks primary-first ordering, internal-only geometry guards, quarantine handling, and the separation between migration failures and destructive recovery.

"},{"location":"test-results/nrf52_companion_storage_recovery/#t1000-e-hardware-qualification-2026-09-03","title":"T1000-E hardware qualification (2026-09-03)","text":"

A device reporting 25/28 KiB primary fallback and contact enumeration error 5 was flashed with the recovery build. With no explicit repair command, startup activated 100 KiB ExtraFS and loaded all 125 contacts and 40 channel slots. Identity, preferences, radio settings, and channels matched the pre-update API snapshot. Add, edit, and delete of an owned temporary contact each survived a separate normal reboot; the device ended with the original 125 contacts and 27/100 KiB usage. The 161 offline/native regression tests and target build passed.

This was not lossless recovery of every historical RAM value: 21 contacts had older advertisement/local-modification timestamps, including one older advertised location. No contact was missing, and no computer-backup restoration was used. A subsequent read-only scan found no readable cached advert for any of the 125 contacts. Without a surviving newer copy, firmware cannot infer those earlier values; receiving new verified advertisements can refresh them normally.

"},{"location":"test-results/pair_4p6_300us_validation/","title":"Pair 4p6 300us validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pair_4p6_300us_validation/#sf7625-sf8500-46-slow-chirps-03-ms-reserve","title":"SF7/62.5 + SF8/500: 4.6 slow chirps, 0.3 ms reserve","text":"

User-requested follow-up to the original pair test. Completed 2026-09-15 00:21:27\u201300:23:52 UTC (September 14 local time).

"},{"location":"test-results/pair_4p6_300us_validation/#result","title":"Result","text":"Profile Requested dwell Stationary control Scanning received Slow: SF7 / 62.5 kHz 9.421 ms, approximately 4.6 chirps 100/100 97/100 Fast: SF8 / 500 kHz 21.847 ms, approximately 42.67 chirps 100/100 100/100

Full scan: 197/200 (98.5%), not lossless. All three misses were slow-profile timeouts with no device error and the chip still in RX. The collector completed every scheduled probe, with no RF retry, retune/reset on a miss, or early stop. The two controls plus the scan contain 400 unique probes; each fixed transmitter acknowledged exactly 200 packets. There were no USB result replays.

This is one finite sample. It does not establish that 4.6 is inherently worse than 4.1 or better than 5.1, nor identify the cause of the three losses.

"},{"location":"test-results/pair_4p6_300us_validation/#timing-and-unchanged-conditions","title":"Timing and unchanged conditions","text":"

Same V4 RX, RAK4631 slow TX and T1000-E/LR1110 fast TX; 909.5/910.5 MHz, CR4/5, explicit header, CRC, 32-symbol preambles, 16-byte synthetic payloads, -9 dBm chip TX power, normal RX gain 0x94, randomized interleaving and 10\u2013180 ms arrival delays. Only one transmitter sends at a time. Each TX configures once; every packet has zero frequency/modulation writes.

The single-pass production guarded switching path uses warm XOSC and buffered 8 MHz SPI. Each hop issued exactly one frequency and one modulation command, with one optimized RX restart. No double writes, detours, rollback, added settling, off-channel acceptance, cache errors, RX errors or retune failures. As before, this is a small HIL application, not full production UI/network load.

"},{"location":"test-results/pair_4p6_300us_validation/#verification-and-artifacts","title":"Verification and artifacts","text":"

Only the V4 application was updated. Exact USB identity/MAC, idle ownership and existing partition metadata were checked before writing. The nRF52 transmitter images, bootloaders, SoftDevices, gateway and unrelated radios were not flashed. All three test radios were rebooted and verified idle afterward; HIL firmware was left installed. ModemManager was restored active, gateway and host CLI services are active, and the previously paused memory soak remains inactive.

No production timing defaults changed, and no commit or push was made.

"},{"location":"test-results/pair_preamble32_validation/","title":"Pair preamble32 validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pair_preamble32_validation/#sf7625-sf8500-fast-switching-and-preamble-32","title":"SF7/62.5 + SF8/500, fast switching and preamble 32","text":"

User-requested hardware validation, 2026-09-14. This is a finite HIL experiment, not a production reliability guarantee. No production timing defaults or automatic-preamble overrides are changed by this test.

"},{"location":"test-results/pair_preamble32_validation/#fixture-and-method","title":"Fixture and method","text":" Case Loop reserve (not a delay) Slow visit Fast visit 4.1-chirp floor 0 ms 8.397 ms 23.171 ms Spare time used for 5.1 slow chirps 0 ms 10.445 ms 21.123 ms Previous reserve comparison 4 ms 8.397 ms 19.171 ms

The 5.1 case reallocates 2.048 ms from the fast visit to the slow visit; it does not increase the nominal cycle. The 4 ms reserve is omitted from the first two cases. The theoretical switch allowance is 0.6 ms per hop; actual switching is measured, never forced to equal that allowance. Changed SF/BW requires one modulation command per hop; frequency-only cache-hit timings do not apply.

The scheduler uses the same asymmetric two-profile visit budget and the actual production guarded tuneProfile() / fast RX implementation, but runs in a small RAM-only HIL application, without the full production networking/UI load. All retunes are single-pass, warm-XOSC, buffered 8 MHz SPI, zero added settling; double frequency writes, detours and rollback experiments are off. Packet/busy ownership guards are retained. Explicit 32 on both sides bypasses the historical automatic SF8/500 88-symbol rule without removing it from production.

The selected radios already had disposable HIL firmware. Updates are application-only: matching nRF52 SoftDevice IDs/address ranges and exact USB serial/topology are checked. V4 partition metadata is read back and compared before its application-only write. No bootloader, SoftDevice, settings backup, gateway firmware or unrelated radio is changed. Prior images/captures remain.

"},{"location":"test-results/pair_preamble32_validation/#results","title":"Results","text":"

Finished all 800 unique probes, 2026-09-15 00:04:54\u201300:11:19 UTC (September 14 local time). Both stationary controls passed 100/100. All three scans completed their full 100-packet sample per profile. No scan achieved 200/200.

Case SF7/62.5 received SF8/500 received Total Switch mean / max Idle cycle mean / max 4.1 chirps, no loop reserve 99/100 100/100 199/200 0.581 / 0.829 ms 32.717 / 34.733 ms 5.1 chirps, no loop reserve 100/100 97/100 197/200 0.572 / 0.834 ms 32.706 / 34.978 ms 4.1 chirps, 4 ms loop reserve 97/100 99/100 196/200 0.567 / 0.836 ms 28.701 / 31.195 ms

Switch timing includes every successful hop during each stage, including post-reception hops. The RX-only five-second windows averaged 0.554 ms per hop. The largest observed switch was 0.836 ms: 0.6 ms is a nominal allowance, not a measured worst-case bound. Direction-specific timing is in the validated capture. Idle-cycle samples exclude packet-held visits but still include USB command/loop scheduling; their occasional roughly 2 ms overshoots are real.

Every scan hop issued exactly one RF-frequency and one modulation command, with one optimized RX restart. There were zero added settling delays, double passes, frequency repeats, RF retries, USB result replays, device errors, retune failures, RX mode errors, cache errors or off-channel acceptances. All eight missed packets were sequence-matched RX timeouts with the receiver still in RX mode and no device error. Both transmitters acknowledged exactly 400 transmissions and zero frequency/modulation writes during every packet.

Successful scan packets had mean SNR approximately +12.0 to +12.1 dB on the slow profile and +12.8 dB on the fast profile. Mean RSSI was approximately -20.4 to -20.7 dBm and -18.5 to -18.9 dBm, respectively. These are strong-signal bench results; the controls passed, but weak-signal performance and possible strong-signal/front-end effects are not established by this test.

"},{"location":"test-results/pair_preamble32_validation/#what-the-extra-chirp-changes","title":"What the extra chirp changes","text":"

An extra SF7/62.5 symbol is 2.048 ms, equivalent to four SF8/500 symbols. Increasing the slow dwell from 4.1 to 5.1 preserves the full-sweep budget by shortening the fast dwell, but increases the fast profile's uninterrupted blind interval. With the nominal 0.6 ms hops, the 32-symbol fast preamble's remaining time after the slow visit plus two hops falls from 6.787 to 4.739 ms (13.26 to 9.26 fast symbols), before acquisition and scheduling overruns. This is a timing-budget calculation, not an experimentally proven acquisition threshold or a per-miss trace.

The observed 5.1 sample improved the slow result from 99 to 100, while the fast result fell from 100 to 97. This is consistent with a dwell/acquisition tradeoff; one finite sample per setting does not isolate the cause or establish a statistically reliable ranking. Simply fitting a sweep inside a preamble does not establish lossless acquisition. The 4 ms reserve is an allowance, not a sleep; retaining it also did not produce a lossless result here.

Using genuine spare time above the 4.1 floor remains a reasonable policy to investigate, but allocation must protect both profiles' acquisition/return budgets and measured switching/scheduling overruns. Do not automatically spend all of the reserve on longer slow visits, or treat the historic 88-symbol rule as disproven by this nearly-lossless sample. No production defaults or warning formula were changed by this hardware-test turn.

"},{"location":"test-results/pair_preamble32_validation/#capture-integrity-deployment-and-cleanup","title":"Capture integrity, deployment and cleanup","text":""},{"location":"test-results/pair_preamble32_validation/#follow-up","title":"Follow-up","text":"

The requested 4.6-chirp / 0.3 ms reserve follow-up completed 197/200 scanning packets: slow 97/100, fast 100/100, after fresh 100/100 controls on both profiles. Its captures are separate from this run.

"},{"location":"test-results/pr7_review_validation/","title":"Pr7 review validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/pr7_review_validation/#pr-7-review-and-validation","title":"PR #7 review and validation","text":"

Validated on 2026-09-10 against integration commit fa010196b130dee9a8a6fc6325d794e2f4fb9ede, which combines base 01cc7b8a with PR head c002966c and the review fixes. This includes the stacked ESP32 memory changes already present in PR #7.

"},{"location":"test-results/pr7_review_validation/#review-fixes","title":"Review fixes","text":""},{"location":"test-results/pr7_review_validation/#automated-validation","title":"Automated validation","text":""},{"location":"test-results/pr7_review_validation/#indicator-hardware-validation","title":"Indicator hardware validation","text":"

The original SenseCAP Indicator LoRa with 8 MiB flash and 8 MiB octal PSRAM was reached through the MercerWoodMesh Pi over Wi-Fi. Its MAC, public identity, board reply, and existing partition layout were checked before updating. USB interfaces were absent, so this run does not qualify USB behavior.

The Full profile was applied to SenseCapIndicator-LoRa_comp_radio_usb_wifi. The exact application is 2,149,208 bytes:

This direct profile build uses the repository's default v1.17.1 version text and 14 Aug 2026 build-date text; the hashes above identify the tested image.

The Wi-Fi updater returned HTTP 200 and booted the application in app1 at 0x400000. The previous application remains in app0 at 0x10000; the partition table, NVS, and SPIFFS were not rewritten. Public identity, radio parameters, and USB logging preference matched their pre-update values.

The existing Companion stress harness, using a TCP socket adapter, completed 100 persistent cycles and 100 reconnects in 109.9 seconds: 501 requests and 501 validated responses, with no discarded bytes, malformed frames, or device error flags. Device info, time, storage, and core statistics were exercised. The text terminal exposed all four display-power controls and display.inbox. The OTA folder service issued a valid COUNT request, received an empty-folder reply, and returned to its disconnected state after the client closed.

After these checks, four memory snapshots showed 69,548\u201369,996 bytes of free internal heap and a 61,428-byte largest block. PSRAM availability stayed at 6,408,731 bytes. Final uptime was 262 seconds, with zero error flags and an empty outbound queue. This is a short regression check, not a long-term soak. Raw baseline, upload, stress, and final-state records are retained on the Pi under /home/mikec/hwtest/runs/pr7-review/.

No Linkflow module or RF power meter was available in this run. Linkflow power calibration is supported by the pinned upstream table and executable driver tests, not a physical RF output measurement. The Indicator's ESP32-S3 does not exercise the classic ESP32 heap-OTA default; host tests and classic ESP32 builds cover that path. Physical display readability was not inspected.

"},{"location":"test-results/preamble_detection_investigation/","title":"Preamble detection investigation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preamble_detection_investigation/#off-channel-preamble-investigation","title":"Off-channel preamble investigation","text":"

Measured 2026-09-14. The SF8 off-channel preamble indication reproduces with a stationary, fully initialized receiver, without profile hopping or fast RX reuse. Both boards reproduce it when their transmitter/receiver roles are reversed. This is evidence of transmitter-associated off-channel detection in this close-range bench setup, not evidence that the channel jump needs more settling time.

The earlier SF8 scan's long software guard explains its missed listening opportunity. The earlier SF6 scan miss remains unresolved: its trace had no preamble indication or guard hold. No production guard or acquisition policy was changed during this investigation.

"},{"location":"test-results/preamble_detection_investigation/#controlled-experiment","title":"Controlled experiment","text":"

The three completed matrices contain 128 windows: 112 transmitted probes and 16 silent controls. A separate initial 450 ms receiver-only smoke check was also performed. These are bounded diagnostic controls, not another 100-packets-per-channel capacity sweep.

"},{"location":"test-results/preamble_detection_investigation/#results","title":"Results","text":"

Entries below are windows with a preamble indication / tested windows. Each positive window reported one indication; latched IRQs do not count continuous detector activity or prove a valid packet.

Transmitter offset SF8, XIAO RX SF8, Indicator RX SF6, XIAO RX Silent 0/8 0/4 0/4 On frequency 8/8 4/4 4/4 -250 kHz 8/8 4/4 1/4 +250 kHz 8/8 4/4 0/4 -500 kHz 5/8 3/4 0/4 +500 kHz 1/8 3/4 0/4 -1,000 kHz 5/8 4/4 0/4 +1,000 kHz 5/8 3/4 0/4

All 16 on-frequency probes delivered valid payloads. None of the 96 off-frequency probes delivered a valid payload. No silent control detected a preamble. Completed windows ended in RX mode with zero device errors; there were no foreign valid payloads or trace overflows.

Most off-frequency detections stopped at PreambleDetected (0x0004). Four progressed further but failed CRC (seen_irq=0x0056: preamble, header, RxDone, CRC error): two at +250 kHz with XIAO RX, and one each at +250 and +1,000 kHz with Indicator RX. Therefore HeaderValid alone would not prove that these off-frequency indications are legitimate traffic.

In forward SF8 on-frequency controls, receiver signal strength (RSSI) sampled at the first preamble was -42 to -41 dBm; at +/-250 kHz it was -91 to -88 dBm. These are single-point receiver readings, not calibrated measurements of transmitted leakage or a sensitivity test. Board positions and attenuation were not systematically varied. Reversing roles reproduces the behavior but does not isolate its physical path.

"},{"location":"test-results/preamble_detection_investigation/#what-this-explains","title":"What this explains","text":"

The earlier SF8 four-channel scan missed probe 13 on 910.250 MHz after detecting a bare preamble while tuned to 910.000 MHz. The existing CustomSX1262::isReceiving() guard allowed 91 ms for preamble progression, consistent with the observed 92.257 ms from first preamble observation to the stale-bit clear. That produced approximately 102 ms total occupancy on the wrong channel and a 135.145 ms target-channel revisit gap. The programmed 32-symbol preamble lasted only 65.536 ms.

The delayed retune itself took 550 us. These controls reproduce the suspect off-frequency indication with no retunes at all. Together the evidence supports off-channel acquisition plus a long guard hold as the SF8 failure mechanism, rather than slow SPI or a demonstrated need for a longer post-jump delay. It does not independently prove the RF source of that one historical scan event.

The controlled-transmitter association and quiet silent controls make random ambient RF alone an inadequate explanation of the repeated bench result. Exact RF cause remains unmeasured: receiver selectivity/false acquisition, transmitter spectral leakage, and coupling between the nearby boards are not separated by these tests. No spectrum-analyzer capture or calibrated attenuation sweep was performed. Increasing channel spacing is not established as a fix: SF8 indications also occurred at +/-1 MHz.

The earlier SF6 miss had no observed nonzero IRQ or guard hold. The four successful stationary SF6 positive controls show the fixed-channel link works, but do not establish the acquisition time needed after a retune. These small, differently sized samples are not packet-error rate estimates or proof of a zero-loss channel count.

"},{"location":"test-results/preamble_detection_investigation/#follow-up-candidates-not-implemented-fixes","title":"Follow-up candidates, not implemented fixes","text":"

Semtech documents that false LoRa detections can occur and describes SetLoRaSymbNumTimeout as validating reception over multiple symbols before continuing when the modem locks. This is a candidate to investigate, not a drop-in verified fix. Its behavior in continuous RX and through each retune must be measured. See section 13.4.9, page 99 of the Semtech SX1261/2 datasheet, revision 2.2.

Useful next experiments would qualify/revalidate preambles before granting a long scan hold, and repeat the controls with lower signal levels or increased physical isolation. Any candidate must preserve legitimate weak signals and long preambles. Blindly shortening the current guard, or requiring only a header indication, is not justified by these results. No production radio change was made in this diagnostic turn.

"},{"location":"test-results/preamble_detection_investigation/#reproduction-provenance-and-checks","title":"Reproduction, provenance, and checks","text":"

See the stationary diagnostic instructions. The same diagnostic firmware binaries were used for all three matrices. Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets built and uploaded successfully. All 29 selected host/native tests passed after testing.

Raw results and SHA-256:

Tested firmware SHA-256:

"},{"location":"test-results/preamble_detection_investigation/#device-handoff","title":"Device handoff","text":"

Fresh full 8 MiB backups were taken and separately verified before flashing, in the private current-user MeshCore backup directory. No backup contents or identity/channel secrets are in the diagnostic results.

Both original images were restored after the controls, separately verified against the complete 8 MiB flash with matching digests, and hardware-reset. The RAK4631 and Indicator RP2040 were untouched; the XIAO soak logger was not restarted. The temporary HIL build overlay was removed. Investigation source, tests, and results were initially handed off locally before publication.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/","title":"Radio2 simultaneous receivers 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#simultaneous-radio2-receiver-comparison-2026-10-22","title":"Simultaneous radio2 receiver comparison, 2026-10-22","text":"

The matched-PHY five-receiver test passed: 915.000 and 916.000 MHz, both SF7 / BW 62.5 kHz / CR4/5 (RadioLib value 5) / preamble 32. Every applicable packet arrived, and all six radios passed final preservation and session checks. The preceding mixed-PHY 915/916 campaign stopped cleanly at a case boundary under user steering; its measurements remain a separate cohort. Historical 910.525 results are not current test frequencies and are not merged into either 915 cohort.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-campaign-three-scanners-and-two-fixed-witnesses","title":"Latest campaign: three scanners and two fixed witnesses","text":"

One Xiao S3 sender sent one fresh 40-packet case, with 20 primary and 20 secondary packets at preamble 32. Five receivers listen to the same physical packets concurrently. This is not separate unicast delivery to each receiver. No earlier packet is resent or replaced by this new measurement.

Role Board Source Applicable packets per case Scanning receiver Heltec V3 9b70827d 20 primary + 20 secondary Scanning receiver Heltec V4.3 OLED 9b70827d 20 primary + 20 secondary Scanning receiver Heltec T096 4773ac42 20 primary + 20 secondary Fixed primary witness Tag 08650f64 20 primary; secondary not applicable Fixed secondary witness SenseCAP Indicator 08650f64 20 secondary; primary not applicable Sender Xiao S3 WIO ebb59d48 One direct TX per planned packet

All six nodes matched their bound identity and full firmware version. The controller rejected a recorded saved baseline other than 915.000 MHz before opening hardware. V3 and V4 used normal firmware. The older T096 lacked the later shared RX fixes. Board hardware and firmware both differ, so this comparison cannot isolate either as a cause.

Primary is 915.000 MHz / BW 62.5 kHz / SF7 / CR5; secondary is 916.000 MHz / BW 62.5 kHz / SF7 / CR5, both at preamble 32. Sender power is 2 dBm. The three scanners enable radio2; each fixed witness disables scanning and stays on its assigned tuple. Receiver power saving and RXPS are off. All five USB receive collectors operate concurrently. The seeded shuffled profile order uses minimum gaps after the prior physical TX_DONE confirmation; retuning can lengthen a gap. There are no RF retries or replacement packets.

Both profiles now have the same 2.048 ms symbol duration. Scanner-derived visit policy and receive ownership still apply; matched SF/BW/preamble does not assert identical measured visits or an acquisition guarantee. This new cohort cannot be treated as another preamble-only row of the mixed-PHY test.

The migration passed on all six radios: saved primary readback was 915.000 MHz / BW 62.5 kHz / SF7 / CR5, and each physical-apply command returned ACK 00. This confirms configuration acceptance, not an RF frequency measurement. The migration explicitly saved preamble 32; earlier effective 32-symbol status did not distinguish saved auto from explicit 32. Channels, credential hashes and all other compared saved settings were retained. No test datagrams were requested by migration.

The new baseline was taken after migration. Cleanup must retain 915.000 and must not restore 910.525 on these radios, while preserving the other compared settings. This frequency change prevents treating earlier 910.525 data and the new 915.000 cases as one preamble-only comparison.

Each 92-byte DEV application payload contains MCHILGR1, the sender public key, a common group digest, a fresh 16-byte nonce and a global ordinal. The digest binds all six role-ordered public keys, witness profile assignments, the campaign nonce and exact preamble/PHY/packet plan. Keys, raw identities and payload bytes are excluded from this report. Each ordinal requires one local acceptance and exactly one physical direct TX, without a flood.

Receivers independently match exact application bytes, their owned local channel slot, DEV type and direct route. Logical profile comes from the sender ledger, not an RX-origin field in the Companion response. Queue drain, duplicates, wrong-profile witness receipts, receiver transmissions and core health are checked separately from application delivery.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-matched-phy-results","title":"Latest matched-PHY results","text":"

Test: COMPLETE, VALID, PASS. The single RF case and all six final preservation/session proofs passed, with no reported errors.

Preamble V3 P / S V4.3 P / S T096 P / S Tag primary / 20 Indicator secondary / 20 Evidence status 32, both SF7/BW62.5 20 / 20 20 / 20 20 / 20 20 20 Complete, valid; finite delivery PASS

Packet labels are one-based within each profile: P01-P20, S01-S20. All applicable missing sets are empty. Each scanner received the same 40 packets and each fixed witness received its assigned 20. The case has no reported evidence or collection errors. All 40 requests were accepted once, with 40 confirmed direct transmissions, zero floods and zero retries. All five collectors completed, with zero duplicates, wrong-profile receipts or receiver transmissions; every before/after core error flag was zero. Every applicable pair has Jaccard 1.000. The fixed witnesses have no common applicable frequency; their opposite-profile entries remain not applicable, not a 40-packet expectation.

Masks use V3, V4.3, T096, Tag, Indicator order. The opposite-frequency witness is NA, never a miss. Complete primary reception is 1|1|1|1|NA; complete secondary reception is 1|1|1|NA|1. An owned wrong-profile witness receipt is a confound. The observed masks are 1|1|1|1|NA for all 20 primary packets and 1|1|1|NA|1 for all 20 secondary packets: 40 all-applicable receptions and no fixed-witness/scanner discrepancy. A fixed-witness receipt with a scanner miss confirms delivery at that witness, not a cause of the scanner miss. No applicable receipt does not prove no RF transmission. Correlated loss alone does not assign a cause.

A valid completed measurement can still fail delivery. A campaign pass requires every applicable packet plus all evidence and preservation gates.

This finite 20-per-profile result is not a permanent-fix or zero-loss guarantee. The preceding mixed-PHY 32-symbol case at the same 915/916 MHz had scanner misses; this case changes the secondary SF/BW, airtime and derived scan timing. Run time, order and ambient traffic were not controlled between the cases. Their difference therefore does not isolate a causal setting or explain a particular earlier missing nonce. The new packets were not retries.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#latest-completion-and-evidence","title":"Latest completion and evidence","text":"

Migration and its original six-node saved baseline: PASS. All six final readbacks retained primary 915.000 and passed preservation of the other compared settings, permissions, leases, channels, credentials and PINs. All six final session proofs passed. V4 MQTT reconnected with status (ok); this check did not independently prove a fresh publish acknowledgement. No production firmware was changed. USB collection remains active, so these rates are not a repeated measurement of earlier USB-quiet experiments.

The current controller proactively revalidates strict binary mode, public identity, full version, core health and uptime after 45 seconds of idle time, only outside RF. It performs final proofs on all six sessions before close and retains outer-finally errors. These are host measurement changes, not firmware or watchdog changes. A graceful stop marker replaces signals during an active CLI operation.

The matched-PHY controller SHA-256 is 187ab7de2464d1496ddee5235899d115270953e4ae38beaa1b0e1791823b0bb1; its comparison core SHA-256 is 6920179844efe720542368d2ea257d94c18c8c49c063efb18a60ccf6b8204b31. The completed matched-PHY raw receipt SHA-256 is 46ef6da2dabe8ba48f7acb8e8b5aec2370c8b41397787ac78020e044d50c918e; its sanitized export SHA-256 is 3bcc79cc02722c8438340c5fad49b9c9c7921f2b4e8b433669e748388a04a94f. Machine-readable evidence is the matched-PHY report. The separate saved-frequency migration source SHA-256 is a763aec8d1bd692c524f5dee773b3d1663e1eac0cc26308568e8d38153fb7cf0. The completed migration receipt SHA-256 is c523274d78c9dd3aa7511cb91ef0330866684f7f12bfc54d94967787f0d22257; the passed six-node baseline SHA-256 is bb6685fa03182395e7a697d4b7b843c023b59d7f12feebe074f2c7de76d26f78.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#previous-915916-cohort-different-sfbw-profiles","title":"Previous 915/916 cohort: different SF/BW profiles","text":"

This cohort uses primary 915.000 / BW 62.5 / SF7 and secondary 916.000 / BW 125 / SF9, with matched preambles varied across cases. All three executed cases, at 32, 64 and 128 symbols, completed as valid controlled comparisons. The user requested a graceful stop after 128; no active case was interrupted. Preamble 256 was not run. The original four-case experiment is incomplete by user steering, not a setup failure. Every executed case had 40 accepted and 40 confirmed direct transmissions, zero floods, retries, duplicates, wrong-profile receipts, receiver transmissions or evidence errors. All five collectors completed and all before/after core error flags were zero.

Preamble V3 P / S V4.3 P / S T096 P / S Tag primary / 20 Indicator secondary / 20 Evidence status 32 19 / 20 20 / 20 20 / 18 20 20 Valid; scanner delivery FAIL 64 20 / 20 20 / 20 20 / 20 20 20 Valid; all applicable delivery PASS 128 20 / 20 20 / 20 20 / 20 20 20 Valid; all applicable delivery PASS 256 NOT RUN NOT RUN NOT RUN NOT RUN NOT RUN Skipped by user steering

Packet labels are one-based within each profile: P01-P20, S01-S20. At 32, V3 missed P04 and T096 missed S02 and S15; V4.3 received all 40. Both witnesses received all 20 applicable packets. Thus each of the three scanner misses had a same-packet fixed-witness receipt. This proves delivery at that witness, not a cause of the scanner miss. All applicable receivers received the other 37 packets.

At both 64 and 128, all three scanners received 40/40 and both witnesses received their 20/20 applicable packets. There was no extraneous receiver TX or core/evidence error. This is a finite sample pass, not proof of a permanent fix or a guarantee that longer preambles prevent loss.

Masks use role order V3, V4.3, T096, Tag, Indicator, with NA for the opposite frequency witness. For example, complete primary reception is 1|1|1|1|NA; complete secondary reception is 1|1|1|NA|1. NA is never a miss. An owned wrong-profile witness receipt is retained as a confound.

Preamble Applicable reception mask Packet count 32 1\\|1\\|1\\|1\\|NA 19 primary 32 0\\|1\\|1\\|1\\|NA 1 primary 32 1\\|1\\|1\\|NA\\|1 18 secondary 32 1\\|1\\|0\\|NA\\|1 2 secondary 64 1\\|1\\|1\\|1\\|NA 20 primary 64 1\\|1\\|1\\|NA\\|1 20 secondary 128 1\\|1\\|1\\|1\\|NA 20 primary 128 1\\|1\\|1\\|NA\\|1 20 secondary

The 32-symbol witness classifications are 37 all-applicable receptions and three fixed-witness-received/scanner-missed packets: P04 at V3, S02 and S15 at T096. At both 64 and 128 they are 40 all-applicable receptions. The fixed witnesses missed no applicable packets in any executed case. The 256-symbol case was not run.

Same-packet observation Supported interpretation Fixed witness receives; scanner misses Delivered at that witness; scanner missed the same packet All applicable receivers receive Exact shared application delivery to those receivers Scanner receives; fixed witness misses Witness missed a packet received elsewhere No applicable receiver receives No local receipt; does not prove no RF transmission Opposite-frequency witness receives Confound requiring investigation; not clean applicable delivery

The raw and sanitized closed receipts agree on these counts. Correlated loss alone does not assign a receive-path cause. A valid completed measurement can still fail delivery. A campaign pass requires all applicable packets and all evidence/restoration gates.

Changing the matched preamble also changes the scanner's derived primary visit duration. At these tuples the primary symbol is 2.048 ms and the secondary symbol is 4.096 ms. The nominal idle primary visit is the larger of 9.421 ms and 2.048 * preamble - 18.842 - 2 * guarded_switch_ms; secondary visit is 18.842 ms. This is a preamble-plus-derived-dwell comparison, not an isolated preamble-only experiment. The actual guarded switch term remains part of the per-board runtime timing state (RadioProfiles::listenUs). Actual visits can be extended by receive ownership; the formula is not a timestamped account of an individual packet's reception or miss.

The mixed-PHY private controller/core SHA-256 digests are e092276f44f06d714c49befc78ae6a9738efe3cc369ede4a0b4fbe731794455b and 3c002da26512ba9324e491adb7fa8595944e921ee5bd841cec6fe8271ab5a504. All six nodes' restoration and final session proofs passed, with no reported errors. The planned-four-case aggregate completion/validity fields remain false because 256 was skipped; each of the three executed cases has its own completed/valid flags true. The raw receipt SHA-256 is 5d120a579863ffaf8ba9c29d7463f2d546e82a0ffdd012f599b0208e9908f00a; its sanitized export SHA-256 is d03d8f65bc974d56c8c49fdddcdd988496b7eca29f5bc401a7ce1196bf00036e. Machine-readable evidence is the separate mixed-PHY report.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#appendix-earlier-three-receiver-measurements","title":"Appendix: earlier three-receiver measurements","text":"

These historical runs used V3/V4 9b70827d, T096 4773ac42 and sender ebb59d48, with the old primary 910.525 MHz / BW 62.5 kHz / SF7 / CR5 and secondary 916.000 MHz / BW 125 kHz / SF9 / CR5. They used 2 dBm, matched preambles, power saving/RXPS off and three active USB collectors. Every scanner case below sent 40 distinct packets once. These are not results for the new 915.000 MHz primary.

Preamble V3 P / S V4.3 P / S T096 P / S Shared scanner masks 32 16 / 18 16 / 18 16 / 18 000=5, 001=1, 110=1, 111=33 64 20 / 16 20 / 16 20 / 16 000=4, 111=36 128 20 / 16 20 / 16 20 / 15 000=4, 110=1, 111=35 256 NOT RUN NOT RUN NOT RUN User-directed stop before RF

These three-bit masks use V3, V4.3, T096 order; omitted masks have zero count. All three measured cases were valid controlled comparisons with failed all-packets delivery. Scanner receipt sets and misses are retained:

V3/V4 receive sets matched exactly in each earlier case. At preamble 32, their Jaccard with T096 was 15/17 primary, 18/18 secondary and 33/35 overall. At 64 all pairs had Jaccard 1. At 128 their Jaccard with T096 was 20/20 primary, 15/16 secondary and 35/36 overall. Empty-union Jaccard is undefined. These correlations do not establish a loss cause or a preamble remedy.

The preamble-32 window lasted 130.650 seconds. All collectors drained, reported zero duplicates and core flags 0; no receiver transmitted. Physical RX deltas were V3 117, V4.3 112, T096 113, each including 34 owned packets; other RX deltas were 83, 78 and 79, with RX error deltas 1, 3 and 1. These background totals do not identify why a particular nonce was missed.

The fresh fixed-secondary control delivered 20/20 to all three, with zero duplicates, errors or receiver transmissions, over 65.075 seconds. The later fresh fixed-primary follow-up also delivered 20/20 to all three with no duplicates and complete drains, but an unaccounted T096 flood TX made that controlled comparison INVALID, not a clean control pass. Neither control resent a scanner packet or erased its misses.

"},{"location":"test-results/radio2_simultaneous_receivers_2026-10-22/#appendix-failed-attempts-and-restoration-history","title":"Appendix: failed attempts and restoration history","text":"

The stopped old-frequency five-receiver attempt's private controller/core SHA-256 digests were 7e3dba8ca3c3387e601111714f46741bdd933b6f041f8213aadad8eef69d3cc1 and ba0a116f90a17825d1718aef9e4138a27002fdd2ff2127be46815280245ceab2.

Private failed/measured receipt SHA-256 digests, without raw identifiers:

Receipt SHA-256 Preamble 32 and fixed-secondary c5e7cec34c376c718e936f4a86b21b197bbb2dc4b02dbdc7e079617f2d229511 Fixed-primary original failed comparison ffe5a65c17df98b444eb690f9208090637724dc7906b2fb97aedc5f2bc63ee44 First five-receiver setup, zero RF b3613de719584be3c033c31a8a973c30c88e22146d9060293aa1ec7fb574a1c9 Stopped old-frequency five-receiver partial, 25 TX 87026d5c9938ae855983f529906aca09bafa9975e01ce0176d9eef82391eed2f

Other detailed earlier attempt/recovery records are retained privately. Earlier failures and causal limits also remain in the ESP32 BLE, radio2 and sleep report.

"},{"location":"test-results/radio_chirp_math_validation/","title":"Radio chirp math validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_chirp_math_validation/#shared-radioradio2-chirp-timing-and-preamble-warnings","title":"Shared radio/radio2 chirp timing and preamble warnings","text":"

Historical validation record: the nominal 600 us policy described below has been replaced by the activation self-test and 10% guarded measured budget. The later automatic policy also removed the pair-specific 88-symbol SF8/500 floor and the fixed 300 us loop allowance; the older calculations below remain as test history. See radio profiles for current behavior.

Implemented in the keymindCascade working tree, 2026-09-14. The initial 4.1/4000-us implementation record below is retained as history; the 4.6/300-us policy update was a prior stage. No radios were flashed or RF experiments started for either function-only change.

"},{"location":"test-results/radio_chirp_math_validation/#initial-implementation-scope","title":"Initial implementation scope","text":""},{"location":"test-results/radio_chirp_math_validation/#evidence-and-limits","title":"Evidence and limits","text":"

The five-radio single-pass bench received 392/400 at 5.1 chirps. The longer-dwell repeats scored 342/348 before the user stopped 6.1, and 395/400 at 7.7. They do not establish any lossless four-channel dwell. They also do not justify treating one full sweep fitting inside the preamble as proof of acquisition.

The function retains the earlier production two-profile policy: two return opportunities on the slower channel, blind time plus sixteen acquisition symbols on the faster channel. Its 4.1 floor is the requested policy boundary, not an experimentally established detection minimum. This change requires RF qualification before describing it as a reliability improvement.

Single-pass V4 switching peaked at 804 us including post-reception cache refresh. Indicator reached 8428 us in the prior receive-only study. The pure function can model either allowance. Per the follow-up request, runtime scheduling now uses a nominal 600 us switch / 4000 us loop budget; advisory calculations raise the switch allowance if the observed maximum exceeds 600 us. The 0.6 ms nominal value does not cover all measured switches. They do not dynamically retune the production scheduler or silently lengthen already configured preambles.

See the formula and CLI examples.

"},{"location":"test-results/radio_chirp_math_validation/#verification","title":"Verification","text":""},{"location":"test-results/radio_chirp_math_validation/#automatic-46-chirp-300-us-policy-update","title":"Automatic 4.6-chirp / 300-us policy update","text":"

After the separate 4.6/0.3 ms hardware test, the user requested this policy in the shared production function. That sample was 197/200 (slow 97/100, fast 100/100), not evidence of lossless operation.

Verification covers 1,600 SF/BW pairs with exact role-dependent rounding and fast-dwell arithmetic, profile reversal, equal-symbol ties, larger explicit slow preambles, infeasible short slow preambles, single-profile off, long explicit visits, and advisory-only switch overruns. Production CLI checks assert the actual 4.60,42.67 chirp readout, loop=300us, warning values and unchanged explicit 32 on the tested SF8/500 pair. The frozen HIL plan now checks equality against the production 4.6/300-us allocation.

"},{"location":"test-results/radio_dwell_policy_validation/","title":"Radio dwell policy validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_dwell_policy_validation/#automatic-dwell-policy-46-chirps-on-both-profiles","title":"Automatic dwell policy: 4.6 chirps on both profiles","text":"

Historical validation record: the nominal 600 us switch allowance below has been replaced by the activation self-test and a 10% guarded measured budget. The current automatic policy also removed the fixed 300 us loop allowance and the pair-specific SF8/500 88-symbol floor. Values below are retained as bench history, not current automatic defaults. See radio profiles for current behavior.

The shared RadioProfiles function uses a 4.6-symbol minimum for both logical profiles of one physical transceiver, rounded upward to a whole microsecond. The slower profile is selected by symbol duration, with primary winning ties. The fast visit receives the remaining half-slow-preamble budget, subject to the same 4.6-symbol minimum. The nominal allowances are 600 us per switch and 300 us per cycle, not added delays.

The scheduler, automatic preambles, configuration acceptance and diagnostic calculator use the same minima. Preserve explicit preambles, safe-airtime validation, too-short slow-preamble rejection, recommendation rounding to multiples of eight (minimum 32), and the existing SF8/500 versus SF7/62.5 88-symbol recommendation floor. Measured switching overruns raise advisory warnings without rewriting configured values. Single-profile RX still does not perform a scan.

For SF7/62.5 plus SF8/500 with slow preamble 32, the allocation remains 9,421 us slow and 21,847 us fast, in either profile order. The fast visit already exceeds 4.6 fast symbols. Raising the fast minimum matters when less budget is available: equal SF7/500 profiles with slow preamble 30 no longer fit, because only 1,162 us would remain versus the new 1,178 us floor. Slow preamble 32 fits with a 1,418 us fast visit.

"},{"location":"test-results/radio_dwell_policy_validation/#hardware-context","title":"Hardware context","text":"

The preceding V4 RX / fixed RAK4631 and T1000-E TX bench used explicit 32-symbol preambles on both profiles, 100 stationary controls per profile and a complete 100/profile scan. Controls passed 100/100 each; scanning received slow 97/100 and fast 100/100 (197/200 total). Switching averaged 582 us and peaked at 827 us. Successful-packet mean SNR was +12.27/+13.47 dB. No RF retry or USB result replay was used. Radios were left idle.

That test used the same 9,421/21,847 us windows, so the higher fast floor does not change this particular allocation. It does not establish lossless performance or qualify other SF/BW combinations. Raw capture SHA-256: 1c2e0eae3f304fcaeb6addfeef4f2f09a3c2bec8d78c60d3f151c952d0f6d5f5. No additional RF run is required to apply the user-requested policy change.

"},{"location":"test-results/radio_dwell_policy_validation/#software-verification","title":"Software verification","text":"

The portable C++ test sweeps 1,600 SF/BW pairs and checks both 4.6-symbol minima, exact fast-window allocation, preamble/diagnostic consistency and accepted return deadlines. It also covers reversed/equal symbol times, explicit preambles, newly infeasible short settings, long requested visits, invalid parameters and advisory-only switching overruns. CLI tests exercise warnings, both timing aliases, saved/temporary configuration, persistence and bounded replies. Native integration covers routing, retries and packet ownership alongside the timing model.

Verification completed on 2026-09-14 (local date):

"},{"location":"test-results/radio_profile_switch_validation/","title":"Radio profile switch validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_profile_switch_validation/#sx1262-profile-switch-validation","title":"SX1262 profile-switch validation","text":"

Measured locally on 2026-09-13 using an XIAO ESP32-S3 with WIO SX1262. Later tests also use the SenseCAP Indicator ESP32-S3 LoRa processor. The V8 integration is the current implementation; V1-V7 sections/captures document the development history.

"},{"location":"test-results/radio_profile_switch_validation/#initial-result-v1","title":"Initial result (V1)","text":"

The production retune path averaged 1.434 ms with XOSC held, versus 3.166 ms with RC standby. Holding the oscillator saved 1.732 ms per retune (54.7%). Warm-mode maximum was 1.467 ms across 6,000 measured retunes. The 6 ms switch allowance and 4 ms loop allowance are unchanged.

Each row combines both directions and two reversed-order trials, with 2,000 measured retunes per standby policy. Times run from production tuneProfile entry until BUSY is observed low. GetStatus confirmed RX after every timed hop.

Secondary profile RC mean / maximum XOSC mean / maximum SF7 / 500 kHz 3.161 / 3.199 ms 1.434 / 1.467 ms SF8 / 500 kHz 3.166 / 3.209 ms 1.435 / 1.464 ms SF9 / 500 kHz 3.171 / 3.193 ms 1.433 / 1.437 ms "},{"location":"test-results/radio_profile_switch_validation/#scope-and-reproducibility","title":"Scope and reproducibility","text":"

The timing harness compiles the actual production wrapper, not the earlier fast/minimal RX benchmark. RC and XOSC use the same executable: the baseline changes only the standby policy after entering dual-profile mode. No TCXO supply or initial startup delay is disabled. The normal guard checks and frequency/SF/BW/CR/preamble setters remain active.

This isolates radio switching from UI, Wi-Fi, BLE, mesh dispatch, and scan-loop scheduling. BUSY polling/timer overhead is included; the RX status query is outside the timing. This is not a packet-delivery test, first-startup timing, power measurement, wide-frequency image-recalibration test, or proof of analog RF settling beyond BUSY/RX status. Indicator I/O-expander latency and nRF52 timing still need their own measurements before reducing common budgets.

Raw results: profile_switch_results.json. The collector requires all requested samples, zero failure/timeout/mode-error counts, and successful RC restoration before accepting a trial.

Build provenance:

Host regression checks also passed: profile scan/CLI (3 tests), receive contract (2 tests), and SX126x RX-status decoding (1 test). A normal heltec_v4_companion_radio_usb build and the XIAO timing build succeeded.

The XIAO's original 8 MiB flash snapshot was verified against the device while held in the bootloader before the timing firmware was uploaded. Raw flash backups stay in the current user's private backup directory, outside Git. After testing, the entire original snapshot was restored and verified again against the device before reboot. The live USB CLI then confirmed Xiao S3 WIO running v1.17.1-soak-xiao-B (11-Sep-2026). Its soak logger was left stopped; existing logs were preserved. The Indicator and RAK Tag were not flashed.

"},{"location":"test-results/radio_profile_switch_validation/#follow-up-batched-modulation","title":"Follow-up: batched modulation","text":"

On the same date, a second 12,000-retune experiment held XOSC warm in both cases and compared the old three-setter sequence with one combined LoRa modulation update. Both paths ran in the same V2 executable, with the second round reversing their order. Each mode completed 6,000 timed hops.

Secondary profile Separate setters: mean / maximum Batched: mean / maximum SF7 / 500 kHz 1.442 / 1.473 ms 1.159 / 1.198 ms SF8 / 500 kHz 1.444 / 1.480 ms 1.158 / 1.192 ms SF9 / 500 kHz 1.446 / 1.478 ms 1.156 / 1.163 ms

Overall: 1.444 ms to 1.158 ms, saving 0.286 ms (19.8%) per retune. Use this matched V2 baseline for the batching comparison; the earlier V1 warm-standby numbers came from a different executable layout.

There were zero retune failures, BUSY timeouts, RX-mode errors, or SF/BW/CR cache mismatches. All 12 trials restored RC standby after disabling profile 2. 84 packet/busy deferrals were excluded (54 separate-setter, 30 batched). First eight successful hops per trial were excluded as before.

The batching removes two repeated modem checks and two modulation writes. The helper validates a complete MeshCore bandwidth/SF/CR tuple, updates RadioLib's cached SF/BW/CR before its automatic LDRO calculation, and restores the old cache if the command fails. The wrapper's existing physical rollback and safe-transition paths are retained. Frequency changes, preamble updates, standby, IRQ/buffer setup, packet parameters, and RX restart remain unchanged. This does not qualify eliminating those other repeated operations.

Raw results: profile_switch_batch_results.json. V2 test firmware SHA-256: b4403f6f5eb0a2b0a31c2d79859122225f247296699a578ad2596ef17eaed0fd. Toolchain and RadioLib pin match the first experiment. The normal heltec_v4_companion_radio_usb build also succeeded with batching enabled.

The two new host tests execute the production method over 1,200 valid bandwidth/SF/CR/LDRO combinations, invalid inputs and wrong modem, command failures with cache rollback/retry, and the wrapper's failure short-circuit. All eight relevant host tests passed. RF packet delivery and application/UI scheduling remain outside this timing experiment; shared budgets are unchanged.

"},{"location":"test-results/radio_profile_switch_validation/#expanded-screening-redundant-rx-setup-and-spi-v5","title":"Expanded screening: redundant RX setup and SPI (V5)","text":"

Corrected V5 measurements on 2026-09-13 cover 28,800 timed retunes in 300 trials across XIAO S3/WIO SX1262 and the Indicator ESP32/SX1262. Each sweep uses SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, both directions, reversed case order in round two, and eight excluded initial hops per trial. All 300 trials completed their sample count and restored RC standby on exit. There were zero timed switch failures, BUSY timeouts, RX-mode errors or SF/BW/CR cache mismatches. The normal packet guards deferred 175 attempted hops; these were not counted as timed retunes.

Twenty-one distinct cases were screened: seven individual redundant-operation changes, a full manual RX control, combinations, 2/4/8 MHz SPI and whole-buffer ESP32 SPI transfers. Shared controls appear in both groups. All new shortcuts are HIL-only, not included in normal targets.

"},{"location":"test-results/radio_profile_switch_validation/#individual-and-combined-rx-changes","title":"Individual and combined RX changes","text":"

Means in milliseconds, with warm oscillator and batched modulation throughout. Each XIAO row has 768 timed hops; each Indicator row has 384.

RX/SPI case XIAO mean Indicator mean Production batching, 2 MHz SPI 1.182 15.190 Manual RX sequence, no omissions 1.181 15.135 Skip second standby 1.124 14.195 Skip repeated IRQ setup 1.096 14.399 Skip repeated buffer-base setup 1.146 14.437 Skip repeated packet-parameter setup 0.991 12.364 Skip RX modem query 1.145 14.427 Defer preamble to the one RX packet-parameter write 0.956 11.659 Skip wake NOP when already awake 1.146 14.576 Combined omissions, early preamble write 0.771 9.096 Combined omissions, deferred preamble 0.732 8.384 4 MHz SPI only 0.985 14.996 8 MHz SPI only 0.889 14.889 Combined/deferred, 4 MHz SPI 0.627 8.280 Combined/deferred, 8 MHz SPI 0.579 8.224

The largest individual saving is programming preamble/packet parameters once. Compare individual omissions with the manual full control when attributing their isolated costs: replacing RadioLib's staging dispatch also has overhead. Individual savings are not additive; some remove overlapping work.

"},{"location":"test-results/radio_profile_switch_validation/#whole-buffer-spi-follow-up-same-v5-binaries","title":"Whole-buffer SPI follow-up (same V5 binaries)","text":"Case XIAO mean / maximum Indicator mean / maximum Production batching, byte loop, 2 MHz 1.184 / 1.221 ms 15.185 / 15.785 ms Bulk transfer only, 2 MHz 1.070 / 1.073 ms 15.092 / 15.134 ms Bulk transfer only, 4 MHz 0.890 / 0.903 ms 14.924 / 14.983 ms Bulk transfer only, 8 MHz 0.797 / 0.806 ms 14.836 / 14.908 ms Combined/deferred, byte loop, 8 MHz 0.579 / 0.588 ms 8.222 / 8.843 ms Combined/deferred + bulk, 2 MHz 0.671 / 0.700 ms 8.369 / 9.027 ms Combined/deferred + bulk, 4 MHz 0.582 / 0.595 ms 8.252 / 8.315 ms Combined/deferred + bulk, 8 MHz 0.535 / 0.545 ms 8.203 / 8.233 ms

The fastest XIAO candidate saves 0.649 ms (54.8%) versus that sweep's already-warm, already-batched production control. At 8 MHz, bulk transfers alone add about 0.043 ms of benefit to the combined RX shortcut. The Indicator remains dominated by I2C-mediated chip-select/BUSY/DIO access; the final bulk step improves its combined mean by only 0.019 ms. Small HAL effects varied between executable layouts in preliminary screens; do not generalize them.

The Indicator exceeds the generic 6 ms switch allowance even in its fastest experimental path. These are board measurements, not a justification for reducing the shared 6 ms switch / 4 ms loop constants. Indicator scheduling and effective preambles need board-specific treatment before a production rollout. Application/display/network scheduling is still excluded here.

Raw accepted sweeps:

"},{"location":"test-results/radio_profile_switch_validation/#test-corrections-and-provenance","title":"Test corrections and provenance","text":"

V3/V4 broad screening accidentally called SX1262::startReceive() instead of CustomSX1262::startReceive() in the fallback, omitting MeshCore's extra preamble-detection IRQ flag. Their raw files are marked superseded and are not included in the counts or conclusions above. The V4 packet sweep was stopped on discovery. V5 preserves that flag in both normal and manual RX paths; a host regression test checks the Custom fallback and mapped flags. The earlier V1/V2 warm/batching experiments did not use that experimental subclass and are unaffected.

An initial V5 XIAO capture stopped on a partial serial JSON line during guarded mode exit. It is retained as incomplete, not counted above. The host now accumulates reads until newline, with regression coverage. Initial setup rejections are retried only for the explicit pre-measurement packet-guard reply, bounded to three seconds and counted; packet losses and timed failures are never retried away.

Final V5 firmware SHA-256:

Base commit/toolchain/RadioLib pin match the earlier experiments. Thirteen unique host tests passed (eight production regressions, two HIL boundary/buffer tests, three serial-reader/setup-guard tests). The HIL tests cover all 255 nonzero omission masks, normal-path fallbacks, preserved IRQ clear/SetRx, error short-circuit and full-duplex buffer lengths 1..260. They are not full driver lifecycle tests. An additional normal t1000e_companion_radio_usb nRF52 build passed with the production warm/batching changes: 591,484-byte app, runtime RAM guard passed. It was not flashed to the RAK or any nRF52 board.

"},{"location":"test-results/radio_profile_switch_validation/#qualification-boundary","title":"Qualification boundary","text":"

No BUSY wait, command status validation, stale-IRQ clear, RX command or wrapper packet-ownership guard is removed. The fastest path relies on a known RX-to-RX window and established RX setup. Correct invalidation after TX, CAD, sleep, reset, failed commands, and recovery must be designed and tested before promoting these shortcuts. The per-byte/bulk HAL experiment is ESP32-specific. Sensitivity/PER, oscillator power draw, large-frequency image recalibration, and application-driven continuous scanning remain unqualified.

"},{"location":"test-results/radio_profile_switch_validation/#actual-packet-checks-and-limitations","title":"Actual packet checks and limitations","text":"

Final forward matrix: Indicator transmitter to XIAO receiver, -9 dBm, 21 cases x SF7/8/9 x both profiles x 16/64/255-byte payloads = 378 probes. 359 sequence/length/payload/profile checks succeeded, 13 receiver-declared wait timeouts occurred, and six receiver USB replies did not arrive before the host deadline. Transmitter replies returned success. Successfully read packets had RSSI -42 to -39 dBm and SNR 10.2 to 14.2 dB. Timeout diagnostics reported RX mode and no device errors; some retained a preamble IRQ. These observations do not establish why packets were missed.

The production-batched baseline delivered 17/18 in that matrix; the fastest combined/bulk/8 MHz candidate delivered 18/18. An earlier reversed-order smoke test of the fastest candidate had one receiver timeout, so the clean 18-packet batch is not a reliability qualification. Earlier 0 dBm checks also had baseline and candidate misses. Reducing power did not eliminate them.

Reverse direction: XIAO transmitter to Indicator receiver, -9 dBm, baseline, combined/8 MHz and combined/bulk/8 MHz, with the same SF/profile/length matrix. All 54 payloads arrived intact, 18/18 per case. However, one XIAO USB TX acknowledgement was delayed past the host deadline, after which 32 later records picked up the previous command's acknowledgement. Thus the raw file has only 21 fully matched transactions despite 54 correctly received payloads. This is sequence-correlated evidence of a host acknowledgement problem, not 33 RF losses. Subsequent collector code rejects/stores stale sequence replies instead of associating them with a new command, and now asserts DTR on Windows to match the existing s3_memory_soak.py HWCDC session policy. The captures above used DTR deasserted; a causal DTR A/B test was not performed. Sequence handling has host regression coverage, but these last host changes were not rerun on HIL firmware after the boards were restored.

USB acknowledgements and actual receiver payload validity are recorded separately. Missing acknowledgements never trigger an automatic retransmit. The host drains the receiver concurrently with the transmitter's command and records timeouts without hiding them. These tests establish some real packet functionality, not a controlled packet-error-rate comparison. Fix/qualify the test transport, repeat with a stable RF reference, then test TX/CAD/reset/RXPS and continuous application scanning before promoting the shortcuts.

Earlier incomplete packet captures and superseded screens are retained for audit, not counted in the 378/54 final matrices. No identity or channel secrets were transmitted: probes used synthetic sequence-tagged bytes.

"},{"location":"test-results/radio_profile_switch_validation/#production-integration-and-usb-recovery-v8","title":"Production integration and USB recovery (V8)","text":"

Normal XIAO S3 WIO and Indicator LoRa variants now opt in to buffered 8 MHz SPI and a guarded fast RX-to-RX path. Other SX1262 variants retain default SPI/full RX setup; modulation batching and warm standby remain shared.

The fast path keeps the TCXO warm, applies one SF/BW/CR tuple and one packet tuple, clears stale IRQs and starts RX with the original BUSY handling. It reuses an established LoRa IRQ map and buffer bases only within an owned continuous-RX retune. Ordinary standby, staging/TX, CAD, sleep, reset/init, RX duty cycling and command failures invalidate that reuse. RX startup failure now fails the whole profile transaction and restores the old profile/caches with full setup; failed recovery keeps the profile marked for refresh.

"},{"location":"test-results/radio_profile_switch_validation/#sleep-recovery-discovered-during-integration","title":"Sleep recovery discovered during integration","text":"

Initial lifecycle captures found XOSC_START_ERR (0x20) after retained sleep, despite successful RX status. RC-policy controls reproduced the same flag: this was not established as a regression caused by the fast path. Simply waking in RC and entering standby before sleep did not clear it. Reapplying the already configured TCXO voltage and 1,600 us delay resolved it.

The production sleep override now enters standby first, wakes in RC, and restores TCXO configuration on wake. It refuses to clear unrelated device faults. The warm policy resumes after full RX setup. This follows the standby entry requirement in section 13.1.1 of the Semtech SX1261/2 datasheet. Section 13.3.6 describes TCXO control and the XOSC error flag; it does not by itself prove the cause of this retained-sleep observation.

Diagnostic files profile_switch_v6_recovery_xiao.json, profile_switch_v7_recovery_xiao.json, profile_switch_v7_recovery_xiao_complete.json (despite its filename, its complete field is false), and profile_switch_v7_sleep_diagnostic_xiao.json retain these failures. They are not counted as passing lifecycle tests.

"},{"location":"test-results/radio_profile_switch_validation/#usb-fix-and-scope","title":"USB fix and scope","text":"

The HIL host distinguishes Espressif native CDC (DTR asserted) from CH340 bridges (DTR/RTS deasserted). Replies are newline-assembled and sequence-matched. Firmware caches completed packet and timing reports before emitting and flushing them. A timeout or malformed report triggers a sequence-scoped result query, never a repeated TX or timing run. Randomized session sequence starts avoid a cached result from an earlier host session.

V7 had successfully fixed/retested packet result replay but a long native-USB timing sweep still lost one response. V8 adds whole-report timing buffering and replay. The incomplete V7 XIAO timing capture is retained, not silently completed. The public MeshCore Companion USB protocol is unchanged; these USB fixes are for the radio-lab transport. The previous broad Windows-DTR policy described in the V5 history above has been replaced by native-device identification.

Both boards passed 20 reopen cycles, deliberate lost timing replies, deliberate lost TX replies, stale TX reply isolation, and 18 lifecycle cases each. The cases cover TX, CAD, sleep, reset/init, RC-policy sleep and explicit TCXO-restoration controls, repeated three times. Fast counters prove one full RX fallback after invalidation and reuse only on the following retune. All command results, RX-mode checks and device-error checks passed.

V8 test firmware SHA-256 (same pinned RadioLib/toolchain as above):

Normal USB Companion builds passed for Xiao_S3_WIO_companion_radio_usb (813,901 app bytes / 63,012 static RAM), SenseCapIndicator-LoRa_comp_radio_usb (879,809 / 62,960), and t1000e_companion_radio_usb (591,980 / 179,868). Runtime RAM budgets passed on all three. These normal application images were compiled, not substituted for the user's configured soak firmware during the HIL tests.

Host/native regression coverage includes production modulation caches/LDRO, standby failure short-circuit, RX-start rollback failure, ownership guards, fast-state invalidation, TCXO wake fault preservation, buffered SPI semantics, USB result overflow/replay, partial reads and stale-sequence handling.

"},{"location":"test-results/radio_profile_switch_validation/#final-steady-state-timing","title":"Final steady-state timing","text":"

V8 ran 256 timed retunes per trial, four production configurations, SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, and two reversed-order rounds on each board: 12,288 measured retunes / 48 trials. The first eight hops per trial are excluded. There were zero retune failures, BUSY timeouts, RX-mode errors or modulation-cache errors. All 48 trials restored RC policy on exit. The normal packet guards deferred 21 XIAO and 59 Indicator hops; no setup retries or naturally missing/replayed USB reports occurred in these final sweeps.

Production configuration XIAO mean / maximum Indicator mean / maximum Warm + batching, per-byte SPI 2 MHz 1.193 / 1.227 ms 15.174 / 15.836 ms Fast RX, per-byte SPI 2 MHz 0.744 / 0.770 ms 8.426 / 8.471 ms Full RX, buffered SPI 8 MHz 0.828 / 0.840 ms 14.816 / 15.386 ms Fast RX + buffered SPI 8 MHz 0.549 / 0.556 ms 8.263 / 8.879 ms

The combined production path saves 54.0% on XIAO and 45.5% on Indicator relative to each same-image warm/batched control. Small differences from V5-V7 reflect different binaries/code layout; only within-image A/Bs are compared. Both fast configurations must increment the production success counter; full RX controls must not. Mode/cache verification happens after the timed region.

The common 6 ms switch and 4 ms loop allowances are unchanged, not delays inserted by the driver. Indicator still exceeds 6 ms. These measurements do not qualify continuous application scheduling, scan-overlap reception, sensitivity/PER, power use, temperature extremes, or image recalibration on frequency changes larger than this 1 MHz pair. Do not shorten common transmit preambles or advertise the XIAO number for other boards.

"},{"location":"test-results/radio_profile_switch_validation/#final-over-air-packet-matrix","title":"Final over-air packet matrix","text":"

288/288 payloads and command transactions passed: 144 Indicator-to-XIAO and 144 XIAO-to-Indicator. Each direction covers all four production cases, SF7/8/9 secondary profiles, both target profiles, 16/64/255-byte synthetic payloads and two reversed-order rounds. TX used buffered 8 MHz SPI at -9 dBm. Fast receiver readiness reports had to show actual production fast resumes.

There were zero RX errors, receiver timeouts, missing replies, stale replies, or natural transport-recovery queries in these matrices. Deliberately lost and stale replies were separately exercised by the recovery tests above. RSSI ranged -43 to -39 dBm and SNR 10.2 to 14.5 dB. This clean short-range matrix addresses the failures seen in the earlier V5 harness; it does not identify which single change caused each historical miss or establish a field packet-error rate.

All 18 host/native regression tests passed. The HIL builds do not include the unrelated pre-existing Companion/repeater/Wi-Fi soak edits; the normal application compile checks include the current working tree and preserve them.

A subsequent requested SF6/125 kHz multi-channel test used 32-symbol preambles and 4.8-symbol visits. It stopped on the third packet at the initial four channels (two received, one timeout); no higher count was tried. That continuous scan experiment is separate from the 288/288 stationary-after-retune matrix above.

"},{"location":"test-results/radio_profile_switch_validation/#unchanged-modulation-tuple-follow-up","title":"Unchanged modulation tuple follow-up","text":"

The initial software-only checks below were followed by same-image hardware timing and separated-radio tests. Frequency-only means improved from 548.880 to 451.897 us on XIAO and from 8,263.438 to 7,601.193 us on Indicator. Four-channel SF6 reception was not loss-free. The following paragraph records the initial implementation stage, not the current hardware-test status.

Implemented 2026-09-14 after commit 0e5955f8, without restarting hardware tests. On opted-in fast RX-to-RX hops, setLoRaModulationParams() now compares the requested SF, encoded bandwidth, encoded CR and effective LDRO against the last successfully acknowledged modulation write. An exact match within a valid owned standby window omits only SetModulationParams (0x8B). Frequency programming, the modem query, preamble/packet parameters, IRQ clears, RF-switch control and BUSY waits remain unchanged.

The acknowledgement is separate from RadioLib's mutable software fields. Auto/manual LDRO compares the resulting command byte, with the pinned RadioLib >=16 ms symbol rule. A failed write revokes the whole fast context before rollback. Ordinary SF/BW/CR/forced-LDRO setters invalidate the saved tuple even on failure. Existing full-RX staging, ordinary standby, TX/CAD staging, reset/init, sleep, duty cycling and failed RX/standby operations also invalidate it. The first hop after lost context therefore writes again; frequency-only hops that retain the context can skip subsequent writes.

All 30 selected host/native tests passed, including repeat-write counts for all supported bandwidth/SF/CR combinations and auto/forced LDRO modes, each independently changed field, LDRO policy transitions, failed-write retry, modem-query failure, ownership gates, ordinary setters and lifecycle invalidation. Both profile_switch_xiao and profile_switch_indicator compiled successfully against the pinned RadioLib. No firmware was uploaded, no radio command was issued, and no new RF result was collected. The temporary build overlay was removed. The earlier ~0.549 ms XIAO and ~8.263 ms Indicator timings do not measure this added optimization; its time saving and RF behavior await testing.

"},{"location":"test-results/radio_profile_switch_validation/#mercermesh-heltec-t096-sx1262-experiment-2026-09-21","title":"MercerMesh Heltec T096 SX1262 experiment (2026-09-21)","text":"

The Pi has one Heltec T096 (USB serial 651F8E496197F882); the other Heltec-named nRF52 is a MeshTower V2, not another T096. An application-only T096 HIL image tested two further RX-to-RX shortcuts with a buffered 8 MHz SPI bus: use SX1262 SetFs instead of warm XOSC standby, and skip SetPacketParams only when the LoRa RX packet tuple is unchanged. Both switches defaulted off in this HIL image. The image explicitly enabled the existing production fast-RX path. Normal T096 firmware does not currently enable that path or use the buffered SPI HAL, so these numbers are not stock T096 switch timings.

The same image ran 256 measured hops per configuration, after eight warm-up hops, on 909.5/909.75 MHz. freq held SF7/BW125/CR5; both alternated SF7/SF8 at BW125/CR5. Preamble was 32 symbols. The DWT cycle counter measured through RX re-entry and BUSY release. Every 8 MHz configuration had zero switch failures, mode/cache/RSSI errors, or BUSY timeouts.

8 MHz HIL configuration Frequency-only mean Frequency + SF mean Fast RX baseline 467.3 us 553.6 us SetFs only 422.5 us 517.4 us Unchanged packet tuple skip only 357.7 us 444.0 us Both 313.0 us 407.8 us

Combined savings were 154.3 us (33.0%) for frequency-only and 145.8 us (26.3%) for frequency-plus-SF relative to the same-image fast-RX baseline. Command counts confirmed one frequency command, IRQ clear and RX command per hop; the optimized cases eliminated the corresponding standby and packet commands. These are controlled short-range, same-band results, not a field packet-error rate or a qualification of large frequency jumps, sleep/wake, TX/CAD interruption, BLE scheduling, or all SX1262 boards.

A separate RAK4631 fixed transmitter sent 16-byte synthetic packets at 909.5 MHz/SF10/BW125/CR5/preamble32, -9 dBm chip power. The T096 hopped to 909.75 MHz and back before each transmission. Baseline and combined shortcuts each delivered 60/60 packets after 120 fast hops, with payload sequence and content checked. The RAK bootloader on this exact fixture enumerated as 239A:002A (the fixture's earlier 0029 expectation was corrected).

Doubling buffered SPI to 16 MHz gave only 12\u201321 us (about 3\u20134%) savings in comparable successful cases, while five of eight setup attempts failed. The combined shortcut had no clean 16 MHz result. Keep this T096 at 8 MHz pending a separately diagnosed and repeatable 16 MHz bring-up; no 16 MHz over-air qualification was attempted.

Both radios were restored via app-only DFU after the experiment: the T096 to Heltec_t096_repeater and the RAK to RAK_4631_companion_radio_full, each built at v1.17.1.7-hil-restore-e7160dfd. The T096 answered normal version and board commands; the RAK answered the Full Companion terminal's version command. The V4 observer's memory-soak service remained active. Reproducible bench code is in tools/hil/profile_switch_t096_sx1262.cpp and tools/hil/t096_sx1262_packet_probe.py.

Follow-up: owned fast RX, SetFs, and acknowledged packet-tuple reuse are now defaults for CustomSX1262 targets, including the original XIAO S3 WIO, Station G3 KISS, and Heltec V4 KISS paths. The Seeed Indicator retains its original owned-RX path but explicitly opts out of the two new shortcuts because its radio control and BUSY/IRQ pins run through an I2C expander. The owned-RX state now remembers only an acknowledged packet tuple and revokes it on ordinary packet setters, TX/CAD staging, standby outside the hop, sleep, reset, RX duty cycling, or command failure. The T096 numbers remain lab measurements, not on-board qualification of every CustomSX1262 target.

"},{"location":"test-results/radio_profiles_validation/","title":"Radio profiles validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_profiles_validation/#dual-profile-receive-validation","title":"Dual-profile receive validation","text":"

These measurements exercise the production two-profile implementation on one SX1262 transceiver, using a Heltec V4 repeater as receiver and a XIAO ESP32-S3 with a Wio LoRa module as transmitter. They follow the dedicated mixed-bandwidth scan experiment.

Counts, diagnostic replies and firmware/source hashes are in radio_profiles_results.json.

The primary profile is 909.5 MHz, SF7, 62.5 kHz, CR 4/5, preamble 32. The secondary is 910.5 MHz, 500 kHz, CR 4/5, with SF9, SF8 or SF7. Scanning uses normal RX, starts with the slower profile, and reserves two slow visits per slow preamble. Transmitters must use the corresponding preamble too.

"},{"location":"test-results/radio_profiles_validation/#method-and-limits","title":"Method and limits","text":"

The transmitter sends MeshCore adverts at varied intervals. The receiver's per-profile MeshCore RX counters are compared with the transmitter's completed TX counters before and after each run. Initial OTA announcements can add packets beyond the manually requested adverts; the table includes those transmissions. These are aggregate counters, not sequence-matched payload captures.

Runs use one transmitting profile at a time and a strong signal. They do not measure simultaneous traffic, weak-signal reception, interference or months of uptime. A single transceiver remains unavailable to the other profile while receiving or transmitting a packet. A clean finite run is evidence for a timing choice, not a guarantee that it cannot drop packets.

"},{"location":"test-results/radio_profiles_validation/#five-symbol-baseline","title":"Five-symbol baseline","text":"

Slow visits were 10.240 ms and fast visits 6.528 ms, plus actual switching time. The longest observed profile switch was 4.416 ms; no switch failures were reported.

Fast profile Fast preamble Fast RX / TX Slow RX / TX SF9 / 500 48 54 / 54 30 / 30 SF8 / 500 72 49 / 50 \u2014 SF8 / 500 80 49 / 50 \u2014 SF8 / 500 88 50 / 50 30 / 30 SF7 / 500 120 50 / 50 30 / 30

The SF8 automatic preamble retains a measured floor of 88 symbols. Automatically selected preambles always round up in steps of eight.

"},{"location":"test-results/radio_profiles_validation/#fractional-symbol-tuning","title":"Fractional-symbol tuning","text":"

The initial sweep started at 4.2 slow symbols, with an increase of 0.1 planned for any slow-channel loss. That first sweep passed all three fast settings. Each completed run used 50 requested adverts per profile. The SF8 fast run was interrupted to check SF9 preamble 32, then restarted; only its completed run is included below.

Slow symbols Fast profile Fast preamble Fast RX / TX Slow RX / TX 4.2 SF9 / 500 48 50 / 50 50 / 50 4.2 SF9 / 500 32 50 / 50 50 / 50 4.2 SF8 / 500 88 50 / 50 50 / 50 4.2 SF7 / 500 120 50 / 50 50 / 50

The four completed pairings received 200/200 packets on the slower channel and 200/200 on the faster channel. No switch failures were reported; the longest measured switch during the 4.2-symbol sweep was 4.407 ms.

The explicit SF9 preamble-32 check uses the same 8.602 ms slow and 8.166 ms fast windows as the preamble-48 check. Only the fast transmitter's preamble changes. The automatic SF9 calculation still reserves a 48-symbol preamble for the configured switching and loop-jitter budgets.

"},{"location":"test-results/radio_profiles_validation/#shorter-fast-preambles","title":"Shorter fast preambles","text":"

The follow-up starts with preamble 48 on both fast settings. Each miss advances the next candidate by eight symbols. The receive-window budgets stay at 4.2 slow symbols and the same remaining fast-channel time.

Fast profile Preamble Fast RX / TX SF8 / 500 48 49 / 50 SF8 / 500 56 50 / 50 SF7 / 500 48 42 / 50 SF7 / 500 56 41 / 50 SF7 / 500 64 45 / 50 SF7 / 500 72 50 / 50

The slower channel passed 50/50 paired with SF8/56, but then received 49/50 paired with SF7/72. Across the completed 4.2-symbol slow-channel runs, that is 299/300. This later miss invalidated the provisional 4.2-symbol choice and triggered the next headroom test at 4.3 symbols. Fast-preamble baselines carry forward as 32 for SF9, 56 for SF8 and 72 for SF7, advancing by eight on a miss.

At 4.3 symbols, the SF9/32 pairing received 100/100 slow packets and 50/50 fast packets. The next SF8/56 pairing received only 42/50 slow packets, so that candidate also failed. A control run with scanning disabled received 50/50 on the slow channel. At 4.4, SF8/56 passed 100/100 slow and 50/50 fast, but the SF7/72 pairing then received 49/50 slow packets.

At 4.5, the SF7 pairing passed 100/100 slow packets. Fast preamble 72 received 47/50, so the next baseline increased to 80 and passed 50/50. The following SF8/56 slow run received 49/50, triggering 4.6 symbols. Baselines now carry forward as SF9/32, SF8/56 and SF7/80.

At 4.6, both SF8/56 and SF7/80 passed 100/100 slow packets and 50/50 fast packets. The following SF9/32 slow run received 49/50, triggering 4.7 symbols. There were no switch failures; the longest observed switch was 4.414 ms.

At 4.7, the SF9/32 slow run passed its first 50 packets but received 99/100 over the full run. This triggered 4.8 symbols, with the same fast preamble baselines. No fast-channel run was started for the 4.7 candidate.

"},{"location":"test-results/radio_profiles_validation/#selected-bench-baseline-48-slow-symbols","title":"Selected bench baseline: 4.8 slow symbols","text":"

The 4.8-symbol sweep passed all three slow-channel checks, totaling 300/300. Slow visits are 9.831 ms and fast visits 6.937 ms, plus actual switching time. The longest measured switch was 4.491 ms, with no switch failures.

Fast profile Fast preamble Fast RX / TX Slow RX / TX SF9 / 500 32 50 / 50 100 / 100 SF8 / 500 56 49 / 50 100 / 100 SF8 / 500 64 50 / 50 \u2014 SF7 / 500 80 50 / 50 100 / 100

The SF8 miss advanced its baseline from 56 to 64, exactly eight symbols. Its slow-channel check preceded that increase; changing the fast preamble leaves the idle receive-window calculation unchanged. The final fast-channel attempts received 150/150, using explicit preambles 32 / 64 / 80 for SF9 / SF8 / SF7. These become the starting values for further tests, increasing by eight on a fast-channel miss. A slow-channel miss instead advances the listen window by 0.1 symbol.

The production automatic preambles remain 48 / 88 / 120, retaining the switching, loop-jitter and acquisition budgets. The shorter explicit values are bench results under the conditions above, not a guarantee for other boards or traffic patterns.

"},{"location":"test-results/radio_profiles_validation/#transmit-policy","title":"Transmit policy","text":"

Hardware checks send three zero-hop adverts from the V4 with the XIAO listening on profile 2. OTA startup traffic is allowed to settle before taking the counter snapshots. The completed cases are:

V4 profiles and policy TX on primary / secondary Received by XIAO Permanent / permanent, auto 3 / 3 3 Permanent / permanent, off 3 / 0 0 Permanent / temporary, auto 3 / 0 0 Permanent / temporary, on 3 / 3 3 Secondary RX only, on 3 / 0 0 Temporary / temporary, auto 3 / 3 3

A separate isolation check issued three explicit OTA announce commands with normal radio, RX-only tempradio2 and radio2.cross auto. The completed TX counters stayed unchanged on both profiles (0 / 0), confirming that locally generated OTA traffic does not fall back to the normal channel in that mode.

Turning radio2 off stopped scanning. A one-minute tempradio2 session also returned to one profile on expiry. Rebooting with a saved SF8 RX profile and an active temporary SF9 RX/TX profile cleared the temporary session and restored the saved SF8 profile, including its automatic 88-symbol preamble.

"},{"location":"test-results/radio_profiles_validation/#ota-discovery-while-retaining-the-main-channel","title":"OTA discovery while retaining the main channel","text":"

Both boards ran normal SF7 / 62.5 kHz on the primary and temporary SF8 / 500 kHz RX/TX on the secondary, using radio2.cross auto and automatic preambles 32/88. The XIAO discovered the V4's firmware catalogue over profile 2, and both boards recorded received and transmitted OTA packets there. Ten V4 zero-hop adverts then arrived on the XIAO's primary profile (10/10), with both temporary second profiles still active. The XIAO reported no switch failures and a longest switch of 4.547 ms during this check.

This hardware check covers discovery, bidirectional OTA traffic and concurrent availability of the main channel between packets. It does not perform a complete firmware transfer or install. Byte-exact transfer validation is provided by the shared OTA software tests described below.

"},{"location":"test-results/radio_profiles_validation/#software-coverage","title":"Software coverage","text":"

Native tests cover identical-packet fanout, separate retry ownership and coding rates, channel-busy backoff, generation changes, temporary-session isolation, deferred replies and stale-packet removal. Tests using the production CLI cover parsing, persistence failures, schedules, expiry and clock rollover. A physical radio mock exercises the production scan transitions, pending RX interrupts, RX power-saving restoration and failed profile switches.

The full native suite passed 1,422 cases after the RX-only OTA isolation fix; all eight KISS modem tests also passed. Subsequent timing changes through 4.8 symbols passed the 63-case retry/profile suite and production CLI and scan harnesses. Shared OTA transfer tests passed byte-exact checks with AddressSanitizer and UndefinedBehaviorSanitizer for ESP32 and nRF52 variants.

Build coverage includes the Heltec V4 repeater, room server, sensor and terminal chat, the Full XIAO Companion, and the nRF52 T1000-E repeater. The nRF52 result is a build check; these receive measurements use the ESP32/SX1262 boards above.

"},{"location":"test-results/radio_profiles_validation/#hardware-restored-after-validation","title":"Hardware restored after validation","text":"

Both boards were returned to their original B soak images after the checks: v1.17.1-soak-v4-B and v1.17.1-soak-xiao-B. The V4 retained 909.5 MHz, SF7 / 62.5 kHz, CR 4/5 and TX 0; the XIAO retained 910.525 MHz with the same modulation and TX 22. Both reconnected to Wi-Fi and their custom MQTT service. The soak logs resumed under B-post-radio2-soak. These restored images are the earlier memory-soak firmware; the new dual-profile code is validated by the finite tests above, not by a months-long uptime run. The Pi's soak service and Indicator telemetry were also healthy after restoration.

"},{"location":"test-results/radio_receive_calibration/","title":"Radio receive calibration","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/radio_receive_calibration/#radio-receive-calibration-and-recovery","title":"Radio receive calibration and recovery","text":"

Noise calibration collects 64 idle RSSI samples at least 50 ms apart. A quiet block takes about 3.2 seconds. Both continuous RX and temporary RXPS calibration windows allow up to 30 seconds; a partial block times out without publishing. Periodic requests coalesce while a block is active. RXPS resumes after a complete block, including a held block, or at timeout. Receive and transmit activity are never interrupted just to finish calibration.

If a reception delays the return to RXPS, the pending restoration is retried after every Dispatcher receive poll, including empty polls. A false preamble that expires without another packet interrupt therefore cannot leave the radio in continuous RX until the next calibration or AGC reset. An unread packet or active reception still owns the radio; TX completion retains its normal rearm. The retry wake query reports this pending restoration as immediate work, keeping event-driven sleepers awake until the existing preamble/header timeout or RX completion releases the owner. The query does not issue SPI commands.

A calibration request waiting for successful RX does not keep the MCU awake. Repeated failed receive starts retry after 20, 40, 80, 160, 320, 640, and 1280 ms, then at most once every 2000 ms. Only the repeated idle fallback is paced: packet/TX completion and explicit retune/CAD restoration still rearm promptly. Calibration and recovery requests retain the failed-start backoff; successful RX or a new radio session clears it. A retry blocked by BUSY or an RX owner waits 2000 ms without issuing another RX command. The Dispatcher includes this retry deadline in its wake query, and deferred adaptive AGC joins a later RX retry instead of forcing 50 ms wakeups throughout a persistent fault.

In single-profile RXPS, routine refreshes wait at least 60 seconds after the last completed or timed-out block before requesting another continuous-RX window. Both periodic maintenance and passive retry checks use this cooldown; retries do not restart a partially collected block. With RXPS off, or when the selected tuple requires continuous-RX fallback, the existing two-second maintenance tick and passive-probe age check are unchanged. Startup, explicit gain/tuning changes, AGC resets, and radio recovery still request a fresh block promptly. Dual-profile scanning retains its separate refresh policy.

Frequent configured AGC resets can still dominate calibration time: Cascade repeater builds start with an eight-second AGC interval, and MQTT observers use 28 seconds. With device power saving on, their adaptive AGC schedule backs off separately, up to 60 seconds; see AGC reset settings. AGC resets still bypass the routine noise-floor cooldown. After a timed-out startup block with no valid floor, the next routine RXPS attempt also waits 60 seconds; passive retries retain their existing bounded busy escape while waiting for a valid baseline.

The longer RXPS cooldown trades a slower response to ambient noise changes for more time in duty-cycle RX. It does not reduce sample count, shorten settling, or put the radio to sleep between samples. A large upward change still needs three complete high blocks to pass the rise hold. No battery-life improvement has been measured for this scheduling change.

The block estimate is the 12.5th percentile: the average of the eighth and ninth sorted samples, retaining fractional dBm. A published update uses 25% of the previous floor and 75% of the new estimate, with a -120 dBm lower bound. Initial calibration and explicit gain/tuning changes seed a new baseline. The old published value remains available while collecting. An AGC reset drops partial samples while retaining a valid baseline for channel checks.

An estimate more than 15 dB above the established floor is held twice. The third consecutive complete high block is accepted, allowing recovery after a permanent ambient rise. A normal or quieter complete block clears the hold. Partial blocks do not advance it. There is no admission threshold based on the old floor, so an old low baseline cannot reject every sample from a higher floor.

Hardware captures with sustained different-SF/BW traffic showed the median and lower quartile following the interferer. The 12.5th percentile retained the quiet background in those captures. It needs at least nine background samples out of 64 to avoid higher-valued traffic entirely. Nearly continuous interference or enough low outliers can still mislead it. The rise hold delays large changes but cannot identify their source.

RSSI sampling waits 7 ms on SX1262, SX1268, and LLCC68 builds, or 20 ms on other families, after every receive re-arm, including CAD and TX completion, to avoid unsettled frontend readings. The ten-second trial window often failed to complete during heavy traffic with that settling guard; the 30-second cap allows more idle opportunities. Partial blocks still time out without publishing. Longer collection spends more time in continuous RX when RXPS is enabled. A quiet minimum is about 3.2 seconds; real completion time also depends on packet activity and main-loop scheduling.

On radios with a mode probe, each eligible RSSI sample first checks that the chip has not silently left RX. Confirmed standby/non-RX discards the partial block without changing the published floor. Rejected attempts are also spaced 50 ms apart. BUSY and packet ownership block sampling; families without a mode probe retain their existing RSSI path.

CAD has a bounded wait, stops RX duty cycling before scanning, and re-arms the configured receive mode on success, busy detection, or error. A completed or currently receiving packet owns the radio and blocks CAD. A packet interrupt arriving during re-arm is retained. Failed re-arm and a newly arrived packet both defer transmission.

SX126x wrappers additionally run a recovery watchdog at most once per 10 seconds while continuous RX is expected. RXPS sleep, busy SPI, pending packets, and invalid status reads do not count as non-RX evidence. Two confirmed non-RX observations request soft recovery; a third uses the existing hard recovery path where supported. An observed healthy RX state clears the sequence. Recovery preserves tuning, TX power, receive gain, and the operator's power-saving intent. The existing radio liveness and RXPS watchdogs remain active for other failures.

The radio liveness watchdog measures time without a successfully received packet. Only a positive recvRaw() result restarts its observation window; TX completion, CAD interrupts, and failed packet reads do not. Reception counts even if application parsing rejects the packet or the packet pool is full. The check runs after pending RX is drained and TX completes, before the next queued transmission, so sustained outbound traffic cannot postpone recovery.

The default silence thresholds are 30 minutes for a soft RX/AGC re-arm and 12 hours for a radio-only hard recovery where supported. MQTT observer radio.watchdog settings can override the soft interval. A successful hard recovery starts a fresh observation window to limit repeated resets on a quiet mesh; a failed hard recovery remains escalated and retries after 30 seconds. Targets with RADIO_LIVENESS_SOFT_ONLY repeat soft recovery at the soft interval because their radio cannot be reset independently. These recoveries do not reboot the MCU. The repeater's rx.watchdog is a separate whole-board reboot watchdog, enabled by default with a 24-hour sliding deadline since the last received packet passed MeshCore parsing. Its timer is updated before receive delays and routing filters; raw LoRa reads rejected by the parser do not count. MeshCore parsing checks the packet structure, not sender authentication or decryption of traffic addressed to other nodes. Temporary radio windows of at least 12 hours use a 12-hour deadline; shorter windows disable that reboot watchdog until the window ends. The temporary timing overrides are held in RAM and do not replace saved normal-mode preferences.

The status read uses the two-byte GetStatus transaction. RadioLib 7.8.1 also reads the response byte directly; the dedicated probe additionally preserves the existing SPI layout, checks transport errors, and avoids waiting on BUSY. It does not wake a duty-cycling radio to inspect its mode.

Validation lives in test_noise_floor_estimator, test_rx_power_saving, test_radio_liveness, test_radio_receive_contract.py, and test_sx126x_receive_mode.py, and test_radio_receive_retry.py. These host checks do not establish battery-life or hardware fault-recovery results.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/","title":"Rak3401 all roles cpu sleep 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#rak3401-all-firmware-cpu-sleep-checks-21-october-2026","title":"RAK3401 all firmware CPU sleep checks, 21 October 2026","text":"

Follow-up: the targeted RAK release checks record a new Full Room Server PASS at adb11e7b4: three physical LoRa round trips, 79.31% sampled CPU sleep and zero post-check errors. The original failed capture below remains unchanged; its RF cause was not established.

All 15 applicable images were installed and measured sequentially on the same RAK3401. All 13 profiles that support power saving passed the CPU sleep capture with PS on. Twelve also passed every runtime check. Full Room Server slept correctly but raised a channel-busy timeout flag, so its overall result remains failed. Terminal Chat and KISS expose no PS setting and showed no sleep. The campaign therefore is not an all-modes qualification pass.

The board is left running Full unified Repeater at efad0076, PS on. Its final core snapshot had zero error flags and an empty queue. The original mctomqtt.service and exact USB ownership were restored without changing its configuration. Updates used application-only packages; the reported bootloader version is unchanged.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#results","title":"Results","text":"

Each row has a complete 90-second CPU capture and an independently matched whole-application readback. Percentages are weighted estimates from sampled processor states, not exact sleep residency or current measurements.

Firmware/profile Source PS Sleeping/total samples Sleep estimate Result Full Companion e16db810 on 5,510/8,000 68.78% Pass USB Companion e16db810 on 6,115/7,999 76.41% Pass BLE Companion e16db810 on 5,931/8,014 74.03% Pass Full Room Server e16db810 on 5,929/8,009 74.01% CPU pass; health flag Reduced Room Server e16db810 on 5,487/7,980 68.67% Pass Full Sensor e16db810 on 5,890/7,992 73.82% Pass Reduced Sensor e16db810 on 6,099/8,028 76.00% Pass Terminal Chat e16db810 unsupported 0/8,012 0.00% PS unsupported; no sleep KISS modem e16db810 unsupported 0/8,054 0.00% PS unsupported; no sleep Full Serial1 RS232 repeater efad0076 on 6,257/8,134 76.94% Pass Reduced Serial1 RS232 repeater efad0076 on 6,428/8,094 79.53% Pass Full internal-flash OTA repeater efad0076 on 6,275/8,106 77.38% Pass Reduced internal-flash OTA repeater efad0076 on 6,138/8,073 75.94% Pass Reduced unified OTA repeater efad0076 on 6,393/8,019 79.69% Pass Full unified OTA repeater efad0076 on 5,875/8,013 73.19% Pass

The safe receipt records application/package hashes, source commits, capability and memory checks, capture flags, timings and final handback health. All 15 capture-integrity checks passed; no halt, step, interrupt masking, lockup or reset was sampled inside any capture window. Debug-control state remained unchanged.

Full Room Server's core.errors changed from 0 to 2 at uptimes 51 and 148 seconds. The value is a bitmask: 0x02 is the latched CAD/channel-busy timeout flag. The dispatcher bounds its channel-busy wait, records this flag, then permits a transmit attempt. The observation does not establish the cause of that busy condition or prove subsequent RF delivery. The CPU capture passed at 74.01%, but the strict zero-error post-check failed and remains visible. The failed result was retained, and this unchanged capture was not repeated.

Terminal Chat and KISS have no power-saving command or application idle-sleep integration. Both completed valid captures with zero sleeping observations; they are unsupported PS coverage, not PS-on passes.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#hardware-and-procedure","title":"Hardware and procedure","text":"

The tested assembly is one RAK3401 core with an external RAK13302 radio and no external NOR flash. Coverage includes Full/USB/BLE Companion; Full/Reduced Room Server and Sensor; Terminal Chat and KISS; and Full/Reduced Serial1 RS232, internal-flash LoRa OTA and unified OTA repeaters. The dedicated RAK13302 plus W25Q16 image is excluded because its required external flash is absent. The unified image exercised its no-NOR/internal-storage path.

The nine non-repeater images are bound to e16db810; the six repeaters are bound to efad0076, after the build corrections below. No claim is made that all images came from one revision. One application was active at a time, with an exact application readback before each measurement. No bootloader or SoftDevice package was installed.

Supported roles acknowledged set powersaving on, confirmed the saved value, and provided a fresh RAM-only diagnostic window. USB command ports were closed for capture. Companion intentionally prevents idle sleep with a configured USB host, so only this target's USB data authorization was temporarily disabled while retaining power, then restored. BLE sessions were disconnected for idle capture and authenticated again afterward.

The nonhalting SWD sampler used MEM-AP reads of DHCSR at jittered 7\u201313 ms intervals for 90 seconds. Sleep acceptance requires at least 5,000 samples, 100 sleeping observations, complete timing and chip-identity checks, unchanged debug controls, and no sampled reset/halt/step/masking/lockup. Read timing must remain within the 10 ms read and 25 ms gap limits. This measures observed CPU sleep states, not just a firmware eligibility counter.

Five early preparations stopped before PS setting or CPU sampling: Full/USB Companion on an arrow-prefixed reply, Full/Reduced Room Server on the first version reply, and Terminal on LF/CRLF handling. The private reader was corrected; a bounded retry of only the read-only version getter retained the first unsupported reply. Their first actual CPU captures appear above, with the earlier failed preparations retained in the JSON. Completed captures were not repeated. An interrupted read-only Sensor application readback was also preserved separately; it did not alter the installed image or repeat a CPU capture.

"},{"location":"test-results/rak3401_all_roles_cpu_sleep_2026-10-21/#build-corrections","title":"Build corrections","text":"

Commit efad0076 fixes two defects exposed while preparing the matrix. All 15 selected packages then passed their build, capability and memory gates.

The Full internal-flash and unified repeaters originally missed the required runtime heap reserve by 412 and 564 bytes. Qualified nRF52 repeaters now use the existing grouped Merkle-proof workspace instead of retaining every source leaf: a 1,536-byte allowance replaces 4,096 bytes, saving 2,560 bytes of worst-case active heap. The 1,024-block source capacity, wire proofs, sensors, tables, stack and remaining reserves are retained. No admission gate was lowered.

The Serial1 RS232 recipe already disables GPS because the bridge owns that UART, but its release capability contract incorrectly required GPS. Both RAK3401 bridge profiles now follow the existing UART ownership contract while retaining the INA monitor drivers. Compatible recipes retain their GPS checks. Validation passed 49 RAM tests, four source-proof suites and two GPS-contract tests. The proof suites include sanitized nRF52 Full/Reduced runs through the unchanged 1,024-block maximum, byte-identical proofs, allocation ownership, backpressure and simultaneous fetching/serving.

This finite sleep campaign does not qualify RF delivery, LoRa OTA installation, UART peer traffic, external storage, regulator savings or battery life. Application readback covers the package bytes, not protected storage or user data; an unchanged runtime bootloader version is separate from a full bootloader-byte preservation check.

"},{"location":"test-results/rak_release_fixes_2026-10-22/","title":"Rak release fixes 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#rak-release-fixes-and-targeted-hardware-checks-22-october-2026","title":"RAK release fixes and targeted hardware checks, 22 October 2026","text":"

Status: RAK4631 Full/Reduced Repeater builds and physical radio checks PASS; RAK3401 Full Room Server radio, CPU sleep and health checks PASS. All six selected RAK release packages passed their existing capacity, capability, Bluetooth DFU and packaging gates.

Firmware and hardware results are bound to adb11e7b4. The later 302e441e2 changes only Mini-selected CLI messages; selecting the non-Mini branches reproduces the prior RAK source byte for byte. Unchanged RAK checks were retained. The machine-readable receipt includes package hashes, RAM requirements, radio counters and CPU observations.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#ram-repair","title":"RAM repair","text":"

The fresh starting source, e34e83e6b, failed both RAK4631 Repeater profiles: Full had 81,668 internal bytes available against 85,152 required; Reduced had 84,680 against 85,152. These are separate from the earlier Full Companion flash repair.

Repeater rule targets cannot contain both a scope and a region. One tagged 32-byte name replaces their two separate names, reducing each actual rule from 332 to 300 bytes. All 63 slots remain, saving 2,016 allocated heap bytes. FPF6/7/8 file formats retain their existing field widths and validation.

Ordinary nRF52 SSD1306 builds now use the existing source-enforced 2 KiB allocation bound for this same concrete driver, correcting its previous 4 KiB estimate. This is a 2,048-byte accounting correction, distinct from the rule-table saving. Stacks, packet pools, allocator/transient reserves, sensor features and admission boundaries remain unchanged.

Target/profile Available internal bytes Required heap bytes Margin Result RAK4631 Repeater Full 81,676 81,088 588 PASS RAK4631 Repeater Reduced 84,680 81,088 3,592 PASS RAK3401 Room Server Full 153,940 58,416 95,524 PASS RAK3401 Room Server Reduced 156,928 58,416 98,512 PASS RAK3401 unified Repeater Full 78,364 74,176 4,188 PASS RAK3401 unified Repeater Reduced 81,368 74,176 7,192 PASS

These are canonical build.sh build-firmware Full/Reduced pairs with Cascade settings, --radio-preset usa-cascade-fixed and --require-ota. The canonical RAK4631 Repeater and Room Server packages retain their normalized internal-OTA target identities. PlatformIO commands ran sequentially.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#channel-busy-repair","title":"Channel-busy repair","text":"

A proven dispatcher defect retained a profile's channel-busy timestamp after its last waiting packet was cancelled or retired. A later unrelated packet could inherit the old age and immediately force a transmit with error 0x02.

New work now retires an unused wait before joining that profile. Queued work, retained driver retries and foreign/direct-OTA owners preserve their live backoff. begin() resets previous CAD state. No RAM fields or idle-loop scans were added. Genuine sustained contention still raises the original timeout.

The original failed Room Server capture lacked enough RF data to prove this was its cause. Both old installed repeaters also had latched 0x02 flags. The new software regressions and finite new-image hardware results are the evidence for this repair; historical failures remain recorded.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#physical-results","title":"Physical results","text":"

Application-only DFU installed the source-bound images on the Mercerwood Pi. USB reopen checks confirmed role, version, EndF body identity and continuous uptime. No chip/data erase, filesystem backup, bootloader or SoftDevice update occurred.

The RAK Tag was the independent LoRa witness. Each case used three fresh eight-byte nonces, exact routed trace replies and advancing target RX/TX counters. USB carried commands and observations; the request and reply crossed the physical radio link. Settings were 917 MHz, SF7, BW62.5, CR5, preamble32, with two-minute temporary radio leases. The RAK targets retained 2 dBm TX power; the Tag used its saved 22 dBm. The excluded 910.525 MHz channel was not used.

Installed image Exact-nonce round trips Core errors before/after Result RAK4631 Reduced Repeater 3/3 0 / 0 PASS RAK4631 Full Repeater 3/3 0 / 0 PASS RAK3401 Full Room Server 3/3 0 / 0 PASS

After radio cleanup, the Room Server completed one 90.009-second nonhalting SWD capture with power saving on: 6,374 sleeping observations among 8,035 samples, or 79.31% weighted sampled sleep. There were no sampled resets, halts, steps, interrupt masking or lockups; debug controls stayed unchanged. Maximum read time was 3 ms and maximum sample gap 16 ms. Uptime advanced continuously; the post-check had zero errors and an empty queue. This measures sampled CPU states, not electrical current or exact sleep residency.

The first Room preparation stopped on an unsupported ota bootloader status getter before any RF or CPU capture. Its failure and repeater restoration are retained. The corrected room-only follow-up treats that optional status page as unavailable while retaining mandatory bootloader-version and application identity checks. It did not repeat the already-passing RAK4631 tests.

Final handback leaves both RAKs on patched Full Repeater images, saved 917 MHz settings and power saving on. The RAK3401 gateway service is active again with its original configuration and exact USB owner; final core errors were zero.

"},{"location":"test-results/rak_release_fixes_2026-10-22/#regression-checks-and-ci","title":"Regression checks and CI","text":"

The shared CAD fix initially exposed a 44-byte Mini Repeater flash overflow. Mini-only CLI text was shortened without removing commands, values or checks. Its exact release-wrapper build now passes at 245,696 of 245,760 application bytes; RAM passes at 43,920 available against 32,288 required. Four replay integration and eight Mini-profile tests passed. Other platforms select their unchanged messages.

The updated CI run passed all 12 firmware and support jobs, including both new RAK build pairs. The main test job exposed an outdated trace-timing extraction fixture: it omitted the dispatcher's new queue helpers. The fixture now extracts those production methods and supplies their state dependencies; all five test methods and ten normal/mutation invocations pass with ASan/UBSan. This fixture-only correction requires no repeated firmware or hardware checks. These targeted checks do not claim a new all-board campaign, RS232 cable traffic, physical OTA installation, maximum-load operation or a long RF soak.

"},{"location":"test-results/regulator_recovery_2026-10-18/","title":"Regulator recovery 2026 10 18","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/regulator_recovery_2026-10-18/#regulator-selection-and-recovery-qualification","title":"Regulator selection and recovery qualification","text":"

Baseline firmware source: 388134599. Later STM32 compatibility and fixture source: a2b2a8616. Post-campaign timing correction: 5083c236b. Version base: v1.18.0.1-halo-keymind-cascade-dev; package filenames and build records identify their source suffix. PlatformIO commands ran one at a time. Local software qualification is complete: eleven baseline target selections produced twelve packages at 388134599, five STM32 targets passed at a2b2a8616, and a later RAK4631 Full build passed at 5083c236b. The scoped RAK4631 recovery, shipping-image checks and final restoration also passed; hosted workflow status is recorded separately below.

The machine-readable evidence records the exact requested and packaged targets, application and container SHA-256 values, memory admission, profile reductions and source-bound linked checks. These are locally built qualification packages; this record does not publish a release or establish a successful GitHub workflow run.

"},{"location":"test-results/regulator_recovery_2026-10-18/#behavior-covered","title":"Behavior covered","text":"

The four main firmware roles save independent mcu.regulator and radio.regulator choices (auto, dcdc, ldo) where supported. Saving changes the next boot; it does not change an active radio. auto retains the board default. An explicit setter clears only that domain's fallback and crash streak, including when the same value is saved again. Unsupported hardware rejects the selection. Failed initialization reports an unknown active mode.

A domain can fall back to LDO after three consecutive known crash resets while its previous active mode was DC-DC, using a valid voltage-derived battery estimate of at least 50% and a sample at most 60 seconds old at the last heartbeat. Five healthy minutes clear the consecutive streak. The saved LDO fallback remains until an explicit setter clears it; there is no automatic return to DC-DC. Clean resets, cold starts, sleep wakes, unclassified resets, bad retained CRCs and ineligible battery samples cannot extend the streak. ESP32 brownouts are excluded.

Choices are prepared after primary filesystem/ADC startup and before display/radio/wireless initialization. Volatile fallback storage cannot acknowledge a durable setting. Crash bookkeeping uses retained RAM, and failed fallback persistence is retried after boot. Checked nRF52 APIs preserve SoftDevice ownership. KISS and other roles outside the saved-settings integration retain their original board default.

Supported CPU control covers approved nRF52 REG1 circuits. Radio controls cover supported SX1262, LR1110/LR1121 and verified Meshnology W12 LR2021 circuits. Current integrated STM32WL targets retain their existing shared MCU/radio supply configuration and expose neither selector. Their ordinary CLI reports an unsupported setting/command. The exact STM32+STM32WLx build guard removes unreachable persistence/recovery code, not a supported control. Other platforms or future STM32 external-radio drivers retain the generic implementation; regression checks require reconsidering the guard if current STM32 classes gain selection. STM32WL supply requests need a coordinated design because MCU and radio share the regulator (ST datasheet); Wio-E5 already uses SMPS for high-power TX (Seeed documentation).

CLI usage and policy details are in CLI commands and power diagnostics. ESP32 WebConfig exposes authenticated save/reboot controls under Radio \u2192 Advanced \u2192 Power regulators. Firmware-picker metadata does not advertise these new regulator controls.

"},{"location":"test-results/regulator_recovery_2026-10-18/#baseline-packages-at-388134599","title":"Baseline packages at 388134599","text":"Requested target Packaged target and profile Application bytes RAM admission margin Application SHA-256 prefix RAK_4631_companion_radio_full RAK_4631_companion_radio_full (full) 712,600 4,516 39babcaf8228 heltec_v4_3_companion_radio_full_femoff heltec_v4_3_companion_radio_full_femoff (full) 2,559,064 34,432 af0e8cc40a2a PicoW_companion_radio PicoW_companion_radio (auto) 911,148 58,432 23fc6982cec8 muzi_base_duo_companion_radio_full muzi_base_duo_companion_radio_full (full) 571,940 32,560 1d9773c16872 wio_wm1110_repeater wio_wm1110_repeater_lora_ota_no_external_sensors-full-ota (full sensors) 521,908 87,256 588f8e6c2c70 wio_wm1110_repeater wio_wm1110_repeater_lora_ota_no_external_sensors-reduced-ota (reduced sensors) 492,120 88,904 731bb26bcf44 meshnology_w12_sensor meshnology_w12_sensor (full) 1,597,944 127,288 ba348f2791d4 LilyGo_Tlora_C6_room_server_ LilyGo_Tlora_C6_room_server_lora_ota_no_external_sensors (reduced) 1,584,776 228,736 d654c84d85ca wio-e5-mini_repeater wio-e5-mini_repeater (auto) 245,652 12,240 c7297a2fadd9 RAK_4631_kiss_modem RAK_4631_kiss_modem (auto) 278,656 172,988 ae7537531dd8 Xiao_nrf52_companion_radio_full Xiao_nrf52_companion_radio_full (full) 583,772 43,944 540f935740b3 Heltec_t114_companion_radio_full Heltec_t114_companion_radio_full (full) 633,672 22,680 c6459844b69e

The C6 Room Server selector uses the build system's Auto policy. The baseline-source build selected LilyGo_Tlora_C6_room_server_lora_ota_no_external_sensors, Reduced LoRa OTA on Auto pass 2 after the expanded Full attempt exceeded flash/partition capacity. Only that exact packaged target/profile is qualified; this is not a C6 Full pass. Wio WM1110's sensor OTA policy produced both Full-sensor and Reduced-sensor packages, recorded separately. The matrix also covers all three nRF52 Full Companion CI smoke targets: RAK4631, XIAO and Heltec T114; their individual status remains visible above.

For each completed baseline package, the verifier recomputed every memory manifest's bound file hash, read the actual application BIN (directly or from the nRF52 DFU ZIP), and checked embedded version/source suffixes where present. KISS has no embedded version/source string; its source binding is the clean source-bound build record and artifact hashes. Each bound UF2 was decoded and matched against that exact BIN/DFU application, allowing only uniform final-block padding; each ESP32 merged image contained one exact copy of its application BIN. Container hashes and application hashes have separate meanings. RAM margins are modeled linked capacity and do not measure runtime allocation or physical high-water.

The baseline RAK4631 Full application is 712,600 bytes of its 712,704-byte application region, leaving 104 bytes. Its RAM admission model has 79,092 bytes available against 74,576 required, a 4,516-byte margin. The package remains the Full profile and retains its verified BLE/USB, logging, mOTA, Meshtext and Reader capabilities.

The first matrix at ef2edc52005f6bc64880911e9d08d7f782a0ddfb passed seven targets, then failed Wio-E5 Mini Repeater with a 1,908-byte flash overflow. Commit 7e3359c1a added the exact unsupported STM32WL path; its final rebuild result is recorded in the table rather than inferred from the change. No deployed filesystem boundary or working feature was removed to resolve that failure.

"},{"location":"test-results/regulator_recovery_2026-10-18/#stm32-compatibility-packages-at-a2b2a8616","title":"STM32 compatibility packages at a2b2a8616","text":"

The earlier CI run at 388134599 exposed a flash overflow in the small RAK3x72 USB Companion and a missing regulator mock in an extracted fleet-command fixture. The compatibility change keeps the exact integrated STM32WL driver on its existing shared supply, omits eight unsupported regulator vtable entries and lets the ordinary CLI report an unknown selector. The RAK3x72 conversion preserves all eight 12-bit ADC samples and the exact prior calibration/truncation using integer arithmetic. Native tests compare all 32,761 valid sample sums; the maximum product is 283,374,000 and fits uint32_t. Contacts, channels, interfaces and deployed filesystem boundaries are retained.

Target Actual BIN bytes Application region Actual flash margin RAM admission margin Application SHA-256 prefix RAK_3x72_companion_radio_usb 229,284 229,376 92 6,848 613708364b7d wio-e5-mini_companion_radio_usb 229,356 229,376 20 6,816 e1aab0f4e12f wio-e5_companion_radio_usb 229,204 229,376 172 6,856 e797ca7e77b1 Tiny_Relay_companion_radio_usb 229,200 229,376 176 6,848 5882f6965730 wio-e5-mini_repeater 245,280 245,760 480 12,240 80f462312e8d

These are the four exact STM32 USB Companion CI targets/flag sets plus the Mini Repeater rebuild. The USB Companions retain 32 KiB LittleFS; the Mini Repeater retains its existing 16 KiB LittleFS. Each BIN includes 352 bytes of alignment/layout extent beyond PlatformIO's summed flash sections; the table uses the actual packaged BIN size. In particular, Mini USB has 20 bytes of application space remaining. Independent verification matches every initialized ELF flash-load byte and Intel HEX byte to its BIN, checks all memory-bound file hashes, verifies the embedded version/source suffix and excludes private regulator fault markers. These checks establish packaged size and consistency, not hardware behavior or wireless self-update. The cable-only Mini Repeater retry omitted --require-ota after that flag was rejected during preflight.

The earlier eleven targets were not all rebuilt at a2b2a8616. Their results keep source 388134599. Read-only preprocessing of the actual MainBoard/Radio/mode headers and complete settings implementation shows identical tokens to the baseline in six ordinary configurations: generic, ESP32 with the STM32 radio name, nRF52, RP2040, future STM32 with an external SX1262, and STM32 with no specified driver. Eight-method override/runtime-dispatch controls preserve the generic virtual API; only exact STM32+CustomSTM32WLx loses those unsupported slots. The preprocessing proof uses native adapters and is a conditional-source/API comparison, not a new whole-firmware build for the other platforms. It compares 388134599 with a2b2a8616; it does not include the later intentional Morse scheduling change below.

The source a2b2a8616 GitHub workflow finished with eleven jobs passing and the main test job failing. The failure was a missing regulator lifecycle adapter in an extracted Morse role-loop fixture. Fixture-only commit a08d7f07a fixes that dependency and two downstream console/setup-GUI fixtures; 24 affected and related suites (121 tests) pass locally. The new source 5083c236b workflow is in progress; no successful hosted workflow is claimed for these corrections.

"},{"location":"test-results/regulator_recovery_2026-10-18/#software-browser-and-linked-checks","title":"Software, browser and linked checks","text":"

These are focused campaign checks. Method counts identify suite coverage; they are not a claim that the entire GitHub unit-test workflow completed in one run.

Check Result Coverage Pure recovery policy PASS, 16 scenarios Three crashes; domain independence; >=50% boundary; stale/invalid samples; CRC/corruption; clean reset; healthy interval; sticky fallback. Settings integration and actual API dispatch PASS, 21 methods Real settings implementation with ESP32/RP2040/nRF52 filesystems; deferred application; failed writes/rename/corruption; absent/volatile storage; capacity guards; exact STM32WL unsupported path and future external-radio guard controls. STM32 Companion profile/ADC checks PASS, 21 methods Existing capacities, storage, interfaces and all 32,761 ADC sums retain the prior result. Fleet and related extraction fixtures PASS, 65 tests across nine suites Real-codec fleet 44 with sanitizers; local/remote access 4; primary persistence 1; lost reply 1; save results 3; uncertain save 1; G3 dispatch 7; Mini logging 2; fleet role wiring 2. Radio regulator selection PASS, 6 methods SX1262, LR1110, LR1121 and LR2021 boot choices, supported circuits, initialization failure, repeated LoRa/GFSK initialization. nRF52 CPU/reset/watchdog integration PASS, 5 methods Checked SoftDevice API; actual versus requested mode; early reset capture; clean planned watchdog expiry and cancellation/rearm. nRF52 linker retention reservation PASS, 16 methods All eight application linker scripts; 64-byte NOLOAD reservation; deliberate overflow rejection; unchanged clock and OTA authentication ranges. nRF52 BLE DFU handoff PASS, 11 methods Clean callback only after successful final handoff checks; actual ARM/Unicorn path; aborted handoffs remain armed. WebConfig regulator controls PASS, 4 methods One native JSON/authenticated-save/forced-reboot method and three Chromium methods; supported choices, clearing fallback and failed-save behavior. Reader packing/lookup/UI PASS, 15 methods All 879 references and canonical ASCII bytes; terminal pages; screen layouts/fonts/navigation; bookmarks; backpressure and Morse guards. Adjacent CLI/preferences/button/G3 suites PASS, six suites Settings contract, CLI error/capacity bounds, CommonCLI argument bounds, Companion preferences transactions, headless buttons and G3 dispatch Complete build-profile selection suite PASS test_build_profiles: OK Existing WebConfig coverage PASS 25 real Chromium methods, three lost-reply methods, 13 source methods, seven secrets methods and five generated-gzip methods Baseline RAK Full ELF check (388134599) Result Linked RAK crypto PASS Linked RAK workspace PASS Linked RAK reader PASS

The crypto probe executes linked ARM RFC8032 keypair/signing vectors and bounds their function frames. The OTA workspace probe executes empty-queue acquisition/release and checks protected authentication ranges. The Reader probe executes the actual linked replyVerse instructions, checks canonical output/continuation commands, 157-byte reply guards and NUL termination, and measures callee frames plus a 1,536-byte caller reserve. These checks do not establish full-firmware timing, ISR/SoftDevice stack use or hardware behavior. Linked Reader negative controls also reject a valid DEFLATE stream with one altered word and a balanced enlarged function frame measuring 9,984 bytes. Only disposable ELF copies were mutated; they were never flashed or published.

"},{"location":"test-results/regulator_recovery_2026-10-18/#post-campaign-morse-scheduling-correction-at-5083c236b","title":"Post-campaign Morse scheduling correction at 5083c236b","text":"

Review of the failed Morse fixture also exposed a foreground timing issue: the periodic median-of-three battery read includes two 5 ms gaps, and a failed fallback save can block on flash. The production service now defers those two periodic operations while keyed Morse TX/RX is active, using the existing Morse timing policy. Active-domain tracking and one-second recovery heartbeats continue. Deferred sample/retry deadlines stay unchanged, stale samples cannot qualify a crash, and overdue work runs on the next eligible service tick. Unkeyed CW does not defer these operations. Boot-time sampling and deliberate recovery rearming retain their fresh-measurement requirements.

The real settings/API implementation passes 23 methods with ASan/UBSan, including TX/RX controls, no ADC/delay/flash retry during timing, changing active-domain masks, heartbeats, samples older than 60 seconds, resumed overdue work and saved LDO after a cold boot. Disabling the guard in a disposable source copy produces the expected ten failing platform subtests. The fixture-only correction at a08d7f07a separately passes 24 suites / 121 tests; it does not change firmware source.

A clean source-bound RAK4631 Full rebuild at 5083c236b passes with an actual application of 712,616 bytes, leaving 88 bytes in the existing 712,704-byte region. The modeled RAM margin remains 4,516 bytes, and the Full capability set is unchanged. Bound file hashes, ZIP/BIN/UF2 and ELF flash-load bytes agree; no private regulator fault markers are present. Linked crypto, OTA workspace and all-reference Reader checks pass. The new linked Reader check reconstructs all 879 references / 95,689 text bytes through 1,091 calls and 379,674,263 ARM instructions. Its maximum callee frame remains 6,016 bytes; with the 1,536-byte caller allowance this is 7,552 of 8,192 bytes, within the same model as the baseline.

The earlier package matrix and completed physical campaign retain their own tested revisions. Other target packages were not all rebuilt at 5083c236b, and the new periodic-task guard has not received physical Morse timing or fault qualification. The source change keeps the exact unsupported STM32WL branch unchanged.

"},{"location":"test-results/regulator_recovery_2026-10-18/#reader-flash-and-stack-preservation","title":"Reader flash and stack preservation","text":"

The Reader's pinned source, all 879 references and 95,689 ASCII text bytes (96,568 bytes including the 879 NUL terminators) are unchanged. Packing uses 4,096-byte blocks instead of 2,048-byte blocks. Compressed data plus index fell from 42,775 to 38,654 bytes, saving 4,121 flash bytes with no removed text, feature or heap cache. Canonical source JSON SHA-256: 916bf9d1feae0ad410d69f61e6c5f328ac6acba10651d751b5bb29719732c49e.

ARM GCC source-compiled frame bounds including decoder helpers were 6,264 bytes for terminal output and 6,400 bytes for the adaptive screen path. Adding the 1,536-byte outer-caller reserve gives 7,800 and 7,936 bytes, within the existing 8,192-byte loop stack. The linked replyVerse probe reconstructs all 879 references through 1,091 calls and 379,674,263 ARM instructions, checking all 95,689 text bytes. Its maximum callee stack is 6,016 bytes; adding the 1,536-byte caller allowance gives 7,552 of 8,192 bytes, leaving 640 bytes within that model. Neither compiler bounds nor emulated stack use substitutes for a physical high-water measurement.

"},{"location":"test-results/regulator_recovery_2026-10-18/#mercerwood-hardware-qualification","title":"Mercerwood hardware qualification","text":"

The physical campaign used one RAK4631 connected to Mercerwood Mesh Pi, bound to source 388134599. The private fault image was Full Companion with Reader omitted, retaining the Full USB/BLE transport configuration. Fault injection was identity-bound and explicitly armed once per fault boot. The separate shipping phase used the unmodified Full Companion with every shipping feature, with no fault instrumentation. Earlier legacy USB transport failures do not establish a recovery pass or show that the fault timer caused those failures.

Both images passed all five saved-mode combinations: (ldo, ldo), (dcdc, ldo), (ldo, dcdc), (dcdc, dcdc) and (auto, auto). The ten completed cases verified independent MCU/radio selection, unchanged active modes until reboot, the expected active modes after reboot, and zero crash counts/fallback after intentional reboots. On this board, auto selected DC-DC for both domains.

Three explicitly armed ARM UDF faults produced observed counter transitions 0 \u2192 1 \u2192 2 \u2192 3 in both domains, with three USB-instance changes and lower post-reset uptime. Both domains stayed in DC-DC without fallback at counts 0\u20132; both selected sticky LDO at count 3 while the requested choices remained auto. The voltage readings at these observations were 4,253\u20134,284 mV, estimated 100%. USB was connected; physical battery presence was not independently verified. These observations qualify the voltage-estimate gate, not battery-only operation or the exact 50% boundary on hardware.

The healthy window advanced device uptime by 314 seconds on the same boot. Neither a fourth reset nor an automatic return to DC-DC occurred. Both crash streaks cleared to zero after five healthy minutes while both sticky LDO fallbacks remained. Updating to unmodified shipping Full preserved both fallbacks.

Shipping Full also returned exact canonical ASCII for Reader references 1:1 (79 bytes, one page) and 21:17 (290 bytes, three pages), through four bounded queries with valid continuation commands. The USB instance stayed the same, uptime did not decrease and no new core errors appeared. This is a read-only USB check; it does not establish shipping Full/BLE fault or load qualification.

The first shipping manual attempt stopped at a serial ownership guard during reconnect. A subsequent read-only check proved both interfaces unowned. The unchanged guarded driver then completed all five cases from the start without another DFU. The stopped attempt remains separate evidence and is not counted among the ten completed cases; no owner was killed, service stopped or guard bypassed.

Cleanup removed only the owned trial regulator files and directly proved all three paths absent, matching the directly observed pretrial state. Original identity/preferences file fingerprints remained unchanged. The earliest pre-session absence of the new REG1 path was inferred from source/session provenance rather than captured directly: a prior diagnostic's exact 12-byte record was privately exported and verified before guarded cleanup. The current campaign directly observed absence before its mode tests and again after cleanup.

Finally, the exact original application package was restored. The original full version, complete device information/PIN, identity, contacts, all 40 meaningful channel entries and all eight checked power/USB preferences matched the saved baseline. Channel comparison preserves names and complete keys, excluding only unused bytes after the name's first NUL; it does not claim every raw reply-padding byte is identical. No bootloader update, SWD, hub power operation or test mesh message was used. Private diagnostic images were kept out of releases and moved out of the normal build-discovery directory after testing.

Physical check Result Private Full saved choices and normal reboots PASS, five combinations Private Full three qualifying real faults, then LDO PASS, both domains Healthy interval leaves sticky LDO PASS, 314-second uptime advance; streaks clear Unmodified shipping Full manual/read-only Reader phase PASS, five combinations and four Reader queries Cleanup restores original trial-file state PASS, direct absence proof Original firmware and stored state restored PASS, independently compared against baseline

The machine-readable record includes the exact private diagnostic, shipping and restored-original package hashes. Independent evidence review confirmed the completed mode matrices, fault sequence, healthy window, Reader output, cleanup and restoration. Private snapshots, keys, channel payloads and device serials are excluded.

"},{"location":"test-results/regulator_recovery_2026-10-18/#physical-acceptance-still-required","title":"Physical acceptance still required","text":"

Battery percentage is a voltage estimate, not independent proof of a physically connected battery. Sample freshness is relative to the last retained heartbeat, not elapsed wall time after a hard hang. Runtime load, current and supply measurements remain separate acceptance evidence.

"},{"location":"test-results/room_mailboxes/","title":"Room mailboxes","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/room_mailboxes/#personal-room-mailbox-validation","title":"Personal room mailbox validation","text":"

Validated on 2026-10-10 against the source changes in this commit. These checks cover the room-side implementation, not a sleeping tracker client or a live LoRa/Wi-Fi transfer. No lab radio was flashed for these mailbox checks.

"},{"location":"test-results/room_mailboxes/#behavior-exercised","title":"Behavior exercised","text":""},{"location":"test-results/room_mailboxes/#automated-checks","title":"Automated checks","text":"

The mailbox store passed 3,978 scenarios on each of five filesystem API variants (19,890 total), using AddressSanitizer, UndefinedBehaviorSanitizer and OpenSSL SHA-256. The wire/CLI protocol passed 1,393 scenarios per variant (6,965 total), using the firmware's actual Crypto SHA-256 implementation and the same sanitizers. Adafruit file mocks reject default construction, matching the actual nRF52 library.

Production room transport tests passed 18 cases. Actual JSON handlers passed seven existing base, six existing administrator and six new mailbox groups with real ArduinoJson and stores. The delivery scheduler, room login, receive admission, post permissions, HTTP ownership and stock app compatibility checks passed after updating their production-code adapters.

Chromium passed 17 existing room browser tests and nine mailbox tests, including new radio mailbox provisioning without downloading another owner's body. These checks are included in the unit-test workflow.

"},{"location":"test-results/room_mailboxes/#firmware-builds","title":"Firmware builds","text":"

The following Room Server builds passed the normal flash and runtime RAM gates:

The first nRF52 build exposed a non-default-constructible Adafruit file handle; the implementation and fixtures were corrected before final builds passed.

"},{"location":"test-results/room_mailboxes/#stack-review","title":"Stack review","text":"

The actual T1000-E ARM ELF retains the existing 8 KiB loop stack allocation. The largest mailbox store frame is 1,320 bytes. Streamed policy reads avoid loading the full receipt table in the delivery scheduler; write and readback verification run sequentially rather than stacking both operations.

A representative LoRa CLI send chain has 5,632 bytes of visible frames; the binary request path has 4,352. These estimates exclude deeper filesystem, library, virtual-call and task overhead, and are not proven worst-case bounds. They do not replace a live long-duration stack-watermark test.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/","title":"Rs232 bridge diagnostics 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#rs-232-bridge-diagnostics-22-october-2026","title":"RS-232 bridge diagnostics, 22 October 2026","text":"

Follow-up: the RAK RAM repair and hardware checks close the Full/Reduced build failure below at adb11e7b4. Physical RS232 cable and line-driver testing remain outside that follow-up's scope.

Status: host tests and ESP32 Full build PASS. The RAK4631 Reduced sensor/LoRa OTA build passes, but its Full sensor build fails the RAM gate. The same Full failure occurs on the previous source, before these diagnostics. The required nRF52 Full/Reduced pair remains incomplete and is not a qualified release. No radio was flashed or contacted in this work.

The feature implementation is 281f1a519, with additional host UART platform adapters in df08877c1. The ESP32 build used the implementation source with those test-only adapters already present locally. The nRF52 build used committed df08877c1.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#behavior","title":"Behavior","text":"

The existing get rs232.status heartbeat/automatic-standby report remains. get rs232.transport adds UART state, baud, RX/TX pins, packet source, partial/pending frame state and historical errors. Separate RX, TX, drop and test counter pages distinguish framing/checksum failures, pool pressure, blocked/short writes, filters and duplicates. Statistics occupy 120 bytes inside the bridge object, with no new persistent preferences.

rs232 test [8 hex digits] queues a reserved, one-way UART control frame. The receiver consumes it without allocating a mesh packet, updating duplicate tracking, establishing heartbeat liveness, replying or forwarding onto LoRa. Confirm receipt using the other updated board's increased probe rx count and matching rxid; local UART write completion does not prove delivery.

Fault testing found that cancelling a partially emitted control frame could confuse an older receiver's following frame. The implementation cancels an entirely unsent probe at its deadline, but retains and drains an already-started frame's original suffix. It never retransmits the prefix or interleaves another frame. A timeout records one failure; a later local completion can also count as sent. Each loop's diagnostic work remains bounded.

Both usb.logging on and usb.debug on enable bounded RS232: summaries in ordinary bridge images, at most once per second when diagnostic state changes. CLI pages and test probes require neither logging gate. See the setup and diagnostic guide for exact commands.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#embedded-build-checks","title":"Embedded build checks","text":"

Canonical builds used version v1.18.0.1-halo-keymind-cascade-dev, Cascade runtime defaults and the USA Cascade preset. These were compile/package checks, with no physical UART, LoRa, USB capture or OTA operation.

Target/profile Available internal bytes Required heap bytes Result Heltec_v3_repeater, selected Full MQTT/UART/ESP-NOW image 209,472 203,920 PASS RAK_4631_repeater, Full sensors + LoRa OTA 82,324 85,152 FAIL RAK_4631_repeater, Reduced sensors + LoRa OTA 85,336 85,152 PASS Previous e7b863494, same RAK Full recipe 82,324 85,152 FAIL

The ESP32 Full image passes its application/merged package and linked capability checks with no feature reductions. Its largest internal region is 176,704 bytes against a 21,184-byte contiguous requirement. These are linked capacity estimates, not running free-heap or maximum-load measurements.

The RAK Full shortfall is exactly 2,828 bytes on both sources. No memory guard or reserve was lowered. The new nRF52 bridge allocation is 2,656 bytes, within the unchanged 2,816-byte source assertion. Reduced is a separate supported profile; its pass does not qualify Full or replace the failed required pair.

"},{"location":"test-results/rs232_bridge_diagnostics_2026-10-22/#host-validation","title":"Host validation","text":"

Physical cable/line-driver behavior, actual peer probe receipt, UART sleep rendezvous, USB debug capture and end-to-end LoRa delivery remain untested for this change.

"},{"location":"test-results/s3_memory_soak_validation/","title":"S3 memory soak validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/s3_memory_soak_validation/#esp32-s3-ota-memory-experiment","title":"ESP32-S3 OTA memory experiment","text":"

This experiment compares the current static S3 OTA workspace (A) with OTA_HEAP_CONTEXT=1 (B), with WiFi and a real TLS MQTT connection enabled. It is isolated on experiment/s3-ota-memory-soak; published recipes retain their existing defaults. A short stress test cannot establish months of uptime.

"},{"location":"test-results/s3_memory_soak_validation/#hardware-and-scope","title":"Hardware and scope","text":"Board Role Flash PSRAM Lab identity Heltec V4.3 OLED, standard V4 Observer/MQTT repeater 16 MiB 2 MiB QSPI USB MAC 44:1B:F6:69:CF:98 Original SenseCAP Indicator LoRa Full companion plus experimental MQTT 8 MiB 8 MiB octal WiFi MAC D8:3B:DA:75:23:AC Seeed XIAO ESP32-S3 with WIO LoRa Observer/MQTT repeater 8 MiB 8 MiB octal MAC 28:84:85:B4:09:80, Windows COM31

This is not an R8 test. Both Indicator variants use the same 320x320 canvas; the native 480x480 Full-plus-MQTT configuration is not qualified by this run. One custom TLS MQTT slot per board publishes only to the isolated Pi broker. These results do not qualify five concurrent slots, JWT renewal, heavy RF traffic, every OTA receive/apply path, or months of operation.

"},{"location":"test-results/s3_memory_soak_validation/#reproducible-builds","title":"Reproducible builds","text":"

Base commit: bc0fb2c335938c97220de51098cea6162f2f0fe0, following PR #7's merge. Copy tools/hil/s3_memory_soak.ini to ignored platformio.local.ini and build the six named environments sequentially, with MESHCORE_ESP32_FULL_BUILD=1. Never run multiple PlatformIO processes in this checkout. Archive each image, ELF, map, partitions, bootloader, and memory report before the next build.

The private broker's public CA must be in ssl_certs while generating these experimental images. Remove it and platformio.local.ini afterward. Private keys stay on the Pi. WiFi credentials and the Windows logger token are in private local files; do not commit them or the generated certificate bundle. Broker certificate verification remains enabled.

The V4 and XIAO lab recipes also enable MESH_SOAK_WIFI_KEY_OVERRIDE and require the ignored local header tools/hil/S3SoakWiFiKey.h. To reproduce diagnostics using ordinary WiFi credentials, remove that define from the lab recipe; the private header is then unnecessary. Published recipes do not enable either lab flag.

Image Application bytes Link-time available internal bytes SHA-256 V4 A 1,910,392 237,360 68c2749e0ea40db91d953f6355010bc602f85424526ca6acbe1cad7fd11c8630 V4 B 1,911,256 252,760 6847ad4ad6bac2e558b8aed67de37417a497972391f13a71e0a42b5c9704dcff Indicator A 2,313,256 244,248 3d0ac27f95ddc54a32b59a3892d157713dcded285dbeb98d1bdfdb1ce0b951ab Indicator B 2,314,136 255,496 83045d7ec48befc43f3460c08c68b71da037a5476e14dd59def5706fc5a1b07e XIAO A 1,845,000 238,448 6fcc657c5ccbc5dd3d53a0cf1ceebde54479d59d329f8625e7cb7141a27d9ed8 XIAO B 1,845,912 253,864 2b233e42e0c515b152ce83ca8fe38b1f35780970bb6350dbadf2591b6a2699a1

All six firmware builds passed their RAM/flash gates. The linked internal capacity gain is 15,400 bytes on V4, 11,248 bytes on Indicator, and 15,416 bytes on XIAO. This is capacity returned while idle, not a reduction in peak OTA allocation needs. Both A/B variants keep the existing MQTT client reuse policy and internal MQTT task stack; this experiment does not move that stack into PSRAM.

"},{"location":"test-results/s3_memory_soak_validation/#instrumentation","title":"Instrumentation","text":"

MESH_SOAK_DIAGNOSTICS exposes local-only get soak, soak ota-cycle, and soak wifi-drop commands. They report free/largest/minimum internal heap, free PSRAM, OTA context size/presence, WiFi state, and uptime. No published recipe enables these controls.

The lab network has a valid 64-digit raw WiFi key. Production MQTT preferences currently accept only 63 characters. The V4 experiment uses a separate, test-only NVS key through MESH_SOAK_WIFI_KEY_OVERRIDE, without changing the production binary preference layout. This does not fix released raw-key input. The network's original nine-character passphrase was subsequently recovered from the connected Windows profile; XIAO uses it through ordinary settings. The override is compiled into both XIAO images but no override key is stored.

The Pi logger owns one persistent V4 USB handle: opening this particular USB device resets it, even with DTR/RTS disabled. Firmware installations and logger restarts therefore begin new uptime runs. Indicator telemetry uses TCP 5002. The logger distinguishes the 49.7-day millis() rollover from a reboot.

Passive samples omit get mqtt.stats: its outbox query can block behind the MQTT client lock during a TLS handshake and stall the CLI. The logger reads get mqtt.status and separately counts messages received through a verified TLS subscription. It stores topic counts, not MQTT payloads or credentials.

Two exploratory rapid-flap runs used an overly short 90-second recovery cutoff. The first also used the blocking outbox probe. Preserve those pilot results; do not count them as successful tests. Production MQTT backoff deliberately grows to five minutes and clears after two stable connected minutes, so rapid flapping is not equivalent to repeated independent connection tests.

"},{"location":"test-results/s3_memory_soak_validation/#results-and-continuing-soak","title":"Results and continuing soak","text":"

V4 and Indicator both passed the matched A/B workload: 200 explicit OTA context acquire/release attempts, four MQTT bridge restarts, one WiFi reconnection and one five-second private-broker outage per image. An initial bridge restart preconditions each run. No unexpected uptime resets occurred in these runs. A was paused after its four bridge restarts to investigate the display report, then resumed for the network faults; its elapsed duration includes that pause.

After the workload, with WiFi and one TLS MQTT slot connected:

Board A free internal bytes B free internal bytes Gain A largest block B largest block Standard V4 95,396 111,040 15,644 (15.28 KiB) 81,908 98,292 Indicator 107,168 118,520 11,352 (11.09 KiB) 94,196 106,484 XIAO (same saved settings, cold boot) 151,028 166,444 15,416 (15.05 KiB) 139,252 147,444

Free heap varies with network activity; link-time capacity is the cleaner measurement of storage moved out of static RAM. PSRAM remained about 2.01 MB on V4 and 6.33 MB on Indicator. In each B run the OTA context returned to absent after the allocation workload. Neither board's largest block shrank through those 200 allocation cycles or the reconnect workload. WiFi/MQTT recovery took about 4.3 seconds on A and 3.8 seconds on B; broker recovery took about 19.5 and 21.8 seconds respectively. These are single observed recovery times.

Workspace recreation has an observed latency cost. The 200 command cycles took approximately 189 seconds on V4 B and 379 seconds on Indicator B, versus 32 and 15 seconds on A. This includes command transport, OTA initialization, and ordinary firmware service, so it is not an allocator microbenchmark. The static comparator already owns an initialized workspace; the dynamic variant recreates it. This experiment does not claim faster OTA startup, and this cost should be checked against real session use before a default change. Code inspection shows that OtaContext::begin() calls ota_self_firmware(), which scans the running ESP32 application for its EndF identity on each call. All six experimental images contain valid EndF trailers. Caching immutable boot identity is a candidate for a separate change, not part of these results.

The real WiFi mOTA folder endpoint was tested separately on Indicator: 20 empty-folder sessions on A and 40 on B. All completed and B released its context. During B's first batch, free internal RAM went from 118,552 to 116,316 bytes, and its largest block from 106,484 to 102,388. A second batch ended at 116,308 bytes with the same 102,388-byte largest block. This retained allocation/layout change is not explained by the now-absent OTA context; record it separately and watch it during the soak. It did not keep growing in the second batch.

The original A folder script also tried V4, whose observer recipe has no TCP 5001 folder source. Its overall result therefore says passed: false, despite Indicator completing all 20 sessions. That endpoint mismatch is preserved in the evidence; it is not an OTA failure on V4. The corrected script targets only the Indicator. These empty-folder checks do not qualify a full firmware transfer.

XIAO passed 200 context cycles, four bridge restarts and one WiFi reconnect per image, with no unexpected reset. Its largest block stayed unchanged within each workload. WiFi/TLS recovered in about six seconds for both. This local test did not inject a separate broker outage or exercise a TCP folder endpoint. Verified subscriptions on the Pi confirmed actual TLS publications for both.

The first XIAO A run followed initial device configuration and finished with 126,140 internal bytes free, which would have overstated the OTA saving. It is preserved as xiao-live/A-configuration-stress.json. Reflashing A without an erase, retaining exactly the saved settings used by B, and rerunning the whole workload produced the 151,028-byte baseline above. Its resulting gain exactly matches the 15,416-byte linked-capacity change. The earlier larger gap is not credited to OTA storage; the precise retained allocation was not isolated.

The Pi stores evidence under /home/mikec/hwtest/runs/s3-memory-soak:

meshcore-memory-soak.service holds the USB connection and samples each minute. meshcore-soak-broker.service supplies the private TLS broker. Both are enabled on the Pi. The logger never reboots a board to hide a firmware failure, but reopening the V4 USB transport can itself reset it; inspect transport counts, service restarts, and uptime together when diagnosing any interruption. The final passive phase started at 2026-09-11T07:18:07Z. Installing the final logger revision (which serializes concurrent samples) deliberately restarted the service and therefore reset V4 once. That begins its long-soak uptime run; Indicator retained its uptime. Both then reported WiFi/TLS connected, absent OTA contexts, and actual broker-side publications.

The local XIAO logger owns COM31 and writes under C:\\git\\MeshCore\\out\\s3-memory-soak\\xiao-live. Its control endpoint is loopback-only with a private token. It runs while this Windows session remains alive; it is not a Pi service and will not survive a computer shutdown. A separate verified TLS subscription on the Pi confirmed XIAO publications for both images. The Pi logger's per-device counts cover V4 and Indicator only. The final XIAO B passive phase was verified at 2026-09-11T07:21:21Z, with 166,508 free internal bytes, a 155,636-byte largest block, WiFi/TLS connected, and no active OTA context. The largest block differs from the post-stress sample because this is a fresh boot, which illustrates why comparisons must use the same workload history. A Pi subscription received its publication two seconds later.

Read-only checks on the Pi:

systemctl is-active meshcore-memory-soak meshcore-soak-broker\ncat /home/mikec/hwtest/runs/s3-memory-soak/latest.json\n

On Windows, read out/s3-memory-soak/xiao-live/latest.json. Keep the computer awake for continuous local samples. Neither logger automatically reboots a radio in response to a failed sample.

The public lab CA was removed from ssl_certs, and the temporary local PIO configuration and generated test trust bundle were removed after building. Archived experimental firmware and public evidence remain in ignored out/s3-memory-soak; no test CA enters the published build configuration.

No published default has changed. Leave the experimental B images running to collect longer evidence before deciding whether to expand S3 heap allocation. More idle internal RAM gives WiFi/TLS useful headroom; it does not itself prove stability or eliminate the need to allocate the OTA workspace during use.

"},{"location":"test-results/s3_memory_soak_validation/#physical-display-checks","title":"Physical display checks","text":"

The user reported unresponsive screens during MQTT restart testing. The V4 lit after temporarily choosing on; after restoring battery and USB profiles to button, 15 seconds, the user confirmed that pressing the button wakes it. The synchronous reconnect/diagnostic work may have delayed UI service, but the exact cause of the original visual report was not established.

The T096 is a separate nRF52840 board, USB identity 651F8E496197F882. It was reflashed with current Full firmware and briefly instrumented. The trace showed the radio, sensor and UI loop advancing; the display had initialized but was marked off. Its functional CDC0 was in Binary mode after a host-session close. Sending a newline-delimited +++MESHCORE-TERM-START restored ASCII control. With display mode set to on, the user confirmed the normal screen appeared. Temporary tracing was removed by reinstalling the non-instrumented image, and battery/USB profiles were restored to button-pairing, 15 seconds. The user then confirmed that its button wakes the normal screen after timeout.

T096 recovery firmware: v1.17.1-t096-check, Full/shared OTA queue, -Os size profile, available linked internal RAM 99,892 bytes (73,728 required). ZIP SHA-256 f5cfcb001efda559ae9b4e7b105ac9766a604808f5fd708b7c533f8a11c64578. No device-wide filesystem erase was needed for this recovery. T096 does not count as an S3 A/B test.

Host validation: python -m unittest discover -s tools/hil -p test_s3_memory_soak.py. The five checks cover diagnostic filtering, missing data, resets, and counter rollover. They are not substitutes for the hardware evidence.

"},{"location":"test-results/sensor_dog_tracker/","title":"Sensor dog tracker","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sensor_dog_tracker/#sensor-dog-tracker-validation","title":"Sensor dog tracker validation","text":"

The opt-in Sensor dog tracker was checked on 2026-10-10 using executable host tests and firmware builds. This report qualifies the code paths named below; no radio was flashed for these checks, and battery current, GPS acquisition outdoors, and live RF delivery were not measured.

"},{"location":"test-results/sensor_dog_tracker/#executed-regressions","title":"Executed regressions","text":"Suite Checks test_tracker_route_policy.py 99 boundary and byte-format assertions: six-hour gates, repeater postponement, sticky four-day cutoff, invalid clocks, overflow and restart. test_sensor_tracker_store.py 6,063 storage and failure scenarios over five filesystem API modes, including torn saves, unavailable storage, durable flood reservations and a monotonic request-tag floor. test_sensor_tracker_client.py 22 tests execute actual Tracker.cpp, peer dispatch, route/store/codec and packet hash implementation: setup, Safe/Lost/Unknown cadence, fresh GPS, transmit-based response deadline, exact owner identity, nonce reuse across reboot, path updates and packet ownership. test_tracker_owner_response.py Actual Companion responder across four feature configurations: persistent contact authentication, flags and padding, time-bounded position updates, rate limits, and preserved ordinary message state. test_sensor_tracker_gps.py 5 tests: fresh parser/PVT samples, GPS ownership, 120-second bound, validated UTC recovery after a cold start, stale sync notifications and saved preference restoration. test_sensor_tracker_sleep.py 4 tests: actual sleep gates, nRF52 timer allocation/queue failures, warm radio pause and resume, Dispatcher watchdog anchors, and feature defaults. test_sensor_subscriptions.py 18 tests: existing delivery behavior plus suppression in dog mode and restoration after leaving it. test_radio_interrupt_recovery.py 9 tests: radio recovery preserves the no-flood-retry policy when a missing transmit-complete interrupt could conceal a transmission.

Host C++ tests use AddressSanitizer and UndefinedBehaviorSanitizer where available. Negative controls intentionally remove guards and verify that assertions detect the resulting regressions. Transport, UART, receiver and filesystem hardware are replaced by bounded adapters; they do not qualify physical RF range or current draw.

The ordinary PlatformIO suites native, native_kiss_modem and native_ota_channel also passed: 1,829 test cases, with no failures.

The client checks also cover:

"},{"location":"test-results/sensor_dog_tracker/#firmware-builds","title":"Firmware builds","text":"

All six firmware builds passed their flash and runtime memory checks. Values below are linked firmware bytes, rather than measured running heap usage.

Environment Flash used / limit Static RAM used Runtime internal RAM available / required t1000e_sensor 377,520 / 811,008 64,400 171,112 / 54,320 RAK_WisMesh_Tag_sensor 421,084 / 815,104 66,336 169,176 / 54,320 heltec_v4_sensor 1,490,461 / 6,553,600 97,024 277,760 / 95,296 t1000e_companion_radio_usb 427,964 / 708,608 187,272 48,240 / 38,944 RAK_3x72_companion_radio_usb 229,000 / 229,376 39,780 24,728 / 17,408 wio-e5-mini_companion_radio_usb 229,012 / 229,376 39,804 24,704 / 17,408

The compact STM32 Companion retains its existing feature exclusions and application/storage boundary. Its limited remaining flash is checked by the existing CI build. The workflow now also builds T1000-E and RAK Tag Sensors and the ESP32-S3 V4 Sensor, in addition to the executable tracker regressions. The Wio-E5 Mini check uses the complete v1.17.1.9-halo-keymind-cascade-dev version string and USA Cascade profile.

"},{"location":"test-results/sensor_dog_tracker/#limits","title":"Limits","text":"

Bluetooth Find My/OpenHaystack advertising and automatic Room mailbox polling are not implemented by this change. RP2040's interrupt-driven physical wake has not been tested on a board. The GPS/RF and current measurements still needed on T1000-E and RAK Tag are listed in the tracker guide.

"},{"location":"test-results/separated_radio_modulation_cache_validation/","title":"Separated radio modulation cache validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#separated-radios-normal-xiao-rx-gain-and-unchanged-modulation-timing","title":"Separated radios, normal XIAO RX gain, and unchanged-modulation timing","text":"

Measured 2026-09-14 on keymindCascade, base commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus local changes.

The unchanged-modulation optimization reduces XIAO frequency-only retunes from 548.880 to 451.897 us (17.7%) in a same-image control. The separated four-channel SF6 test still misses packets. Separation and disabling XIAO boosted gain were changed together; their RF effects are not isolated.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#settings-and-controls","title":"Settings and controls","text":""},{"location":"test-results/separated_radio_modulation_cache_validation/#same-image-receive-only-timing-ab","title":"Same-image, receive-only timing A/B","text":"

Each board ran four three-second windows in off/on/on/off order, rebooting between windows. SF6/125 kHz, four channels, 2,612 us nominal dwell, no extra IRQ tracing, no TX. A HIL-only hook invalidates just the acknowledged modulation tuple before each baseline hop. Optimized windows use production cache behavior. The binary and all other settings remain identical per board.

Time is entry to the production retune through BUSY-low observation. It does not include the listening dwell or full application scheduling, and is not an independent RF settling measurement. Unlike the older mixed-bandwidth tests, this frequency-only workload can skip 0x8B.

Board / policy Hops Mean Min\u2013max Actual 0x86 / 0x8B writes XIAO, always write 1,898 548.880 us 535\u2013663 us 1,898 / 1,898 XIAO, reuse unchanged tuple 1,957 451.897 us 441\u2013580 us 1,957 / 0 Indicator, always write 564 8,263.438 us 8,228\u20138,428 us 564 / 564 Indicator, reuse unchanged tuple 602 7,601.193 us 7,583\u20137,762 us 602 / 0

Pooled means are sample-weighted from reported per-window means. Savings are 96.983 us (17.7%) for XIAO and 662.246 us (8.0%) for Indicator. All 5,021 hops passed RX-mode/software-cache checks with zero retune errors or deferrals. Counters observe outgoing SPI opcodes, not merely which software branch ran. Initial setup before counter snapshots is excluded. The Indicator remains above the common 6 ms switching allowance; no common timing budget changed.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#stationary-rf-controls","title":"Stationary RF controls","text":"

Full initialization before every case, fixed RX at 910 MHz, ordinary RX with fast reuse disabled, shuffled offsets, 450 ms capture windows. Method matches the earlier investigation.

TX offset SF6 preamble windows SF8 preamble windows Silent 0/4 0/8 On frequency 4/4 8/8 -250 kHz 0/4 1/8 +250 kHz 0/4 1/8 -500 kHz 0/4 2/8 +500 kHz 0/4 0/8 -1,000 kHz 0/4 0/8 +1,000 kHz 0/4 0/8

All 12 on-frequency probes delivered valid payloads. No off-frequency header or valid payload, silent preamble, device/read error, foreign valid payload, or trace overflow occurred. SF6 off-channel detections changed from 1/24 to 0/24; SF8 from 32/48 to 4/48. These are small bounded samples, not sensitivity or packet-error-rate estimates. Signal strength (RSSI) sampled on-frequency was -41 to -40 dBm at SF6 and -42 dBm at SF8: the signal remains strong, but these are not calibrated attenuation measurements. Improvement cannot be attributed solely to separation or solely to normal gain.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#four-channel-sf6125-51-symbol-visits","title":"Four-channel SF6/125, 5.1-symbol visits","text":"

Same image, baseline then optimized, reboot both boards between runs. Each requested 100 packets/channel with varied unsynchronized arrival timing and stopped at its first miss. Extra nominal 64 us IRQ polling was enabled here, unlike the receive-only timing windows. No higher channel count was tested.

Policy Received before first miss Failed channel Timed hops Mean retune Always write 0x8B 24, then probe 25 missed 1 / 909.750 MHz 4,746 551.253 us Reuse unchanged tuple 51, then probe 52 missed 1 / 909.750 MHz 7,748 455.157 us

Optimized RF run: 7,697 hops omitted 0x8B, averaging 453.081 us; 51 hops reapplied it after received packets, averaging 768.529 us. These are different RX lifecycle paths, not a pure 315 us command-cost comparison. Both runs had one frequency write per hop and no RX-mode/cache/retune errors. Successful packets read -42 to -40 dBm with SNR 10.0\u201311.5 dB. Neither met 100/100 on every channel. More packets before the first miss does not establish a reliable improvement in packet-error rate.

The baseline miss captured a target-channel preamble at 176,194 us after arming, 1,830 us after a hop onto that channel. A guard held it there; no header or packet followed. An IRQ clear was observed at 195,025 us and the next hop completed at 195,240 us. This is not the earlier SF8 wrong-channel hold: the preamble was observed on the intended channel. Its RF source and reason for failing to progress remain unproven.

The optimized miss captured 3,256 hops over ten seconds with no observed nonzero IRQ and no guard hold, reproducing the pre-acquisition symptom of the earlier SF6 miss. Target-channel revisit intervals were 12.249\u201312.324 ms (mean 12.286 ms), shorter than the 16.384 ms programmed preamble, yet no packet was acquired during those short individual visits. This does not establish that additional post-retune settling is needed. Trace buffers did not overflow. TX returned success for both failed probes; the MCU clocks are not synchronized and there was no independent RF capture.

"},{"location":"test-results/separated_radio_modulation_cache_validation/#provenance","title":"Provenance","text":"

RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both targets built and uploads were verified. These captures precede the subsequent SF5/250 bandwidth-selector addition.

Tested firmware SHA-256:

Raw files in tools/hil, preserved byte-for-byte:

File SHA-256 XIAO timing 45e7a640323a3c9028b6c92a5128e51307db1bd94a4846e8744d3c3ec098adfd Indicator timing f0a85826f500b3a9173bf22c6207fbfba3b6cb17f3d70b849a9fe7621ae3dfc5 Stationary SF6 1d981e5af41eabbc43f92641813bf8f7c5e7015439f5ba53ce0467fe6477d3f3 Stationary SF8 36e63da7435d0a16f61db7db7730893de392d028feca7a96960fd17ae1fa624b SF6 baseline scan cbfd200170891b334ef3958479694c6e318146993b94073eb9aee981f97fde97 SF6 optimized scan d88a621ef9aa358ce52bb3efb6aa9c4bcb77c681af20b2c66e4aebf3721e17ca

Fresh complete 8 MiB backups were saved and separately verified in the private current-user backup directory before testing. Restores had already started when the user requested leaving HIL firmware installed; those in-flight writes and verification finished safely. Both boards were subsequently loaded with the newer SF5/250 HIL build for the user's next sweep. Do not restore them again unless requested. No private backup contents were added to Git.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/","title":"Sf10 10 vs 100hz full sample validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#sf10125-full-samples-10-hz-versus-100-hz-first-pass-detour","title":"SF10/125 full samples: 10 Hz versus 100 Hz first-pass detour","text":"

2026-09-14. Both 400-packet samples completed: 100 attempts on each of four channels, with no RF retries or early stop on misses. Payload loss was 64/400 (16.0%) at 10 Hz and 81/400 (20.25%) at 100 Hz. Strict same-channel reception was almost identical: 230/400 versus 231/400. The larger offset showed no improvement in this comparison.

These are two sequential finite bench samples, not proof that the offset caused the difference in payload loss. The earlier stop-first tests could not provide this error-rate estimate and are not combined with these counts.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#settings-and-method","title":"Settings and method","text":"

The complete comparison ran from 21:09:11 to 21:42:39 UTC (14:09\u201314:42 PDT), about 33 minutes. The 10 Hz sample took about 15.4 minutes; 100 Hz about 18.0. The extra elapsed time is largely the additional ten-second timeouts.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#overall-results","title":"Overall results","text":"

\"Payload loss\" means no intact expected payload was delivered within the expectation window, regardless of reported RX channel. \"Strict error\" also counts intact payloads received while the scanner reports another channel.

Requested offset Attempts Correct-channel delivery Intact wrong-channel delivery Timeouts / payload loss Strict errors 10 Hz 400 230 (57.5%) 106 (26.5%) 64 (16.0%) 170 (42.5%) 100 Hz 400 231 (57.75%) 88 (22.0%) 81 (20.25%) 169 (42.25%)

All scored delivery failures were timeouts. The receiver also counted six packet-read errors at 10 Hz and eleven at 100 Hz. The error codes were not captured, so these cannot be identified specifically as CRC failures or mapped one-to-one to timeout probes. \"Zero invalid payloads\" in the summary does not mean there were no packet-read/CRC errors before timeout.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#per-channel-results","title":"Per-channel results","text":"

Each cell uses exactly 100 attempts. Loss counts therefore also equal percent.

Center (MHz) 10 Hz payload loss 100 Hz payload loss 10 Hz strict errors 100 Hz strict errors 909.5 10% 11% 26% 25% 910.5 23% 23% 51% 45% 911.5 19% 26% 48% 55% 912.5 12% 21% 45% 44%

The extra 17 payload losses at 100 Hz occurred on channels 0 (+1), 2 (+7) and 3 (+9). Correct-channel deliveries differed by only one packet overall. Neither offset eliminated wrong-channel acquisition/delivery or timeouts. This comparison does not isolate the underlying RF failure mechanism.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#switching-and-signal-measurements","title":"Switching and signal measurements","text":"Metric 10 Hz 100 Hz Completed hops 16,992 20,676 Mean complete two-pass switch 1263.765 us 1262.551 us Maximum complete switch 1533 us 1532 us Mean first pass 660.712 us 659.458 us Mean second pass 592.344 us 592.406 us RF commands 67,968 82,704 Modulation commands / RX starts 33,984 / 33,984 41,352 / 41,352 Maximum unheld four-channel cycle 174,139 us 173,722 us Maximum held channel visit 590,757 us 640,492 us Packet-read errors 6 11

Unheld cycles fit within the 262,144 us programmed preamble; packet-related held visits can exceed it. Aggregate timing without IRQ traces does not prove which hold caused any particular timeout. The approximately 1.2 us difference in mean switching time is negligible here and does not explain the losses.

All completed hops passed the internal first-offset/final-correction and CR command checks: 16,992/16,992 and 20,676/20,676, detour.bad=0. Second-pass blocks, retune failures, mode errors and cache errors were zero. Counts match four RF writes, two modulation writes and two RX starts per completed hop.

Telemetry limitation: the diagnostic detour.first_rf/first_mod fields are updated on a guard-deferred first attempt as well as an applied attempt, while final_rf/final_mod retain the last completed second pass. Thus the final snapshot's displayed pair can be from different attempts and appears equal in these captures. Those fields must not be interpreted as the last verified pair. Verification itself runs only after two applied passes and increments detour.n only then; its per-hop comparisons, zero failures and matching counts establish the commanded sequence. Original capture bytes are retained.

Packet group 10 Hz mean RSSI / SNR 100 Hz mean RSSI / SNR Correct-channel delivery -34.05 dBm / +8.56 dB -34.15 dBm / +8.62 dB Intact wrong-channel delivery -105.18 dBm / -13.75 dB -105.22 dBm / -13.62 dB

The markedly weaker wrong-channel readings are observations, not proof of a specific filter/image/overload mechanism.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#implementation-and-verification","title":"Implementation and verification","text":"

The HIL scanner gained scancontinue 1, default off, so its ordinary completion and timeout accounting can leave scanning active. The host gained bounded --continue-on-miss --max-channels 4 with separate delivery/strict summaries. scandetouroffset 10|100 and --first-pass-offset-hz 10|100 select the integer offset while stopped. Production radio defaults and receive guards were not changed. The controller is tools/hil/profile_switch_offset_compare.py.

23 native profile tests and 51 host tests passed. Coverage includes continuing through all 400 attempts without restart/retry, balanced channels, separated error definitions, default first-miss behavior, aborting fixture failures, both integer offsets and exact restoration. Build/upload succeeded with hash verification: RAM 94,948 bytes, reported flash 370,213 bytes.

There was one cached USB RX-result replay in each sample, with no RF retransmit. All TX commands returned success. Host/device strict receive and miss counters matched; both captures are complete under fixed_sample_complete. The master manifest is complete under both_400_packet_samples_complete, with no fixture or cleanup error.

Both boards rebooted idle afterward. RX confirmed scanner inactive, zero channels, one-pass default, detour/continue disabled and normal gain 0x94. TX confirmed no autonomous TX. HIL remains installed; RAK COM29 was not opened. The temporary PlatformIO overlay was removed. No background collector remains. This work made no commits or pushes. Existing unrelated changes were preserved.

"},{"location":"test-results/sf10_10_vs_100hz_full_sample_validation/#provenance","title":"Provenance","text":"

Source: live keymindCascade working tree at HEAD 2b5a6f209835f0f67ed73fee78d36447a4566ce6, plus existing dirty changes and these HIL additions. The RX image hash identifies the exact tested build. Earlier captures were not overwritten.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/","title":"Sf10 10hz cr detour validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#sf10125-tiny-first-pass-frequency-offset-and-coding-rate-detour","title":"SF10/125: tiny first-pass frequency offset and coding-rate detour","text":"

2026-09-14. The requested offset no longer rounds to zero. Actual outgoing commands verified a +10-step RF-word change (nominal +9.536743 Hz), then exact restoration of the original word, with CR4/6 then CR4/5. Two passes averaged 1.259 ms in RX-only timing. The first packet still timed out. This is a bounded one-probe failure, not a 400-packet loss-rate estimate.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#scope-and-precision","title":"Scope and precision","text":"

The request was to apply a slightly different center frequency and coding rate on the first full retune, then the correct settings on the second, with about 0.01 kHz difference. At these centers a float in MHz advances in roughly 61 Hz increments: adding 0.00001 MHz can produce the same float.

The HIL implementation does not make that float addition. A scoped integer offset changes the four-byte argument of the actual 0x86 frequency command by ten steps. The nominal SX1262 step is 32 MHz / 2^25; ten steps are 9.5367431640625 Hz, the nearest achievable nominal command offset to 10 Hz. See the frequency-step specifications in the Semtech SX1261/2 datasheet. Pinned RadioLib 7.7.1 also specifies this step and sets the standard sensitivity configuration bit for bandwidths other than 500 kHz. This establishes the programmed offset, not a spectrum-analyzer measurement of physical frequency.

First pass: destination RF word + 10 and CR4/6. Second pass: original RF word and CR4/5. The offset is applied to both existing duplicate frequency writes inside the first pass. Caller buffers remain unchanged; a scoped restore prevents the offset leaking into the corrected pass or an unrelated command. Rollback applies the offset only when restoring the temporary CR4/6 tuple.

Both passes retain ordinary packet/ownership/BUSY guards, command-result checks and RX startup. A blocked second pass fails the diagnostic rather than overriding a packet guard. Each completed hop verifies both outgoing frequency words and modulation words (0x0a040200 then 0x0a040100). The host requires a verification count equal to completed hops and zero mismatches.

This feature is HIL-only, off on reboot. Production defaults were not changed. The earlier +31.25 kHz draft was superseded before flashing or RF testing.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#fixture-and-method","title":"Fixture and method","text":"

Same-image RX-only ABBA controls compare two identical full passes (A) with the first-pass detour followed by correction (B), six seconds each, rebooting between windows. This is not a comparison against the original single-pass approximately 0.53 ms cached-modulation path.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#timing","title":"Timing","text":"Window First-pass detour Hops Mean total (us) Maximum (us) First pass mean (us) Second pass mean (us) A1 Off 139 1230.094 1464 631.727 594.281 B1 On 139 1259.770 1490 647.165 603.245 B2 On 139 1258.806 1481 646.547 602.957 A2 Off 139 1230.597 1467 633.209 593.252

Mean two-pass control 1230.346 us; detour/correction 1259.288 us, about 28.943 us additional total including configuration/verification bookkeeping. The detour passes themselves averaged 646.856 + 603.101 us. Both detour windows verified 139/139 completed hops with zero mismatches; all four windows had exactly 556 RF commands, 278 modulation commands and 278 RX starts, with zero guard blocks or RX/mode/cache errors.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#packet-result","title":"Packet result","text":"

Probe 1021099889, TX channel 3 at 912.5 MHz, timed out: 0 received / 1 attempted, then stopped. TX rc=0, setup 86,914 us, transmit call 529,297 us. RX was in receive mode, hardware device-errors=0. No payload-only diagnostic ran because this was a timeout, not an intact payload attributed to another channel. There were no RF retries.

During that packet window:

Across both detour timing windows and the packet window, 498/498 completed hops verified the nonzero first-pass RF offset, original final RF word and CR4/6-to-CR4/5 sequence. This proves the change was exercised; it did not resolve reception in this bounded test. The underlying RF failure remains unidentified.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#usb-setup-issue-and-verification","title":"USB setup issue and verification","text":"

The first controller attempt stopped before transmitting because the new 126-byte capability JSON plus CRLF formed an exact 128-byte native-USB reply. It remained buffered until another command was sent. This was preserved as a fixture failure, not counted as packet loss. A single LF-terminated write (127 bytes) fixed this specific reply; three independent reads then passed without a follow-up command, and the complete controller ran successfully.

22 native profile tests and 49 host tests passed. Added tests cover integer carry, unchanged caller bytes, both SPI paths, observation of shifted wire bytes, exact restoration, scope exit/nesting, non-frequency commands, overflow, detour verification failures and the USB reply length. Build/upload passed with flash hash verification: RAM 94,948 bytes, reported flash 369,593 bytes. git diff --check passed (unrelated existing CRLF warning only).

Both boards rebooted idle after completion. RX confirmed scanner inactive, zero channels, one-pass default, detour disabled, normal gain 0x94. TX reports no autonomous transmission. HIL firmware remains installed as requested. RAK COM29 was not opened. Temporary PlatformIO overlay removed; no collector left running. This experiment made no commits, pushes or production-default changes.

"},{"location":"test-results/sf10_10hz_cr_detour_validation/#provenance","title":"Provenance","text":"

Successful manifest complete, stopped bounded_tests_complete; strict capture complete under first-miss stopping. No fixture or cleanup error in that run. The build used the live keymindCascade working tree with existing dirty changes and these HIL additions, not a clean-release image. HEAD observed at completion was 2b5a6f209835f0f67ed73fee78d36447a4566ce6; the unrelated Companion USB commit advanced HEAD from the earlier experiment's base during this ongoing work. Its files were not reverted. The RX image hash above is the exact tested-image identifier. Prior captures were preserved.

"},{"location":"test-results/sf10_full_retune_twice_validation/","title":"Sf10 full retune twice validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_full_retune_twice_validation/#sf10125-apply-the-full-radio-settings-twice-per-hop","title":"SF10/125: apply the full radio settings twice per hop","text":"

2026-09-14. Two full retune passes were verified, averaging 1.276 ms per channel switch. This did not resolve reception in the bounded packet test: the first packet timed out. This is one failed probe, not a 400-packet loss measurement. No production defaults or receive guards were changed.

"},{"location":"test-results/sf10_full_retune_twice_validation/#requested-interpretation-and-fixture","title":"Requested interpretation and fixture","text":"

\"The 125 run\" was interpreted as uniform SF10/125 on four channels, not the preceding mixed-SF/BW experiment. Apply the complete retune twice to each destination: frequency, modulation, preamble/packet configuration and RX restart, retaining normal guards and command-result checks for both passes.

The earlier approximately 0.53 ms path could skip unchanged modulation. This test deliberately writes those settings again, so it measures more than 0.53 + 0.53 ms. No artificial time padding was added to meet an expected value.

"},{"location":"test-results/sf10_full_retune_twice_validation/#same-image-rx-only-timing","title":"Same-image RX-only timing","text":"

Six-second windows in 1/2/2/1-pass order, with a reboot before each. Both one- and two-pass controls force modulation writes, so the comparison isolates complete repetition rather than also changing cache policy.

Window Passes per hop Hops Mean total (us) Max total (us) Mean first pass (us) Mean second pass (us) A1 1 141 647.284 847 644.220 \u2014 B1 2 139 1275.410 1491 648.899 622.835 B2 2 139 1276.086 1488 649.000 623.288 A2 1 141 647.858 836 644.872 \u2014

Equal-weight/hop-count means: one full pass 647.571 us; two full passes 1275.748 us. The first and second passes average 648.950 + 623.062 us; the total includes approximately 3.7 us additional wrapper/BUSY bookkeeping. Both two-pass windows had 556 frequency writes, 278 modulation writes and 278 optimized RX starts. No second-pass guard block or hardware/cache error.

"},{"location":"test-results/sf10_full_retune_twice_validation/#packet-test","title":"Packet test","text":"

First probe: TX channel 3, 912.5 MHz, sequence 600920621. TX returned rc=0, setup 86,914 us, transmit call 529,297 us. RX timed out with mode=1 and device errors=0. The controller stopped without retrying or testing further packets. No separate payload-only diagnostic ran because this was a real timeout, not an intact wrong-channel delivery.

During the packet window:

This shows that completing two real settings applications does not by itself guarantee successful reception in this fixture. It does not prove every packet would fail or independently identify the cause of this particular timeout.

"},{"location":"test-results/sf10_full_retune_twice_validation/#implementation-and-verification","title":"Implementation and verification","text":"

HIL BenchWrapper::hop performs the first ordinary guarded tuneProfile, then waits boundedly for BUSY-low and requests a refresh of the same profile before the second ordinary guarded transaction. This bypasses only the same-generation no-op, not receive ownership or packet-in-progress checks. The modulation cache is invalidated for each requested full pass.

If the second pass is blocked by newly acquired reception, the diagnostic fails closed. Returning ordinary BUSY would wrongly imply that no channel change occurred even though pass one had completed; it is therefore treated as a fixture failure, not silently skipped or counted as an RF loss.

scanretunepasses 1 is the reboot default. scanretunepasses 2 and --retune-passes 2 --modulation-cache off select this bounded test. The standalone controller is tools/hil/profile_switch_retune_repeat.py. 19 native profile tests and 49 host tests passed, including real refresh, first/second failure handling, BUSY timeout, no guard override, and rejection of missing physical write/resume counts. git diff --check passed with only unrelated existing CRLF warnings.

Receiver build/upload succeeded with hash verification: 94,908 bytes RAM, 368,389 bytes reported flash. Transmitter firmware was not changed or reflashed. Unrelated RAK COM29 was not opened. Both boards rebooted idle after completion, retaining HIL firmware; no autonomous TX or background collector. Temporary PlatformIO overlay removed. No commit/push requested or performed.

"},{"location":"test-results/sf10_full_retune_twice_validation/#provenance","title":"Provenance","text":"

Manifest complete, stopped bounded_tests_complete; strict capture complete under first-miss stopping. No fixture or cleanup error. Previous captures were not rewritten. Source remains keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 plus existing dirty changes and these HIL-only additions, not a clean-release comparison.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/","title":"Sf10 nrf52 double write settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#sf10-125-khz-xiao-nrf52-tx-double-write-1-ms-delay-grid","title":"SF10 / 125 kHz, XIAO nRF52 TX, double write, 1 ms delay grid","text":"

2026-09-14. Fixed-channel positive control passed 100/100. The subsequent four-channel, double-write sweep failed at every 1 ms step from +0 to +23 ms.

New sender serial B35E71C1C3726CE7 first enumerated as Zephyr CDC COM33 (2FE3:0004). A 1200-baud touch exposed Seeed XIAO bootloader COM25 (2886:0044, XIAO-BOOT). The application-only DFU ZIP was accepted with its normal compatibility checks, reported programmed successfully and reappeared as COM15 (2886:8044), with the same serial and ready XIAO nRF52840/Wio SX1262 HIL identity. No bootloader/SoftDevice replacement or full erase was performed. The unrelated RAK COM29 (4D6F77FD74011872) was not opened.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#requested-test","title":"Requested test","text":"

Budget uses 539 us nominal double-write retune (ceil of the measured 538.192 us RX-only mean), 41,780 us post-settle visit and 262,144 us preamble: 262144 / 4 - 41780 - 539 = 23217 us. Last 1 ms grid point is 23,000 us. This is a nominal ceiling, not a worst-case jitter or zero-loss guarantee.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#preparation","title":"Preparation","text":"

The settling controller now accepts --step-us 1000 --frequency-repeat on; its existing defaults are unchanged. Capture policy checks reject mixed single/double evidence. The 42 host collector tests and 14 selected profile native tests passed, including grid/first-pass stop, nRF USB DTR behavior, transmitter replay, setup/TX failure, stationary payload integrity, zero-retune and host/device-count checks. git diff --check passed (unrelated CRLF warnings).

Minimal profile_switch_xiao_nrf52_tx firmware compiled successfully: 95,340 bytes reported flash usage, 18,212 bytes static RAM. It uses the production XIAO pin map and CustomSX1262 initialization, exposes only reference TX and cached USB results, and has no Mesh identity or application settings writes. Includes the existing build-local bounded nRF USB READY/HFCLK fix.

Platform: Nordic nRF52 10.8.0; Adafruit nRF52 framework 1.10701.0+sha.d5413016; ARM GCC 14.2.1; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c.

Generated DFU ZIP contains application only, requiring SoftDevice ID 0x0123. Upload used normal DFU validation without compatibility overrides. The V4's earlier no-backup erase authorization is not treated as authority to erase this new board's bootloader or unrelated storage.

Flashed nRF52 transmitter SHA-256:

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#baseline-requested-before-continuing-the-sweep","title":"Baseline requested before continuing the sweep","text":"

The initial four-channel run was interrupted at the user's request after 13 completed settings, added delay 0..12,000 us. Each missed its first packet: nine matched the test sequence but were reported on channel 2 when channel 3 was expected; four timed out. These are not 13 full 400-packet measurements and must not be presented as an unbiased loss rate. The incomplete original top manifest and all raw captures under tools/hil/profile_switch_sf10_nrf52_double_1ms_results* remain unchanged.

Added an independent HIL-only stationary receiver: one full normal radio initialization, fixed 910.25 MHz (channel 3), normal RX rearm, no profile scheduler/fast-switch/cache shortcut. All 100 packets use the same transmitter, SF10/125, CR4/5, 32 preamble symbols, 16-byte CHS1 body and -9 dBm request. No RF retries and no early stop on an RF miss; hardware/fixture faults abort.

Result: 100/100, CRC/read errors 0, foreign packets 0, hardware errors 0, observed frequency writes after setup 0. Normal gain register 0x94 and RX mode 1 verified. RSSI mean -33.610 dBm, range -34..-33; SNR mean +8.736 dB, range +7.8..+10.0. Baseline cleanly stopped and both devices rebooted idle. This confirms the fixed-channel link in this finite sample, not all-channel or interference immunity.

Capture: tools/hil/profile_stationary_nrf52_sf10_100_results.json. SHA-256: 547c30efc89c29e3f60cb975f7fc34de3e9f14bed3ed4cc9f1ba01f63402bf15. Receiver image with stationary capability compiled/flashed and hash-verified: SHA-256 0daab2e3293359280714042492639ab8c176609143cf01996ad4ba7026704768. Reported flash 364,657 bytes; static RAM 94,796 bytes. Existing double-write and four-channel switching implementation is unchanged; baseline is a separate inactive command mode during the scan.

"},{"location":"test-results/sf10_nrf52_double_write_settling_validation/#post-baseline-four-channel-run","title":"Post-baseline four-channel run","text":"

Fresh prefix: tools/hil/profile_switch_sf10_nrf52_postbaseline_double_1ms_results. Command: profile_switch_settling.py --receiver COM31 --sender COM15 --sf 10 --frequency-repeat on --step-us 1000 --output <fresh-prefix>.json. No old captures are reused. Stop at the first 400/400 or after +23 ms.

Completed all 24 settings. No 400/400 pass. At zero added delay, the first packet passed and the second timed out. Each +1..+23 ms setting failed its first packet. Total 25 probes: one valid, 17 timeouts and seven test-sequence packets reported on channel 2 while expecting channel 3. Each setting stops on its first miss; this is deliberately censored evidence, not a measured unbiased 4% packet-delivery rate. The lone valid packet was -33 dBm / +8.2 dB SNR. The wrong-channel records do not alone distinguish real off-channel acquisition from channel bookkeeping; no root cause is claimed.

Measured 3,086 retunes, weighted mean 526.783 us (0.527 ms), maximum 876 us (0.876 ms), excluding the separately applied settling delay. Exactly 6,172 SetRfFrequency commands confirm two per hop. One modulation command was issued across the measured scan windows (after the sole received packet); no retune failures, RX-mode errors, cache errors or CRC/read errors. All gain checks remained 0x94. This measurement is from the receiver image with the new independent baseline mode, not a controlled performance A/B against the previous image's 538.192 us RX-only mean.

At +23 ms the measured settling interval was 23,000..23,001 us and mean switch-plus-settle 23,567.2 us. That short failed window did not contain a complete unheld scan cycle, so it provides no measured final-step full-sweep latency. The ceiling remains the nominal 23,217 us calculation above.

Post-baseline top manifest SHA-256: badda7f64a7bdd445942da19dc76dea0d1aace2ae451772edf4744d4670ac28c. Interrupted pre-baseline top manifest SHA-256: 90f7b213bc44ae73714c5d9d32216ff3f1760fafd657feb5ea9299ed1e10ddf3.

Conclusion: the fixed-channel link passes its 100-packet control, but extra post-switch settling through the nominal budget does not cure this scanner failure. Investigate acquisition/hold behavior and channel attribution next; do not claim more waiting, a second frequency write, or a transmitter swap has fixed the issue. No guard/default production timing was changed here.

Collector exited successfully at the nominal timing limit, with no cleanup error; both radios rebooted idle. The previous follow-up automation remains paused. No commit or push requested; HIL firmware remains installed. Temporary build overlay removed. No background test collector remains running.

"},{"location":"test-results/sf10_rollback_failure_validation/","title":"Sf10 rollback failure validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf10_rollback_failure_validation/#sf10125-rollback-and-wider-spacing-failure-isolation","title":"SF10/125 rollback and wider-spacing failure isolation","text":"

2026-09-14. No strict four-channel 400/400 pass. Reverting the switching optimizations did not remove failures. Off-channel reception also occurs with a fixed, fully initialized receiver, without any channel switching. A traced timeout shows an off-channel preamble holding the scanner away from the intended channel until after its transmit preamble. Wider spacing did not eliminate either wrong-channel delivery or actual missing packets.

This isolates a concrete scanner failure mechanism, not the underlying physical RF cause. Commanded frequency words are observed, but actual emitted spectrum, antenna-port power, image responses and overload have not been independently measured. Do not claim front-end saturation or a specific spur as proven. Do not treat these first-failure-censored samples as unbiased PER.

"},{"location":"test-results/sf10_rollback_failure_validation/#fixture-and-requested-controls","title":"Fixture and requested controls","text":"

Rollback modes are runtime HIL controls on the same current source, not historical firmware checkouts. No production default or receive safety guard was changed during this investigation. Tests preserve ownership, packet-in- progress, BUSY and command-result checks. Existing unrelated working-tree changes were left intact. No commit or push was requested this turn.

"},{"location":"test-results/sf10_rollback_failure_validation/#maximum-practical-settling-control","title":"Maximum practical settling control","text":"

At SF10/125, a symbol is 8,192 us, the programmed preamble is 262,144 us, and each 5.1-symbol post-settle visit is 41,780 us. For each rollback mode:

  1. Measure six seconds of RX-only switching without added delay.
  2. Choose the largest 100 us delay step satisfying 4 * (41780 + observed_max_switch_us + added_delay_us + 500) <= 262144.
  3. Measure another six seconds; require at least eight full idle cycles and maximum observed idle cycle below the preamble before allowing TX.

The 500 us per-hop reserve covers some observed scheduling variation, not all possible jitter. RX is active during added settling. Near-ceiling cycle timing is not itself a guarantee of acquisition for every arrival phase, especially when an IRQ causes an extended packet hold. Idle-cycle statistics exclude guard/packet-held visits; those are measured separately.

"},{"location":"test-results/sf10_rollback_failure_validation/#250-khz-spacing-eleven-rollback-cases","title":"250 kHz spacing: eleven rollback cases","text":"

Each case stopped on its first probe, TX channel 3, with a real timeout. No strict 400/400 pass, TX fault, RX mode/cache failure, or hardware error. The first nine cases used the checkout's existing 1,600 us TCXO setting; the final two explicitly restored 6,000 us. Production timing was not edited.

Case Added delay (us) Zero-delay mean switch (us) Max settled idle cycle (us) Fast, cached modulation, double frequency write 22500 535.88 259297 Always write modulation 22500 625.50 259682 Individual SF/BW/CR writes 22200 840.97 259346 Full normal RX restart 22200 896.89 259577 Full RX + individual modulation 22000 1113.95 259623 Above + byte-wise SPI, warm XOSC 21800 1215.79 259220 Above + RC standby, 1.6 ms TCXO 20100 2942.50 259339 Fast, cached modulation, single frequency write 22600 450.25 259341 RC/full RX/individual/byte-wise, single write, 1.6 ms TCXO 20200 2837.50 259329 RC/full RX/individual/byte-wise, double write, 6 ms TCXO 15800 7293.25 259550 RC/full RX/individual/byte-wise, single write, 6 ms TCXO 15800 7190.31 259139

Actual frequency/modulation command counts and fast-RX counts verified each policy. Slower modes received less added delay to preserve the total sweep budget. These are eleven probes with eleven timeouts, not eleven complete 400-packet runs. Packet arrival pauses use the same seeded scheduling method but not hardware-synchronized identical RF phases across modes.

"},{"location":"test-results/sf10_rollback_failure_validation/#fixed-frequency-controls-off-channel-responses-without-switching","title":"Fixed-frequency controls: off-channel responses without switching","text":"

Each pair received three independent probes after full normal initialization. Positive controls were interleaved. The receiver remained at one frequency: zero observed frequency commands after setup, no hopping or cache shortcut. Payload success requires the exact expected sequence/channel/body, length 16 and clean CRC. It does not require RX center to equal TX center in this explicit offset diagnostic. Offset below means programmed RX minus TX.

Spacing RX-TX offset (kHz) Probes Intact expected payloads Preamble IRQ seen 250 kHz 0 6 6 6 250 kHz -250 3 2 3 250 kHz -500 3 0 3 250 kHz -750 3 0 3 250 kHz +250 3 0 2 1 MHz 0 6 6 6 1 MHz -1000 3 2 3 1 MHz -2000 3 0 0 1 MHz -3000 3 3 3 1 MHz +1000 3 3 3

At 250 kHz spacing, the two valid -250 kHz-offset packets reported RSSI -91/-90 dBm and SNR -12.8/-10.8 dB. At 1 MHz spacing, valid off-channel packets reported RSSI -102..-109 dBm and SNR -11.5..-15.8 dB. Wider on-channel controls were all -33 dBm, SNR +8.0..+8.8 dB. These are radio-reported values, not calibrated measurements of transmitted spurs or receiver rejection.

The non-monotonic offset pattern means that increasing spacing alone cannot be assumed to remove this response. These fixed-frequency tests rule out a channel-hop shortcut as a necessary condition for the observed off-channel packets; they do not establish which physical component produces them.

"},{"location":"test-results/sf10_rollback_failure_validation/#a-traced-real-timeout-at-250-khz-spacing","title":"A traced real timeout at 250 kHz spacing","text":"

Fast/double-write RX, +22,500 us settling. One channel-3 probe timed out with TX rc=0, RX mode=1 and device errors=0. The trace contains 454 events, overflow 0. Trace adds 64 us IRQ polling; use it for ordering, not an unperturbed timing A/B comparison. Times below are relative to receiver expectation arming.

RX time (ms) Evidence 169.190 Frequency command for channel 3, 910.25 MHz 234.032 Leaves channel 3 for channel 0 298.869 Frequency command for channel 1, 909.75 MHz 330.030 PREAMBLE_DETECTED (0x0004) while on channel 1 363.632 Requested next hop is held by receive-in-progress guard 726.734 Guard clears the lingering preamble IRQ 726.826 Frequency command for channel 2 791.654 Finally commands intended channel 3 again 814.586 Channel-3 hop/settling completes

TX setup was 86,914 us and its transmit call 529,297 us. Host/firmware timing places TX RF start approximately 287..289 ms after RX arming and the end of its programmed preamble around 550 ms. This estimate is not a synchronized RF measurement, but the receiver's several-hundred-millisecond absence from the intended channel is much larger than USB timestamp uncertainty.

The held channel-1 visit measured 427,395 us, versus normal idle four- channel cycle maximum 259,358 us. The guard timeout is consistent with RadioLibWrapper::calcMaxPacketMillis: (32 + 8 + 4.25) * 8192 = 362496 us, rounded to 363 ms. CustomSX1262::isReceiving() starts that timer when the guard first observes the preamble, here later than the diagnostic IRQ poll.

Thus an off-channel preamble can monopolize the scanner long enough to miss the real channel. Do not simply remove or shorten this safety guard as a production fix: it also protects legitimate weak or long-preamble reception.

"},{"location":"test-results/sf10_rollback_failure_validation/#1-mhz-spacing-strict-results-versus-payload-delivery","title":"1 MHz spacing: strict results versus payload delivery","text":"

Three strict rollback cases (fast double, fast single, fully rolled back single/6 ms) each stopped at their first probe. All three actually decoded the intact channel-3 payload while programmed for channel 2: RX 911.5 MHz, TX 912.5 MHz, RF word at read 955777024. They are channel mismatches, not lost payloads. RSSI was -102/-101/-101 dBm; SNR -11.5/-11.5/-11.2 dB.

An explicitly separate payload-delivery diagnostic then continued through such packets while retaining strict validity in raw evidence:

Mode Added delay Delivered before stop Strict same-channel Stop Mean switch Max idle cycle Fast, cached, double write 22.5 ms 3 of 4 probes 0 TX channel 0 timed out 557.721 us 259381 us RC/full RX/individual/byte-wise, single write, 6 ms TCXO 15.8 ms 2 of 3 probes 0 TX channel 1 timed out 7310.510 us 259643 us

Every delivered packet in these two runs arrived one programmed channel below its transmitter. Both runs had zero mode/cache/RX/device errors and actual switch-policy counts matched. Neither is a 400/400 pass, even under the relaxed delivery-only criterion. The true timeout in the fully rolled-back path also shows the problem is not limited to the accelerated RX restart or 0x8B cache. No traces were enabled for these two runs, so do not assign their individual timeouts the exact IRQ timeline from the earlier traced probe.

"},{"location":"test-results/sf10_rollback_failure_validation/#changes-and-verification","title":"Changes and verification","text":"

Next useful isolation is the physical off-channel response (controlled RF attenuation or reversed radio roles), followed by safe acquisition/hold-policy experiments. More settling delay alone does not address the demonstrated false-hold mechanism. No claim of an identified silicon defect or verified production fix is made here.

"},{"location":"test-results/sf10_rollback_failure_validation/#evidence-provenance-sha-256","title":"Evidence provenance (SHA-256)","text":"

Raw JSON files below are under tools/hil/; prior captures were not rewritten. Matrix manifests include per-case paths and timing controls. All listed new experiments completed under their stated stopping policies without fixture or cleanup errors. Completed first-failure runs still have incomplete 400-packet levels by design.

Capture SHA-256 profile_switch_sf10_rollback_results.json e87c504b3c89fbafe90c18fcd0ab37df34a6aeb51d95b5c69f58d60f70de185a profile_stationary_sf10_offset_results.json d3d87315513a70fc3f00cc7f17369043328f53ee3028d499087b21e767370ab7 profile_switch_sf10_rollback_fast_trace_results.json 8ac9db50f0f06eb7511d7d233957119618df62afdc7112dd83eb68d292fcd725 profile_switch_sf10_rollback_6ms_results.json 74e7bfeb23bc569a5786e2322b5e3b1753cba9f628e2969bf4f5c4296ca5817a profile_switch_sf10_wide_rollback_results.json d79096fbe3e15e0375457b487fd40be3418789e798a978efb79d8298c880eea2 profile_switch_sf10_wide_payload_results.json 1316a2c2f32a8c0773fc56587c4220ee8f6f6cdf4ab9b87e00fd361fc9395257 profile_switch_sf10_wide_legacy_payload_results.json fcf2123270634e2f27800787e20b9b8b7ae63bcfb03e17d26a4a4f6f4e23d32f profile_stationary_sf10_wide_offset_results.json 4c494a59fa793d274b0366185cfe3230b6c9fce7a57dfaa61f9f29c36465bbd3

RX firmware images used, in chronological order:

Stage firmware.bin SHA-256 Initial nine-mode rollback 5f8dbf9c4584e0de94a7aebda5a20e961b644a880ecb6a34ab5871621958696f Fixed 250 kHz offsets and traced timeout cd839caa33c869dd448cc55e9c8b959e39d105f80a2ac4a8029393698b993fd3 Explicit 6 ms TCXO rollback 0ba044035c4986749c9ad4f50c702dc8c57e8e0fecf9d46fc11515cad5678166 Wider strict rollback 5fc919c80438635b4a96021b39fae2ada9e767508d7bb73854e4e108f0d4920f Wider delivery diagnostics and fixed offsets; final installed 7b313a0f8f1eb04f6fc12197af5e9481ad43154b981ab7627e50167d2a97d8c2

The 250 kHz tests used the prior nRF52 TX ZIP hash 7466625ef5e7bcc8f7dd16db2b843db29b4312a33b0d32bc0b1e1193d6aee03a. All wider tests used the new selectable-spacing application:

Build artifacts are generated and may be replaced by a subsequent build; hashes identify the images used, not a promise that every historical binary remains in .pio/build. Source starts from keymindCascade commit 0e5955f889788a4317863e731b38ffe09e3d5441 with the existing dirty changes and these uncommitted HIL additions; this is not a clean-release comparison.

"},{"location":"test-results/sf125_settling_limits_validation/","title":"Sf125 settling limits validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf125_settling_limits_validation/#descending-sf10sf5-125-khz-settling-limits","title":"Descending SF10..SF5 / 125 kHz settling limits","text":"

Experiment started 2026-09-14. Stopped at the user's request to replace the Indicator transmitter with a V4. No 400/400 pass was found. The saved top-level manifest remains incomplete; the SF10 manifest contains all completed settings. Original evidence is not rewritten to make an interrupted experiment look complete.

"},{"location":"test-results/sf125_settling_limits_validation/#results-at-user-requested-stop","title":"Results at user-requested stop","text":"

94 SF10/125 settings completed: every added delay from 0 through 9,300 us in 100 us increments. Each stopped on its first RF miss; none reached 400/400. The best individual setting was +1,000 us: 25 valid packets, then packet 26 failed. No lower SF was reached in this descending sweep. No result is claimed for SF10 delays above 9,300 us or for SF9..SF5 in this run.

Across these completed settings: 641 probes, 547 valid and 94 failed. Because each setting ends at its first miss, these totals are not an unbiased packet error-rate measurement and do not establish a reliable lower/upper delay limit.

For the 547 valid receptions, reported SNR averaged +8.311 dB, minimum +7.5 dB and maximum +9.5 dB. RSSI averaged -40.547 dBm, range -42 to -39 dBm. These are conditional on successful reception; no SNR is inferred for missing packets.

24,055 retunes averaged 456.620 us, weighted by hop count and excluding the intentional settling delay. Reported retune failures, RX-mode errors and cache errors were all zero. Four RX errors were reported, one each at +1,100, +5,900, +6,700 and +6,900 us. These counters do not identify the transmitter as the cause. At +9,300 us, the unheld four-channel cycle averaged 206.134 ms (maximum 206.157 ms), versus a 262.144 ms programmed preamble; this nominal time margin still did not produce an all-pass setting.

Controllers and collector were stopped, and both boards were rebooted to idle HIL. Follow-up polling is paused. Subsequent info checks returned ready on both boards, with XIAO gain register 0x94 and Indicator 0x96. Test firmware was retained; no original firmware was restored. Replacement-board testing has not started.

Saved manifest SHA-256 at stop:

"},{"location":"test-results/sf125_settling_limits_validation/#requested-method","title":"Requested method","text":"

XIAO ESP32-S3/WIO SX1262 COM31 RX, SenseCAP Indicator ESP32-S3 COM28 TX, user-separated positions, XIAO boosted gain off (actual register 0x94), Indicator gain unchanged at 0x96. Four centers 909.500/909.750/910.000/910.250 MHz; bandwidth 125 kHz, CR4/5, 5.1-symbol visits, 32 programmed preamble symbols, 16-byte synthetic packets at -9 dBm. Warm fast RX, buffered 8 MHz SPI and unchanged-modulation cache enabled. No extra IRQ polling/tracing.

Start SF10 and descend one SF at a time through SF5. For each SF, test added post-BUSY delay in 100 us increments starting at zero. Every failed setting stops on its first invalid reception/timeout; a pass requires 100/100 on each of four channels. Latest user instruction: stop the entire sweep at the first 400/400 pass, without continuing to find an upper limit or a lower SF. Without any pass, continue to the ceiling, then lower SF. Fixture errors abort the entire experiment. No failed RF probe is retransmitted.

This searches for a sampled all-pass setting, not proof of monotonic delay behavior, an upper delay limit, or zero PER. The RX modem remains active during the extra delay; the independent 5.1-symbol visit starts afterward. Thus extra settling is also extra RX occupancy, not a pre-SetRx blanking interval.

SF Visit Last 100 us delay step Programmed preamble 10 41,780 us 23,300 us 262,144 us 9 20,890 us 11,400 us 131,072 us 8 10,445 us 5,400 us 65,536 us 7 5,223 us 2,500 us 32,768 us 6 2,612 us 1,000 us 16,384 us 5 1,306 us 200 us 8,192 us

Budget: preamble_us / 4 - dwell_us - 453, using the nominal retune cost. Actual cycle means/maxima are recorded and may exceed that nominal budget. The randomized arrival pause now includes added settling when estimating two scan cycles; earlier saved captures retain their original distributions.

The stop policy changed after SF10 delays 0..5,600 us had all failed. The old supervisors were stopped while the active 5,600 us collector finished normally. Original captures are preserved. A fresh first-pass manifest reuses completed, configuration-validated raw captures with their SHA-256 hashes, then resumes at 5,700 us; no completed setting is repeated. The earlier top-level manifest is incomplete because its upper-limit policy was superseded, not due to an RF or fixture fault.

"},{"location":"test-results/sf125_settling_limits_validation/#build-and-initial-checks","title":"Build and initial checks","text":"

Both HIL targets compiled and verified uploads succeeded. The selected 53 host/native tests passed: 36 host collectors, seven profile native, four SX1262, five radio and one receive-mode test. After the stop-rule change, all 39 host collector tests passed, including new whole-sweep first-pass stopping and matching, hash-preserving capture reuse.

Actually loaded firmware SHA-256:

A one-second RX-only SF10/+23,300 us smoke check recorded 15 hops with minimum delay 23,300 us, minimum post-delay dwell 41,782 us, and zero retune/RX-mode/cache errors. Its two clean cycles averaged 262,125 us, maximum 262,127 us: only a tiny margin against the 262,144 us preamble, not a robust worst-case bound. No RF probe was transmitted during that check.

Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade, plus local changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17.

python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_limits_results.json\n

Updated run:

python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_first_pass_results.json --stop-on-first-pass --reuse-prefix tools/hil/profile_switch_sf125_limits_results.json\n

The controller reboots to idle HIL after each per-SF child completes or fails; test firmware is to remain installed, per user request. Original private flash backups are retained. RAK4631, Indicator RP2040 and unrelated S3 soak work are not part of this experiment. No commit/push requested. Temporary build overlay has been removed.

"},{"location":"test-results/sf5_250_dwell_validation/","title":"Sf5 250 dwell validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf5_250_dwell_validation/#sf5-250-khz-four-channel-dwell-sweep","title":"SF5 / 250 kHz: four-channel dwell sweep","text":"

Measured 2026-09-14. No tested dwell from 4.1 through 8.1 chirps passed. Each setting stopped at its first invalid reception or timeout; the sweep stopped at the requested 8.1 limit. None reached 100 packets per channel.

"},{"location":"test-results/sf5_250_dwell_validation/#setup","title":"Setup","text":"
python tools/hil/profile_switch_dwell.py --receiver COM31 --sender COM28 --output tools/hil/sf5_250_dwell_normal_results.json\n
"},{"location":"test-results/sf5_250_dwell_validation/#results","title":"Results","text":"

\"Received\" means correct sequence, payload and reported receive channel. Each row is a first-failure sample, not a 100-packet/channel PER measurement.

Chirps Requested dwell Received / attempted First failure Mean retune 4.1 525 us 1 / 2 channel mismatch 458.122 us 4.6 589 us 0 / 1 timeout 450.787 us 5.1 653 us 0 / 1 timeout 450.667 us 5.6 717 us 0 / 1 timeout 452.270 us 6.1 781 us 2 / 3 timeout 452.991 us 6.6 845 us 0 / 1 timeout 451.082 us 7.1 909 us 0 / 1 timeout 453.796 us 7.6 973 us 0 / 1 timeout 453.568 us 8.1 1,037 us 4 / 5 timeout 457.886 us

All 16 requested transmissions returned success. Seven met the full receive checks. Of nine failures, one returned a matching synthetic sequence on a different reported receive channel and eight timed out. Every timeout run also recorded one rx_errors increment. These are receive/read/decode errors, not retune hardware faults; without an IRQ trace their exact cause and association with the expected probe cannot be reconstructed.

All 64,507 timed hops passed RX-mode/software-cache checks with zero retune failures. Weighted mean switch time was 452.739 us, excluding listening dwell and full application scheduling. Each hop programmed a frequency. Modulation rewrites occurred only when the prior context needed re-establishing, rather than on every frequency-only hop.

The 4.1 second probe was transmitted for channel 2 (910.000 MHz), but its sequence was returned while the receiver reported channel 3 (910.250 MHz), 16 bytes, RSSI -66 dBm, SNR 0.5 dB. It therefore failed the channel check; the saved valid=false also does not independently certify all payload bytes. No synchronized RF/frequency-register capture was taken, so this is evidence of wrong-channel reporting/reception, not proof of its physical mechanism. Successful correct-channel probes read -42 to -41 dBm, SNR 7.8\u20138.8 dB.

"},{"location":"test-results/sf5_250_dwell_validation/#interpretation","title":"Interpretation","text":"

SF5/250 did not establish a solution to the close-range problem, and increasing dwell alone did not produce a clean four-channel result. The wrong-channel sequence and receive errors prevent interpreting this as a pure minimum-dwell/acquisition test. It is not evidence that 8.1 is a reliable setting merely because four probes passed before its miss.

There is also a timing tradeoff: a symbol lasts 128 us, so 32 programmed preamble symbols occupy only 4.096 ms. A ~453 us retune costs about 3.54 symbols. At 4.1, four nominal dwells plus four such retunes consume about 3.91 ms before other loop costs; at 8.1 they consume about 5.96 ms. Longer visits give acquisition more uninterrupted time but increase the time spent away from each channel. This arithmetic motivates testing a longer preamble or fewer channels separately; it does not prove the cause of any particular failure. No such follow-up was transmitted after the 8.1 limit.

Unlike the earlier separated SF6 run, this changes SF, bandwidth, absolute preamble duration and diagnostic polling. The RF settings also leave channel spacing unchanged at 250 kHz. It is not a matched comparison isolating receiver sensitivity or the modulation cache.

"},{"location":"test-results/sf5_250_dwell_validation/#provenance-and-handoff","title":"Provenance and handoff","text":"

Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade, plus uncommitted production cache and HIL changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c; Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL builds and verified uploads succeeded. All 39 selected host/native tests passed, including bandwidth/dwell calculation and bounded sweep stopping. No commit or push was requested for this work.

Tested firmware SHA-256:

Raw manifest references all nine per-dwell captures. SHA-256 values (all files under tools/hil):

File suffix after sf5_250_dwell_normal_results SHA-256 .json aaf00ef8531cb2c5c7e4dfb78d3b8b03ce61d9afe8323fb33e70be09656adf54 _4p1.json a9c6baf0bece79c9b9c96bfa97e3cd9932e0c04fc76b43f61ed350512306341a _4p6.json 63863b55108374011afbe555bfeb97034b010e194cdad83a212d5d286d1992fe _5p1.json ff1d2927365e3634984bc0b843a09f2934c5dcac49b60b862ff4db030e0f7e74 _5p6.json 33c91687d0b61dd83e2a08d8358b43f21fa27f9e80f9503feedeb657453a7efd _6p1.json 5baa8d1bfcf0ccc947c92d563dc555802ac599ce1e8d260a623964936a99da4e _6p6.json 90f6b4d7785add771e30da7dfa78672fd75ad13d8a5965c21ec1a37e15d8dabb _7p1.json eb99afd932a5579c765f8a6c0a18db4bacc317829b0654d11fbf2f569c3a8618 _7p6.json bdb88da82437fa6e01ad90ab927d364b4d67aaadbac7597c3aacb9799b053211 _8p1.json 404ee179e4c10333bd6fddae9f0faa9da81f6193dcb5fff6d9bd37a466c319a3

Both boards were rebooted after the sweep and answered ready with correct HIL capabilities; XIAO gain remained 0x94. Per user request, HIL firmware is left installed, idle with no autonomous test transmissions. Private original flash backups remain available. The RAK4631, Indicator RP2040 and unrelated S3 soak work were not modified. The temporary build overlay was removed.

"},{"location":"test-results/sf6_125_settling_validation/","title":"Sf6 125 settling validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_125_settling_validation/#sf6125-four-channel-post-switch-settling-sweep","title":"SF6/125 four-channel post-switch settling sweep","text":"

Measured 2026-09-14. No setting from +0.0 through +1.0 ms passed 400/400. The requested delay was applied after BUSY went low without consuming the subsequent 5.1-symbol listening visit. Each run stopped on its first miss; the sweep would have stopped at the first 400/400 pass, but reached its nominal timing limit instead. No RF probes were sent beyond that limit.

"},{"location":"test-results/sf6_125_settling_validation/#method-and-timing-budget","title":"Method and timing budget","text":"

At SF6/125, a symbol is 512 us. The requested listening visit rounds up to ceil(5.1 * 512) = 2,612 us; 32 programmed preamble symbols last 16,384 us. Using the user's nominal 453 us retune, the extra per-hop budget is:

16,384 / 4 - 2,612 - 453 = 1,031 us

Thus the 100 us grid runs from 0 through 1,000 us; 1,100 us would exceed the nominal full-sweep calculation. Sync/header time is not spent as extra scan budget. This is not a worst-case scheduling or acquisition guarantee.

The delay is HIL-only: after a successful guarded production retune and BUSY-low observation, the CPU waits without SPI/IRQ operations, then starts a separate 2,612 us listening clock. The SX1262 is already receiving throughout that delay and may acquire a packet. This tests extra RX occupancy/settling, not a pre-SetRx delay or a period with RX disabled. Ordinary RX restarts after processing packets reset the visit clock without another channel delay. Zero delay preserves the previous production RX-start clock.

An initial receive-only, 300 ms smoke check at +1,000 us measured 74 hops, minimum applied delay 1,000 us and minimum subsequent dwell 2,612 us. Its 17 clean full cycles averaged 16,235.941 us, maximum 16,260 us. No RF probe was transmitted in this check. The longer packet experiments below capture more scheduling variability, including host command handling.

"},{"location":"test-results/sf6_125_settling_validation/#results","title":"Results","text":"

All times in the last two columns are measured means, not target values. \"Received\" is successes before the next probe timed out; every row ended in one timeout. These first-failure samples do not estimate comparable PERs.

Added delay Received before miss Switch + delay Clean full cycle 0.0 ms 44 0.452381 ms 12.256886 ms 0.1 ms 4 0.548387 ms 12.669976 ms 0.2 ms 34 0.650424 ms 13.074105 ms 0.3 ms 53 0.753410 ms 13.485859 ms 0.4 ms 47 0.850830 ms 13.868454 ms 0.5 ms 66 0.952289 ms 14.273736 ms 0.6 ms 14 1.050333 ms 14.671156 ms 0.7 ms 21 1.151351 ms 15.076094 ms 0.8 ms 63 1.255113 ms 15.482272 ms 0.9 ms 19 1.352843 ms 15.880361 ms 1.0 ms 25 1.452750 ms 16.283488 ms

All 401 transmissions returned success: 390 valid receptions and 11 timeouts, spread across the eleven independently stopped settings. None completed 100/100 on each channel. All 52,609 timed hops passed RX-mode and software-cache checks, with one observed frequency write per hop and zero retune failures. Each positive-delay hop waited at least the requested time; every run's minimum subsequent idle dwell was 2,612 us. No receive-error counter increment or device error was reported. Successful RSSI was -42 to -40 dBm and SNR 9.5\u201311.8 dB.

switch measures production retune entry through BUSY-low observation; settling measures the following wait, including timer overhead; switch_and_settle combines them. Even zero requested delay records roughly 1\u20132 us of timer/bookkeeping overhead in the combined boundary. Listening dwell and full application scheduling are not included in retune time.

idle_cycle measures consecutive channel-0 return times, excluding initial cycles and cycles containing packet/guard-held visits. It still includes loop and USB work encountered during otherwise unheld visits. At +1.0 ms its mean is 100.512 us inside the preamble budget, but its maximum was 17.475 ms, exceeding 16.384 ms. At +0.9 ms the observed maximum was 16.153 ms. Therefore the last grid point fits only the nominal/mean calculation, not every observed full cycle. Legitimate packet holds can extend scan timing further.

"},{"location":"test-results/sf6_125_settling_validation/#conclusion-and-limits","title":"Conclusion and limits","text":"

Extra post-switch settling through the available timing budget did not establish loss-free four-channel reception. The 66 successes at +0.5 ms are not evidence that it is an optimum; the results are small, first-failure samples with different uncontrolled RF arrival phases. The added delay also extends total RX occupancy, so any improvement would not isolate analog settling from longer acquisition time.

Every failure here was a timeout without a reported receive/decode error. Without an IRQ trace or synchronized independent RF capture, that does not prove absence of a preamble/header indication or identify the precise cause. It does not justify changing production packet guards or adding a permanent settling penalty. No production default was changed by this experiment.

"},{"location":"test-results/sf6_125_settling_validation/#reproduction-provenance-and-handoff","title":"Reproduction, provenance and handoff","text":"
python tools/hil/profile_switch_settling.py --receiver COM31 --sender COM28 --output tools/hil/sf6_125_settling_normal_results.json\n

See harness instructions. Base 0e5955f889788a4317863e731b38ffe09e3d5441 on keymindCascade plus local changes. RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets compiled; only XIAO needed an upload for the new receiver-side delay. All 45 selected host/native tests passed, including delay/visit separation, packet restart, timer wraparound, clean-cycle accounting, bounded grid and first-pass stop.

Actually loaded firmware SHA-256:

Raw manifest and per-delay captures are preserved byte-for-byte. SHA-256, file suffixes after tools/hil/sf6_125_settling_normal_results:

Suffix SHA-256 .json 41a20c3d5b809cd225772980fbe0d305c14391659f10d685bf78f8bdf326e61c _0us.json 47a73a35ed72ebc61b88bfe315e86bf7e15ba1d2d93e68ce92ce681e1ef73cef _100us.json b585ea01f605bc876da86ba2a35fbe6e7cc6543eb5e827ba190c51dca10dcb85 _200us.json 6ce378cd079eefae8450c7f5672281095b9b99e28e0c7920f024a4396606bb40 _300us.json d93e7a258dddb6731c030e395e09c3f06b4d5988638cc85dc5cd0745256c6202 _400us.json 56bfe4cb7cbab2a660a232a2bf85b9bb87221ffade7c9bbce6d1116e6c30fcbf _500us.json fffb6c33a506f537913084fc4f5cc9f8009a4f0f61e4f290dd81a7409f53d1bd _600us.json 7bc6dd41f0079f37bdbf77fc48c4113e26589bcce704850b106c33d057e34659 _700us.json 54170f5b63bbe8ffa231852bf892ea55d8579c33631775f29fb7c127197afb2a _800us.json f7c53375b29764b4369b84e8ac3244aa37e385c8738a9dfd7f3257d85a64a705 _900us.json 56bc3691f2046cafcf2115b2f6b7bd164e9c9039733ea51028db292deea10230 _1000us.json f708af37caee8f689f2436567382458d63aba1269e8306ac6e4d0079677b9115

Both boards rebooted to idle HIL after testing and answered ready. XIAO gain remained 0x94 and settling policy was returned to zero. Test firmware stays installed, per user request; no restore, commit or push was performed this turn. RAK4631, Indicator RP2040 and unrelated S3 soak work were untouched. The temporary build overlay was removed; no autonomous test TX remains.

"},{"location":"test-results/sf6_5p1_trace_validation/","title":"Sf6 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_5p1_trace_validation/#sf6-125-khz-four-channels-at-51-chirps","title":"SF6 / 125 kHz: four channels at 5.1 chirps","text":"

Measured 2026-09-14. Seven packets received, then packet 8 missed. The run stopped at that first miss. No RF retry, other dwell, or higher/lower channel count was tested. The target of 100 packets per channel was not reached.

Follow-up: SF8/125 at the same 5.1-chirp dwell missed on packet 13 and captured an off-channel preamble hold. That differs from the absence of observed preamble in this SF6 failure.

"},{"location":"test-results/sf6_5p1_trace_validation/#setup","title":"Setup","text":"
python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output tools/hil/sf6_125_5p1_trace_results.json --samples 100 --preamble 32 --max-channels 4 --dwell-symbols 5.1 --trace\n
"},{"location":"test-results/sf6_5p1_trace_validation/#results-and-what-the-trace-establishes","title":"Results and what the trace establishes","text":"

Probes 1\u20137 succeeded on channels 3, 2, 1, 0, 2, 3, 1. Probe 8, sequence 944962150, missed on channel 0 (909.500 MHz). Thus channel 0 had already received a valid probe; this was not a channel that never worked at all. Successful packets had RSSI -42 to -40 dBm and SNR 9.5 to 10.8 dB.

The failed probe has a complete, non-overflowed 3,160-event trace:

The scanner was not stuck on the wrong channel by a false preamble. It also did not acquire a header and then fail a CRC check. The evidence locates the failure before observed preamble acquisition, not at payload decoding. It does not prove why acquisition failed: a short-visit/arrival-phase issue remains a candidate, as does an unobserved RF/reference issue. There was no independent RF capture, and the two MCU clocks were not synchronized.

"},{"location":"test-results/sf6_5p1_trace_validation/#actual-timing-during-the-failed-expectation","title":"Actual timing during the failed expectation","text":"Measurement Minimum Mean Maximum Receive dwell, 3,158 hops 2,613 us 2,628.706 us 2,646 us Retune, trace hop rows 526 us 538.700 us 630 us Channel 0 revisit interval, 789 cycles 12,643 us 12,666.829 us 12,705 us

The repeatable scan timing rules out a large software scheduling stall in the failed capture. The 32 programmed symbols occupy 16.384 ms, longer than that idle scan cycle; nevertheless, revisiting within the preamble did not ensure successful acquisition during these short individual visits.

"},{"location":"test-results/sf6_5p1_trace_validation/#successful-acquisition-traces","title":"Successful acquisition traces","text":"

Times below are relative to the logged hop completion onto the eventual receive channel, not a precisely captured RF edge or hardware RX-start edge. The trace observed the following progression for every successful probe: PreambleDetected (0x0004), HeaderValid (0x0014), RxDone (0x0016), consumption.

Probe Channel First observed preamble after hop RxDone after hop 1 3 1,838 us 32,982 us 2 2 1,836 us 35,470 us 3 1 1,844 us 35,151 us 4 0 2,332 us 37,329 us 5 2 1,847 us 35,532 us 6 3 1,831 us 30,731 us 7 1 1,329 us 32,567 us

After detecting preamble, the production guard kept those visits on-channel until reception completed. The failed probe never reached that observed state. This is not evidence that 5.1 symbols always suffice, nor does 7/8 estimate a stable packet-error rate or establish a channel-capacity limit.

"},{"location":"test-results/sf6_5p1_trace_validation/#instrumentation-and-reporting-caveats","title":"Instrumentation and reporting caveats","text":"

The failed capture made 115,783 diagnostic IRQ polls, costing a total 2,821,617 us (about 24.4 us each; maximum 61 us). These reads are not free: they alter loop load, although actual dwell and revisit timing were measured. This is not a matched A/B against the earlier untraced 4.8-symbol run, so seven successes versus two cannot be attributed solely to the extra dwell.

The run used channel_trace: 1. During analysis, a reporting bug was found in the newly added held_dwell aggregate: production startRecv() resets the visit timestamp after reading a packet. That aggregate therefore omitted the preceding held reception time. Do not use its 2.709 ms mean as total held occupancy. The timestamped IRQ/hop trace above is the evidence for the actual 30.7\u201337.3 ms hop-to-RxDone intervals. The failed capture had no RX restart, so its dwell rows are unaffected.

The source now keeps a separate channel-occupancy timestamp (channel_trace: 2, trace_format: 2). Both HIL targets compiled with this reporting-only correction; it was not flashed or RF-tested, and the saved raw result was not rewritten. Scheduler, packet guard and radio commands were not changed by that correction. Regression coverage includes passive SPI observation, bounded/frozen capture, occupancy accounting, page identity, requested dwell, and stopping at the first miss.

"},{"location":"test-results/sf6_5p1_trace_validation/#provenance-and-device-handoff","title":"Provenance and device handoff","text":"

Raw result SHA-256: 7caa16483b7b0000b388a1906395fe3d381bd0e40de73f48cd5c876d96deab3b.

Actually tested firmware SHA-256 (trace version 1, before reporting fix):

Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. No normal production firmware change this turn.

Fresh full 8 MiB snapshots were saved and digest-verified before flashing:

Both snapshots were restored afterward, separately verified against the full flash, and hardware-reset. Snapshots remain in the private current-user backup directory. The RAK4631 and Indicator RP2040 were not modified; the XIAO soak logger was not restarted. Changes were initially handed off locally before publication.

"},{"location":"test-results/sf6_channel_scan_validation/","title":"Sf6 channel scan validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf6_channel_scan_validation/#sf6-125-khz-channel-count-test","title":"SF6 / 125 kHz channel-count test","text":"

Measured 2026-09-13. The run stopped on the third packet at the initial four channels: two arrived intact, then the receiver timed out. No five-channel test, lower-channel test, retry or retransmission was performed. Consequently this run does not establish a maximum channel count with 100% reception.

Follow-up: four channels at 5.1 symbols with an IRQ trace stopped at packet 8 and observed no preamble acquisition during the miss.

"},{"location":"test-results/sf6_channel_scan_validation/#requested-setup","title":"Requested setup","text":"

The 100-packet-per-channel target was not reached: the explicit first-miss stop rule ended the run during its first shuffled pass.

"},{"location":"test-results/sf6_channel_scan_validation/#observations","title":"Observations","text":"Probe Channel center Outcome 1 910.250 MHz Valid 16 bytes, RSSI -40 dBm, SNR 10.8 dB 2 910.000 MHz Valid 16 bytes, RSSI -40 dBm, SNR 11.0 dB 3 909.750 MHz Receiver timeout; transmitter returned success

All three transmit acknowledgements and receiver reports matched their sequence IDs, with no missing/replayed/stale USB replies. On the miss, the receiver reported RX mode and zero device errors. The scanner recorded zero retune failures, RX-mode errors, modulation-cache errors or CRC/read errors. No probe was sent on 909.500 MHz before the stop condition.

Timing Minimum Mean Maximum Production retune to BUSY low, 3,467 hops 504 us 514.652 us 726 us Idle receive dwell, 3,463 visits 2,458 us 2,462.480 us 2,468 us

These samples include scanning during the bounded 10-second wait that confirmed the missing packet. There were 2,451 deferred attempts while production preamble/header/packet ownership held a visit; this is not a count of packets or failed retunes. Held visits are excluded from idle-dwell timing. All 3,467 successful retunes used the production fast-RX path.

The means imply about 11.91 ms per four-channel idle cycle, excluding packet-held visits and small loop bookkeeping. Fast retuning alone did not produce 100% reception in this setup. This short stop-on-first-miss experiment does not identify the exact acquisition/phase cause, estimate a stable PER, prove that three channels would pass, or establish a universal board limit.

"},{"location":"test-results/sf6_channel_scan_validation/#implementation-and-reproduction","title":"Implementation and reproduction","text":"

This is a lab-only N-channel scheduler over the real tuneProfile() path. It reuses the two production profile slots as staging slots for the next frequency. Normal MeshCore radio2 remains two-profile. The explicit test schedule intentionally does not use the normal two-profile automatic-preamble fit calculation or generic 6 ms switch / 4 ms loop allowances. It keeps the existing packet-protection guards and measures real dwell/retune overhead.

python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output results.json --samples 100 --preamble 32\n

The controller has regression tests for balanced samples, starting at four, incrementing only after a perfect level, stopping without another TX, and distinguishing fixture failure from an RF miss. A separate RX-only 0.5-second setup check verified the scan timing before any capacity-test packets were sent. The capacity run itself was not repeated.

Build provenance: base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus the local fast-switch and HIL changes; Arduino ESP32 2.0.17, Espressif32 6.11.0, RadioLib 7.7.1 pinned at 187ef24791c3d844939b2be13a68bd890bd04e4c. This is the extended V8 harness with channel_sweep: 1; its binaries differ from the earlier two-profile V8 measurements, so timings should not be treated as a same-image A/B.

Firmware SHA-256:

"},{"location":"test-results/sf6_channel_scan_validation/#device-handoff","title":"Device handoff","text":"

After the first-miss stop, both original 8 MiB flash snapshots were restored. Separate full-flash verification matched each backup's digest, followed by hardware reset. The private backups remain in the current-user backup directory. The Indicator RP2040 and RAK4631 were not modified. The interrupted XIAO soak logger remains stopped, and the temporary PlatformIO HIL overlay was removed. Implementation, tests and reports were initially handed off locally before publication.

"},{"location":"test-results/sf8_5p1_trace_validation/","title":"Sf8 5p1 trace validation","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/sf8_5p1_trace_validation/#sf8-125-khz-four-channel-comparison-at-51-chirps","title":"SF8 / 125 kHz: four-channel comparison at 5.1 chirps","text":"

Measured 2026-09-14. 12 packets received, then packet 13 missed. The first three balanced rounds passed (3/3 on every channel); the next probe on channel 3 failed. The run stopped at that miss. The target of 100 packets per channel was not reached. No RF retry or other channel count was tested.

"},{"location":"test-results/sf8_5p1_trace_validation/#setup-and-comparison","title":"Setup and comparison","text":"

Same XIAO ESP32-S3 + WIO SX1262 scanner (COM31) and SenseCAP Indicator ESP32-S3 reference transmitter (COM28). Four centers: 909.500, 909.750, 910.000 and 910.250 MHz. CR4/5, 32-symbol programmed preamble, 16-byte synthetic packets, -9 dBm, warm production fast RX and buffered 8 MHz SPI. Same production packet guards and read-only 64 us nominal IRQ polling; events buffered until each result. Randomized balanced channel order, seed 606125, varied arrival timing.

Setting Earlier SF6/125 This SF8/125 One symbol 512 us 2,048 us Requested 5.1-symbol dwell, rounded up 2,612 us 10,445 us 32 programmed preamble symbols 16.384 ms 65.536 ms Whole-run average measured retune 539.427 us 524.124 us Outcome before first miss 7 received, packet 8 missed 12 received, packet 13 missed Miss trace No observed preamble or hold Off-channel preamble and prolonged hold

Changing SF increases real listening and preamble durations together. This is not a matched same-image A/B or an isolated post-retune settling experiment. The small retune-time difference must not be treated as a new optimization; no extra settling delay was inserted. The earlier reporting-only held-visit timestamp correction is included in this trace-version-2 run.

"},{"location":"test-results/sf8_5p1_trace_validation/#failed-probe-an-off-channel-hold","title":"Failed probe: an off-channel hold","text":"

Failed sequence 388715032 targeted channel 3 / 910.250 MHz. The scanner instead observed PreambleDetected on channel 2 / 910.000 MHz, then its normal packet guard deferred the intended jump to channel 3. It never observed a valid header, header error, CRC event, or RxDone for that preamble.

Times are software observations relative to expectation arming:

Time Event 207.649 ms Last hop onto target channel 3 before the long gap 240.542 ms Hop onto channel 2 250.030 ms PreambleDetected (0x0004) observed on channel 2 251.013 ms Guard defers channel 2 \u2192 3 at the 5.1-symbol deadline 342.287 ms Guard issues a clear of the still-latched preamble bit 342.794 ms Hop onto channel 3 finally completes 342.851 ms IRQ read confirms zero bits 9,999.270 ms Expectation timeout, RX mode, zero device errors

Channel-2 occupancy was about 102 ms. The target channel's revisit gap grew to 135.145 ms, compared with its ordinary ~43.875 ms cycle and the 65.536 ms programmed preamble. The delayed retune itself took only 550 us.

The preamble-to-header budget from calcMaxPacketMillis() is ceil((32 + 8 + 4.25) * 2.048) = 91 ms. The strict millisecond timeout in CustomSX1262::isReceiving() clears a preamble that does not advance to a header. The observed 92.257 ms from the first IRQ observation to that clear is consistent with this existing protection mechanism.

This identifies a prolonged off-channel hold that removed the next listening opportunity. It does not show that the ~0.52 ms jump needs more settling time. The RF source of that preamble is not proven: adjacent-channel pickup/ false detection or unrelated RF are possibilities. There was no independent RF capture or synchronized MCU clock. The earlier SF6 miss had no observed preamble or hold; this result does not establish the same cause for that miss.

Simply shortening the guard risks cutting off valid long-preamble packets. Successful traces show that reception legitimately keeps a channel occupied well past 10.445 ms. No production guard change was made in this diagnostic.

Follow-up: the stationary preamble investigation reproduced transmitter-associated off-frequency indications with full RX initialization and no hopping, on both boards. It strengthens the off-channel detection explanation without independently identifying the RF source of this historical missed probe. The SF6 miss remains a separate unresolved case.

"},{"location":"test-results/sf8_5p1_trace_validation/#measurements-and-checks","title":"Measurements and checks","text":"

This first-miss sample neither estimates a stable packet-error rate nor establishes a channel count with 100% reception.

"},{"location":"test-results/sf8_5p1_trace_validation/#reproduction-and-provenance","title":"Reproduction and provenance","text":"
python tools/hil/profile_switch_channels.py --receiver COM31 --sender COM28 --output tools/hil/sf8_125_5p1_trace_results.json --samples 100 --preamble 32 --max-channels 4 --dwell-symbols 5.1 --trace --sf 8\n

Raw result SHA-256: 0d7e97173937f42708febf8276cdadc36578740735058d5da39b1b576e9ca49e.

Tested firmware SHA-256:

Base commit c329cf1fc1e7f2e8c8733ab0d000094730458fe3 plus local changes; RadioLib 7.7.1 at 187ef24791c3d844939b2be13a68bd890bd04e4c, Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets built successfully. All 27 selected host/native tests passed, including SF scaling, propagation to both RX staging slots and TX, cache checks, trace handling and first-miss stop.

"},{"location":"test-results/sf8_5p1_trace_validation/#device-handoff","title":"Device handoff","text":"

Fresh full 8 MiB backups were saved and digest-verified before flashing:

Both were restored after the test, separately full-flash digest-verified, and hardware-reset. Backups remain in the private current-user backup directory. No identity or channel secrets were used in the synthetic RF probes. The RAK4631 and Indicator RP2040 were not modified; the XIAO soak logger was not restarted. The temporary HIL build overlay was removed. Source, tests and results were initially handed off locally before publication.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/","title":"Solarxiao 33s head bootloader lora ota 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#solarxiao-33s-head-bootloader-lora-ota-and-repeater-acceptance-2026-10-21","title":"SolarXiao 33S HEAD bootloader, LoRa OTA and repeater acceptance, 2026-10-21","text":"

The SolarXaio MPPT 12V LiFePO4 carrier with a Seeed XIAO nRF52840 and Ebyte E22-900M33S passed a full application update over LoRa and subsequent repeater acceptance on the Mercerwood bench. TX was configured to -6 dBm and repeating enabled. Both settings survived the application update and a further warm restart. Final core error flags were zero.

Machine-readable results bind this result to the following revisions. These were the checked-out HEAD revisions used for qualification; subsequent commits require separate testing.

Component Qualified revision or value MeshCore source efad0076816f7ba2293c68f56f8db18ac17344b9 Application environment solarxiao_33S_repeater, Full OTA package Application version v1.18.0.1-halo-keymind-cascade-dev-efad0076 OTAFIX bootloader source b07255d4cdc601f1313e4cbe020caad3b5544821 Bootloader profile xiao_nrf52840_ble, board 28860044, name XIAO_DFU Bootloader packed test version 0x02040B7E, displayed as OTAFIX2.4.11-preview.126 Verified bootloader manifest CRC 76EBABBF; ABI 3, capabilities 0E SoftDevice S140 7.3.0 Application body hash 6280E8156DB1042F Application image size 578,752 bytes Application image SHA-256 1a182b5338f7655b4fe057436768b84069c00aa151971c96d6c2b6f7c98e8524

The bootloader is the named HEAD build. Its preview version string retains the 2.4.11 base label; this campaign did not install the 2.4.11 release artifact. The bootloader and SoftDevice were updated through serial DFU, followed by restoration of the preceding application. Identity and saved settings remained intact, and the running application confirmed QSPI bootloader-apply support.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#full-lora-application-update","title":"Full LoRa application update","text":"

A Heltec V4.3 Full Companion running source revision 2bd4cb4d seeded the exact Full .mota package. The destination started on application revision e16db810 with body hash C9137AAAB9A5FCDB. USB CLI controlled the target; application image bytes traveled over LoRa. The target received zero firmware bytes over USB during this application OTA test.

Both radios used a temporary 909.5 MHz, 500 kHz, SF7, CR 4/5 profile with a 32-symbol preamble. Target RX power saving was temporarily disabled for the transfer; its configured TX remained -6 dBm. The source retained its saved power-saving policy and TX setting.

Manifest 55E532FA reached 283/283 blocks, ready to install, with destination session age 439 seconds. Host seeder logs showed reads of all 283 payload blocks. After the install request, the target booted the expected version and reported body hash 6280E8156DB1042F. Its bootloader identity and CRC still matched the qualified HEAD build.

A preliminary attempt to activate scheduled temporary channels through remote administration failed before payload transfer. The completed test used local USB CLI to activate the maintenance channel. Fully remote scheduled activation and OTA with target RX power saving enabled remain unqualified by this run.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#restart-and-repeater-acceptance","title":"Restart and repeater acceptance","text":"

Ten USB query cycles, three close/reopen cycles and a warm restart passed. The version, body hash, board, role, public identity, TX setting and repeating state remained consistent. An independent XIAO S3 WIO Companion then sent three nonce-bound trace requests using the target's exact four-byte prefix route on the normal channel.

Round Exact nonce and one-hop route Target direct RX/TX delta Trace elapsed Receive error delta 1 PASS 1 / 1 0.498 s 0 2 PASS 1 / 1 0.555 s 0 3 PASS 1 / 1 0.536 s 0

These traces establish reception and reply forwarding by the updated target. The final core error flags were zero; the final packet counters reported zero receive errors. The source and trace partner retained their saved settings.

"},{"location":"test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/#final-configuration-and-cleanup","title":"Final configuration and cleanup","text":"

The first source cleanup check sampled the radio before its deferred restore finished and reported an active window. A subsequent check confirmed temporary radio inactive and exact equality with all saved source settings. The JSON preserves that initial cleanup result and the completed restoration check.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/","title":"Solarxiao advanced 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#solarxiao-advanced-carrier-profile-2026-10-21","title":"SolarXiao Advanced carrier profile, 2026-10-21","text":"

The older SolarXiao 2W Advanced already supplies battery voltage and processor temperature through XIAO self telemetry on channel 1. This change adds no new physical measurements. It adds an explicit saved carrier selection, capability reporting, live self-reading CLI aliases and guidance to existing history.

The published Advanced Standard schematic uses a BQ24650 analog charger. It has no INA monitor or digital charger interface; status LEDs, charger NTC and analog setpoints are not connected to XIAO measurement/control inputs. Firmware cannot recover panel V/I, battery current/power, battery temperature or charger flags from that design.

Both existing solarxiao_33S_repeater Full and Reduced packages built and passed package, capability and runtime memory checks. Native host fault tests passed under ASan/UBSan. These are build/host results, not hardware acceptance. No firmware was installed on SEA Sherwood, which the user confirmed is unreachable. Its exact installed target, bootloader and physical revision were not verified. The earlier Mercerwood MPPT bench report does not qualify this new image on physical hardware.

See the controls guide and machine-readable validation record.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"Item Result Application source de1bb0f2e6a37b02f1a9f0995ea539c818c3abe9 Version v1.18.0.1-halo-keymind-cascade-dev-de1bb0f2 Target / hardware ID solarxiao_33S_repeater / solarxiao_33S, unchanged EndF OTA target 0x3caf966e, unchanged Full application 597,816 bytes; body hash C5339ADF076AEC89 Reduced application 567,756 bytes; body hash 68D86AA255060031 Full memory check PASS: 150,584 internal bytes available; 76,960 required Reduced memory check PASS: 152,232 internal bytes available; 76,960 required DFU ZIP, UF2, linked capabilities PASS for both profiles Linked carrier commands and history PASS for both profiles Source changes to RF/PA, bootloader, flash layout or battery protection None

Both packaged applications retain the native MPPT marker and contain the Advanced selector, BQ24650 capability text, live XIAO-reading response, history guidance and unavailable-measurement explanation. Application body hashes and EndF identity fields were checked independently. The build's artifact manifest hashes matched every published local package, UF2 and capability file.

These packages contain compiled LoRa/Bluetooth update support. No LoRa OTA, boot, radio forwarding, charging or electrical test was performed for this image. Future deployment requires checking the destination's installed target and matching board/storage-aware bootloader; no remote update was attempted.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#host-coverage","title":"Host coverage","text":"

The following checks passed:

Reproduction:

python3 -m unittest discover -s test -p 'test_solarxiao*.py' -v\npython3 -m unittest discover -s test -p test_xiao_battery_safety.py -v\nbash build.sh build-firmware solarxiao_33S_repeater \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascadia --profile cascade\n

PlatformIO ran once at a time; the build entry point produced Full and Reduced sequentially. Local packages and logs are retained under /home/mesh/hwtest/solarxiao-advanced-20261021/.

"},{"location":"test-results/solarxiao_advanced_2026-10-21/#using-the-profile-after-a-compatible-update","title":"Using the profile after a compatible update","text":"
set solar.board advanced\nget solar.board\nget solar capabilities\nget solar readings\nget solar history\n

The operator selects the board. A missing/unpowered I2C device never identifies Advanced automatically. The absent-record default remains MPPT for existing installations; a failed presence query must yield a complete valid record or block native access. The separately stored selection does not alter existing charger overrides. Selecting MPPT again re-enables and reloads those overrides.

Channel-1 voltage and MCU temperature are unchanged. Voltage/temperature history remains the existing 30-minute, seven-day RAM history and resets on reboot. The MCU temperature is not battery temperature, and a voltage trend does not measure charge current, solar power or charger state.

"},{"location":"test-results/solarxiao_auto_2026-10-21/","title":"Solarxiao auto 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#solarxiao-automatic-carrier-detection-and-base-support-2026-10-21","title":"SolarXiao automatic carrier detection and Base support, 2026-10-21","text":"

SolarXiao repeater firmware now defaults to automatic carrier detection when there is no saved selection. A verified INA260 identity enables MPPT support; until then, the firmware reports self-only telemetry without guessing Base versus Advanced. Existing saved manual selections retain their meanings. Manual auto, base, advanced and mppt settings remain available.

Base needs no additional telemetry driver. The published Base Standard schematic uses a CN3791 analog charger with no digital interface or INA monitor. Its battery connects to the XIAO BAT input; charge status drives an LED. Like the older BQ24650 Advanced design, it provides existing XIAO battery voltage and MCU temperature only. There are no new physical measurements for either board. Base Lite publishes a README but no schematic; its exact wiring is not independently qualified here.

Radio target selection stays separate. The published Base schematic specifies E22-900M30S; the pictured 2W Advanced has M33S. The existing 30S/33S target must match the fitted module. Charger detection does not change RF settings, PA limits, hardware/OTA identity, bootloader, flash layout or battery protection.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"Item Result Application source 5c44324330a4b3a2e79059dbfa5e086d4cfc1e08 Version v1.18.0.1-halo-keymind-cascade-dev-5c443243 33S OTA target / hardware 0x3caf966e / solarxiao_33S, unchanged 30S OTA target / hardware 0xb38ebf22 / solarxiao_30S, unchanged Native host checks 22 PASS: 19 carrier methods and 3 existing power methods; native harnesses use ASan/UBSan XIAO startup/battery safety checks 6 PASS Full/Reduced DFU, UF2, linked capabilities, memory PASS for both targets Target Profile Application bytes EndF body hash Internal bytes available solarxiao_33S_repeater Full 599,132 7AB98467B6E3A468 150,576 solarxiao_33S_repeater Reduced 569,136 217075127849EE7C 152,224 solarxiao_30S_repeater Full 599,132 0C386CB1FC49608C 150,576 solarxiao_30S_repeater Reduced 569,136 ACBB1554247BB0B5 152,224

Every memory check requires 76,960 heap/transient bytes. Both profiles in both targets retain compiled LoRa/Bluetooth update support and telemetry history. Independent package checks verified the application version, EndF body length, body hash and target/hardware IDs. Package, UF2 and manifest file hashes matched. All four applications contain the native power marker, verified/pending Auto responses, Base/CN3791 and Advanced/BQ24650 descriptions, live self-reading response and history guidance.

The first 30S dependency installation failed before compilation because of a local disk quota. Reusing the identical inherited 33S dependency cache resolved the build setup; the final 30S Full/Reduced pair passed. PlatformIO processes ran sequentially throughout. Logs and packages are retained under /home/mesh/hwtest/solarxiao-auto-20261021/.

See the machine-readable validation record for exact package/application/UF2 hashes and detection parameters, and the controls guide for commands and hardware limits.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#detection-and-host-acceptance","title":"Detection and host acceptance","text":"

Auto reads only INA260 identity registers at carrier address 0x40: manufacturer 0xfe = 0x5449 and device 0xff & 0xfff0 = 0x2270. Address ACK, wrong identity, missing response and an incomplete second read do not enable MPPT. No INA or charger configuration is written by detection.

The first probe occurs after startup initialization. Unverified detection retries every five seconds, including across the millisecond timer wrap. A positive identity is retained until reboot or a successful mode change; temporary communication loss does not demote it. Before verification, the adapter does not read the LTC4162, replay saved overrides or emit native carrier telemetry/history. Self battery/MCU readings remain available.

The actual adapter harness verifies delayed discovery and reloads retained charger overrides only on the transition that enables native access. A dedicated long-uptime test first services manual MPPT at a low timestamp, disables it, then detects Auto at UINT32_MAX - 1000: replay occurs immediately once, the next check waits exactly five seconds across wrap, and a simulated charger reset reapplies the override without redundant writes afterward.

Manual Base/Advanced and unconfirmed/corrupt selections make zero native bus calls. Saved Auto restarts as unverified; original eight-byte manual MPPT and Advanced records keep their exact meanings without being rewritten. Fault tests cover metadata/read errors, wrong-size/corrupt records, partial writes, failed rename, corrupt publication and ambiguous public readback. Explicit successful selection recovers. Existing parser bounds, reply canaries, missing readings, history feature propagation, core charger checks and telemetry permissions/channel-order tests also pass.

"},{"location":"test-results/solarxiao_auto_2026-10-21/#deployment-limits-and-commands","title":"Deployment limits and commands","text":"

No firmware was flashed to any device in this run. No physical Auto detection, Base/Advanced electrical measurement, boot/soak, radio forwarding or LoRa OTA transfer was tested for this image. SEA Sherwood remains unreachable and unchanged; its installed target and bootloader have not been verified. Earlier Mercerwood hardware results belong to their named firmware revisions.

With no saved selection, Auto requires no setup. To replace a saved manual selection after installing compatible firmware:

set solar.board auto\nget solar.board\nget solar capabilities\n

Manual overrides use set solar.board base|advanced|mppt. When native access becomes enabled, retained /solar_power overrides are reloaded. Fresh LTC4162 chemistry/cell-count checks still independently guard all charger writes.

"},{"location":"test-results/solarxiao_basic_2026-10-21/","title":"Solarxiao basic 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#solarxiao-three-power-profiles-2026-10-21","title":"SolarXiao three power profiles, 2026-10-21","text":"

Base and Advanced share the same onboard power telemetry: XIAO battery voltage and MCU temperature. Their analog charger designs differ, but the published layouts expose no additional digital charger measurements or controls. The firmware now combines them into one Basic power profile without claiming which charger is fitted. Additional power measurements require added hardware/wiring.

There are three canonical choices:

Auto remains the default without a saved selection. Old base and advanced command values remain compatibility aliases for basic, not separate modes. Readbacks and help show only the three canonical choices. Existing Advanced record value 1 and Base value 3 both load as Basic without a startup write. New Basic/alias saves use value 1. MPPT and Auto retain values 0 and 2.

Radio target choice remains independent: select the existing 30S/33S target for the fitted E22 module. No RF settings, PA limits, OTA identities, bootloader, flash layout, battery protection or automatic detection behavior changed.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#qualified-source-and-packages","title":"Qualified source and packages","text":"

Application source: aa91fffae41465ba932f069f2a45e2a3ae1c7ad4. Version: v1.18.0.1-halo-keymind-cascade-dev-aa91fffa.

Target Profile Application bytes EndF body hash solarxiao_33S_repeater Full 598,824 37EE3060D73CAB2E solarxiao_33S_repeater Reduced 568,828 2174A0D9CB1EA7B9 solarxiao_30S_repeater Full 598,824 3525D6629B234891 solarxiao_30S_repeater Reduced 568,828 8037A8127A7D324A

All four DFU/UF2 builds passed linked capability, packaging and runtime memory checks. Full has 150,576 internal bytes available; Reduced has 152,224, against 76,960 required. Independent package checks verified version, EndF body length, body hash, unchanged target/hardware IDs, artifact hashes and linked Basic capabilities. Every application contains the three-option help and omits the old four-option help. PlatformIO ran one process at a time.

"},{"location":"test-results/solarxiao_basic_2026-10-21/#host-acceptance-and-scope","title":"Host acceptance and scope","text":"

All 28 checks passed: 19 carrier methods, 3 existing power methods and 6 XIAO startup/battery safety checks. Native harnesses use ASan/UBSan. Compatibility tests cover every old record value 0..3, both aliases, canonical Basic saves, identical actual-adapter gating and no startup rewrite. Every out-of-range record value 4..255 fails closed with no native bus access.

The actual adapter retains live self readings in Basic mode while native commands, periodic access, telemetry and voltage-history calls make no native bus calls. Existing detection/retry, timer-wrap/long-uptime replay, persistence fault, identity, permission, parser-boundary, canary and history checks pass.

These are host/build results only. No device was flashed and no physical Auto, radio, charger, boot/soak or LoRa OTA test was performed for this image. SEA Sherwood remains unreachable and unchanged. Earlier hardware reports apply only to their named firmware revisions.

See the validation record for package hashes and the controls guide for hardware sources and commands. Packages and logs are retained under /home/mesh/hwtest/solarxiao-basic-20261021/.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/","title":"Solarxiao ina260 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#solarxiao-ina260-battery-telemetry-2026-10-21","title":"SolarXiao INA260 battery telemetry, 2026-10-21","text":"

The SolarXaio MPPT 12V LiFePO4 carrier on the Mercerwood bench now reports its INA260 measurements in the normal MeshCore Full repeater build. With the user reporting a 13.2 V bench supply applied to BAT, all three LoRa telemetry replies reported 13.26 V, with signed current readings of -11, -8 and -8 mA. Three subsequent nonce-bound repeater trace tests passed at -6 dBm.

Machine-readable results contain the measurements, exact firmware identity, trace evidence, cleanup and limitations.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#discovery-and-correction","title":"Discovery and correction","text":"

The original Full build contained the INA260 driver, but its carrier I2C pins were reversed and its generic INA260 address was 0x41. Read-only diagnostic transactions identified this board's INA260 at 0x40, manufacturer ID 5449, device ID 2270, with SDA=P0.09 (variant pin 30) and SCL=P0.10 (variant pin 31). Three USB-only readings were about 0.11125 V before the user connected the BAT supply.

The SolarXiao target now supplies these carrier pins through PIN_BOARD_SDA and PIN_BOARD_SCL, and overrides TELEM_INA260_ADDRESS to 0x40. Generic XIAO wiring and the generic INA260 address remain unchanged. Discovery tries the INA260's device-ID check before the INA219 driver can claim the same address and write an incompatible configuration.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#qualified-build-and-live-acceptance","title":"Qualified build and live acceptance","text":"Item Result Source revision a947f936ecd279b7b3c97b4d832c4efc9a8a696b Environment and installed profile solarxiao_33S_repeater, Full Application version v1.18.0.1-halo-keymind-cascade-dev-a947f936 Application body hash E6E4E0E7CA5DB232 Application image 579,792 bytes Application SHA-256 0ba24397e11c0c30b0e3213ea597c536c4bc075c25fae44fc39fa43ce0e5626b Full and Reduced builds PASS; memory, DFU and nine capability markers per package Board startup tests Six PASS, including carrier pin override and GPIO safety negative controls; ASan/UBSan Warm restart PASS; sensor discovery, identity and saved settings preserved LoRa telemetry Three replies; channel 2 has voltage, current and power Repeater traces Three PASS; exact nonce, four-byte route and one target hop Trace direct RX/TX 1 / 1 per round; zero added receive errors Final core error flags Zero

The requested setup retained USB power, applied the supply to BAT and left solar disconnected. This checks sensor discovery and functional readout; it does not calibrate the INA260 against an independent voltage reference. Channel 1 is the XIAO internal ADC reading. Channel 2 is the carrier INA260. Reported power was 0 W in these near-zero signed-current samples; it is not an independent power or charger-performance measurement.

The normal radio remained 910.5250244,62.5,7,5, repeating stayed on, and TX stayed -6 dBm. RX power saving remained on,level=8,preamble=16,rx=39657,sleep=15975. The telemetry controller's temporary contact and forwarding connection were removed, the trace partner's settings were preserved, and USB endpoints were closed.

"},{"location":"test-results/solarxiao_ina260_2026-10-21/#scope-and-retained-evidence","title":"Scope and retained evidence","text":"

The installed application was updated through USB serial DFU. The qualified HEAD bootloader remained at revision b07255d4cdc601f1313e4cbe020caad3b5544821, manifest CRC 76EBABBF. The earlier full LoRa OTA transfer on application efad0076 is documented in the bootloader and LoRa OTA report. This run verifies LoRa telemetry and repeater forwarding on a947f936; it does not repeat a complete firmware transfer on that application.

An additional UART-pin diagnostic probe timed out after the successful INA260 reads. The original application and settings were restored before installing the final fix. The first trace run passed all three RF traces but failed a cleanup assertion that compared the entire changing radio2.status response. The corrected check compares saved radio settings exactly and verifies radio2 remains off; the subsequent complete run passed. Its final cumulative receive error counter was 7, with zero added during each owned trace round.

No LTC4162 charger configuration registers were written. The charger SKU is LTC4162-FSTM as reported by the user. Charging behavior and other carrier revisions are outside this acceptance result. The Reduced package was build-checked only; live INA260 acceptance used the Full package.

"},{"location":"test-results/solarxiao_power_2026-10-21/","title":"Solarxiao power 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_power_2026-10-21/#solarxiao-mppt-and-battery-controls-2026-10-21","title":"SolarXiao MPPT and battery controls, 2026-10-21","text":"

The Mercerwood SolarXiao 33S repeater now has a native LTC4162-FST charger interface and INA260 battery monitor in both Full and Reduced firmware. All 75 named register fields were read on the board. Supported setting changes passed live readback and persisted across a warm restart. The original charger register values were restored and all test overrides/calibration flags cleared. Three nonce-bound repeater forwarding tests passed at -6 dBm.

Machine-readable acceptance record contains exact build identities, readouts, command replies, fault-test coverage, RF samples, cleanup and limits. See the SolarXiao controls guide for commands, units, permissions, persistence, calibration and telemetry channels.

"},{"location":"test-results/solarxiao_power_2026-10-21/#qualified-firmware-and-hardware","title":"Qualified firmware and hardware","text":"Item Result Source ea8176ddc6b2caa7e52f8dcba46178f69f6cbbea Target / installed profile solarxiao_33S_repeater / Full Version v1.18.0.1-halo-keymind-cascade-dev-ea8176dd Application body hash 5A46FDAABF34C62D Application size 595,040 bytes Application SHA-256 d6068baf837c0348ea841a04a2412bd4724a93022f2d8b750af15ca1fb4fbe76 Full / Reduced qualification PASS: build, runtime memory, DFU, UF2 and linked capabilities Native power marker in both images PASS Native host test methods 3 PASS, including eight core scenarios and real Wire/atomic-store boundaries; ASan/UBSan XIAO startup/shutdown checks 6 PASS, including GPIO safety negative controls Powered USB readings 75 named register fields; FST chemistry code 6, four cells Settings / persistence 37 commands including 10 rejected writes; readback, restart and exact register restoration PASS LoRa carrier telemetry PASS: three replies with all carrier channels 2..13 Repeater forwarding 3 PASS; exact nonce, four-byte route, one target hop Bootloader HEAD b07255d4cdc601f1313e4cbe020caad3b5544821, CRC 76EBABBF, preserved

Full has 150,608 internal bytes available against 76,960 required by the runtime memory check; Reduced has 152,256 available. The installed public key, primary radio, RX power saving and bridge settings remained unchanged. Repeating stayed on and TX stayed -6 dBm. Each trace added one direct RX and TX with zero receive errors. The cumulative receive-error counter was 2 after the trace run and 17 after all telemetry work; core error flags remained zero. The trace partner's settings were preserved and endpoints closed.

"},{"location":"test-results/solarxiao_power_2026-10-21/#powered-bench-acceptance","title":"Powered bench acceptance","text":"

The user supplied 13.2 V at SOLAR/VIN with BAT disconnected and USB connected. Initial native readouts were approximately 13.2926 V input, 13.2785 V output, 27.76 C die and 27.73 C modeled NTC temperature. BAT was open: its changing floating/capacitor voltage is not a battery measurement or a charger load test. The charger reported battery_short while BAT was disconnected.

The test exercised force/fast telemetry, MPPT, input-current limiting, manual input-voltage floor, suspend/resume, JEITA selection, manual charge current and cell voltage, C/x threshold, CV/total timers and saved calibration metadata. For example, 13000 mV input floor read back as 13078.1 mV, matching the hardware step. Manual targets of 1000 mA and 3500 mV/cell read back correctly with JEITA off. These are register-interface tests without a connected battery.

Saved policy reloaded after restart with nine register overrides and the explicit calibration-source flag. Every modified register was restored to its captured value, then mppt reset cleared saved overrides and calibration flags. Final MPPT and JEITA were on; force/fast telemetry were off. Host fault tests also simulated a charger reset, read/write faults, ignored writes, corrupt or wrong-size storage, failed atomic publication, identity mismatch, masked-bit preservation, thermal ordering and one-shot/W0C behavior.

"},{"location":"test-results/solarxiao_power_2026-10-21/#supply-state-radio-checks-and-scope","title":"Supply state, radio checks and scope","text":"

The first install check found the charger unavailable. It restored the verified previous firmware with identity/settings intact; the user then confirmed the bench supply had been off and switched it on. The final firmware installed and passed powered reading/control tests.

During a later interval when the charger was unavailable again, authenticated LoRa CLI returned an explicit unavailable status while INA260 readings remained available. Three telemetry replies carried channel-2 voltage/current/power; charger channels were omitted. Temporary contacts and forwarding connections were removed. One remote version read required a retry.

After the supply was switched on again, authenticated LoRa commands identified FST/four cells and read charger/battery/status values. Three subsequent telemetry replies contained every carrier channel 2..13: input 13.29 V, output 13.27..13.28 V, die 25.4..25.5 C and modeled NTC 24.9 C. Two telemetry attempts missed replies before a successful retry. An earlier powered CLI session also needed retries and missed its first telemetry response; all temporary contacts/forwards were cleaned up. Final USB checks confirmed VIN 13.2926 V, MPPT/JEITA on, zero saved overrides, zero calibration flags and the same firmware/bootloader identities. These results establish successful readout, not a loss-free radio link.

No actual battery charging, loaded-current calibration, resistance measurement or ship-mode entry was tested. The default 10 milliohm sense resistors and 10 kohm / 3490 K NTC model come from reference documentation; the supplied Standard schematic uses a single-cell L42M, not this exact 12 V FSTM revision. Actual carrier component values and the temperature model remain configurable.

The new application was installed by USB DFU. This run does not repeat a full LoRa firmware transfer; the earlier successful HEAD bootloader/OTA transfer is in the separate OTA report. Reduced was build-checked, while live tests used Full. An exploratory Room Server build exceeded its flash/UF2 limits; native power controls are therefore scoped to the requested SolarXiao repeater targets. No Room Server or Companion package is qualified by this report.

"},{"location":"test-results/solarxiao_room_2026-10-21/","title":"Solarxiao room 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/solarxiao_room_2026-10-21/#solarxiao-room-server-power-profiles-and-history-2026-10-21","title":"SolarXiao Room Server power profiles and history, 2026-10-21","text":"

The canonical SolarXiao QSPI OTA Room Server builds now initialize and service the native power interface. Both 30S and 33S support the same auto, basic and mppt profiles as Repeater, including saved overrides, native telemetry and authenticated administrator commands. Basic still supplies only existing XIAO battery voltage and MCU temperature; no Base/Advanced measurements were invented.

The earlier size failure used a legacy plain Room recipe. This run qualifies the existing solarxiao_30S_room_server_lora_ota and solarxiao_33S_room_server_lora_ota recipes with Full and Reduced sensors. Both fit the unchanged 757,760-byte OTAFIX application region. No features, client capacities or flash reservations were reduced to make them fit. Plain Room compatibility/recovery targets remain separate and unchanged.

"},{"location":"test-results/solarxiao_room_2026-10-21/#qualified-source-and-artifacts","title":"Qualified source and artifacts","text":"

Source: 1d2fb6a1b471f2984a5a4133854cad4b52150337. Version: v1.18.0.1-halo-keymind-cascade-dev-1d2fb6a1.

Target Profile Application bytes EndF body hash solarxiao_33S_room_server_lora_ota Full 524,424 761289FD9CEFE79B solarxiao_33S_room_server_lora_ota Reduced 494,888 9CA04F37C960797A solarxiao_30S_room_server_lora_ota Full 524,424 AB4F7D7F85872201 solarxiao_30S_room_server_lora_ota Reduced 494,888 FBD9FAA560F5D0CE solarxiao_33S_repeater Full regression 598,824 DEB7475A4B4B0BBC solarxiao_33S_repeater Reduced regression 568,828 8CDBFB5FA2615124

All six builds passed DFU/UF2, linked capability and runtime-memory qualification. Room Full/Reduced have 156,352/157,984 internal bytes available against 65,280 required; Repeater has 150,576/152,224 against 82,752 required. The model now explicitly reserves 5,792 bytes for capped I2C voltage history and allocator overhead, retaining the previous general sensor allowance.

Independent package checks verified application version, body length/hash, artifact hashes, three power modes, native power marker and self/history responses. Room packages also contain persistent Room history and board commands. The linked Room code retains posting, access control, mailbox, board articles and persistent post history. Existing Room target IDs remain 30S 0x5bb567cd and 33S 0xa8cb8e8b; their hardware IDs remain solarxiao_30S/solarxiao_33S. Repeater keeps 0x3caf966e.

"},{"location":"test-results/solarxiao_room_2026-10-21/#history-correction-and-host-checks","title":"History correction and host checks","text":"

History previously configured channels once before Auto's first probe. An MPPT discovered later could therefore provide live telemetry but never gain voltage-history storage. The shared driver now reserves history IDs 2..5 at boot without any bus access. Only active, valid readings produce samples; empty channels remain absent from populated history results. Late INA260 detection, later charger input power and Basic-to-MPPT changes can supply samples without rebooting or clearing other sensor history. This fixes both Room and Repeater.

Four native channels allocate 1,440 bytes; the shared 16-channel cap limits native plus optional history to 5,760 bytes. The new budget test compiles the real history class and binds that allocation to the RAM policy. It also checks disabled-history/native cases and unrelated platforms.

Passing checks include:

PlatformIO ran one process at a time. Final packages and logs are retained at /home/mesh/hwtest/solarxiao-room-20261021/final-firmware/ and its parent. The validation record contains exact hashes.

"},{"location":"test-results/solarxiao_room_2026-10-21/#deployment-scope","title":"Deployment scope","text":"

This is host/build qualification, not physical Room acceptance. No device was flashed, no Room boot/soak or RF test was run, and no LoRa firmware transfer was performed for this image. SEA Sherwood remains unreachable and unchanged. The matching board/storage-aware OTAFIX bootloader is required for these Room targets; the existing bootloader/scratch layout and 30S/33S PA settings were preserved. See the controls guide for commands and target selection.

"},{"location":"test-results/spiffs_regular_file_reads/","title":"Spiffs regular file reads","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/spiffs_regular_file_reads/#spiffs-regular-file-reads-and-login-replay-state","title":"SPIFFS regular-file reads and login replay state","text":"

Arduino-ESP32 SPIFFS can return a truthy directory handle from a read-open of a nonexistent filename. File::operator bool() alone does not prove that a regular file exists. A directory's size() and read() are zero.

The first privileged login on an upgraded G2 encountered this in ClientACL::writeClientLoginReplayCeiling(): opening the not-yet-created /s_login_replay appeared successful, then subtracting the eight-byte trailer from size zero underflowed. Copying the supposed records failed and login was rejected. Waiting or changing the repeater clock cannot fix that file-open bug.

"},{"location":"test-results/spiffs_regular_file_reads/#fix","title":"Fix","text":"

Use mesh::openFileRead() for regular-file reads. It checks existence and rejects directory handles, while preserving real empty files. Replay record counts also validate the minimum trailer size, record alignment, and maximum count before subtraction. An existing replay file that becomes unreadable is not treated as a new store.

No replay records are cleared and authentication is not weakened. Invalid or unwritable replay state still fails closed. The first successful privileged login creates a 44-byte file: one 36-byte identity/ceiling record plus its eight-byte integrity trailer.

Setting the repeater clock must not clear this file. Admission compares the sender's timestamp with that sender's saved boundary, not with the repeater's current time. Clearing the boundary could make captured requests reusable. A sender clock rollback is a separate condition and remains subject to the saved boundary after this fix.

Companion directory enumeration uses the separate openDirectory() API, which deliberately permits SPIFFS virtual directories. Regular-file reads are also enforced for companion data, repeater/room logs, flood-rule verification, and HTTP packet-log downloads (missing logs return 404; real empty logs 200).

"},{"location":"test-results/spiffs_regular_file_reads/#audit-boundary","title":"Audit boundary","text":"

The audit covered file opens and size arithmetic in src and examples. Identity, region, clock, and common preference loaders already gate reads with filesystem existence checks; SPIFFS's exists() explicitly excludes directory handles. Relevant MQTT length subtraction follows validated headers and exact reads. ESP32 OTA staging uses partition APIs, not these SPIFFS file handles. Intentional directory enumeration must not be changed to a regular-file read.

"},{"location":"test-results/spiffs_regular_file_reads/#regression-checks","title":"Regression checks","text":"

The pre-fix actual ACL code was reproduced as first_login=rejected, one missing read-open, and no replay file. The fixed code accepted that identical scenario without a missing read-open and created the valid 44-byte record. Host simulations do not replace a post-flash LoRa login test on the G2.

"},{"location":"test-results/station_g3_g2_compatibility/","title":"Station g3 g2 compatibility","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/station_g3_g2_compatibility/#station-g3-and-g2-compatibility-audit","title":"Station G3 and G2 compatibility audit","text":"

Source audit on 2026-10-18, starting from 1147c28b279f01443c9ec44c3b71cc767f11482e. A user reported that G2 firmware works on a physical G3 while G3 firmware has problems. The comparison below identifies differences in configuration and startup; it does not establish the cause of that device's symptoms or claim a successful hardware retest.

"},{"location":"test-results/station_g3_g2_compatibility/#radio-and-peripheral-pins","title":"Radio and peripheral pins","text":"

The G2 radio map matches G3 physical slot 1 exactly. G3's _r2 suffix refers to physical socket 2, not motherboard revision 2. Current G3 firmware detects the populated socket at boot and retains both legacy target names.

Signal or build macro G2 G3 slot 1 G3 slot 2 SX1262 DIO1 / interrupt 48 48 2 SPI NSS / chip select 11 11 43 SX1262 reset 21 21 44 SX1262 BUSY 47 47 1 SPI SCK 12 12 39 SPI MISO 14 14 41 SPI MOSI 13 13 40 External LNA control Not controlled by G2 firmware 10, active low 42, active low PA PL1 control Not controlled by G2 firmware 9, shared; high selects higher level 9, shared I2C SDA / SCL 5 / 6 5 / 6 5 / 6 User button 38 38 38 PIN_GPS_RX / PIN_GPS_TX 15 / 7 15 / 7 15 / 7

The radio and FEM maps agree with the manufacturer's ESP32-S3 pinout viewer and its pin data, and the tracked G2 recipe, G3 recipes, and runtime socket table. The GPS macros name the peripheral's signals: the ESP32 startup calls Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX), giving MCU RX GPIO7 and TX GPIO15. No incorrect radio map was established. Both base recipes select a 1.8 V TCXO and disable the SX1262's internal boosted RX gain by default. The base TX power is 7 dBm in both; their configured maximum chip-drive limits differ (G2: 19 dBm; G3: 22 dBm). This audit does not change those power limits.

"},{"location":"test-results/station_g3_g2_compatibility/#ordinary-profile-flash-compatibility","title":"Ordinary-profile flash compatibility","text":"

Before this change, both board manifests declared QIO flash, but the G2 recipe overrode it to DIO. Ordinary G3 profiles inherited QIO. The G3 board manifest now selects DIO as well, following the known-working G2 startup policy. All G3 role recipes, including the legacy _r2 targets, inherit that setting.

ESP32-S3 Full builds already force DIO through the Full partition policy. This change therefore corrects an ordinary-profile difference; it does not explain a failure in an already verified DIO Full image. The PSRAM memory type remains qio_opi. Flash capacity, partition layouts, USB configuration, and radio pin assignments are unchanged.

Ordinary G2 firmware uses the legacy default.csv partition table, while G3 uses default_16MB.csv. Electrical compatibility does not make those layouts interchangeable. When moving a node that currently runs a G2 image to a G3 image, check its installed layout and use the documented partition migration if needed to retain its identity and settings. This fix does not change either layout.

The two board manifests otherwise have matching build/upload settings; their board names and manufacturer-page URLs distinguish them. DIO is a compatibility choice here, not a claim that QIO caused the user's reported problem.

"},{"location":"test-results/station_g3_g2_compatibility/#bounded-radio-startup-probe","title":"Bounded radio startup probe","text":"

The G3-only socket probe added in f765604cb8 imposed a 100 ms BUSY wait and only three RC-standby attempts before rejecting a socket. G2 goes directly through RadioLib's normal initialization, which permits a longer reset and RC-standby settling interval. This is a separate, reproducible startup compatibility gap, independent of the flash-mode difference.

The corrected probe shares one rollover-safe 1,000 ms deadline across reset, BUSY waits, and standby/version retries. It services cooperative USB while waiting. It retains identification-only operation, safe chip-select/reset cleanup, explicit socket overrides, and the existing fallback rules.

The native production-code fixture models both sockets and timer rollover. Elapsed times below are simulated fixture clocks, not physical measurements:

Modeled condition Previous probe Corrected probe BUSY remains high for 200 ms after reset Rejected at 111 ms, no version-register reads Detected at 202 ms, 16 bytes read First five standby commands ignored Rejected at 44 ms, 48 bytes read Detected at 67 ms, 96 bytes read BUSY never clears Rejected early Rejected at exactly 1,000 ms, no register reads Socket absent Rejected after three attempts Rejected at exactly 1,000 ms 800 ms BUSY followed by persistent command refusal Rejected early Rejected at exactly 1,000 ms total

An immediately responsive chip still completes its probe in under 20 ms in the fixture. Negative controls restoring the old short BUSY budget or the three-attempt limit fail the delayed-startup regressions. These results prove the software tolerance gap; they do not prove it caused the user's hardware failure. No transmit or TCXO-setting override was introduced.

"},{"location":"test-results/station_g3_g2_compatibility/#physical-socket-commands-in-the-normal-infrastructure-cli","title":"Physical socket commands in the normal infrastructure CLI","text":"

The follow-up audit also found that Repeater, Room, and Sensor infrastructure commands consumed all get and set requests in the shared CLI. Their final board-command hook was reached only for other verbs. Consequently, the G3's normal get radio.slot and set radio.slot ... commands never reached the board implementation. The separate pre-radio USB recovery console and KISS binary commands called the board directly and were unaffected.

Unknown shared getters and setters now reach the board-command hook before returning an unknown-setting reply. Shared commands, including the guarded FEM setters, retain priority. Socket changes still save only the next-boot selection; they never switch the running radio or reboot behind the operator's back.

"},{"location":"test-results/station_g3_g2_compatibility/#receive-gain-changes-during-transmission","title":"Receive-gain changes during transmission","text":"

The follow-up comparison from eb00b5370 found another G3-specific runtime defect. G2 firmware leaves the external amplifier controls alone. G3's setLNAEnable() reapplied receive mode unconditionally, so even repeating set radio.fem.rxgain on during an active transmission enabled the LNA before the transmit-completion hook. This was reachable through both the normal infrastructure CLI and Companion gain setters.

The G3 FEM now tracks startup/sleep, receive, and transmit mode. A gain setter updates the requested receive preference while preserving the transmit bypass; the requested state is applied when receive mode resumes. Startup and terminal sleep also keep the LNA disabled until an explicit receive transition. PA-level changes retain their existing policy of applying at the start of the next transmission. The inactive socket's LNA is left disabled.

The unconditional receive-mode call came from the runtime FEM preference implementation in 7509e51e3d, Kevin Le, 2026-08-12. This finding concerns mode handling; the PA/LNA pin assignments and polarities match the manufacturer documentation.

"},{"location":"test-results/station_g3_g2_compatibility/#complete-saved-radio-restoration-in-room-and-sensor-roles","title":"Complete saved-radio restoration in Room and Sensor roles","text":"

The same comparison found a shared role issue that can affect G3, G2, and other boards. Room and Sensor could clear their saved-radio retry flag after accepting the frequency/modulation tuple even when the subsequent saved TX-power command was rejected. Room also ignored rejection of its saved internal RX-gain command. An incoming packet or BUSY response during runtime restoration could therefore leave only part of the saved configuration applied.

Their saved-radio helper now reports success only after every applicable setting has been accepted. Rejection keeps the existing retry/backoff active, including restoration after a temporary radio setting expires. No preference layout, default setting, or Sensor RX-gain feature is added by this fix. This shared issue alone does not explain why G2 firmware works better on the user's G3.

"},{"location":"test-results/station_g3_g2_compatibility/#saved-fem-and-socket-settings","title":"Saved FEM and socket settings","text":"

G3 firmware actively drives its LNA and PA-level GPIOs. G2 firmware does not, so a saved FEM setting can have a visible effect only after switching to G3 firmware. Once the normal CLI has started, read these values before changing settings:

get radio.slot\nget radio.fem.rxgain\nget radio.fem.txgain\n

If startup reports Station G3 radio unavailable, the normal CLI has not started. Press Enter and use the limited USB recovery commands instead:

!g3 help\n!g3 get radio.slot\n

That recovery console accepts only socket selection, help, and reboot; the normal FEM getters are unavailable there.

radio.slot reports the saved selector, active socket, and detected sockets. An explicit socket override does not silently fall back to another socket. radio.fem.rxgain controls the external LNA and is distinct from radio.rxgain, which controls the SX1262's internal boosted gain.

Preserve the saved values and jumper positions while diagnosing the device. The manufacturer documents GPIO control as effective with the corresponding LNA P/LNA S or PA PL1 jumper removed; PA PL2 selects the hardware power-level pair. Changing gain or jumpers is an operator choice, not an automatic part of this compatibility fix.

"},{"location":"test-results/station_g3_g2_compatibility/#git-history-and-attribution","title":"Git history and attribution","text":"Change Introducing commit and author Initial G2 support and board manifest acac20dc89, Rastislav Vysoky, 2025-03-07 G2 radio pin flags moved into the variant recipe 7cb2e0863a, recrof, 2025-10-12 Original G3 slot-1 map, PA9 and LNA10 75fb07fc2c, Chris / Cisien, 2026-05-09; merged in upstream PR #2515 G2 DIO recipe override 1a43123d2c, mikecarper, 2026-08-24 G3 second-slot recipes and GPIO map d8d72b7833, Adam Gessaman / agessaman, 2026-09-05 Runtime socket discovery and shared pin binding f765604cb8, mikecarper, 2026-10-15

git blame and the introducing diffs were checked, rather than attributing copied pin values to the latest refactor. Original G3 support is present in upstream dev; the second-slot recipes and runtime discovery were added in this fork. The original G3 pull request reports hardware testing by its author. That historical report does not qualify the current firmware or the user's device.

"},{"location":"test-results/station_g3_g2_compatibility/#validation-boundary","title":"Validation boundary","text":"

The host recipe regression resolves every tracked G3 environment, checks that ordinary roles cannot override DIO, and verifies that qio_opi PSRAM remains unchanged. It also executes the existing Full policy for canonical Repeater, legacy _r2 Repeater, and Full Companion configurations. A deliberate nested QIO override must fail that check.

Check Status G3 recipe suite, including all 16 current role environments and the deliberate QIO override PASS, 10 test methods Existing ESP32 Full partition policy suite PASS, 11 test methods Native socket-detection suite, including 144 selection cases and 20 delayed/deadline cases PASS, 6 test methods, ASan/UBSan Combined socket detection/settings, boot recovery, radio pins, KISS slot and GPIO-hold suites PASS, 36 test methods, including the 6 detection methods above Ordinary Repeater, legacy _r2 Repeater and KISS firmware builds PASS; all bootloader and application image headers verified as DIO Canonical Full Companion package PASS; 24 linked capability checks, internal RAM and application-slot checks passed; bootloader/application and merged-image headers verified as DIO Follow-up G3 gain-mode tests through production CLI/Companion setters and GPIO helpers PASS, 2 test methods; both sockets, failed saves and old-setter negative control Follow-up Room/Sensor saved-radio startup and runtime restoration PASS, 4 test methods; actual wrapper rejection, retry/backoff and temporary-radio expiry paths Follow-up normal infrastructure socket-command routing PASS, 7 test methods; real board/NVS dispatch, shared guards and old-dispatch negative controls Combined follow-up native regression batch PASS, 30 test methods with sanitizer coverage Follow-up Repeater, Room, Sensor and ESP32-C6 Room firmware builds PASS, all 4 environments Follow-up canonical Full Companion package PASS; 24 linked capability checks, internal RAM and application-slot checks passed Follow-up hosted documentation link checks PASS, 7 test methods Physical G3 boot and RF receive/transmit Not performed for this change

No successful physical G3 boot, RF receive/transmit trial, or reproduction of the user's original problem is claimed by these host checks.

"},{"location":"test-results/t1000_usb_bluetooth_priority_2026-10-20/","title":"T1000 usb bluetooth priority 2026 10 20","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/t1000_usb_bluetooth_priority_2026-10-20/#t1000-e-usb-priority-20-october-2026","title":"T1000-E USB priority, 20 October 2026","text":"

Source: ac0daf3cef31a13163d469c1fd538c06708fe05a.

T1000-E Companions now stop Bluetooth advertising and disconnect the Bluetooth client whenever a USB data host is enumerated. This implements the requested USB-priority policy after the simultaneous USB/BLE investigation did not establish a reliable coexistence fix.

USB priority does not depend on an open serial application or DTR. A charge-only supply does not acquire priority. Bluetooth is requested again after the data host has been absent for 500 ms, preserving manual-off and wireless controls. The policy does not change saved preferences, pairing credentials or identity. It applies to the T1000-E Bluetooth and Full Companion recipes.

Check Result Full Companion build, capability markers, runtime RAM and application-only image bounds PASS Bluetooth Companion build, capability markers and application-only image bounds PASS Priority, startup/wake, manual controls, reply routes, bus-reset hold and timer wrap Six host tests PASS Existing Bluetooth preference/CLI and wireless control tests Eleven host tests PASS nRF52 BLE regressions, including late connect after disable 42 tests OK; eight emulator-dependent tests skipped Contact-stream concurrency Two host tests PASS Hosted documentation links Seven Python tests and JavaScript checks PASS Install Full on the exact Pi-connected T1000-E PASS; application only, identity unchanged USB runtime and saved on/off commands while paused PASS; status reports bluetooth off (USB data connected) Saved on during USB priority PASS; command acknowledges saved on while remaining paused Twenty-second BLE scan with USB enumerated and serial handles closed Zero target events among 100 scan callbacks Post-test USB readback PASS; same identity/version, zero core flags, zero recorded crashes Physical unplug, BLE recovery and reconnect qualification on this build Pending user unplug USB insertion during an established BLE session Host regression covered; hardware check pending Charge-only and cold-boot behavior on this build Host policy covered; hardware checks pending

The installed version is v1.18.0.1-halo-keymind-cascade-dev-ac0daf3c (Build: 20-Oct-2026). Its application ZIP SHA-256 is 64e09ff2eb8b5e3228f656c7ea99b000c053736b8a020b7eab1429b4d00ab43f. The bootloader and SoftDevice were not changed. All opened USB ports were closed, with no framing rejects in the recorded runtime checks. The original saved-on Bluetooth choice was restored after the temporary on/off test.

This verifies USB-priority behavior, not simultaneous active USB/BLE, complete T1000-E feature qualification or qualification of the other campaign boards. Historical battery and wall-power passes belong to their original builds. Safe evidence summary records the current source and artifact bindings.

Later accepted measurements are recorded in the 2026-10-20 fleet qualification. Results and failures above retain their original firmware sources.

"},{"location":"test-results/tdeck_radio2_display/","title":"Tdeck radio2 display","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/tdeck_radio2_display/#t-deck-radio2-scheduling-during-display-redraw","title":"T-Deck radio2 scheduling during display redraw","text":"

Measured on 18 October 2026 (America/Los_Angeles), using one ESP32-S3 T-Deck with 16 MiB flash and 8 MiB PSRAM. The receive-only harness uses the production CustomSX1262Wrapper, profile scheduler and ST7789LCDDisplay. It never sends radio packets or starts WiFi/Bluetooth. Source base: 22cdb7014, with the changes and application readback hash identified in the machine-readable results.

"},{"location":"test-results/tdeck_radio2_display/#change","title":"Change","text":"

T-Deck renders into a 153,600-byte RGB565 canvas allocated explicitly in PSRAM. It transfers at most 960 pixels per TFT transaction (384 microseconds of wire time at 40 MHz). Each transfer ends and releases chip select before servicing the radio. CPU rasterization also offers bounded cooperative checkpoints. The callback runs only the radio driver, without recursively entering the mesh dispatcher or UI. Pending-packet, active-reception and transmit guards remain.

The T-Deck radio also uses the existing buffered 8 MHz SPI HAL. The TFT and radio retain their shared HSPI peripheral and separate chip selects. Display wake restores the backlight without repeating controller initialization. Other boards keep their existing display path. If PSRAM allocation fails, the driver warns and falls back to direct rendering.

"},{"location":"test-results/tdeck_radio2_display/#same-image-measurements","title":"Same-image measurements","text":"

The two profiles were 909.5 MHz / SF7 / BW62.5 and 910.5 MHz / SF8 / BW500, both CR5 with explicit 32-symbol preambles. These are measurement controls, not a packet-delivery or preamble recommendation.

Each stress case ran three times for five seconds, requesting 20 redraws per second. Rendering could take longer than the requested frame interval; actual frame counts and elapsed times are retained in the results. Values below use all samples across the three trials, with worst observed maxima.

Display path Radio SPI Mean retune Maximum retune Maximum gap between radio service calls Static screen Bytewise, 2 MHz 497 us 525 us 584 us Original direct redraw Bytewise, 2 MHz 610 us 629 us 76,089 us PSRAM canvas and bounded strips Bytewise, 2 MHz 581 us 668 us 1,343 us PSRAM canvas and bounded strips Buffered, 8 MHz 468 us 581 us 1,237 us

At a normal requested refresh rate of one frame per second (two four-second trials per case), the original 2 MHz path's maximum service gap was 73,383 us; the new 8 MHz path's was 1,251 us. Reducing refresh frequency alone does not remove the individual blocking redraw.

Retune samples bracket the production retune hooks. They are separate from display frame duration and the gap between radio service calls. Histogram percentiles in the JSON are bucket upper bounds, not exact percentiles.

All 30 trials completed 7,025 profile switches with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. Every final RX-mode and modulation-cache check passed. The minimum remaining loop stack was 3,340 bytes in this harness. No packets were decoded, so these measurements do not establish packet delivery under redraw, loaded-channel behavior, Bluetooth/ WiFi interference, or long-term reliability.

"},{"location":"test-results/tdeck_radio2_display/#reproduction-and-checks","title":"Reproduction and checks","text":"

Include tools/hil/tdeck_display_switch.ini in platformio.local.ini, build tdeck_display_switch, and flash the exact T-Deck. The board needs a ROM-compatible DIO boot header; the first QIO-header attempt failed before the application started. The supplied harness recipe selects DIO.

Run the collector with --fps 20 --seconds 5 --repeats 3, then with --fps 1 --seconds 4 --repeats 2. Only one tool may own the selected serial port, and PlatformIO commands must run sequentially.

Sixteen focused native display/R8/radio tests passed. They cover pixel parity, clipping, partial updates, strip bounds, released SPI before callbacks, callback recursion protection, PSRAM failure, mode toggling and sleep/wake. The new display regression is included in CI.

"},{"location":"test-results/tdeck_radio2_display/#full-companion-smoke-check","title":"Full Companion smoke check","text":"

The optimized canonical Full Companion image v1.18.0.1-tdeck-radio2-fast-22cdb701 compiled and passed its unchanged RAM gate (230,680 internal bytes available; 164,896 required). It was installed on the same board. USB ASCII commands and Binary Companion device queries worked; Bluetooth reported on. Device queries retained 350 contacts and 40 channels. A Bluetooth client connection was not tested.

With the screen held on, a temporary receive-only secondary accumulated 759 switches with zero switch errors during a short runtime check. Its maximum successful active-pair retune was 1,013 us and the guarded budget was 1,115 us. One additional primary packet was received during that interval. This is a smoke check rather than a controlled packet-delivery trial. Returning to single-profile RX power saving raised the lifetime max diagnostic to 4,510 us; that teardown is outside the active pair's measured switching budget.

The board was left running Full Companion with Bluetooth on, radio2 off and its original button-pairing USB display mode. Seven hosted-document tests also passed after adding this report.

"},{"location":"test-results/tdeck_radio2_retune_experiments/","title":"Tdeck radio2 retune experiments","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#t-deck-retune-command-priority-and-spi-experiments","title":"T-Deck retune command, priority and SPI experiments","text":"

Measured on 18 October 2026 (America/Los_Angeles) on one ESP32-S3 T-Deck with 16 MiB flash and 8 MiB PSRAM. This follows the display redraw scheduling measurements. The receive-only harness uses the production radio wrapper, profile scheduler and display driver. It does not start Bluetooth or WiFi or send radio packets. The sanitized measurements retain each result once, firmware image hashes, counters, sample counts and timing summaries; serial-port identifiers and raw console transcripts are omitted.

For this tested board, the selected setting is 8,888,888 Hz with the confirmed modem and IQ caches enabled, normal owner priority, and the original SPI clock phase programming. This was the highest bulk-transfer clock that passed these checks. It does not establish a board or transceiver physical maximum. The canonical Full Companion build and runtime smoke checks are recorded below.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#measurement-method","title":"Measurement method","text":"

The primary profile was 909.5 MHz / SF7 / BW62.5 / CR5 with a 32-symbol preamble. The secondary was 910.5 MHz / SF8 / BW500 / CR5, using a 32-symbol preamble for the main and load comparisons and 40 symbols for the IQ comparison. These controlled settings are not a packet-delivery or preamble recommendation.

All 42 controlled trials used the same 394,304-byte application, SHA-256 9afce4f59227382dfe383b6b53ac52001644b05f8636c8f4c4f3ed6e12937668, built from base 22cdb7014 with the experimental changes applied. Each case ran three times for five seconds with either a static screen or requested 20 Hz buffered redraws. Actual frame counts are retained because rendering can take longer than the requested interval.

Retune timings bracket the radio wrapper's production retune hooks. Radio service gaps, display frame durations and receive-window deadline lateness are separate measurements. Table means combine both retune directions and all three trials, weighted by sample count; maxima are the worst observed. The source means were recorded to three decimal places. JSON percentile fields are histogram bucket upper bounds, not exact percentiles.

Command counters count the instrumented RadioLib HAL transfers, transferred bytes, packet-type queries and acknowledged cache skips. They describe actual execution of the selected policy rather than an estimated command saving.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#confirmed-modem-query-cache","title":"Confirmed modem query cache","text":"

The cache omits the repeated packet-type query only while a previously confirmed LoRa modem remains valid. Lifecycle invalidation and the existing receive and packet-ownership guards remain part of the production path.

Policy at 8 MHz Static mean retune Static maximum Buffered mean retune Buffered maximum Query on each hop 428.810 us 450 us 486.632 us 595 us Reuse confirmed modem 400.021 us 423 us 459.761 us 575 us Query on each hop, scoped priority 3 439.244 us 460 us 495.471 us 605 us

Across the cache-enabled main cases, 1,844 hops recorded 1,844 skipped modem queries and zero actual modem queries. The corresponding uncached baseline recorded 1,844 modem queries. Scoped priority alone did not improve these quiet harness timings.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#scoped-priority-under-a-competing-task","title":"Scoped priority under a competing task","text":"

The single existing radio/UI owner normally ran at priority 1 on core 1. The experiment temporarily raised only the retune call to priority 3, then restored priority 1. It did not create a second task that could manipulate radio state. The competing task ran on the same core at priority 1, performing a bounded 250-microsecond CPU burst every two RTOS ticks.

Both policies enabled the confirmed-modem cache. The load cases recorded 25,269 competing-task bursts. Every result confirmed the selected actual retune priority and restoration of the original owner priority.

Priority during retune Static mean / maximum Buffered mean / maximum Static maximum service gap Buffered maximum service gap 1 429.704 / 665 us 594.379 / 825 us 730 us 1,494 us 3, then restore 1 405.178 / 492 us 527.755 / 832 us 738 us 1,519 us

Scoped priority reduced mean retune time with this particular competitor, but did not improve the worst complete radio-service gap. This synthetic task does not establish behavior with a connected Bluetooth client, network traffic or the other services in Full Companion.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#iq-workaround-reuse-when-packet-parameters-change","title":"IQ workaround reuse when packet parameters change","text":"

Using a 40-symbol secondary preamble makes the packet tuple change between the two profiles. The comparison tests reuse of the already applied normal-IQ workaround while retaining the packet-parameter writes required for that change. Both cases used the confirmed-modem cache and normal owner priority.

IQ workaround cache Static mean / maximum Buffered mean / maximum Off 511.358 / 533 us 584.149 / 708 us On 430.664 / 457 us 491.475 / 600 us

All 12 IQ trials passed their runtime checks. The enabled cases recorded 1,843 IQ workaround skips across 1,843 hops. This is timing and state evidence from a quiet receiver, rather than a packet-delivery comparison.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#spi-sweep-and-transfer-path-comparisons","title":"SPI sweep and transfer-path comparisons","text":"

The fresh-boot sweep and bytewise diagnostic used a separate 395,408-byte application, SHA-256 d0124aac39a949f8a1cc292cb039f64ac61351d3fd3ca248137f14b4fb06ac90. Every independent clock attempt began with a device reboot. Each inspection performed 100 paired reads of packet type and the LoRa sync word, with standby and recovery at 8 MHz. Static and buffered timing trials followed only after the preceding checks passed. Clock order alternated between repeats.

The frequency column is calculated from the SPI peripheral's configured clock divider, captured during the radio HAL transfer. It is not an oscilloscope measurement of the clock signal.

Requested MHz Divider-derived MHz Complete passing attempts Incorrect modem reads per 100, across three inspections 8 8.000000 3/3 0, 0, 0 9 8.888888 3/3 0, 0, 0 10 10.000000 0/3 6, 7, 7 11 10.000000 0/3 5, 5, 6 12 11.428571 0/3 20, 20, 28 13 11.428571 0/3 22, 20, 20 14 13.333333 0/3 59, 49, 67 15 13.333333 0/3 58, 54, 55 16 16.000000 0/3 64, 68, 64

These inspections recorded no sync-word mismatch or explicit SPI read error; the failing check was an incorrect modem value. The sweep's highest passing divider-derived frequency was 8.888888 MHz for this bulk-transfer implementation. This observation must not be treated as the radio's physical SPI limit.

A separate bytewise diagnostic read all 100 modem values correctly at both 10 MHz and 16 MHz, with no sync-word mismatch or reported read error. Its 16 MHz bulk comparison returned 29 correct values and 71 incorrect values; returning to bulk 8 MHz read all 100 correctly. The different transfer paths therefore prevent interpreting the bulk failures as a transceiver clock limit.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#clock-register-experiment-and-longer-read-checks","title":"Clock-register experiment and longer read checks","text":"

The final diagnostic and comparison image was 396,560 bytes, SHA-256 6e830f93dd3bb632e73e47878e8bbe61795a0389fa1ba75aab3c2a1ff9e14a85. With the original phase programming, 10,000 paired modem/sync-word reads passed at both 8 MHz and 8.888888 MHz, with zero incorrect modem values, sync-word mismatches or reported read errors. A 10 MHz check returned 58 incorrect modem values in 1,000 pairs.

Inspection found differences between some N/pre-divider/phase fields and an experimental reconstruction of the clock register. Changing those phase fields did not improve the link: at 8.888888 MHz it introduced 587 incorrect modem values in 10,000 pairs; at 10 MHz it returned 64 incorrect values in 1,000 pairs, compared with 58 using the original programming. The experiment also failed at higher tested clocks. It was rejected for production and remains confined to the HIL helpers under tools/hil.

These observations neither explain every transfer-path difference nor establish a causal driver defect. The reported frequencies remain divider-derived values; clock waveforms and phase lengths were not measured with an oscilloscope.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#final-comparison-with-exact-clock-settings","title":"Final comparison with exact clock settings","text":"

All 24 final trials used that same final image, with exact clock settings enabled and the rejected phase-field correction disabled. Each policy ran twice for five seconds in both display modes. A nominal 9 MHz request in the earlier sweep produced an 8,888,888 Hz divider; the final setting requests that exact frequency, avoiding the rounded-frequency divider search. The timing comparison below is within this final image rather than across different instrumentation builds.

Secondary preamble Clock setting Caches Static mean / maximum retune Buffered mean / maximum retune 32 8 MHz Neither 426.751 / 457 us 518.300 / 626 us 32 8 MHz Modem 404.635 / 438 us 493.754 / 610 us 32 8.888888 MHz Modem 402.134 / 436 us 491.574 / 608 us 40 8 MHz Neither 562.482 / 605 us 652.882 / 777 us 40 8 MHz Modem and IQ 437.898 / 472 us 528.139 / 648 us 40 8.888888 MHz Modem and IQ 435.327 / 471 us 526.319 / 653 us

The additional clock increase saved about 2\u20133 microseconds in mean retune time for these cached paths. Avoiding redundant commands supplied the larger saving. The cache-enabled final trials recorded 4,912 modem-query skips; the IQ-enabled cases recorded 2,457 IQ workaround skips.

Across the cached 8.888888 MHz final cases with secondary preambles 32 and 40, the best observed individual retune was 378 us and the worst was 653 us. The separate Full Companion smoke check below reached 1,224 us under its application workload; those measurements are not interchangeable.

The selected T-Deck configuration uses MC_SX1262_SPI_HZ=8888888, MC_SX1262_FAST_MODEM_CACHE=1 and MC_SX1262_FAST_IQ_CACHE=1. Scoped priority stays off because it did not improve the maximum complete radio-service gap. The clock-register correction stays off because its hardware comparison was worse. This selection is limited to the tested board and workload.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#full-screen-redraw-duration","title":"Full-screen redraw duration","text":"Renderer and radio policy Frames Mean redraw Minimum Maximum Original direct redraw, bytewise 2 MHz 204 73.603 ms 72.298 ms 75.131 ms Final buffered redraw, 8.888888 MHz, preamble 32 176 57.035 ms 56.123 ms 57.839 ms Final buffered redraw, 8.888888 MHz, preamble 40 176 57.161 ms 56.085 ms 57.964 ms

A typical redraw therefore fell from about 73.6 to 57.1 ms: roughly 16.5 ms saved, or 22% less time. The worst observed redraw fell from 75.1 to 58.0 ms. These durations include the cooperative radio service performed during rendering.

The original result combines three five-second trials from the first display HIL image; each final row combines two five-second trials from the final comparison image. They are observations across different images and combined policies. Most of the saving comes from the PSRAM canvas path; this comparison does not isolate the benefit of increasing the radio clock above 8 MHz.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#checks-limits-and-remaining-follow-up","title":"Checks, limits and remaining follow-up","text":"

The 42 controlled trials completed 12,920 profile switches and 1,801 display frames, with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. All final RX-mode, modulation-cache and priority-restoration checks passed. The minimum remaining owner-task stack was 3,288 bytes.

The 24 final exact-clock trials also passed every runtime check, completing 7,367 switches and 1,054 frames with zero reported switch failures, failed retunes, BUSY timeouts or chip errors. All final RX-mode, cache and priority checks passed. Their minimum remaining owner stack was 3,224 bytes.

No packets were decoded in those trials. They do not establish reception under redraw, channel-load behavior, Bluetooth/WiFi coexistence or long-term reliability. The SPI sweep failures are retained separately from the passing 42-case control matrix, and were not followed by dependent timing trials on the failed link.

All seven hosted-document link tests passed after adding this report. Twenty-three focused native regression tests also passed. The final HIL source, including the isolated clock experiment helpers, compiled.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#reproduction","title":"Reproduction","text":"

Build and install the receive-only HIL using the display report's build instructions. The experiment controls change RAM only and the harness has no radio transmit path. On its USB console, reproduce the final buffered, cached comparison with:

exactclock 1\nclockfix 0\ninspect 9 1 10000\ntrial 2 9 1 1 0 0 40 5 20 1\n

The trial selects buffered display, nominal 9 MHz, modem and IQ caches on, normal owner priority, no synthetic load, secondary preamble 40, five seconds and requested 20 Hz redraws; the last number is the result sequence. Check actual_spi_hz for 8,888,888, exact_clock for true and clock_timing_corrected for false, alongside the error and cache counters. actual_spi_hz is derived from the configured divider during the transfer; it is not an oscilloscope measurement.

For repeated 8 MHz control cases, the collector accepts a port placeholder:

python tools/hil/tdeck_retune_experiments.py <port> --suite main --spi 8 --seconds 5 --repeats 3 --fps 20 --output retune-main-8mhz.json\n

tools/hil/tdeck_spi_sweep.py explicitly reboots before every independent clock attempt. Reboot resets the RAM-only exact-clock flag to false. Its original clock matrix and the final manual exact-clock comparison therefore use different clock-setting policies and must be interpreted separately.

"},{"location":"test-results/tdeck_radio2_retune_experiments/#full-companion-follow-up","title":"Full Companion follow-up","text":"

The canonical Full Companion v1.18.0.1-tdeck-radio2-tuned-22cdb701 compiled with the selected 8,888,888 Hz radio SPI setting, both caches enabled, and normal owner priority. Its unchanged RAM gate reported 230,680 internal bytes available, 164,896 required and a 197,912-byte largest region. These are linked capacity checks rather than free heap after boot. The application body size check was 2,261,101 bytes; the packaged application was 2,261,784 bytes, SHA-256 9251694050ad0acdf4699cbedfabc11fe0597d28e57156f1e30d2b80df5ac9a3. All 31 capability verification checks passed.

The application was flashed into app0 while retaining saved device state. Five intentional reboots returned the expected firmware, Bluetooth on and radio2 off. USB Binary Companion device queries reported protocol 14, 350 contacts, 40 channels and manufacturer LilyGo T-Deck.

In the first 45-second runtime check, the screen was held on and a temporary RX-only secondary used a 40-symbol preamble. The node completed 2,450 switches with zero switch errors. At least six primary packets arrived while scanning (RX=6 advanced to RX=12). The longest observed active-pair retune was 1,224 us, with a provisional guarded budget of 1,347 us. The timing recommendation window had not finished, so this remains a runtime smoke check.

A second runtime check also retained zero switch errors. Its lifetime max field already contained 4,518 us before that active test, following the earlier return to single-profile operation. That value is not attributed to an active-pair retune. The first run's results remain in its log; the second run's JSON replaced the earlier runtime JSON, so the sanitized report identifies these sources separately.

Windows completed a fresh PIN pairing, and a subsequent connection confirmed ENCRYPTION_AND_AUTHENTICATION. A requested 30-second, 10 Hz DEVICE_INFO exercise returned 69 successful queries and then timed out; it recorded no notification-queue overflow. It did not complete the intended 300-query workload and is not a passing sustained BLE test. That earlier run overlapped administrative settings writes.

A controlled retry configured the screen and secondary before starting BLE, kept USB open without configuration changes during traffic, and performed cleanup after traffic finished. It completed 30.060 seconds with 208 successful DEVICE_INFO queries, achieving 6.886 queries/second against the requested 10 Hz. Reply latency was 118.109 ms minimum, 145.063 ms mean, 239.963 ms p95 and 300.103 ms maximum. There were 213 notifications, including five unrelated notifications, and no queue overflow or traffic disconnect. The authenticated connection was still alive immediately before normal cleanup. APP_START was not required. This passes the sustained-duration check while documenting the lower achieved query rate; it does not establish the cause of the earlier timeout.

During that retry, radio switches advanced from 6,494 to 8,402 with zero errors, and four primary packets arrived (RX=125 to RX=129). The provisional guarded switch budget was 1,281 us. The lifetime 4,527 us max was already present before the retry and is not attributed to an active-pair retune.

The documented cleanup returned radio2 to off and the USB display mode to button-pairing, and Bluetooth was on.

"},{"location":"test-results/udp_bridge_2026-10-22/","title":"Udp bridge 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/udp_bridge_2026-10-22/#eight-peer-wifi-udp-bridge-22-october-2026","title":"Eight-peer WiFi UDP bridge, 22 October 2026","text":"

Status: host tests and all three Full repeater builds PASS. No radio was flashed or contacted. Physical WiFi/UDP delivery, router policy, LoRa forwarding and OTA operation remain outside this validation.

Clean firmware source f4a9a05f7 was built with the canonical builder, version v1.18.0.1-halo-keymind-cascade-dev, Cascade runtime defaults and the USA Cascade preset. The safe receipt binds source, build recipes, linked memory measurements and package hashes.

"},{"location":"test-results/udp_bridge_2026-10-22/#resulting-behavior","title":"Resulting behavior","text":"

Supported ESP32 Full repeaters gain a default-off UDP bridge with eight explicit IPv4/port peer slots. One bounded, nonblocking socket runs only on a connected station with an IP address and exact station-only SDK mode. It pauses on disconnection, AP/AP+STA, browser/deferred WiFi OTA and manual WiFi shutdown, then restores eligible saved intent. It can join using existing WiFi credentials without an MQTT broker or a running MQTT worker.

Packets use versioned HMAC-SHA256 authentication with the existing bridge.secret; no new password is added. Fresh challenge/proof exchanges establish boot sessions, with receiver-boot binding and monotonic receive sequences rejecting replayed mesh packets and probes. Changing the shared secret closes old sessions and discards old queued work before further socket service. Authentication does not encrypt the UDP payload.

Local LoRa traffic can fan out to the eight direct destinations. Incoming UDP enters the local LoRa mesh, with the usual filters and admission rules; it is not forwarded to other UDP peers. A volatile packet-origin marker prevents later LoRa TX callbacks, including OTA retries, from exporting that same UDP ingress packet again. The marker survives retry copies, is never serialized, and fits existing packet padding. Unrelated builds do not gain it.

The setup and diagnostic guide covers saved settings, status, counters, one-way probes and runtime USB summaries. ready counts authenticated boot sessions, not current peer liveness. Match a fresh probe's ID and increased receive count on the destination to establish reachability. Probes do not allocate mesh packets, echo, retry or reach LoRa. Automatic handshake controls have their own bounded retry policy.

"},{"location":"test-results/udp_bridge_2026-10-22/#embedded-build-results","title":"Embedded build results","text":"Requested target Selected Full environment Available internal bytes Required heap bytes Model margin Result Heltec_v3_repeater Heltec_v3_repeater_observer_mqtt 209,392 209,040 352 PASS heltec_v4_repeater heltec_v4_repeater_observer_mqtt 202,496 197,776 4,720 PASS heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt 200,264 195,728 4,536 PASS

Each image passes application and merged packaging, linked capability checks including bridge.udp, and unchanged memory/size gates. All capability receipts have empty feature-reduction lists. No feature or reserve was removed.

The enabled-case model adds 5,120 bytes even though UDP defaults off: a source- asserted 3,072-byte object allowance and 2,048 bytes for the bounded socket and receive buffering. The socket requires a successful 768-byte receive-limit configuration. WiFi stack memory is counted once alongside MQTT, with UART and other existing allocations still reserved. The V3 model margin is small; these linked capacity estimates are not running free-heap, fragmentation or maximum-load measurements. Physical startup/load/soak checks remain necessary.

An initial compile attempt at 5e25faa3d exposed aggregate initialization unsupported by the pinned ESP32 compiler. f4a9a05f7 uses explicit field assignment. The host suite now compiles the production UDP implementation with strict C++11 before linking its test driver. The earlier failure is retained privately and excluded from qualifying packages.

"},{"location":"test-results/udp_bridge_2026-10-22/#host-checks","title":"Host checks","text":"

Router client isolation cannot be diagnosed conclusively from a successful WiFi association alone. The physical network must allow the configured peers to exchange UDP. This work did not change router settings or attempt to bypass network isolation.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/","title":"Udp bridge hardware 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#five-board-wifi-udp-bridge-hardware-qualification-22-october-2026","title":"Five-board WiFi UDP bridge hardware qualification, 22 October 2026","text":"

PASS for the tested Full Repeater configuration. Five physical ESP32 boards passed the directed UDP matrix, rejection checks, reconnects, software-reboot persistence and physical LoRa forwarding through eight configured UDP peers. All final images use source 36d9735e5. Independent raw-packet and cleanup audits passed before consolidation of the machine-readable results.

Physical coverage combines two disjoint scopes: twelve accepted passing cases from failed run05 and eighteen from passing run07. Run05 remains failed; the first twelve cases were not repeated. Failed run06 sent no data and remains preserved separately.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#coverage-and-results","title":"Coverage and results","text":"Check Completed result Canonical Full builds and installation Five boards; capability and memory gates passed; no feature reductions Directed UDP probes 300/300 across 60 directions, five probes each Security rejections 20/20: replay, bad authentication, unknown endpoint and malformed frame on each board Recovery WiFi reconnect and saved UDP-off choice passed on all five Qualified software reboots Five restarts, retained configuration and 20/20 fresh probes Physical LoRa delivery 25/25 exact application receipts, five per ingress board UDP-off negative controls 5/5; none crossed the frequency gap Independent copy audit 100 physical-peer copies and 100 Pi-endpoint copies; zero UDP transit Final cleanup Five boards quiet, original RXPS restored and owned fixture stopped

The matrix covered 20 board-to-board, 20 board-to-Pi and 20 Pi-to-board directions. Initial station readiness was observed on the first read-only poll for every final image. Each board had 23 continuity samples spanning at least 1,043 seconds, with advancing uptime and no new latched error bits. The reboot qualification used deliberate software restarts, not cold power cycles. Identity, exact firmware/model, shared secret, UDP port and all eight peers persisted; eight authenticated sessions returned.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#boards-and-builds","title":"Boards and builds","text":"Board Selected Full environment Modeled internal-memory margin Heltec V3 Heltec_v3_repeater_observer_mqtt 352 bytes Heltec V4.3 OLED heltec_v4_repeater_observer_mqtt 4,720 bytes LilyGo T-Deck LilyGo_TDeck_repeater 53,632 bytes Seeed XIAO ESP32-S3 WIO Xiao_S3_WIO_repeater_observer_mqtt 9,960 bytes SenseCAP Indicator LoRa N8R8 SenseCapIndicator-LoRa_repeater 1,904 bytes

The shared V4 target detects the V4.3 FEM at runtime. The Indicator tested here has 8 MiB flash and OPI PSRAM. Its separate 16 MiB/QSPI N16R2 recipe is not interchangeable and was not physically qualified. Existing display support was retained in the repeater recipes. Build receipts bind source, recipe and application/merged-image hashes, verify both OTA slot capacities, and report empty feature-reduction lists. Slot checks do not qualify an OTA installation.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#physical-method-and-operating-conditions","title":"Physical method and operating conditions","text":"

Each ESP32 had four other physical ESP32 peers plus four controlled Pi UDP endpoints: eight configured destinations, not eight physical peer radios. All joined an existing WiFi network as stations. MQTT, ESP-NOW and RS232 bridges were disabled. UDP used the existing bridge shared secret without another password; authentication does not encrypt the traffic.

Physical trials used 917 MHz ingress and 918 MHz egress, SF7, 62.5 kHz bandwidth, CR5, preamble 32 and TX 2 dBm. Radio2 and LoRa RX powersaving were off during packet trials. Each ingress received one UDP-off control and five fresh positive frames. T096 and Tag served as the physical source/sink witnesses. Their different frequencies prevented a direct LoRa delivery from substituting for bridge forwarding. The Pi did not inject mesh data, and each accepted trial had exactly one physical send with no application retry.

The independent audit checked source counters, exact application bytes and sequence, actual Pi event membership, four matching Pi copies per positive frame, all four physical-peer copy observations, and exactly one designated LoRa egress. Physical-peer observations include explicitly accounted duplicate receipts. No receiving UDP peer re-exported the frame to UDP. Before each role, the harness waited at least 65 seconds after radio activation and proved at least two seconds of stable RF/UDP counters.

Historical watchdog flags and cumulative receive errors were retained in the raw baseline. V3 had four receive errors recorded during setup; no measured packet added one. A previously latched bit 8 was admitted only with explicit live RX history, matching diagnostic/physical error counters, soft-only recovery and an empty queue. This does not attribute every historical flag to idle maintenance. Every measured packet required unchanged latched flags and soft/hard recovery counters, zero new physical receive errors, and zero new witness receive errors. There was no blanket exemption for bit 8.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#runtime-memory-and-final-state","title":"Runtime memory and final state","text":"

These measured internal-memory values are bytes. The boot minimum is the firmware low-water counter read at cleanup, not a minimum across every earlier boot. Modeled build margins above are separate capacity estimates.

Board Before cleanup free Reported boot minimum After cleanup free V3 74,400 42,652 114,800 V4.3 81,164 76,028 106,528 T-Deck 151,584 145,812 176,996 XIAO ESP32-S3 WIO 136,464 131,304 161,844 Indicator N8R8 106,644 101,672 132,036

All final core and memory queues were zero. V3 had no PSRAM; the other boards reported it. Cleanup restored all five original RXPS-on profiles, disabled UDP/repeat/logging, cleared all eight test peers, selected bridge source TX and left WebConfig inactive. Identity, saved radio, WiFi/secret and CPU power policy were preserved. No CPU-clock setter was issued; actual CPU-frequency readback is not claimed. Cleanup performed no flash, erase, reboot, stats clear or intentional data send. The final owned fixture stopped once, following the separately receipted stop of its predecessor.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#fix-and-preserved-failures","title":"Fix and preserved failures","text":"

The original V4.3 recovery failure exposed UDP/WebConfig station ownership: an unused automatic recovery AP prevented the saved station connection from returning. 36d9735e fixes that handoff while preserving explicit setup/OTA ownership and saved UDP-off intent. Twelve native ASan/UBSan tests and independent review preceded the five final builds; the final physical matrix then passed the formerly failing recovery sequence. The UDP protocol did not change.

Earlier failures were retained rather than replaced by later passes. The history hash index records actual receipt outcomes and separate preserved archives.

Preserved attempt Outcome and disposition Baseline matrix01 V4.3 recovery failed; XIAO recovery was not run. Final 36d9735e matrix02 passed all five. Initial Indicator join One WiFi off/on joined with the same saved credentials; separate root cause remains unproven. RF02 / RF03 Zero data trials: busy witness setter, then Pi USB/Ethernet host fault. Recovery03-01 readback adapter failed; corrected recovery03-02 passed all seven devices. This was not measured RF packet loss. RF04 One accepted negative request; automatic OTA bootstrap traffic changed the strict negative-control counters. A bounded bootstrap/quiet wait was added. RXPS restore01/03; setup04 V4.3 restoration rejected; setup04 separately failed on a startup queue. Restore02 and final cleanup restored all five original profiles. Ordinary V4.3 RXPS is supported. RF05 Twelve strict passes, then preflight stopped on a latched watchdog flag before V3 data. Overall failed status retained. RF06 Zero data; historical-baseline gate incorrectly required current last-RX age and absolute zero cumulative errors. Run07 retained baselines and enforced zero new errors per packet. Fixture and offline adapters Original fixture ended during the host fault; two explicit RF fixture replacements preserved identical configuration. Exact getter/model adapters were corrected without firmware changes or repeating accepted packets.

Full CI is separate evidence. Test-only fixture repairs were pushed in 8da70b84d and 52a36c89d; focused tests passed. At report preparation, CI run 39869220511 had twelve successful non-main jobs while its main test was still running; full CI success is not asserted here.

"},{"location":"test-results/udp_bridge_hardware_2026-10-22/#evidence-and-limits","title":"Evidence and limits","text":"

The consolidated result binds all 32 opaque private-file hashes, both independent audits and the disjoint campaign receipts. Its SHA-256 is f6a384cc496f8c1a457ddd10df581925ca0a444ebad51fec65155fed3e68a4f3. The final private archive SHA-256 is 22adb7f49b6c1650248f63acf9f6f9a70971e5c7b3c3e17400c4b28803eb4206. That tar covers final evidence; earlier failures remain in separate preserved archives identified by the history index. Private contents are not published.

The later noise-floor telemetry change in f4ade738d was not installed on these radios and is outside this hardware result.

This qualifies Full Repeater UDP forwarding under the stated radio/power conditions. It does not qualify other firmware types, every power/radio profile, eight physical peers, AP-mode UDP, arbitrary router/VLAN policies or client-isolation bypass. Simultaneous bridge backends, BLE, physical firmware OTA application, cold starts, maximum load and long-duration soak remain outside this campaign.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/","title":"Unit test workflow fix 2026 10 22","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#unit-test-workflow-follow-up-2026-10-22","title":"Unit-test workflow follow-up - 2026-10-22","text":"

Baseline: ab6412e73c1c36a53e2ecf08c79e3c223200cb3d on keymindCascade. The starting failure was workflow run 39886822765.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#failure-and-build-scope","title":"Failure and build scope","text":"

The run exposed three independent failures:

The earlier calibration report's direct PlatformIO builds did not use the release wrapper. In particular, the wrapper links the Mini's runtime USB packet logger and embeds release metadata and Cascade defaults. A direct recipe passing its size check does not qualify that release artifact.

The relevant release builds must use these exact workflow commands, one PlatformIO process at a time:

bash build.sh build-firmware RAK_4631_companion_radio_full \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascade-fixed --profile cascade --require-ota\n\nDISABLE_DEBUG=0 bash build.sh build-firmware wio-e5-mini_repeater \\\n  --firmware-version v1.18.0.1-halo-keymind-cascade-dev \\\n  --radio-preset usa-cascade-fixed --profile cascade --allow-no-ota\n

The exact Mini release-wrapper build passed with 245,376 of 245,760 application bytes used, leaving 384 bytes. Its runtime-memory gate also passed: 43,920 bytes available against 32,288 bytes required. The application boundary and existing 16 KiB filesystem are unchanged. The complete board matrix is tracked in the unit-test workflow.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#rak4631-full-companion-changes","title":"RAK4631 Full Companion changes","text":"

The exact Full Companion release-wrapper build passed with 707,504 application bytes before its EndF trailer. The stamped UF2 application occupies 707,584 of 712,704 bytes, leaving 5,120 bytes within the existing boundary. The runtime memory gate passed with 81,228 bytes available against 74,576 bytes required; the DFU handoff and 13 capability checks also passed. Reader, contacts, Meshtext, sensors, Ethernet, SD support and LoRa OTA remain enabled. No flash boundary or memory gate was relaxed.

The RAK4631 recipe explicitly selects its existing voltage-policy calibration store. RAK4631Board::begin() loads that policy before application settings. This lets the compiler omit an unreachable second /battery_cal backend while retaining the existing 20-byte NVP2 record and its CRC. The store fails closed if called before policy initialization. A missing or corrupt policy initializes safe defaults; unsuccessful writes still return an error. Other board recipes retain their generic calibration backend and fallback behavior. A typed setter also avoids formatting a CLI command only to parse it again.

The linked ARM crypto checks passed both RFC8032 vectors. Key generation used 1,200 bytes of function-frame stack and signing used 1,904 bytes, within the 2,048-byte gate. These figures exclude caller, ISR and task stacks. Linked OTA workspace checks passed four acquisition/release lifetimes without touching the protected SD authentication range. Neither check is hardware qualification. The linked Reader check passed all 879 references (885 calls), comparing 82,624 canonical bytes. Its maximum function-frame stack was 6,016 bytes; the check also reserves 1,536 caller bytes within the 8,192-byte loop stack. This excludes ISR/task stack and physical high-water measurements.

Beacon frequency and bandwidth parsing now shares the radio CLI's plain decimal parser. This removes the general-purpose strtof/strtod conversion code from the Full image and recovered 5,732 bytes in the final comparison. Normal decimal settings, optional signs and decimal fractions remain available; radio limits still reject invalid frequencies and bandwidths. Scientific and hexadecimal numeric syntax is rejected, matching the ordinary radio CLI grammar. NaN, infinity, whitespace and trailing junk are rejected as well.

Constant CLI error replies share their existing bounded formatter and prefix. The existing command replies retain their text, capacities and termination; new calibration replies use shorter wording while retaining the reading, effective multiplier and persistence result.

The first rerun exposed an RS-232 packaging check that requires one complete error-message literal as evidence that the merged command handler survived linking. That handler retains its original literal. The capability check is unchanged. Both Full and Reduced T114 Repeater release-wrapper builds then passed all eight capability checks and their flash, RAM and DFU gates.

The next run passed all twelve other jobs and reached a stale release-inventory assertion in the main test job. The two SenseCAP Indicator LoRa repeater recipes added by 3236164d2 legitimately preserve their N8/N16 partition layouts. The fixture now expects 151 targets and explicitly checks both additions; all partition-migration exclusions remain. The corrected selection suite, nRF52 OTA profile suite and XIAO combined-layout suite pass locally. No firmware source changed for this inventory correction.

Calibration coverage includes three command/scaling tests, three store tests (including four platform backends), 19 battery-forwarding tests and four power policy compatibility tests. The actual nRF52 policy/store integration checks pre-initialization rejection with present or absent storage, failed mounts, corrupt policy CRC, saved calibration, unchanged NVP2 bytes, reset, bounds and write/rename failures. A sentinel /battery_cal remains untouched throughout the RAK policy test. These are host tests, not physical ADC qualification.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#repeater-diagnostic-changes","title":"Repeater diagnostic changes","text":"

Compact RX and TX packet records share one formatter. It emits the same uppercase hexadecimal bytes, R or T prefix, and LF terminator. It retains the existing bounded serial writer and DROP recovery marker.

When neither debug output nor packet-file logging is compiled, retry callbacks avoid formatting discarded diagnostic strings. Flood-retry reachability still updates for eligible successful echoes. Eligibility preserves the pending bridge state, source mask, progress marker, target mask and route-hash checks. The existing logged callback path remains intact.

The Repeater supplies its timestamp formatter whenever debug output, packet-file logging or noncompact USB packet records need it. Builds with no timestamp consumer inherit the base formatter without linking the unused conversion.

These changes retain the Mini's release USB packet logger, calibration, routing, host-service bridge, 50 neighbours and existing 16 KiB filesystem. They do not add feature cuts or change flash and RAM qualification gates.

"},{"location":"test-results/unit_test_workflow_fix_2026-10-22/#host-regression-results","title":"Host regression results","text":"

Passed checks:

The complete native PlatformIO suite also passed all 1,897 test cases. The remaining 502 host scripts passed after fixture repairs, along with the required firmware-memory/ARM checks, both required Room browser suites, voice and G726 sanitizer variants, notification JavaScript checks and paired nRF52 logging checks. These results cover the steps beyond the stale inventory assertion without repeating unchanged firmware builds locally.

Check Coverage test/test_serial_packet_log/test_serial_packet_log.cpp - 8 tests Exact RX/TX bytes and LF; small FIFO; initially absent host; stalled host; 20 ms mid-record stall budget; truncation; drop counting and recovery markers. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. test/test_mini_repeater_logging.py - 3 tests Actual Repeater RX, Dispatcher TX, packet codec and diagnostic callbacks across all eight carrier/management/file-logging combinations; maximum packet sizes; runtime logging gate; stalled writes; file-logging independence; host CLI and authorization contracts. Flood-retry eligibility within that harness Actual eligibility, bridge-heard mask and textual formatter agree for 576 source/target/progress/hash-size combinations in each feature variant. Missing state, empty/invalid paths, source-only echoes and progress-marker-only echoes do not refresh reachability. Timestamp consumers within that harness Production declaration and definition compile and return the expected formatter across all 16 debug/file/packet/compact combinations. test/test_cli_command_utils/test_cli_command_utils.cpp - 62 tests Includes three shared error-formatter regressions: exact output with literal percent directives; every capacity from zero through a complete reply; null destination at zero capacity; termination and untouched guard bytes; truncation at the 160-byte CLI boundary. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. CommonCLI dependency audit - 34 scripts, 166 tests The 32 CommonCLI fixture suites and the Morse CLI and Beacon Manager suites pass after source-extraction fixtures include the real shared formatter. Includes RAK SD context handoff and its negative control, USB/ESP-NOW/RS232/UDP diagnostics, persistence, GPS, OTA, and the Morse FEM guard. test/test_beacon_manager.py - 20 tests, included in the audit above Actual Beacon Manager and extracted decimal parser accept ordinary signed/unsigned decimal tuples; reject exponent/hex notation, NaN/infinity, whitespace, trailing junk and out-of-range radio tuples. Existing scheduling, transactional storage and negative controls also pass.

The Mini harness uses AddressSanitizer and UndefinedBehaviorSanitizer on Linux. Radio and serial hardware are stand-ins. These results do not qualify physical USB, RF, ADC readings, charging, bootloaders or installed devices.

"},{"location":"test-results/usb_serial_backpressure/","title":"Usb serial backpressure","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/usb_serial_backpressure/#native-usb-backpressure-and-radio-liveness","title":"Native USB backpressure and radio liveness","text":"

The native USB defaults for eight ESP32-S3 board families now use hardware USB Serial/JTAG (HWCDC). The TinyUSB behavior below remains available to explicitly selected TinyUSB builds. UART and non-ESP32 transports retain their existing backend.

ESP32-S2/S3 builds using native TinyUSB CDC (ARDUINO_USB_MODE=0 and CDC-on-boot) must not wait for a computer to read USB output. In the bundled Arduino-ESP32 2.0.17 core, USBCDC::write() can wait indefinitely for transmit space; its configured timeout bounds a mutex, not that wait. This can stop the same loop that services LoRa, even while the USB connection still appears open.

"},{"location":"test-results/usb_serial_backpressure/#firmware-behavior","title":"Firmware behavior","text":"

Host software must also keep reading independently of command writes. A serial relay should start its reader before the first command, use finite write and response deadlines, avoid discarding received packet logs, and cancel I/O during shutdown. A response timeout must not allow a late reply to satisfy another command: the CLI has no transaction identifiers. Updating firmware alone does not correct an indefinite host-side serial write.

"},{"location":"test-results/usb_serial_backpressure/#regression-checks","title":"Regression checks","text":"

Run only one PlatformIO process in the checkout at a time.

python test/test_esp32_tinyusb_nonblocking.py\npython test/test_esp32_usb_session_fix.py\npython test/test_esp32_tinyusb_role_hygiene.py\npython test/test_esp32_tinyusb_cooperative_output.py\npython test/test_esp32_usb_serial_hygiene.py\npython test/test_nrf52_usb_logging_contract.py\npython test/test_nrf52_usb_lifecycle.py\npython test/test_common_cli_logging_transaction.py\npython test/test_companion_usb_logging_reply.py\npython test/test_usb_diagnostic_logging.py\npio test -e native -f test_nrf52_debug_output -f test_serial_packet_log -f test_serial_mode_switch -f test_mesh_tables\n

The first test compiles the real USB facade against a simulated 64-byte FIFO, including stopped readers, reconnects, protocol transitions, and other-platform fallbacks. Host simulations cannot establish that every real USB driver or endpoint failure has recovered.

A Full Station G2 validation build with USA Cascadia radio settings and the Cascade profile can be made using the normal build entry point. The portable standard recipe preserves the deployed partition layout but omits LoRa OTA; it still has the browser firmware uploader. The auto/full recipe instead enables the expanded feature set and partition layout: its merged image is not an app-only update for a device with the legacy layout.

MESHDEBUG_OVERRIDE=on PACKET_LOGGING_OVERRIDE=on \\\n  bash build.sh build-firmware Station_G2_repeater --build-profile full \\\n  --radio-preset usa-cascadia --profile cascade\n

Before installing on hardware, preserve the device identity, preferences, and existing partition layout. Then verify USB command responses and repeated LoRa logins with the relay running, paused, and stopped, including a host that leaves USB open without reading. Check LoRa recovery separately from USB OUT recovery; fixing transmit backpressure does not prove an unrelated OUT endpoint fault is resolved. Do not erase or repartition the radio as part of this test.

"},{"location":"test-results/usb_serial_backpressure/#usb-logging-ownership-and-session-recovery","title":"USB logging ownership and session recovery","text":"

On a single-port Companion, enabling USB logging parks framed USB traffic before opening the diagnostic gate. A TCP/browser terminal retains its CLI ownership; USB is log-only during that session. Disabling logging restores USB traffic without accepting bytes typed during the log-only interval. A dedicated nRF52 logging port does not park the separate Companion port.

A framed USB logging command retains its original requester and queues its acknowledgement before changing ownership. The handoff advances without waiting for the host: live logging stays unchanged while the complete USB reply is backpressured. Disconnect/reset cancels that session's pending handoff; an already-saved preference can still take effect at the next boot. BLE/WiFi replies are never used as a fallback for a refused USB acknowledgement.

On native ESP32 TinyUSB, build-local framework hooks capture DTR and USB session boundaries synchronously in the USB owner task, before Arduino's delayed events. They purge old input at the boundary and reject late input while DTR is low, without deleting a new host's first query during main-loop cleanup. An armed old IN packet (or a racing writer) triggers immediate soft-disconnect, a minimum 20 ms detached interval, and quarantine until fresh enumeration. Shared SDK files are not modified; a changed native-CDC descriptor fails the build closed until the hook is reviewed. This path is specific to the pinned Arduino-ESP32 2.0.17 native CDC implementation; hardware Serial/JTAG and UART are unchanged.

Disabling diagnostics is not itself a protocol change and does not discard functional replies from the shared ESP32 text queue. Actual Binary/mOTA changes use the explicit terminal-output barrier. Touch and invalid-Ethernet-hostname diagnostics also honor the runtime logging gate and bounded USB stream.

Ethernet and enabled MQTT diagnostics honor the runtime logging gate and use the bounded USB facade. Infrastructure logging setters commit preferences before changing live output; failed saves leave previous settings unchanged. A successful save with a failed MQTT runtime change reports those two outcomes separately.

ESP32 native-USB repeater and room ACL listings retain short-write suffixes and advance one row per service pass, using their existing output buffer. Reconnect cancels the old listing. The listing is a bounded live view, not an immutable snapshot of clients changed during output.

Stored dumps make progress even when HWCDC falls back to 256/512-byte TX rings. Recent-repeater headers/rows and EOF retain every unwritten suffix; a short write never causes their accepted prefix to be retried.

nRF52 FIFO cleanup also checks for an already-armed IN packet. If one exists at a session boundary (including a pending zero-length packet), both USB ports briefly detach and re-enumerate: a FIFO clear alone cannot recall endpoint RAM. Reattachment waits for the local unplug event and a 20 ms minimum interval, without blocking the mesh loop. Dedicated CDC descriptors are added while detached even if the host has not yet completed enumeration. These recovery paths have native regression coverage; real host/board timing still requires hardware qualification.

The nRF52 callback only drops the pull-up and closes producer gates. The application loop sends the stack's unplug event afterward, so a full owner-task event queue cannot make its consumer wait on itself. The detach interval starts when that application-side stack detach actually occurs. A regression models a full finite event queue as well as the separate armed endpoint packet.

"},{"location":"test-results/usb_serial_backpressure/#local-qualification-2026-10-03","title":"Local qualification (2026-10-03)","text":"

These are the upstream USB-session qualification results before the subsequent USB watchdog, GPS preference, and repeater trace merge. The byte counts below describe those earlier binaries, not the current merged source.

The two custom qualification builds used the lab profile 909.5 MHz / 500 kHz / SF5 / CR5. No board was flashed, and no physical disconnect/reconnect or hardware endpoint recovery was claimed by these checks.

"},{"location":"test-results/usb_serial_backpressure/#esp32-hwcdc-migration-and-upstream-findings-2026-10-04","title":"ESP32 HWCDC migration and upstream findings (2026-10-04)","text":"

The board defaults change 78 native-TinyUSB recipes across Heltec E213, E290, T190, Wireless Tracker V1.1 and V2, Station G2 and G3, and T-Beam 1W. Two G2 recipes already used HWCDC. All 80 recipes in these families select ARDUINO_USB_MODE=1 and ARDUINO_USB_CDC_ON_BOOT=1; upload touch and waiting for a replacement upload port are explicitly disabled. Their framework stays on PlatformIO Espressif32 6.11.0 / Arduino-ESP32 2.0.17. Existing ESP32 UART recipes and all non-ESP32 recipes are unchanged.

HWCDC output uses the existing bounded facade, early RX/TX allocation, shared writer exclusion, and fresh capacity check inside that exclusion. The pinned SDK receives a build-local TX suffix/interrupt backport from upstream Arduino-ESP32 #12606. KISS now prepares buffers before starting the USB driver and uses this facade. Its debug/logging gates stay closed so diagnostics cannot split a binary frame. A USB bus reset drops partial host commands and old queued frame suffixes while preserving radio transmission state and settings. Radio work continues while USB cleanup retries.

On S3, the pinned HWCDC initialization also restores RTC PHY ownership to the hardware Serial/JTAG peripheral. A software restart after running TinyUSB can otherwise retain ownership by the OTG controller, including an OTA update to an HWCDC image. When that retained state is detected, initialization first briefly detaches the old OTG pads so the host can enumerate the changed device. Ordinary HWCDC startup does not incur that extra interval.

HWCDC does not expose the same DTR session boundary as TinyUSB. Closing a host port while leaving USB connected is not proof of a new firmware session; a partial command may survive that close. Bus reset/unplug cleanup and protocol client leases must be tested separately. Hosts should rediscover the port after the backend change: the USB product and /dev/serial/by-id path can change. Espressif confirms that hardware CDC descriptors are fixed in silicon, so firmware cannot preserve a custom TinyUSB product name. Arduino-ESP32 maintainer comment, ESP-IDF maintainer comment.

Upstream reports checked for this transition:

Report Documented behavior Relevance and mitigation ESP-IDF #9826 S3 retains TinyUSB PHY ownership across software/watchdog restart, preventing USB Serial/JTAG operation. Pinned HWCDC initialization restores the official S3 hardware PHY route and detaches the former OTG connection first. Initialization fixtures seed retained RTC state; the controlled physical G2 OTA transition below also passed. Arduino-ESP32 #12782 Open report of HWCDC failing to reconnect after about two minutes idle on C3/C6/C61/S3, latest master / IDF 5.5.4. Reported SDK differs from our pinned Arduino 2.0.17 / IDF 4.4. Synthetic tests cover idle lease expiry and reply admission, not physical enumeration on that SDK. ESP-IDF #18996 Open report of S3 USB Serial/JTAG failing after repeated software resets from macOS, on IDF 6.0.2; includes XIAO S3. Not qualified by Linux port reopen tests and not reproduced on our pinned SDK. CPU reset and host-port reopen are distinct cases. ESP-IDF #13287 S3 may remain in ROM after upload when download mode was entered with physical BOOT/RESET. Our older esptool does not offer the newer watchdog-reset option. Press RESET after a manual BOOT-mode upload if the application does not start.

Espressif's USB Serial/JTAG guide describes the fixed CDC/JTAG peripheral and its light/deep sleep limitations. This migration uses only its serial role; it does not provide TinyUSB composite interfaces such as MSC, HID, or a second CDC.

Additional regression commands (only one PlatformIO process at a time):

python3 -B test/test_esp32_hwcdc_recipes.py -v\npython3 -B test/test_hwcdc_tx_backport.py -v\npython3 -B test/test_hwcdc_write_capacity.py -v\npython3 -B test/test_hwcdc_role_transport.py -v\npio test -e native_kiss_modem\n

The recipe check resolves all 821 tracked environments using PlatformIO's real inheritance and SCons flag/unflag processing. It checks all 500 ESP32 selectors, including all 78 migrated recipes, and the 321 non-ESP32 recipes. The transport test compiles production Companion framing, KISS output methods, setup/loop, and the bounded facade with the patched SDK against simulated USB registers and RTOS queues under ASan/UBSan. It covers withheld readers, stale capacity, partial frames, queue limits, session reset, idle leases, and cleanup retries. The role fixture injects a confirmed session boundary; separate SDK tests execute the bus-reset callback and the S3 retained-PHY initialization. The setup/loop fixture uses stub transport functions to check ordering and continued radio service rather than real peripheral recovery. Native KISS tests also exercise the full modem's radio lifecycle during reset. These tests do not establish physical USB timing or host compatibility for boards that were not connected to the lab.

"},{"location":"test-results/usb_serial_backpressure/#physical-g2-http-ota-and-usb-transition-2026-10-05","title":"Physical G2 HTTP OTA and USB transition (2026-10-05)","text":"

The latest OTA qualification used commit 95913c5301318fb738fb8f50e314a57a2dd9eb07, including the uploader fix from d2983762 and the station ownership fix. A current-source build explicitly selected TinyUSB; the target selected the normal HWCDC backend. Both ran on the physical Station G2 with the same existing partition table and two 0x640000 application slots. The independent audit checked packaged application hashes against metadata and matching preserved ELFs, the USB SDK transforms, and linked RAM requirements. Both ELFs contain the production 30-second admitted-upload timeout, queued-response guards and specific write-failure text; neither links the removed raw Update.printError calls. This is a controlled same-source transition, not released stock 1.17.1 migration evidence.

Artifact App bytes SHA-256 TinyUSB observer, 95913c53 1,978,200 cf72b3ce872fb0887273022255fe5826070b5b40f5aa8057f50e90e7c92d4b72 Normal HWCDC observer, 95913c53 1,950,824 aeeadb1f165164c5b01d5855918e20bb0dc37bedcb9f0a8b88f52faa4c435486

The AP upload used the actual multipart /update handler and an app-only image. Its HTTP identity endpoint was matched to the node read over USB before the POST. USB topology was sampled every 20 ms without serial reads or writes across the upload/restart boundary. After 65,536 wire body bytes, Linux's SIOCOUTQ on that same HTTP socket fell from 59,987 outstanding bytes to zero in 0.554 seconds. Only then did the host pause body sends for 4.510 seconds; there were no serial operations during that pause. TCP acknowledgement was observed; the firmware's application receive timestamp was not directly instrumented.

Measured gate Result App-only multipart upload with the intentional body pause HTTP 200, 22.559 seconds Firmware-owned software restart and USB backend change TinyUSB device 120 disappeared; a transient TinyUSB device 121 preceded hardware CDC device 122 on the same physical topology First plain website time <epoch> after HWCDC discovery Clock-set acknowledgement in 2,997.319 ms; strict deadline 5,000 ms Extra post-restart settling, serial commands during upload, external recovery None Identity and saved runtime settings Preserved against the original preflight Admin access Exactly the original and recovery Admin ACL entries remained Core diagnostics after the upgrade Error flags 0, uptime 4 seconds Pi WiFi rollback Original connection restored and temporary AP profile deleted; runner completed in 35.598 seconds

The final 95913c53 hardware CDC application passed two quiet 180-second trials with power saving enabled. There were no serial reads, writes or keepalives during either interval. With the handle open, the first core reply took 18.551 ms and uptime advanced from 43 to 223 seconds. With the handle closed, the first plain website clock request after reopening was acknowledged in 14.728 ms and uptime advanced from 224 to 404 seconds. No CPU restart occurred. The preexisting radio CAD warning 0x02 remained unchanged throughout these trials; this was not a zero-error radio test. Original power saving off, other saved settings and both Admin ACL entries were restored and verified.

After those trials, a single plain start ota command used the connected station's advertised LAN address. The actual /update page returned HTTP 200 in 1.365 seconds and /update/identity returned HTTP 200 in 0.084 seconds with the exact G2 identity. The command reported WiFi on and WebConfig stopped. Stopping OTA and restoring the original WebConfig were verified; no firmware was uploaded in this endpoint probe. Uptime continued from 533 to 535 seconds, the station remained connected at -80 dBm, and identity, original settings, both Admins and power saving off were retained. These HTTP measurements use a separate diagnostic bound; the first USB website request still uses 5 seconds.

Final restoration also verified the Pi's original WiFi connection, no temporary OTA profiles or return timers, and active mctomqtt, ModemManager and mesh-logger services. Each service reported exit status 0 and no restarts. This verifies service health; MQTT packet delivery was not measured in this run.

Baseline preparation also used app-only HTTP OTA: hardware CDC d2983762 installed TinyUSB 95913c53 in 19.083 seconds, and its first plain clock acknowledgement took 3,163.722 ms. Original identity, settings and both Admins were verified before and after this reverse transition; no rescue action was used. Its Pi WiFi rollback completed in 31.958 seconds.

The first query used the stock website's plain clock command immediately after opening the discovered hardware CDC port. Later queries were tagged to keep unrelated diagnostics from satisfying a response. Neither a longer command deadline nor an arbitrary startup delay was added.

The uploader fix addresses a separate bounded-gap problem: the pinned HTTP listener gives ordinary clients a 3-second receive timeout, which remained in effect while an admitted multipart upload was receiving its body. OTA now extends only that admitted request to 30 seconds after MD5 validation. Normal, unauthorized, malformed-MD5 and concurrent requests retain ordinary admission behavior. send() queues a response before it reaches the wire, so both the upload and completion callbacks preserve an already queued error. Begin/end errors and short writes return HTTP errors without calling raw USB prints from the shared AsyncTCP task.

Three ASan/UBSan uploader tests passed, together with eight OTA startup tests and fifteen WiFi lifecycle tests. The uploader fixture executes production callbacks and the pinned AsyncTCP inactivity condition. A 4-second body pause survives, an abandoned owner remains open at 29.999 seconds and aborts at 30 seconds, ownership is released without reboot, and a later upload completes. Negative controls fail when the timeout extension or queued-response guards are removed, or when raw USB error prints are restored.

Historical results remain distinct:

The earlier d2983762 hardware CDC application passed two quiet 180-second trials with power saving enabled. There were no serial reads, writes or keepalives during either interval. With the handle open, the first core reply took 17.353 ms and uptime advanced 180 seconds. With the handle closed, the first plain website clock request after reopening was acknowledged in 8.905 ms and uptime advanced 181 seconds. Original power saving off, other saved settings and both Admin ACL entries were restored and verified.

A normal start ota probe while the station was disconnected correctly reported its fallback AP address; a Pi still on its original LAN could not reach that separate AP. This does not qualify the LAN endpoint. Source review then identified a missing WebConfig retry owner when MQTT is compiled in but has no running worker. The infrastructure fix refreshes ownership from actual MQTT worker state on the mesh loop. Constructor credential loading, browser credential writes and the 64-hex password capability use a separate immutable storage choice, so taking over reconnects cannot truncate or reroute saved credentials. An active external worker retains its connection; a stopped worker releases it for a fresh portal attempt. Six ASan/UBSan tests exercise production role startup, connection fallback, retry, worker handoff and credential paths, including the existing manual Companion startup behavior. The 95913c53 OTA transition, quiet power-saving trials and normal LAN endpoint checks passed as reported above. The current GitHub unit workflow has not yet completed. Release 1.17.1.9 remains held and unpublished. These results qualify this G2, Linux host and pinned framework; they do not establish physical compatibility for every affected board or host operating system.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/","title":"V4 mqtt bellevue g2 2026 10 21","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#v4-mqtt-setup-and-bellevue-g2-full-repeater-21-october-2026","title":"V4 MQTT setup and Bellevue G2 Full Repeater, 21 October 2026","text":"

Status: PASS for the requested setup and deployment. The Mercerwood V4 is left connected to its saved US analyzer MQTT preset. Bellevue's Station G2 now runs the current Full Repeater application, with repeating enabled and its USB gateway service restored. No firmware source change was needed.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#v4-mqtt","title":"V4 MQTT","text":"

The V4 retains its measured 2bd4cb4d Full Companion application. Its saved MQTT switch was already on, while WiFi was off and the MQTT region was empty. The setup saved region SEA, enabled the existing WiFi configuration and kept the analyzer-us preset. This preset uses WSS and device-signed JWT authentication. WiFi and admin passwords were not changed.

The first connection obtained two broker PUBACKs. A subsequent 137.367-second held observation recorded PUBACK growth from 3 to 5, uptime from 29,881 to 30,017 seconds, zero core errors and empty core queues. Every sample had an empty MQTT outbox and zero queue/drop counters. Free DMA memory stayed at 50,056 bytes or more, with a largest block of 49,140 bytes.

The one-minute test status interval was restored to its original five minutes. That setting restarts the MQTT worker; a separate final check confirmed WiFi, MQTT and a new broker acknowledgment after the restart. WiFi and MQTT remain enabled. Other saved broker slots and radio settings were not changed.

PUBACK verifies that the broker accepted a publication. An independent subscriber payload receipt was not measured because no subscriber account was available. This finite observation does not replace a long-duration WSS soak.

"},{"location":"test-results/v4_mqtt_bellevue_g2_2026-10-21/#bellevue-station-g2","title":"Bellevue Station G2","text":"Item Result Canonical target Station_G2_repeater_observer_mqtt, built through the Full release profile Installed version v1.18.0.1-halo-keymind-cascade-dev-observer-a926341f Application body / target 4233FACEB6C44EAE / DBB6A855 Application SHA256 459f2e1de20460b54ac00cd545dfe11490b13081072c3aed47b1398dd276dcf5 Build gates Full capability and memory gates passed; 200,432 available internal bytes versus 192,656 required heap bytes Installation Actual ESP32-S3 identity and 16 MiB layout verified; CRC-valid OTA metadata selected the active slot at 0x650000, size 0x640000 Readback All 2,118,264 application bytes verified; partition table, OTA metadata and NVS hashes unchanged during the write Persistence Original public identity, name, radio, TX power, WiFi SSID, MQTT settings and power-saving choice preserved Repeater mode Role-specific getter accepted; repeating was already on and remains on Final health Core errors 0, outbound queue 0; local packet counters showed 84 received and 18 sent Service handback Original mctomqtt.service is active and again owns the exact G2 USB endpoint

The combined Full recipe retains observer in its version string and includes the repeater implementation, MQTT, ESP-NOW, USB logging, WiFi OTA and LoRa OTA. The build's Full overlay enables the LoRa update path. Saved operating settings remain 910.525 MHz / BW62.5 / SF7 / CR5 and TX 19 dBm.

The application-only update preserved the existing expanded layout. It made no whole-chip erase or full-flash backup. Small protected NVS reads were hashed and removed; after boot, saved logical settings and the full public identity were independently compared with the original observations.

The first postboot check had zero core errors and one in-flight radio packet at uptime 13 seconds. The initial harness required an immediately empty queue and stopped handback at that check. A bounded follow-up on the same installed firmware verified the exact version, body, identity and repeater role, then recorded an empty queue with zero errors at uptime 149 seconds and restored the original USB gateway. That early failed gate is retained in the evidence; no second firmware installation was performed.

The safe receipt records artifact bindings, timings and evidence hashes. The local packet counters demonstrate observed radio activity; independent peer delivery, RF range, physical OTA installation and long-duration behavior were not tested in this deployment.

"},{"location":"test-results/v4_pixel5_font_trial/","title":"V4 pixel5 font trial","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/v4_pixel5_font_trial/#small-screen-message-fonts","title":"Small-screen message fonts","text":"

Small-screen Companion builds automatically select a compact font for received message text and the channel/sender line, including Ch 0 Public. SSD1306 and SH1106 OLEDs and the U8g2 T-Echo Card display use this on panels smaller than 160 pixels on both axes. ST7735 TFTs now use their native 160x80 dimensions and normal font. The shared renderer selects by panel dimensions, including rotation:

Panel size Font Capital height Line spacing At least 128 x 64, or 64 x 128 rotated; both axes below 160 Squeezed Regular 6 6 pixels 8 pixels Smaller panels, including 72 x 40, 128 x 32 and 64 x 48 Picopixel 5 pixels 7 pixels Either axis at least 160 Normal display font Depends on panel Depends on panel

Both fonts allow one pixel for descenders and one blank pixel row between lines. Character widths vary: most letters advance by 4 pixels, with narrow letters taking less space and M and W taking 6 pixels. Squeezed Regular 6 is a public-domain font by Oliver Kraus. Its bitmap data is stored as constants without heap allocation.

On a 128 x 64 OLED with the single-button message reader, the compact channel/sender line starts at y=8 and message text at y=16. The header uses the same small font. Five complete 6px-font rows fit above the navigation hint at y=56. The software comparison fits three representative 160-character messages in those five rows. Capacity depends on the characters; messages with many wide letters can still overflow. Text wraps at character boundaries, with ... on the last line if necessary. Unsupported characters appear as ?. Long channel/sender names are ellipsized to stay on their own line. An enabled channel footer reserves its own space. Readers without the button hint retain the y=14 origin and y=22 message text.

On V4 and other single-button builds using this reader, the home screen says hold button: inbox. Hold the user button for about 1.2 seconds to open it. The bottom continuously shows 4 <<- 2 <- tap -> 1 ->> 3 hold: Exit without alternating or blinking. Double tap goes to the previous message, one tap advances, three taps select the next channel, and four taps select the previous channel. Channel selection cycles through All, configured channels, and direct messages, starting at the newest message in each filter. The header shows All, Ch N, or DM, including when a channel is empty. Hold to return home; advancing past the last message also returns home. The complete hint fits in the V4's existing 8-pixel footer at y=56, leaving five message rows. The footer keeps the same 6px font, using 108 pixels with 10-pixel margins on the V4. Narrower screens first use the compact hold:X hint without the extra spaces or arrow dashes, then split the same controls across fixed lines. The hint appears even when the inbox is empty. These are button taps. Touchscreen and joystick builds retain instructions appropriate to their controls. During the first eight seconds after startup, holding the button on an ordinary home page enters CLI rescue instead; wait for that startup window to finish before opening the inbox. Exiting a message preview and the WiFi setup page's hold action remain available immediately.

The Reader uses the same button hints. Three taps jump to the first verse of the next chapter; four taps jump to the first verse of the previous chapter. Chapter navigation stops at the beginning and end of the book. It reserves the hint before pagination and resumes saved bookmarks at the page containing the same text, even after the available page size changes.

Larger display classes keep their existing font. Menus, Bluetooth PINs and WiFi setup QR codes keep their normal layout. TFT drivers use native panel coordinates; the SenseCAP Indicator retains its existing scaling. The normal OLED font uses 7-pixel letters, 8-pixel line spacing and 6-pixel character advances.

The preview buffer holds a complete 160-byte MeshCore message plus its terminator. Previously the main message UI allocated 78 bytes, leaving room for only 77 bytes of text. It still retains 32 previews; larger records add about 2.8 KB of RAM. The firmware RAM guard includes that increase. This history is separate from the offline queue and its mOTA policy.

The tiny 72 x 40 T-Echo Card interface now previews the latest received message below its status bar, with three small-font message rows. A button press dismisses it. It stores one full message, and shows ... when the screen fills. Incoming text does not replace an active Bluetooth pairing PIN.

"},{"location":"test-results/v4_pixel5_font_trial/#v4-hardware-trial","title":"V4 hardware trial","text":"

Enable platformio.nimble.ini in the ignored platformio.local.ini as shown in the NimBLE trial guide, then run:

OUTPUT_DIR=.releases/v4-smallfont bash build.sh build-firmware \\\n  heltec_v4_2_v4_3_companion_radio_full_femon_nimble \\\n  --firmware-version v1.17.1.5-halo-keymind-cascade-squeezed6-trial \\\n  --radio-preset usa-cascadia --profile cascade --standard --require-ota\n

This build keeps NimBLE, 350 contacts, 40 channels, the V4's 512-frame PSRAM queue, USB mOTA sending and WiFi OTA support. The smaller font is also the default in ordinary small-screen Companion builds from this source. The older _nimble_pixel5 environment name remains available, but also uses the automatic 5px/6px selection.

Use the application .bin for WiFi OTA. A clean USB install uses the merged image at address 0. When manually writing the application at 0x10000, an existing OTA selector may still boot app1. Check the running ver afterward. On the matching V4 16 MB layout only, clearing the 8 KB otadata partition at 0xe000 selects the newly written app0 without clearing NVS/settings. Confirm the partition table and verify the app write before changing that selector.

A custom build can set -D UI_SMALL_MESSAGE_FONT=0 to restore the old font and spacing. Remove any explicit UI_MSG_PREVIEW_SIZE flag too if the old preview capacity is desired. These are compile-time options, not CLI commands. No settings erase is necessary when changing between matching V4 layouts.

"},{"location":"test-results/v4_pixel5_font_trial/#verification","title":"Verification","text":"
python3 -B test/test_ssd1306_picopixel.py\npython3 -B test/test_firmware_ram.py\npio test -e native -f test_display_driver -f test_companion_message_history\n

The native tests exercise both capital heights, automatic font selection, 160-character messages, complete rows at display edges, long sender lines, navigation hints, reserved footers, overflow markers and tiny/rotated screen geometry. The additional rendering comparison checks all 95 printable ASCII glyphs in each font, under address/undefined-behavior sanitizers. Picopixel is compared pixel for pixel with Adafruit GFX; Squeezed Regular 6 is compared with the upstream BDF fixture independently of the converted C++ tables. This needs a cached PlatformIO Adafruit GFX library; set MESHCORE_GFX_LIBRARY to its directory if needed. It reports a skip when the library is absent. The native tests do not require that dependency. Run only one PlatformIO command at a time.

"},{"location":"test-results/v4_pixel5_font_trial/#automatic-5px6px-results-2026-09-08","title":"Automatic 5px/6px results, 2026-09-08","text":"

Source revision 469b47d1 passed five representative firmware builds. All five retained the same static RAM usage and startup heap margin as their earlier 5px builds. The four Full profiles also passed required OTA packaging.

Hardware/profile Display Selected capital height RAM beyond the required startup budget, bytes V4.2/V4.3 Full NimBLE SSD1306 6 pixels 88,072 T096 Full, FEM on ST7735 6 pixels 24,778 Station G3 ESP32 Full SH1106 6 pixels 82,080 RAK3401 Full SSD1306 6 pixels 46,108 T-Echo Card BLE Companion U8g2, 72 x 40 5 pixels 30,992

The native display/history suites passed 36 tests. The Python font, RAM, pairing, display-profile, queue and QR checks passed 38 tests, including pixel comparisons for all 95 ASCII glyphs in each font. The actual SSD1306 software rendering matches the approved 6px comparison image pixel for pixel.

Both NimBLE-V4-VM and NimBLE-V4-Trial were flashed with v1.17.1.5-halo-keymind-cascade-squeezed6-trial-469b47d1. Their partition tables and application hashes were verified, their settings retained, and running versions confirmed. Each passed 201 USB protocol requests without error flags. The Mercerwood V4 reconnected over bonded Bluetooth at MTU 179 and retained the factory-address policy.

The XIAO sent a private 160-byte LoRa message to the Mercerwood V4. All bytes arrived, and the V4 stayed responsive through 20 seconds of display refreshes with no uptime reset or error flags. Both temporary channel settings were restored. After that interval the V4 had 147,196 bytes of free internal heap, a minimum of 146,328 bytes, and a largest free block of 139,252 bytes.

These are short functional checks. Physical readability remains a user judgment. A message with many wide characters can still need truncation.

V4 application size: 1,778,936 bytes (1,152 bytes larger than the earlier 5px trial). SHA-256: 8e6797b90741bf013b3757668e5a23378ac8c0b0ed9c75830939f13305c56a68.

"},{"location":"test-results/v4_pixel5_font_trial/#earlier-5px-hardware-and-build-results-2026-09-08","title":"Earlier 5px hardware and build results, 2026-09-08","text":"

Source revision eeea15ef passed seven representative firmware builds:

Hardware/profile Display RAM beyond the required startup budget, bytes V4.2/V4.3 Full NimBLE Picopixel SSD1306 88,072 T096 Full, FEM on ST7735 24,778 Station G3 ESP32 Full SH1106 82,080 RAK3401 Full SSD1306 46,108 Wireless Tracker Full NimBLE capacity trial ST7735 28,022 Wio Tracker L1 Full SH1106 45,744 T-Echo Card BLE Companion U8g2 30,992

These are linked-capacity checks before runtime allocation, not live free heap measurements. The six Full profiles also passed their required OTA packaging checks. The T-Echo Card row is a BLE Companion build.

Both physical V4.3 nodes, NimBLE-V4-VM and NimBLE-V4-Trial, were flashed, their image hashes verified, and their running versions confirmed. Each passed 201 USB protocol requests with zero reported error flags. The Mercerwood V4 also reconnected over authenticated, bonded Bluetooth with MTU 179 and the existing factory-address policy.

A private XIAO-to-V4 LoRa test delivered all 160 message bytes. The V4 continued responding during 20 seconds of message redraws, with zero error flags. Both temporary channel configurations were restored. Free internal heap after that interval was 147,200 bytes, minimum 146,340 bytes, and the largest free block was 139,252 bytes. These are short functional checks; physical readability remains a user judgment.

A matching pre/post-boot Mercerwood measurement showed the longer preview using 2,816 additional PSRAM bytes, with unchanged internal free heap. Boards without PSRAM use their ordinary RAM for the larger preview records.

The native display/history suites passed 31 tests. The Python font, RAM, pairing, display-profile, queue and QR checks passed 38 tests. The Adafruit comparison covers all 95 printable ASCII glyphs.

V4 application SHA-256: d914d80124b499d7b719f8427f3794ab1b6d15fd0112e3ef89ba2fac26b5a974.

"},{"location":"test-results/preview-1.17.1.8/","title":"Index","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preview-1.17.1.8/#11718-preview-1-test-results","title":"1.17.1.8 Preview 1 test results","text":"

This preview contains USA Cascade builds for RAK3401 Repeater and Heltec V4 OLED Full Companion, Full Repeater, Full Room Server, and Full Sensor. The GitHub prerelease has the matching firmware assets.

The RAK3401 source test run and Heltec V4 source test run passed. The RAK3401 UF2 and DFU application payloads match, with verified DFU CRCs. Both RAK3401 packages passed capability and memory checks. The four Heltec V4 OLED variants passed capability and memory qualification. These are build/package checks, not physical on-device tests of every variant.

"},{"location":"test-results/preview-1.17.1.8/#preview4-local-regression-validation","title":"Preview4 local regression validation","text":""},{"location":"test-results/preview-1.17.1.8/#rak3401-package-results","title":"RAK3401 package results","text":""},{"location":"test-results/preview-1.17.1.8/#heltec-v4-oled-reports","title":"Heltec V4 OLED reports","text":""},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/","title":"Heltec v4 preview4 bluetooth oled","text":"

View this page on MeshCore Docs.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#heltec-v4-preview4-bluetootholed-regression-validation","title":"Heltec V4 preview4 Bluetooth/OLED regression validation","text":"

Date: 2026-10-02. These are observations on one physical Heltec V4.3 OLED, not qualification of every board or phone. The preview4 images are local, unreleased builds with uncommitted changes on keymindCascade at 7324bf06efef236009dac2d507775fa4e19bc856.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#target-and-method","title":"Target and method","text":"

A complete, hash-checked 16 MB original flash backup was taken first. Only otadata and the application were written for the temporary diagnostic image; the bootloader, partition table, NVS and SPIFFS were retained. No other Pi radio was flashed. The helper did not request LoRa transmissions; ordinary background radio activity was not disabled.

COMPANION_PAIRING_UI_HIL is an opt-in diagnostic macro, absent from production builds. It records actual startup phases, display power/pairing/USB state, public identity, and the real 1024-byte SSD1306 framebuffer. It does not access the private key. Frame capture occurred during an actual outstanding passkey request, not a manually invoked firmware callback.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#observed-hardware-results","title":"Observed hardware results","text":"Check Result Fresh pairing with a non-default PIN Passed twice: exactly one actual passkey request per fresh pairing; BlueZ reported Paired and Bonded. PIN display while USB remains connected Passed twice: display on, pairing active, USB present, and captured frame containing PAIR PIN 246810. This verifies the display driver's real framebuffer and wake state, not an optical photograph. Protected BLE transport Passed: MTU 179, 82-byte device-info response and 176-byte ATT command payload, live CLI/status responses, zero core error flags. Custom MAC with stealth Passed: fresh discovery/pairing, live bonded-peer-only advertising status, bonded reconnect with no new PIN, then USB-commanded reboot and another bonded reconnect with no new PIN. Saved random MAC Passed: USB-commanded reboot retained its address with stealth enabled. random-after-connect, unused boot Passed: reboot over USB, without BLE authentication, retained the address. random-after-connect, authenticated boot Passed: actual authenticated BLE connection armed the next boot; address stayed unchanged during that session and changed on the following USB reboot. The new address was discoverable with stealth retained. random-every-boot Passed: two successive USB-commanded boots used different addresses, with stealth enabled. Saved identity startup Passed: Starting/Loading phases only, no Generating/Saving phase. The same public identity was retained throughout the run.

The suite finished with suite_passed, six observed application addresses, two captured OLED frames, and identity_preserved: true. An earlier harness attempt stopped because it did not recognize a terminal prompt immediately followed by a diagnostic line; the parser was corrected and the complete suite rerun. This was not a BLE pairing failure.

Stealth on ESP32 uses minimal, unnamed, allowlisted advertising after pairing; it is not radio invisibility. Bonded reconnection and policy/rotation behavior were tested here. Actual advertising-payload capture and rejection of an unbonded second central were not part of this run.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#original-target-restored","title":"Original target restored","text":"

After the suite, the complete original 16 MB image was written back. Esptool verified the flash data hash. Live serial readback then confirmed:

The full backup remains private on the Pi. Raw diagnostic logs and captured OLED pixels remain in the ignored local preview4 output folder; neither the backup nor unreviewed radio logs are published. Only test-created BlueZ device records were removed by the helper.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#automated-regression-coverage-and-builds","title":"Automated regression coverage and builds","text":"

The NimBLE regression fixture reproduces the pinned library's dispatch rule: a non-default static passkey bypasses onPassKeyDisplay(). Firmware now leaves NimBLE's sentinel at 123456 and returns the actual configured PIN through that callback, preserving authentication while notifying the UI. Tests cover random and saved PINs, USB coexistence, button wake/navigation, disabled display/BLE, saved identity, generation, persistence failures, and removal of the generation label once generation finishes.

"},{"location":"test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/#remaining-hardware-gates","title":"Remaining hardware gates","text":""}]} \ No newline at end of file diff --git a/sensor_tracker/index.html b/sensor_tracker/index.html index 37b64ebd1..8c43e4e5f 100644 --- a/sensor_tracker/index.html +++ b/sensor_tracker/index.html @@ -2857,7 +2857,7 @@ reporting intervals.

- + diff --git a/separated_radio_modulation_cache_validation/index.html b/separated_radio_modulation_cache_validation/index.html index 199576d0f..9592052ce 100644 --- a/separated_radio_modulation_cache_validation/index.html +++ b/separated_radio_modulation_cache_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf10_10_vs_100hz_full_sample_validation/index.html b/sf10_10_vs_100hz_full_sample_validation/index.html index de023546c..e292db49e 100644 --- a/sf10_10_vs_100hz_full_sample_validation/index.html +++ b/sf10_10_vs_100hz_full_sample_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf10_10hz_cr_detour_validation/index.html b/sf10_10hz_cr_detour_validation/index.html index ccd41afac..f69cb3546 100644 --- a/sf10_10hz_cr_detour_validation/index.html +++ b/sf10_10hz_cr_detour_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf10_full_retune_twice_validation/index.html b/sf10_full_retune_twice_validation/index.html index c78750689..76b01a5d4 100644 --- a/sf10_full_retune_twice_validation/index.html +++ b/sf10_full_retune_twice_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf10_nrf52_double_write_settling_validation/index.html b/sf10_nrf52_double_write_settling_validation/index.html index d20ddca2c..f0e5678bb 100644 --- a/sf10_nrf52_double_write_settling_validation/index.html +++ b/sf10_nrf52_double_write_settling_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf10_rollback_failure_validation/index.html b/sf10_rollback_failure_validation/index.html index d409bd4d4..6531f1159 100644 --- a/sf10_rollback_failure_validation/index.html +++ b/sf10_rollback_failure_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf125_settling_limits_validation/index.html b/sf125_settling_limits_validation/index.html index bf4dbbb77..7c51ad2d7 100644 --- a/sf125_settling_limits_validation/index.html +++ b/sf125_settling_limits_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf5_250_dwell_validation/index.html b/sf5_250_dwell_validation/index.html index 6f84e66dc..ecc6663da 100644 --- a/sf5_250_dwell_validation/index.html +++ b/sf5_250_dwell_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf6_125_settling_validation/index.html b/sf6_125_settling_validation/index.html index 25368daac..e73e64bf1 100644 --- a/sf6_125_settling_validation/index.html +++ b/sf6_125_settling_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf6_5p1_trace_validation/index.html b/sf6_5p1_trace_validation/index.html index 5a46d08c2..6f8184c4e 100644 --- a/sf6_5p1_trace_validation/index.html +++ b/sf6_5p1_trace_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf6_channel_scan_validation/index.html b/sf6_channel_scan_validation/index.html index 9938bf0f7..8a5a648fa 100644 --- a/sf6_channel_scan_validation/index.html +++ b/sf6_channel_scan_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/sf8_5p1_trace_validation/index.html b/sf8_5p1_trace_validation/index.html index 56393f986..032e6b3e7 100644 --- a/sf8_5p1_trace_validation/index.html +++ b/sf8_5p1_trace_validation/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/solarxiao_power/index.html b/solarxiao_power/index.html index dfe8eb16d..a056fccd6 100644 --- a/solarxiao_power/index.html +++ b/solarxiao_power/index.html @@ -2835,7 +2835,7 @@ No state-of-charge percentage or coulomb counting is inferred from voltage.

- + diff --git a/spiffs_regular_file_reads/index.html b/spiffs_regular_file_reads/index.html index 4f391de45..cf02e1e95 100644 --- a/spiffs_regular_file_reads/index.html +++ b/spiffs_regular_file_reads/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/stats_binary_frames/index.html b/stats_binary_frames/index.html index efa255196..a5a14bc1f 100644 --- a/stats_binary_frames/index.html +++ b/stats_binary_frames/index.html @@ -3368,7 +3368,7 @@ function parseStatsPackets(buffer: ArrayBuffer): StatsPackets { - + diff --git a/telemetry_decoder/index.html b/telemetry_decoder/index.html index f955d72df..d5723f2c0 100644 --- a/telemetry_decoder/index.html +++ b/telemetry_decoder/index.html @@ -2862,7 +2862,7 @@ CLI Base64 page rather than analyzer hex.

- + diff --git a/terminal_chat_cli/index.html b/terminal_chat_cli/index.html index dae8d73dc..64e2d12b1 100644 --- a/terminal_chat_cli/index.html +++ b/terminal_chat_cli/index.html @@ -3294,7 +3294,7 @@ length, which also includes the sender-name prefix added over the air.

- + diff --git a/test-results/battery_calibration_2026-10-22/index.html b/test-results/battery_calibration_2026-10-22/index.html index 2f3ba1638..71e2ab01c 100644 --- a/test-results/battery_calibration_2026-10-22/index.html +++ b/test-results/battery_calibration_2026-10-22/index.html @@ -2635,7 +2635,7 @@ records release-wrapper failures that these direct recipe builds did not cover.< - + diff --git a/test-results/bootloader_version_stock_audit/index.html b/test-results/bootloader_version_stock_audit/index.html index f80f9fb6a..bf93ab1ad 100644 --- a/test-results/bootloader_version_stock_audit/index.html +++ b/test-results/bootloader_version_stock_audit/index.html @@ -2841,7 +2841,7 @@ cannot recover a version that was never embedded.

- + diff --git a/test-results/cad_scan_validation/index.html b/test-results/cad_scan_validation/index.html index ed2edc855..4fc21d83e 100644 --- a/test-results/cad_scan_validation/index.html +++ b/test-results/cad_scan_validation/index.html @@ -2681,7 +2681,7 @@ - + diff --git a/test-results/carrier_wave_validation/index.html b/test-results/carrier_wave_validation/index.html index ec95beded..50d9250ac 100644 --- a/test-results/carrier_wave_validation/index.html +++ b/test-results/carrier_wave_validation/index.html @@ -2577,7 +2577,7 @@ Command guide: carrier-wave RF checks.

- + diff --git a/test-results/cli_settings_audit/index.html b/test-results/cli_settings_audit/index.html index b8f9b82ff..060e08177 100644 --- a/test-results/cli_settings_audit/index.html +++ b/test-results/cli_settings_audit/index.html @@ -2520,7 +2520,7 @@ and build checks; subsequent hardware checks are recorded in the - + diff --git a/test-results/companion_ble_refresh_2026-10-19/index.html b/test-results/companion_ble_refresh_2026-10-19/index.html index 2a6509f4e..e521fd7c1 100644 --- a/test-results/companion_ble_refresh_2026-10-19/index.html +++ b/test-results/companion_ble_refresh_2026-10-19/index.html @@ -2652,7 +2652,7 @@ page. The page contains no private device identifiers or raw contact records.

- + diff --git a/test-results/companion_contact_cache_results/index.html b/test-results/companion_contact_cache_results/index.html index 10e4d148c..60bac6751 100644 --- a/test-results/companion_contact_cache_results/index.html +++ b/test-results/companion_contact_cache_results/index.html @@ -2685,7 +2685,7 @@ assets or firmware-picker entries were replaced by these experimental builds.

- + diff --git a/test-results/companion_lost_reply/index.html b/test-results/companion_lost_reply/index.html index 0daf8378e..a1ddd781b 100644 --- a/test-results/companion_lost_reply/index.html +++ b/test-results/companion_lost_reply/index.html @@ -2573,7 +2573,7 @@ boundaries are unchanged on those compact boards.

- + diff --git a/test-results/companion_qualification_f807_followup_2026-10-19/index.html b/test-results/companion_qualification_f807_followup_2026-10-19/index.html index 742935a53..a465cf9b7 100644 --- a/test-results/companion_qualification_f807_followup_2026-10-19/index.html +++ b/test-results/companion_qualification_f807_followup_2026-10-19/index.html @@ -2684,7 +2684,7 @@ Results and failures above retain their original firmware sources.

- + diff --git a/test-results/companion_qualification_followup_2026-10-19/index.html b/test-results/companion_qualification_followup_2026-10-19/index.html index 822180c31..fd0918310 100644 --- a/test-results/companion_qualification_followup_2026-10-19/index.html +++ b/test-results/companion_qualification_followup_2026-10-19/index.html @@ -2523,7 +2523,7 @@ Results and failures above retain their original firmware sources.

- + diff --git a/test-results/companion_usb_ascii_validation/index.html b/test-results/companion_usb_ascii_validation/index.html index 35b99dd08..cf5b5da92 100644 --- a/test-results/companion_usb_ascii_validation/index.html +++ b/test-results/companion_usb_ascii_validation/index.html @@ -2485,7 +2485,7 @@ across the board matrix.

- + diff --git a/test-results/companion_usb_client_validation/index.html b/test-results/companion_usb_client_validation/index.html index 8bd3e8ee7..8e60bc538 100644 --- a/test-results/companion_usb_client_validation/index.html +++ b/test-results/companion_usb_client_validation/index.html @@ -2567,7 +2567,7 @@ reboot so the default-ASCII check has a new session. On ESP32, omit both - + diff --git a/test-results/contact_memory_2026-10-19/index.html b/test-results/contact_memory_2026-10-19/index.html index 8d03904bd..d16abd840 100644 --- a/test-results/contact_memory_2026-10-19/index.html +++ b/test-results/contact_memory_2026-10-19/index.html @@ -2767,7 +2767,7 @@ outside this qualification.

- + diff --git a/test-results/double_frequency_v4_validation/index.html b/test-results/double_frequency_v4_validation/index.html index a8bed3753..4db9319a9 100644 --- a/test-results/double_frequency_v4_validation/index.html +++ b/test-results/double_frequency_v4_validation/index.html @@ -2615,7 +2615,7 @@ and frequency-repeat scope/error propagation. Temporary build overlay removed. - + diff --git a/test-results/esp32_ble_radio2_sleep_2026-10-21/index.html b/test-results/esp32_ble_radio2_sleep_2026-10-21/index.html index c7f868205..30aa0af6f 100644 --- a/test-results/esp32_ble_radio2_sleep_2026-10-21/index.html +++ b/test-results/esp32_ble_radio2_sleep_2026-10-21/index.html @@ -3329,7 +3329,7 @@ and the limits of interpreting wait counters as energy savings.

- + diff --git a/test-results/esp32_full_power_ap_2026-10-06/index.html b/test-results/esp32_full_power_ap_2026-10-06/index.html index cee929def..486a09467 100644 --- a/test-results/esp32_full_power_ap_2026-10-06/index.html +++ b/test-results/esp32_full_power_ap_2026-10-06/index.html @@ -2634,7 +2634,7 @@ CLI success and mocked SDK state alone are insufficient.

- + diff --git a/test-results/esp32_management_password_2026-10-19/index.html b/test-results/esp32_management_password_2026-10-19/index.html index 5de258d9a..749bccc7d 100644 --- a/test-results/esp32_management_password_2026-10-19/index.html +++ b/test-results/esp32_management_password_2026-10-19/index.html @@ -2618,7 +2618,7 @@ roles, partition migration, and other hardware families.

- + diff --git a/test-results/espnow_bridge_diagnostics_2026-10-22/index.html b/test-results/espnow_bridge_diagnostics_2026-10-22/index.html index 1ca9130da..bd869a3c7 100644 --- a/test-results/espnow_bridge_diagnostics_2026-10-22/index.html +++ b/test-results/espnow_bridge_diagnostics_2026-10-22/index.html @@ -2538,7 +2538,7 @@ are not a complete end-to-end delivery ledger.

- + diff --git a/test-results/firmware_variant_consolidation/index.html b/test-results/firmware_variant_consolidation/index.html index ddb96a3d6..95fdf8d85 100644 --- a/test-results/firmware_variant_consolidation/index.html +++ b/test-results/firmware_variant_consolidation/index.html @@ -2600,7 +2600,7 @@ The packages were built locally; physical testing of these combinations remains - + diff --git a/test-results/four_fixed_tx_dwell_validation/index.html b/test-results/four_fixed_tx_dwell_validation/index.html index be7a86b66..241d159a3 100644 --- a/test-results/four_fixed_tx_dwell_validation/index.html +++ b/test-results/four_fixed_tx_dwell_validation/index.html @@ -2678,7 +2678,7 @@ is a finite bench comparison, not a calibrated sensitivity or interference test. - + diff --git a/test-results/four_fixed_tx_single_pass_validation/index.html b/test-results/four_fixed_tx_single_pass_validation/index.html index 677702060..77afe7552 100644 --- a/test-results/four_fixed_tx_single_pass_validation/index.html +++ b/test-results/four_fixed_tx_single_pass_validation/index.html @@ -2726,7 +2726,7 @@ were preserved.

- + diff --git a/test-results/full_usb_logging_validation/index.html b/test-results/full_usb_logging_validation/index.html index e4919ec19..ba717ac98 100644 --- a/test-results/full_usb_logging_validation/index.html +++ b/test-results/full_usb_logging_validation/index.html @@ -2523,7 +2523,7 @@ Room Server: 7ee81f1cb65fc274bd363397c3c80ce9006d575efae50fe952c8ca246613a18c - + diff --git a/test-results/hardware_validation_bluetooth_stealth_2026-09-07/index.html b/test-results/hardware_validation_bluetooth_stealth_2026-09-07/index.html index 535100056..58c8160a2 100644 --- a/test-results/hardware_validation_bluetooth_stealth_2026-09-07/index.html +++ b/test-results/hardware_validation_bluetooth_stealth_2026-09-07/index.html @@ -2729,7 +2729,7 @@ about 5.9 MiB. No USB reset, Pi reboot, or target battery removal was used.

- + diff --git a/test-results/hardware_validation_checklist/index.html b/test-results/hardware_validation_checklist/index.html index 2fa192642..5602a0dbb 100644 --- a/test-results/hardware_validation_checklist/index.html +++ b/test-results/hardware_validation_checklist/index.html @@ -4312,7 +4312,7 @@ network, and reboot record in that manifest.

- + diff --git a/test-results/hardware_validation_esp32_web_console_ota_2026-10-05/index.html b/test-results/hardware_validation_esp32_web_console_ota_2026-10-05/index.html index bf5afad4b..9a65dfae9 100644 --- a/test-results/hardware_validation_esp32_web_console_ota_2026-10-05/index.html +++ b/test-results/hardware_validation_esp32_web_console_ota_2026-10-05/index.html @@ -2811,7 +2811,7 @@ for their exact source revisions and measured scope.

- + diff --git a/test-results/hardware_validation_esp32_web_console_startup_2026-10-05/index.html b/test-results/hardware_validation_esp32_web_console_startup_2026-10-05/index.html index 697f7bc32..6b7acdcf1 100644 --- a/test-results/hardware_validation_esp32_web_console_startup_2026-10-05/index.html +++ b/test-results/hardware_validation_esp32_web_console_startup_2026-10-05/index.html @@ -3090,7 +3090,7 @@ corrected to accept the prefix and never controlled first-connect PASS/FAIL.

- + diff --git a/test-results/hardware_validation_heltec_v4_native_console_2026-10-06/index.html b/test-results/hardware_validation_heltec_v4_native_console_2026-10-06/index.html index ac9a382f2..ab7aeb936 100644 --- a/test-results/hardware_validation_heltec_v4_native_console_2026-10-06/index.html +++ b/test-results/hardware_validation_heltec_v4_native_console_2026-10-06/index.html @@ -2761,7 +2761,7 @@ and unpublished.

- + diff --git a/test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/index.html b/test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/index.html index a901c275b..5cd7e4905 100644 --- a/test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/index.html +++ b/test-results/hardware_validation_heltec_v4_scan_first_ap_2026-10-06/index.html @@ -2614,7 +2614,7 @@ CLI success and simulated state alone are insufficient.

- + diff --git a/test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/index.html b/test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/index.html index a9f2f9e85..a190606c8 100644 --- a/test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/index.html +++ b/test-results/hardware_validation_known_failures_1_18_0_1_2026-10-20/index.html @@ -2617,7 +2617,7 @@ remain visible. Private device identities, addresses and raw responses are omitt - + diff --git a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/index.html b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/index.html index c47ef5262..7882e78ff 100644 --- a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/index.html +++ b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-18/index.html @@ -3153,7 +3153,7 @@ cores remain outside the repository in the dated campaign directories.

- + diff --git a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/index.html b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/index.html index ee09e3270..40c370814 100644 --- a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/index.html +++ b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-19/index.html @@ -2563,7 +2563,7 @@ C2 bootloader entries describe source-qualified installation evidence, including - + diff --git a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/index.html b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/index.html index 1b7d5fb75..3c7f595a6 100644 --- a/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/index.html +++ b/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20/index.html @@ -2574,7 +2574,7 @@ results remain in the - + diff --git a/test-results/hardware_validation_ota_1_18_0_1_2026-10-20/index.html b/test-results/hardware_validation_ota_1_18_0_1_2026-10-20/index.html index 6bddfb450..43f52cc6d 100644 --- a/test-results/hardware_validation_ota_1_18_0_1_2026-10-20/index.html +++ b/test-results/hardware_validation_ota_1_18_0_1_2026-10-20/index.html @@ -2967,7 +2967,7 @@ Earlier accepted Companion tests are not repeated without a relevant change.

- + diff --git a/test-results/heltec_t114_display_detection/index.html b/test-results/heltec_t114_display_detection/index.html index 2c100562a..8fe5dce93 100644 --- a/test-results/heltec_t114_display_detection/index.html +++ b/test-results/heltec_t114_display_detection/index.html @@ -2535,7 +2535,7 @@ screen sleep/wake, Bluetooth/USB connections, and normal radio receive/transmit. - + diff --git a/test-results/heltec_v2_ble_memory_2026-10-22/index.html b/test-results/heltec_v2_ble_memory_2026-10-22/index.html index 9caf3fb69..1d6ce8935 100644 --- a/test-results/heltec_v2_ble_memory_2026-10-22/index.html +++ b/test-results/heltec_v2_ble_memory_2026-10-22/index.html @@ -2535,7 +2535,7 @@ hardware qualification claims.

- + diff --git a/test-results/heltec_v3_full_memory_2026-10-19/index.html b/test-results/heltec_v3_full_memory_2026-10-19/index.html index 6c4772218..fa4057258 100644 --- a/test-results/heltec_v3_full_memory_2026-10-19/index.html +++ b/test-results/heltec_v3_full_memory_2026-10-19/index.html @@ -2546,7 +2546,7 @@ These software results do not establish that hardware acceptance.

- + diff --git a/test-results/heltec_v3_preview3_ram/index.html b/test-results/heltec_v3_preview3_ram/index.html index 120792f48..f5b560721 100644 --- a/test-results/heltec_v3_preview3_ram/index.html +++ b/test-results/heltec_v3_preview3_ram/index.html @@ -2607,7 +2607,7 @@ python3 -B test/test_firmware_ram.py - + diff --git a/test-results/home_text_spacing/index.html b/test-results/home_text_spacing/index.html index bd1486a96..6a9ea6c17 100644 --- a/test-results/home_text_spacing/index.html +++ b/test-results/home_text_spacing/index.html @@ -2416,7 +2416,7 @@ then check the repeater's five rows and the V4's non-blinking reader footer.

- + diff --git a/test-results/index.html b/test-results/index.html index 4b8ead5ab..a21582fa1 100644 --- a/test-results/index.html +++ b/test-results/index.html @@ -2619,7 +2619,7 @@ - + diff --git a/test-results/mbedtls-tls-footprint/index.html b/test-results/mbedtls-tls-footprint/index.html index 868da4b0d..de8a6dfaf 100644 --- a/test-results/mbedtls-tls-footprint/index.html +++ b/test-results/mbedtls-tls-footprint/index.html @@ -2774,7 +2774,7 @@ not the cause, and waev does not cap connections per IP.

- + diff --git a/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/index.html b/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/index.html index e21b915ff..c7302e337 100644 --- a/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/index.html +++ b/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/index.html @@ -2487,7 +2487,7 @@ installation is disabled and was not claimed tested.

- + diff --git a/test-results/mixed_scan_validation/index.html b/test-results/mixed_scan_validation/index.html index de816f7f5..250589bfc 100644 --- a/test-results/mixed_scan_validation/index.html +++ b/test-results/mixed_scan_validation/index.html @@ -2500,7 +2500,7 @@ were restarted in phase B-post-CAD-soak, marking the intentional up - + diff --git a/test-results/mixed_sf_bw_validation/index.html b/test-results/mixed_sf_bw_validation/index.html index 2ea464471..49b082308 100644 --- a/test-results/mixed_sf_bw_validation/index.html +++ b/test-results/mixed_sf_bw_validation/index.html @@ -2679,7 +2679,7 @@ these HIL changes; this is not a clean-release comparison.

- + diff --git a/test-results/morse_rx_hil_2026-10-12/index.html b/test-results/morse_rx_hil_2026-10-12/index.html index ceb965e9f..7e9998d1f 100644 --- a/test-results/morse_rx_hil_2026-10-12/index.html +++ b/test-results/morse_rx_hil_2026-10-12/index.html @@ -2990,7 +2990,7 @@ remains on the identified fast HIL applications, not shipping images.

- + diff --git a/test-results/mqtt_tls_v4_regression/index.html b/test-results/mqtt_tls_v4_regression/index.html index 8695b071a..fbfc1eccc 100644 --- a/test-results/mqtt_tls_v4_regression/index.html +++ b/test-results/mqtt_tls_v4_regression/index.html @@ -2777,7 +2777,7 @@ not the cause of the reporter's hardware failure or website-side availability. - + diff --git a/test-results/nimble_companion_trial/index.html b/test-results/nimble_companion_trial/index.html index 6f0efa1af..007a5da82 100644 --- a/test-results/nimble_companion_trial/index.html +++ b/test-results/nimble_companion_trial/index.html @@ -2617,7 +2617,7 @@ Android camera/pairing compatibility.

- + diff --git a/test-results/nimble_companion_trial_results/index.html b/test-results/nimble_companion_trial_results/index.html index f605f79ba..7ff2a299a 100644 --- a/test-results/nimble_companion_trial_results/index.html +++ b/test-results/nimble_companion_trial_results/index.html @@ -2626,7 +2626,7 @@ profile does not include direct MQTT; the V4 does.

- + diff --git a/test-results/nrf52-bluetooth-dfu-handoff/index.html b/test-results/nrf52-bluetooth-dfu-handoff/index.html index 2ecea7a1f..f4638c40b 100644 --- a/test-results/nrf52-bluetooth-dfu-handoff/index.html +++ b/test-results/nrf52-bluetooth-dfu-handoff/index.html @@ -2840,7 +2840,7 @@ physical charge-only-cable, battery-only, or LoRa transfer qualification.

- + diff --git a/test-results/nrf52-usb-ready-fix/index.html b/test-results/nrf52-usb-ready-fix/index.html index 55004878a..213ca003d 100644 --- a/test-results/nrf52-usb-ready-fix/index.html +++ b/test-results/nrf52-usb-ready-fix/index.html @@ -2439,7 +2439,7 @@ The harness is shared with OTAFIX's TinyUSB fork at - + diff --git a/test-results/nrf52_companion_storage_recovery/index.html b/test-results/nrf52_companion_storage_recovery/index.html index 99a0de172..5ea32b27b 100644 --- a/test-results/nrf52_companion_storage_recovery/index.html +++ b/test-results/nrf52_companion_storage_recovery/index.html @@ -2496,7 +2496,7 @@ values; receiving new verified advertisements can refresh them normally.

- + diff --git a/test-results/pair_4p6_300us_validation/index.html b/test-results/pair_4p6_300us_validation/index.html index 6fe43a6cd..568835c30 100644 --- a/test-results/pair_4p6_300us_validation/index.html +++ b/test-results/pair_4p6_300us_validation/index.html @@ -2520,7 +2520,7 @@ services are active, and the previously paused memory soak remains inactive.

- + diff --git a/test-results/pair_preamble32_validation/index.html b/test-results/pair_preamble32_validation/index.html index cb339ac2d..42ce08851 100644 --- a/test-results/pair_preamble32_validation/index.html +++ b/test-results/pair_preamble32_validation/index.html @@ -2639,7 +2639,7 @@ completed 197/200 scanning packets: slow 97/100, fast 100/100, after fresh - + diff --git a/test-results/pr7_review_validation/index.html b/test-results/pr7_review_validation/index.html index 872e916d5..fcf8b8997 100644 --- a/test-results/pr7_review_validation/index.html +++ b/test-results/pr7_review_validation/index.html @@ -2508,7 +2508,7 @@ cover that path. Physical display readability was not inspected.

- + diff --git a/test-results/preamble_detection_investigation/index.html b/test-results/preamble_detection_investigation/index.html index 251188f41..01b45c7d2 100644 --- a/test-results/preamble_detection_investigation/index.html +++ b/test-results/preamble_detection_investigation/index.html @@ -2670,7 +2670,7 @@ tests, and results were initially handed off locally before publication.

- + diff --git a/test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/index.html b/test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/index.html index cc3a55674..75bb955e4 100644 --- a/test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/index.html +++ b/test-results/preview-1.17.1.8/heltec-v4-preview4-bluetooth-oled/index.html @@ -2599,7 +2599,7 @@ label once generation finishes.

- + diff --git a/test-results/preview-1.17.1.8/index.html b/test-results/preview-1.17.1.8/index.html index 23c800210..9567de6b2 100644 --- a/test-results/preview-1.17.1.8/index.html +++ b/test-results/preview-1.17.1.8/index.html @@ -2465,7 +2465,7 @@ - + diff --git a/test-results/radio2_simultaneous_receivers_2026-10-22/index.html b/test-results/radio2_simultaneous_receivers_2026-10-22/index.html index dfb4047f8..4ef2451fd 100644 --- a/test-results/radio2_simultaneous_receivers_2026-10-22/index.html +++ b/test-results/radio2_simultaneous_receivers_2026-10-22/index.html @@ -2986,7 +2986,7 @@ Earlier failures and causal limits also remain in the - + diff --git a/test-results/radio_chirp_math_validation/index.html b/test-results/radio_chirp_math_validation/index.html index 0c46b8b17..1534575f6 100644 --- a/test-results/radio_chirp_math_validation/index.html +++ b/test-results/radio_chirp_math_validation/index.html @@ -2579,7 +2579,7 @@ checks equality against the production 4.6/300-us allocation.

- + diff --git a/test-results/radio_dwell_policy_validation/index.html b/test-results/radio_dwell_policy_validation/index.html index b94c1ab2e..64d6129ee 100644 --- a/test-results/radio_dwell_policy_validation/index.html +++ b/test-results/radio_dwell_policy_validation/index.html @@ -2487,7 +2487,7 @@ ownership alongside the timing model.

- + diff --git a/test-results/radio_profile_switch_validation/index.html b/test-results/radio_profile_switch_validation/index.html index d89b3c0eb..9dcf2891f 100644 --- a/test-results/radio_profile_switch_validation/index.html +++ b/test-results/radio_profile_switch_validation/index.html @@ -3245,7 +3245,7 @@ measurements, not on-board qualification of every CustomSX1262 targ - + diff --git a/test-results/radio_profiles_validation/index.html b/test-results/radio_profiles_validation/index.html index 10a918caf..55f3706b7 100644 --- a/test-results/radio_profiles_validation/index.html +++ b/test-results/radio_profiles_validation/index.html @@ -2832,7 +2832,7 @@ The Pi's soak service and Indicator telemetry were also healthy after restoratio - + diff --git a/test-results/radio_receive_calibration/index.html b/test-results/radio_receive_calibration/index.html index fea1f6ddd..064de8970 100644 --- a/test-results/radio_receive_calibration/index.html +++ b/test-results/radio_receive_calibration/index.html @@ -2514,7 +2514,7 @@ checks do not establish battery-life or hardware fault-recovery results.

- + diff --git a/test-results/rak3401_all_roles_cpu_sleep_2026-10-21/index.html b/test-results/rak3401_all_roles_cpu_sleep_2026-10-21/index.html index 79deb4d12..4462e35ff 100644 --- a/test-results/rak3401_all_roles_cpu_sleep_2026-10-21/index.html +++ b/test-results/rak3401_all_roles_cpu_sleep_2026-10-21/index.html @@ -2663,7 +2663,7 @@ bootloader-byte preservation check.

- + diff --git a/test-results/rak_release_fixes_2026-10-22/index.html b/test-results/rak_release_fixes_2026-10-22/index.html index 2fa0f949f..7cf6a2b8b 100644 --- a/test-results/rak_release_fixes_2026-10-22/index.html +++ b/test-results/rak_release_fixes_2026-10-22/index.html @@ -2560,9 +2560,14 @@ bytes; RAM passes at 43,920 available against 32,288 required. Four replay integration and eight Mini-profile tests passed. Other platforms select their unchanged messages.

The updated CI run -was still running when this report was prepared. These targeted checks do not -claim a new all-board campaign, RS232 cable traffic, physical OTA installation, -maximum-load operation or a long RF soak.

+passed all 12 firmware and support jobs, including both new RAK build pairs. +The main test job exposed an outdated trace-timing extraction fixture: it +omitted the dispatcher's new queue helpers. The fixture now extracts those +production methods and supplies their state dependencies; all five test +methods and ten normal/mutation invocations pass with ASan/UBSan. This +fixture-only correction requires no repeated firmware or hardware checks. +These targeted checks do not claim a new all-board campaign, RS232 cable +traffic, physical OTA installation, maximum-load operation or a long RF soak.

@@ -2623,7 +2628,7 @@ maximum-load operation or a long RF soak.

- + diff --git a/test-results/regulator_recovery_2026-10-18/index.html b/test-results/regulator_recovery_2026-10-18/index.html index c031d62df..db2231354 100644 --- a/test-results/regulator_recovery_2026-10-18/index.html +++ b/test-results/regulator_recovery_2026-10-18/index.html @@ -2826,7 +2826,7 @@ - + diff --git a/test-results/room_mailboxes/index.html b/test-results/room_mailboxes/index.html index 89efe4237..d0fd512b3 100644 --- a/test-results/room_mailboxes/index.html +++ b/test-results/room_mailboxes/index.html @@ -2512,7 +2512,7 @@ They do not replace a live long-duration stack-watermark test.

- + diff --git a/test-results/rs232_bridge_diagnostics_2026-10-22/index.html b/test-results/rs232_bridge_diagnostics_2026-10-22/index.html index a5d9d296d..789f25470 100644 --- a/test-results/rs232_bridge_diagnostics_2026-10-22/index.html +++ b/test-results/rs232_bridge_diagnostics_2026-10-22/index.html @@ -2542,7 +2542,7 @@ for this change.

- + diff --git a/test-results/s3_memory_soak_validation/index.html b/test-results/s3_memory_soak_validation/index.html index fe905f920..b977ea273 100644 --- a/test-results/s3_memory_soak_validation/index.html +++ b/test-results/s3_memory_soak_validation/index.html @@ -2756,7 +2756,7 @@ rollover. They are not substitutes for the hardware evidence.

- + diff --git a/test-results/sensor_dog_tracker/index.html b/test-results/sensor_dog_tracker/index.html index 6125c8d96..d0685580e 100644 --- a/test-results/sensor_dog_tracker/index.html +++ b/test-results/sensor_dog_tracker/index.html @@ -2570,7 +2570,7 @@ needed on T1000-E and RAK Tag are listed in the - + diff --git a/test-results/separated_radio_modulation_cache_validation/index.html b/test-results/separated_radio_modulation_cache_validation/index.html index 2ea37570e..53159b4a3 100644 --- a/test-results/separated_radio_modulation_cache_validation/index.html +++ b/test-results/separated_radio_modulation_cache_validation/index.html @@ -2703,7 +2703,7 @@ again unless requested. No private backup contents were added to Git.

- + diff --git a/test-results/sf10_10_vs_100hz_full_sample_validation/index.html b/test-results/sf10_10_vs_100hz_full_sample_validation/index.html index 99ab82bc3..33a089d4d 100644 --- a/test-results/sf10_10_vs_100hz_full_sample_validation/index.html +++ b/test-results/sf10_10_vs_100hz_full_sample_validation/index.html @@ -2733,7 +2733,7 @@ Earlier captures were not overwritten.

- + diff --git a/test-results/sf10_10hz_cr_detour_validation/index.html b/test-results/sf10_10hz_cr_detour_validation/index.html index fdcd7156f..ffc81bb2a 100644 --- a/test-results/sf10_10hz_cr_detour_validation/index.html +++ b/test-results/sf10_10hz_cr_detour_validation/index.html @@ -2640,7 +2640,7 @@ the exact tested-image identifier. Prior captures were preserved.

- + diff --git a/test-results/sf10_full_retune_twice_validation/index.html b/test-results/sf10_full_retune_twice_validation/index.html index 73da804c5..1efbb24e2 100644 --- a/test-results/sf10_full_retune_twice_validation/index.html +++ b/test-results/sf10_full_retune_twice_validation/index.html @@ -2605,7 +2605,7 @@ these HIL-only additions, not a clean-release comparison.

- + diff --git a/test-results/sf10_nrf52_double_write_settling_validation/index.html b/test-results/sf10_nrf52_double_write_settling_validation/index.html index 4780f88c5..ffece1cc6 100644 --- a/test-results/sf10_nrf52_double_write_settling_validation/index.html +++ b/test-results/sf10_nrf52_double_write_settling_validation/index.html @@ -2564,7 +2564,7 @@ build overlay removed. No background test collector remains running.

- + diff --git a/test-results/sf10_rollback_failure_validation/index.html b/test-results/sf10_rollback_failure_validation/index.html index 47c4564c3..072849636 100644 --- a/test-results/sf10_rollback_failure_validation/index.html +++ b/test-results/sf10_rollback_failure_validation/index.html @@ -2948,7 +2948,7 @@ these uncommitted HIL additions; this is not a clean-release comparison.

- + diff --git a/test-results/sf125_settling_limits_validation/index.html b/test-results/sf125_settling_limits_validation/index.html index 64d7407ad..be06212b1 100644 --- a/test-results/sf125_settling_limits_validation/index.html +++ b/test-results/sf125_settling_limits_validation/index.html @@ -2581,7 +2581,7 @@ has been removed.

- + diff --git a/test-results/sf5_250_dwell_validation/index.html b/test-results/sf5_250_dwell_validation/index.html index 2619d486c..f4ba8bb23 100644 --- a/test-results/sf5_250_dwell_validation/index.html +++ b/test-results/sf5_250_dwell_validation/index.html @@ -2659,7 +2659,7 @@ S3 soak work were not modified. The temporary build overlay was removed.

- + diff --git a/test-results/sf6_125_settling_validation/index.html b/test-results/sf6_125_settling_validation/index.html index 9b1840e55..9453ec208 100644 --- a/test-results/sf6_125_settling_validation/index.html +++ b/test-results/sf6_125_settling_validation/index.html @@ -2684,7 +2684,7 @@ The temporary build overlay was removed; no autonomous test TX remains.

- + diff --git a/test-results/sf6_5p1_trace_validation/index.html b/test-results/sf6_5p1_trace_validation/index.html index ce256645c..d74bfc757 100644 --- a/test-results/sf6_5p1_trace_validation/index.html +++ b/test-results/sf6_5p1_trace_validation/index.html @@ -2664,7 +2664,7 @@ logger was not restarted. Changes were initially handed off locally before publi - + diff --git a/test-results/sf6_channel_scan_validation/index.html b/test-results/sf6_channel_scan_validation/index.html index 91b275cf3..b6eaae61f 100644 --- a/test-results/sf6_channel_scan_validation/index.html +++ b/test-results/sf6_channel_scan_validation/index.html @@ -2580,7 +2580,7 @@ before publication.

- + diff --git a/test-results/sf8_5p1_trace_validation/index.html b/test-results/sf8_5p1_trace_validation/index.html index 70dc5524b..6ef1825ca 100644 --- a/test-results/sf8_5p1_trace_validation/index.html +++ b/test-results/sf8_5p1_trace_validation/index.html @@ -2641,7 +2641,7 @@ results were initially handed off locally before publication.

- + diff --git a/test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/index.html b/test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/index.html index 19aa0226e..cf7b6a3fa 100644 --- a/test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/index.html +++ b/test-results/solarxiao_33s_head_bootloader_lora_ota_2026-10-21/index.html @@ -2581,7 +2581,7 @@ preserves that initial cleanup result and the completed restoration check.

- + diff --git a/test-results/solarxiao_advanced_2026-10-21/index.html b/test-results/solarxiao_advanced_2026-10-21/index.html index a16807b7e..a80fa37ed 100644 --- a/test-results/solarxiao_advanced_2026-10-21/index.html +++ b/test-results/solarxiao_advanced_2026-10-21/index.html @@ -2566,7 +2566,7 @@ measure charge current, solar power or charger state.

- + diff --git a/test-results/solarxiao_auto_2026-10-21/index.html b/test-results/solarxiao_auto_2026-10-21/index.html index 2ba1c66f9..855b2be11 100644 --- a/test-results/solarxiao_auto_2026-10-21/index.html +++ b/test-results/solarxiao_auto_2026-10-21/index.html @@ -2591,7 +2591,7 @@ chemistry/cell-count checks still independently guard all charger writes.

- + diff --git a/test-results/solarxiao_basic_2026-10-21/index.html b/test-results/solarxiao_basic_2026-10-21/index.html index 6b9a593c4..8c00534f5 100644 --- a/test-results/solarxiao_basic_2026-10-21/index.html +++ b/test-results/solarxiao_basic_2026-10-21/index.html @@ -2511,7 +2511,7 @@ commands. Packages and logs are retained under - + diff --git a/test-results/solarxiao_ina260_2026-10-21/index.html b/test-results/solarxiao_ina260_2026-10-21/index.html index 712d702d7..94a7f2e67 100644 --- a/test-results/solarxiao_ina260_2026-10-21/index.html +++ b/test-results/solarxiao_ina260_2026-10-21/index.html @@ -2551,7 +2551,7 @@ build-checked only; live INA260 acceptance used the Full package.

- + diff --git a/test-results/solarxiao_power_2026-10-21/index.html b/test-results/solarxiao_power_2026-10-21/index.html index 4b1412d65..c744276b1 100644 --- a/test-results/solarxiao_power_2026-10-21/index.html +++ b/test-results/solarxiao_power_2026-10-21/index.html @@ -2576,7 +2576,7 @@ package is qualified by this report.

- + diff --git a/test-results/solarxiao_room_2026-10-21/index.html b/test-results/solarxiao_room_2026-10-21/index.html index 1fcacbbae..2429a3ebb 100644 --- a/test-results/solarxiao_room_2026-10-21/index.html +++ b/test-results/solarxiao_room_2026-10-21/index.html @@ -2555,7 +2555,7 @@ target selection.

- + diff --git a/test-results/spiffs_regular_file_reads/index.html b/test-results/spiffs_regular_file_reads/index.html index e1851062b..897c067e2 100644 --- a/test-results/spiffs_regular_file_reads/index.html +++ b/test-results/spiffs_regular_file_reads/index.html @@ -2491,7 +2491,7 @@ Host simulations do not replace a post-flash LoRa login test on the G2.

- + diff --git a/test-results/station_g3_g2_compatibility/index.html b/test-results/station_g3_g2_compatibility/index.html index 8a2dfe7bf..d6e2782d4 100644 --- a/test-results/station_g3_g2_compatibility/index.html +++ b/test-results/station_g3_g2_compatibility/index.html @@ -2865,7 +2865,7 @@ the user's original problem is claimed by these host checks.

- + diff --git a/test-results/t1000_usb_bluetooth_priority_2026-10-20/index.html b/test-results/t1000_usb_bluetooth_priority_2026-10-20/index.html index 4dcd9c0cb..05a9b02b7 100644 --- a/test-results/t1000_usb_bluetooth_priority_2026-10-20/index.html +++ b/test-results/t1000_usb_bluetooth_priority_2026-10-20/index.html @@ -2493,7 +2493,7 @@ Results and failures above retain their original firmware sources.

- + diff --git a/test-results/tdeck_radio2_display/index.html b/test-results/tdeck_radio2_display/index.html index 06d9d12da..a052e3a86 100644 --- a/test-results/tdeck_radio2_display/index.html +++ b/test-results/tdeck_radio2_display/index.html @@ -2559,7 +2559,7 @@ also passed after adding this report.

- + diff --git a/test-results/tdeck_radio2_retune_experiments/index.html b/test-results/tdeck_radio2_retune_experiments/index.html index a3fa2f14b..330271d6d 100644 --- a/test-results/tdeck_radio2_retune_experiments/index.html +++ b/test-results/tdeck_radio2_retune_experiments/index.html @@ -2980,7 +2980,7 @@ before the retry and is not attributed to an active-pair retune.

- + diff --git a/test-results/udp_bridge_2026-10-22/index.html b/test-results/udp_bridge_2026-10-22/index.html index 890662ed0..446a33431 100644 --- a/test-results/udp_bridge_2026-10-22/index.html +++ b/test-results/udp_bridge_2026-10-22/index.html @@ -2551,7 +2551,7 @@ network isolation.

- + diff --git a/test-results/udp_bridge_hardware_2026-10-22/index.html b/test-results/udp_bridge_hardware_2026-10-22/index.html index dfc83fd84..896d3ed86 100644 --- a/test-results/udp_bridge_hardware_2026-10-22/index.html +++ b/test-results/udp_bridge_hardware_2026-10-22/index.html @@ -2736,7 +2736,7 @@ outside this campaign.

- + diff --git a/test-results/unit_test_workflow_fix_2026-10-22/index.html b/test-results/unit_test_workflow_fix_2026-10-22/index.html index 4845acab6..08bfb6217 100644 --- a/test-results/unit_test_workflow_fix_2026-10-22/index.html +++ b/test-results/unit_test_workflow_fix_2026-10-22/index.html @@ -2602,7 +2602,7 @@ USB, RF, ADC readings, charging, bootloaders or installed devices.

- + diff --git a/test-results/usb_serial_backpressure/index.html b/test-results/usb_serial_backpressure/index.html index 72f379db9..522ab92df 100644 --- a/test-results/usb_serial_backpressure/index.html +++ b/test-results/usb_serial_backpressure/index.html @@ -2877,7 +2877,7 @@ compatibility for every affected board or host operating system.

- + diff --git a/test-results/v4_mqtt_bellevue_g2_2026-10-21/index.html b/test-results/v4_mqtt_bellevue_g2_2026-10-21/index.html index 796e1effe..87437d11a 100644 --- a/test-results/v4_mqtt_bellevue_g2_2026-10-21/index.html +++ b/test-results/v4_mqtt_bellevue_g2_2026-10-21/index.html @@ -2524,7 +2524,7 @@ installation and long-duration behavior were not tested in this deployment.

- + diff --git a/test-results/v4_pixel5_font_trial/index.html b/test-results/v4_pixel5_font_trial/index.html index f5e7aaa25..af9c8c939 100644 --- a/test-results/v4_pixel5_font_trial/index.html +++ b/test-results/v4_pixel5_font_trial/index.html @@ -2715,7 +2715,7 @@ comparison covers all 95 printable ASCII glyphs.

- + diff --git a/tx_beacons/index.html b/tx_beacons/index.html index 4c3d0e15b..6ed6f529d 100644 --- a/tx_beacons/index.html +++ b/tx_beacons/index.html @@ -2517,7 +2517,7 @@ retuning duration on unmeasured hardware.

- + diff --git a/udp_bridge_setup/index.html b/udp_bridge_setup/index.html index a957e2ea8..866ae0825 100644 --- a/udp_bridge_setup/index.html +++ b/udp_bridge_setup/index.html @@ -2689,7 +2689,7 @@ WiFi passwords or get bridge.secret in logs you share.

- + diff --git a/upgrading_1.17.1.8_to_1.17.1.9/index.html b/upgrading_1.17.1.8_to_1.17.1.9/index.html index c6b02b3e3..46917f38a 100644 --- a/upgrading_1.17.1.8_to_1.17.1.9/index.html +++ b/upgrading_1.17.1.8_to_1.17.1.9/index.html @@ -2745,7 +2745,7 @@ browser, phone or starting version. See the - + diff --git a/usb_logging_watchdog/index.html b/usb_logging_watchdog/index.html index 4f14ca179..88b319751 100644 --- a/usb_logging_watchdog/index.html +++ b/usb_logging_watchdog/index.html @@ -2742,7 +2742,7 @@ invalid management sends release their packet without queueing it.

- + diff --git a/usb_serial_backpressure/index.html b/usb_serial_backpressure/index.html index f4c169535..0421a09d6 100644 --- a/usb_serial_backpressure/index.html +++ b/usb_serial_backpressure/index.html @@ -2401,7 +2401,7 @@ - + diff --git a/v4_pixel5_font_trial/index.html b/v4_pixel5_font_trial/index.html index 6588c0623..40d95db37 100644 --- a/v4_pixel5_font_trial/index.html +++ b/v4_pixel5_font_trial/index.html @@ -2401,7 +2401,7 @@ - +