mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-28 22:28:23 +00:00
feat: add Bluetooth-controlled LoRa OTA
Add the framed BLE mOTA transport and companion control path, complete the full-profile build and runtime fixes, and document and test the OTA workflow.
This commit is contained in:
@@ -46,7 +46,10 @@ RADIO_SF_OVERRIDE="$USA_CASCADIA_FALLBACK_SF"
|
||||
RADIO_CR_OVERRIDE="$USA_CASCADIA_FALLBACK_CR"
|
||||
FIRMWARE_PROFILE_OVERRIDE="${FIRMWARE_PROFILE_OVERRIDE:-cascade}"
|
||||
BATCH_BUILD_MODE=0
|
||||
OPTION3_BUILD_WORKERS="${OPTION3_BUILD_WORKERS:-2}"
|
||||
# PlatformIO shares and cleans .pio/build across environments in this checkout.
|
||||
# Keep target-level builds strictly single-process; OPTION3_PIO_JOBS only
|
||||
# controls compiler parallelism inside that one PlatformIO process.
|
||||
OPTION3_BUILD_WORKERS=1
|
||||
OPTION3_PIO_JOBS="${OPTION3_PIO_JOBS:-8}"
|
||||
PROFILE_BUILD_WORKERS=1
|
||||
PIO_BUILD_JOBS_OVERRIDE=""
|
||||
@@ -99,9 +102,9 @@ Commands:
|
||||
help|usage|-h|--help: Shows this message.
|
||||
list|-l: List firmwares available to build.
|
||||
build-firmware <target>: Build the firmware for the given build target.
|
||||
build-firmwares: Build canonical firmwares for all targets. Runtime-setting aliases remain available as explicit builds.
|
||||
build-firmwares-logging-matrix: Build the canonical standard, logging, unified FULL ESP32 USB+WiFi, and FULL logging fallback profiles, logging each target under out/build-logs/ and continuing after failures. MQTT observers and ESP-NOW bridges always use FULL. Full Companion targets provide runtime USB logging through a dedicated port or an input-capable single-TTY terminal.
|
||||
build-companion-firmwares-logging-matrix: Build canonical Companion targets in each applicable standard, MQTT, and expanded FULL profile. Full Companion replaces separate USB, BLE, WiFi, and USB-logging artifacts where an exact combined recipe exists.
|
||||
build-firmwares: Build canonical firmwares for all targets. Runtime-setting aliases and Terminal Chat targets replaced by Full Companion remain available as explicit builds.
|
||||
build-firmwares-logging-matrix: Build canonical standard artifacts with merged runtime USB logging plus unified FULL ESP32 USB+WiFi and FULL fallback profiles, logging each target under out/build-logs/ and continuing after failures. MQTT observers and ESP-NOW bridges always use FULL. KISS, BLE-only Companion, and constrained LoRa-OTA receiver contracts do not gain plaintext USB logging.
|
||||
build-companion-firmwares-logging-matrix: Build canonical Companion targets with merged runtime USB logging where the transport is safe, plus applicable MQTT and expanded FULL profiles. Full Companion replaces separate USB, BLE, WiFi, Terminal Chat, and USB-logging artifacts where an exact combined recipe exists.
|
||||
build-full-esp32-firmwares: Build feature-complete ESP32 profiles with up to 254 neighbors, USB packet logging, WiFi MQTT where supported, LoRa OTA, and expanded dual-OTA partitions.
|
||||
build-full-esp32-logging-firmwares: Build only the FULL USB-logging fallback for targets without a matching WiFi MQTT environment.
|
||||
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
|
||||
@@ -187,8 +190,7 @@ Environment Variables:
|
||||
RESUME_BUILD_OUTPUT=1: Preserves out/ and skips targets whose expected output
|
||||
artifacts already exist. Option 3 resumes by default.
|
||||
OUTPUT_DIR=path: Writes artifacts outside out/ (useful for isolated test builds).
|
||||
OPTION3_BUILD_WORKERS=2: Concurrent targets per Option 3 profile pass.
|
||||
OPTION3_PIO_JOBS=8: Compiler jobs assigned to each concurrent Option 3 target.
|
||||
OPTION3_PIO_JOBS=8: Compiler jobs inside the single active PlatformIO process.
|
||||
|
||||
Examples:
|
||||
Build without debug logging:
|
||||
@@ -1780,16 +1782,6 @@ filter_out_kiss_modem_targets() {
|
||||
RESOLVED_BUILD_TARGETS=("${filtered_targets[@]}")
|
||||
}
|
||||
|
||||
filter_out_bluetooth_targets() {
|
||||
local target
|
||||
|
||||
for target in "$@"; do
|
||||
if ! is_bluetooth_target "$target"; then
|
||||
printf '%s\n' "$target"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
is_lora_ota_only_target() {
|
||||
local target_lc=${1,,}
|
||||
[[ "$target_lc" == *lora_ota* ]]
|
||||
@@ -1813,16 +1805,6 @@ is_rak_i2c_voltage_monitor_ota_target() {
|
||||
esac
|
||||
}
|
||||
|
||||
filter_out_lora_ota_only_targets() {
|
||||
local target
|
||||
|
||||
for target in "$@"; do
|
||||
if ! is_lora_ota_only_target "$target"; then
|
||||
printf '%s\n' "$target"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
is_logging_size_constrained_target() {
|
||||
case "$1" in
|
||||
Tiny_Relay_repeater|RAK_3x72_repeater|wio-e5_repeater|wio-e5-repeater_bridge_rs232|wio-e5-mini_companion_radio_usb|wio-e5-mini_repeater|wio-e5-mini_sensor)
|
||||
@@ -1834,16 +1816,6 @@ is_logging_size_constrained_target() {
|
||||
esac
|
||||
}
|
||||
|
||||
filter_out_logging_size_constrained_targets() {
|
||||
local target
|
||||
|
||||
for target in "$@"; do
|
||||
if ! is_logging_size_constrained_target "$target"; then
|
||||
printf '%s\n' "$target"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
prompt_for_kiss_modem_build_policy() {
|
||||
local kiss_count=0
|
||||
local target
|
||||
@@ -2203,6 +2175,51 @@ apply_debug_overrides() {
|
||||
esac
|
||||
}
|
||||
|
||||
uses_merged_standard_usb_logging() {
|
||||
local env_name=$1
|
||||
|
||||
# These profiles either own Serial for framed traffic, deliberately trade
|
||||
# logging for OTA space, or provide their own logging contract. Keep those
|
||||
# contracts unchanged.
|
||||
if is_kiss_modem_target "$env_name" \
|
||||
|| is_bluetooth_target "$env_name" \
|
||||
|| is_lora_ota_only_target "$env_name" \
|
||||
|| is_mqtt_bridge_target "$env_name" \
|
||||
|| is_companion_radio_full_target "$env_name"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${env_name,,}" in
|
||||
*companion*|*comp_radio*|*repeater*|*repeatr*|*room_server*|*room_svr*|\
|
||||
*sensor*|*terminal_chat*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
apply_merged_standard_usb_logging_profile() {
|
||||
local env_name=$1
|
||||
|
||||
uses_merged_standard_usb_logging "$env_name" || return 0
|
||||
|
||||
# Explicit diagnostic overrides retain their documented meaning. Canonical
|
||||
# builds otherwise compile packet/debug output into the ordinary artifact;
|
||||
# get/set usb.logging controls the live Serial stream at runtime.
|
||||
if [ "${DISABLE_DEBUG:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "${PACKET_LOGGING_OVERRIDE,,}" != "off" ]; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_PACKET_LOGGING=1 -DMESH_USB_LOGGING_MERGED=1"
|
||||
fi
|
||||
if [ "${MESHDEBUG_OVERRIDE,,}" != "off" ] \
|
||||
&& ! is_logging_size_constrained_target "$env_name"; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_DEBUG=1"
|
||||
fi
|
||||
}
|
||||
|
||||
disable_usb_logging_for_mqtt() {
|
||||
local env_name=$1
|
||||
|
||||
@@ -2554,6 +2571,8 @@ declare_build_capability_contract() {
|
||||
record_build_expectation "web.webconfig" "start webconfig"
|
||||
else
|
||||
record_build_expectation "companion.usb_mota_source" "ota folder on"
|
||||
record_build_expectation "companion.ble_mota_source" \
|
||||
"Bluetooth mOTA source"
|
||||
fi
|
||||
elif [ "$env_platform" = "ESP32_PLATFORM" ] \
|
||||
&& is_esp32_companion_build "$env_name" \
|
||||
@@ -2983,7 +3002,7 @@ apply_companion_radio_full_profile() {
|
||||
# preamble. CDC 1 is a write-only plaintext packet/debug logging stream;
|
||||
# BLE remains an independent Companion link.
|
||||
append_platformio_build_unflags "-UOTA_FOLDER_SERIAL"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOTA_FOLDER_SERIAL=1 -DCOMPANION_FEATURE_USB_MOTA_SOURCE=1 -DCOMPANION_FEATURE_DEDICATED_USB_LOGGING=1 -DCFG_TUD_CDC=2 -DMESH_DUAL_CDC_LOGGING=1 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOTA_FOLDER_SERIAL=1 -DCOMPANION_FEATURE_USB_MOTA_SOURCE=1 -DCOMPANION_FEATURE_BLE_MOTA_SOURCE=1 -DCOMPANION_FEATURE_DEDICATED_USB_LOGGING=1 -DCFG_TUD_CDC=2 -DMESH_DUAL_CDC_LOGGING=1 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1"
|
||||
|
||||
if ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/ota/"; then
|
||||
append_platformio_build_src_filter "+<helpers/ota/*.cpp>"
|
||||
@@ -3299,16 +3318,9 @@ get_firmware_filename() {
|
||||
local firmware_version_string=$2
|
||||
local filename_infix=$FIRMWARE_FILENAME_INFIX
|
||||
|
||||
if [ -z "$filename_infix" ] \
|
||||
&& [ "${PACKET_LOGGING_OVERRIDE,,}" == "on" ] \
|
||||
&& [ "${MQTT_BRIDGE_OVERRIDE,,}" != "on" ] \
|
||||
&& ! is_mqtt_bridge_target "$env_name"; then
|
||||
filename_infix="logging"
|
||||
fi
|
||||
|
||||
# Make LoRa-OTA artifacts as obvious as logging artifacts without changing
|
||||
# the PlatformIO environment name or the stable MOTA target identity. FULL
|
||||
# artifacts retain their profile marker as well as the required OTA marker.
|
||||
# Make LoRa-OTA artifacts obvious without changing the PlatformIO environment
|
||||
# name or the stable MOTA target identity. FULL artifacts retain their
|
||||
# profile marker as well as the required OTA marker.
|
||||
if [ "$ESP32_FULL_BUILD" = "1" ] && is_lora_ota_build "$env_name"; then
|
||||
if [ "$filename_infix" = "full-logging" ]; then
|
||||
filename_infix="full-logging-ota"
|
||||
@@ -3489,6 +3501,7 @@ build_firmware() {
|
||||
apply_debug_overrides "$env_name"
|
||||
apply_mqtt_bridge_override
|
||||
disable_usb_logging_for_mqtt "$env_name"
|
||||
apply_merged_standard_usb_logging_profile "$env_name"
|
||||
apply_lora_ota_override "$env_name"
|
||||
apply_logical_ota_tuning_flags "$env_name" "$pio_env_name"
|
||||
apply_companion_radio_full_profile "$env_name" "$pio_env_name"
|
||||
@@ -3669,6 +3682,9 @@ get_nrf52_full_companion_replacement() {
|
||||
*companion_radio_ble*)
|
||||
full_env=${env_name/companion_radio_ble/companion_radio_full}
|
||||
;;
|
||||
*companion_radio_ethernet*)
|
||||
full_env=${env_name/companion_radio_ethernet/companion_radio_full}
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
@@ -3686,7 +3702,8 @@ get_esp32_full_companion_replacement() {
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1
|
||||
case "$env_name" in
|
||||
*companion_radio_wifi_mqtt*) return 1 ;;
|
||||
*companion_radio_usb*|*companion_radio_ble*|*companion_radio_wifi*) ;;
|
||||
*companion_radio_usb*|*companion_radio_ble*|*companion_radio_wifi*|\
|
||||
*companion_radio_serial*|*companion_radio_ethernet*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
@@ -3718,6 +3735,12 @@ get_esp32_full_companion_replacement() {
|
||||
*companion_radio_wifi*)
|
||||
full_env=${source_env/companion_radio_wifi/companion_radio_full}
|
||||
;;
|
||||
*companion_radio_serial*)
|
||||
full_env=${source_env/companion_radio_serial/companion_radio_full}
|
||||
;;
|
||||
*companion_radio_ethernet*)
|
||||
full_env=${source_env/companion_radio_ethernet/companion_radio_full}
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
@@ -3734,8 +3757,148 @@ get_full_companion_replacement() {
|
||||
|| get_esp32_full_companion_replacement "$1"
|
||||
}
|
||||
|
||||
is_companion_transport_replaced_by_full() {
|
||||
get_full_companion_replacement "$1" >/dev/null
|
||||
get_terminal_chat_full_companion_replacement() {
|
||||
local source_env=$1
|
||||
local env_name=${source_env,,}
|
||||
local full_env=""
|
||||
|
||||
case "${PIO_ENV_PLATFORM_BY_NAME[$source_env]:-}" in
|
||||
ESP32_PLATFORM|NRF52_PLATFORM) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
# Most Terminal Chat and Full Companion targets share an exact hardware
|
||||
# prefix. These exceptions use the canonical runtime-configurable Full name
|
||||
# instead of the older revision/FEM-specific spelling.
|
||||
case "$env_name" in
|
||||
heltec_v4_terminal_chat)
|
||||
full_env=heltec_v4_2_v4_3_companion_radio_full_femon
|
||||
;;
|
||||
heltec_v4_tft_terminal_chat)
|
||||
full_env=heltec_v4_tft_companion_radio_full_femon
|
||||
;;
|
||||
heltec_tracker_v2_terminal_chat)
|
||||
full_env=heltec_tracker_v2_companion_radio_full_femon
|
||||
;;
|
||||
*terminal_chat*)
|
||||
full_env=${source_env/terminal_chat/companion_radio_full}
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
is_companion_radio_full_target "$full_env" || return 1
|
||||
printf '%s\n' "$full_env"
|
||||
}
|
||||
|
||||
get_terminal_chat_usb_companion_replacement() {
|
||||
local source_env=$1
|
||||
local env_name=${source_env,,}
|
||||
local usb_env=""
|
||||
|
||||
case "$env_name" in
|
||||
generic_espnow_terminal_chat)
|
||||
usb_env=Generic_ESPNOW_comp_radio_usb
|
||||
;;
|
||||
*terminal_chat*)
|
||||
usb_env=${source_env/terminal_chat/companion_radio_usb}
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "${PIO_ENV_PLATFORM_BY_NAME[$usb_env]+x}" ] || return 1
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$usb_env]:-}" = "${PIO_ENV_PLATFORM_BY_NAME[$source_env]:-}" ] \
|
||||
|| return 1
|
||||
[ "${PIO_ENV_BOARD_BY_NAME[$usb_env]:-}" = "${PIO_ENV_BOARD_BY_NAME[$source_env]:-}" ] \
|
||||
|| return 1
|
||||
printf '%s\n' "$usb_env"
|
||||
}
|
||||
|
||||
get_terminal_chat_companion_replacement() {
|
||||
get_terminal_chat_full_companion_replacement "$1" 2>/dev/null \
|
||||
|| get_terminal_chat_usb_companion_replacement "$1"
|
||||
}
|
||||
|
||||
get_combined_usb_ble_companion_replacement() {
|
||||
case "${1,,}" in
|
||||
heltec_e290_companion_ble|heltec_e290_companion_usb)
|
||||
printf '%s\n' Heltec_E290_companion_usb_ble
|
||||
;;
|
||||
heltec_t190_companion_radio_ble_|heltec_t190_companion_radio_usb_)
|
||||
printf '%s\n' Heltec_T190_companion_radio_usb_ble_
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_merged_rs232_repeater_replacement() {
|
||||
case "${1,,}" in
|
||||
heltec_t096_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_t096_repeater
|
||||
;;
|
||||
heltec_t096_repeater_bridge_rs232_lora_ota_no_external_sensors)
|
||||
printf '%s\n' Heltec_t096_repeater_lora_ota_no_external_sensors
|
||||
;;
|
||||
rak_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|\
|
||||
rak_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors)
|
||||
printf '%s\n' RAK_4631_repeater_lora_ota_no_external_sensors
|
||||
;;
|
||||
rak_4631_repeater_bridge_rs232_serial1|\
|
||||
rak_4631_repeater_bridge_rs232_serial2)
|
||||
printf '%s\n' RAK_4631_repeater
|
||||
;;
|
||||
promicro_repeater_bridge_rs232_serial1)
|
||||
printf '%s\n' ProMicro_repeater
|
||||
;;
|
||||
heltec_t114_without_display_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_t114_without_display_repeater
|
||||
;;
|
||||
heltec_t114_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_t114_repeater
|
||||
;;
|
||||
rak_3112_repeater_bridge_rs232)
|
||||
printf '%s\n' RAK_3112_repeater
|
||||
;;
|
||||
rak_11310_repeater_bridge_rs232)
|
||||
printf '%s\n' RAK_11310_repeater
|
||||
;;
|
||||
waveshare_rp2040_lora_repeater_bridge_rs232)
|
||||
printf '%s\n' waveshare_rp2040_lora_repeater
|
||||
;;
|
||||
solarxiao_30s_repeater_bridge_rs232)
|
||||
printf '%s\n' solarxiao_30S_repeater
|
||||
;;
|
||||
solarxiao_33s_repeater_bridge_rs232)
|
||||
printf '%s\n' solarxiao_33S_repeater
|
||||
;;
|
||||
heltec_v3_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_v3_repeater
|
||||
;;
|
||||
heltec_wsl3_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_WSL3_repeater
|
||||
;;
|
||||
lilygo_tlora_v2_1_1_6_repeater_bridge_rs232)
|
||||
printf '%s\n' LilyGo_TLora_V2_1_1_6_repeater
|
||||
;;
|
||||
*)
|
||||
# Wio-E5 intentionally remains separate: its normal image has only 916
|
||||
# bytes free, and the measured combined image exceeds its 240 KiB app
|
||||
# partition by 2,192 bytes unless the normal USB/MQTT host CLI is removed.
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_firmware_role_replaced_by_canonical_artifact() {
|
||||
get_full_companion_replacement "$1" >/dev/null 2>&1 \
|
||||
|| get_terminal_chat_companion_replacement "$1" >/dev/null \
|
||||
|| get_combined_usb_ble_companion_replacement "$1" >/dev/null \
|
||||
|| get_merged_rs232_repeater_replacement "$1" >/dev/null
|
||||
}
|
||||
|
||||
is_runtime_setting_alias_target() {
|
||||
@@ -3745,16 +3908,22 @@ is_runtime_setting_alias_target() {
|
||||
|| is_exact_companion_recipe_alias_target "$1"; then
|
||||
return 0
|
||||
fi
|
||||
case "${1,,}" in
|
||||
ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_full)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_redundant_bulk_build_target() {
|
||||
# Keep every legacy name available to `build-firmware` and
|
||||
# `build-matching-firmwares`, but do not republish binaries that differ only
|
||||
# by a saved/default setting or by an attached transport already supplied by
|
||||
# a dual-CDC Full Companion.
|
||||
# by a saved/default setting, or roles already supplied by Full Companion.
|
||||
# Its text terminal supersedes standalone Terminal Chat on the same exact
|
||||
# hardware, in addition to its combined attached transports.
|
||||
if is_runtime_setting_alias_target "$1" \
|
||||
|| is_companion_transport_replaced_by_full "$1"; then
|
||||
|| is_firmware_role_replaced_by_canonical_artifact "$1"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
@@ -4158,7 +4327,9 @@ run_logged_build_targets() {
|
||||
local build_status=0
|
||||
local overall_status=0
|
||||
local preserved_log=0
|
||||
local worker_limit=${PROFILE_BUILD_WORKERS:-1}
|
||||
# Never overlap PlatformIO processes in this checkout: environments share
|
||||
# .pio/build and can clean one another's objects.
|
||||
local worker_limit=1
|
||||
local pio_job_limit=${OPTION3_PIO_JOBS:-8}
|
||||
local next_index=0
|
||||
local candidate_index
|
||||
@@ -4501,10 +4672,6 @@ run_logging_matrix_build_targets() {
|
||||
local targets=("$@")
|
||||
local target
|
||||
local standard_targets=()
|
||||
local logging_source_targets=()
|
||||
local logging_targets=()
|
||||
local filtered_logging_targets=()
|
||||
local constrained_logging_targets=()
|
||||
local original_meshdebug_override=$MESHDEBUG_OVERRIDE
|
||||
local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE
|
||||
local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE
|
||||
@@ -4512,12 +4679,9 @@ run_logging_matrix_build_targets() {
|
||||
local original_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX
|
||||
local original_esp32_full_build=$ESP32_FULL_BUILD
|
||||
local original_profile_build_workers=$PROFILE_BUILD_WORKERS
|
||||
local bluetooth_skip_count=0
|
||||
local lora_ota_only_skip_count=0
|
||||
local full_only_standard_skip_count=0
|
||||
local full_companion_logging_skip_count=0
|
||||
local full_profile_logging_skip_count=0
|
||||
local logging_target_count=0
|
||||
local merged_usb_logging_count=0
|
||||
local constrained_merged_logging_count=0
|
||||
local build_status=0
|
||||
local pass_status=0
|
||||
|
||||
@@ -4527,27 +4691,35 @@ run_logging_matrix_build_targets() {
|
||||
fi
|
||||
LOGGING_MATRIX_FAILURES=()
|
||||
PROFILE_BUILD_WORKERS=$OPTION3_BUILD_WORKERS
|
||||
echo "Option 3 parallelism: ${PROFILE_BUILD_WORKERS} target build(s), ${OPTION3_PIO_JOBS} PlatformIO job(s) per target."
|
||||
echo "Option 3 PlatformIO policy: one target build at a time, ${OPTION3_PIO_JOBS} compiler job(s) inside that process."
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if is_mqtt_bridge_target "$target"; then
|
||||
continue
|
||||
fi
|
||||
logging_source_targets+=("$target")
|
||||
if requires_esp32_full_cli_profile "$target"; then
|
||||
full_only_standard_skip_count=$((full_only_standard_skip_count + 1))
|
||||
else
|
||||
standard_targets+=("$target")
|
||||
if uses_merged_standard_usb_logging "$target"; then
|
||||
merged_usb_logging_count=$((merged_usb_logging_count + 1))
|
||||
if is_logging_size_constrained_target "$target"; then
|
||||
constrained_merged_logging_count=$((constrained_merged_logging_count + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Profile 1/3: building ${#standard_targets[@]} standard target(s) with logging off and MQTT bridge off."
|
||||
echo "Profile 1/2: building ${#standard_targets[@]} standard target(s); ${merged_usb_logging_count} embed runtime-controlled USB logging in the ordinary artifact."
|
||||
if [ "$constrained_merged_logging_count" -gt 0 ]; then
|
||||
echo "Keeping verbose MESH_DEBUG off for ${constrained_merged_logging_count} size-constrained STM32 target(s); packet logging remains available at runtime."
|
||||
fi
|
||||
if [ "$full_only_standard_skip_count" -gt 0 ]; then
|
||||
echo "Deferring ${full_only_standard_skip_count} ESP32 ESP-NOW target(s) to their FULL logging fallback; its persistent USB gate also provides normal output-off operation."
|
||||
fi
|
||||
ESP32_FULL_BUILD=0
|
||||
MESHDEBUG_OVERRIDE="off"
|
||||
PACKET_LOGGING_OVERRIDE="off"
|
||||
MESHDEBUG_OVERRIDE=""
|
||||
PACKET_LOGGING_OVERRIDE=""
|
||||
MQTT_BRIDGE_OVERRIDE="off"
|
||||
FIRMWARE_FILENAME_INFIX=""
|
||||
if [ ${#standard_targets[@]} -gt 0 ]; then
|
||||
@@ -4557,87 +4729,12 @@ run_logging_matrix_build_targets() {
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
fi
|
||||
|
||||
mapfile -t logging_targets < <(filter_out_bluetooth_targets "${logging_source_targets[@]}")
|
||||
bluetooth_skip_count=$((${#logging_source_targets[@]} - ${#logging_targets[@]}))
|
||||
|
||||
if [ "$bluetooth_skip_count" -gt 0 ]; then
|
||||
echo "Skipping ${bluetooth_skip_count} Bluetooth target(s) for logging-on pass."
|
||||
fi
|
||||
|
||||
lora_ota_only_skip_count=0
|
||||
for target in "${logging_targets[@]}"; do
|
||||
if is_lora_ota_only_target "$target"; then
|
||||
lora_ota_only_skip_count=$((lora_ota_only_skip_count + 1))
|
||||
fi
|
||||
done
|
||||
mapfile -t logging_targets < <(filter_out_lora_ota_only_targets "${logging_targets[@]}")
|
||||
|
||||
if [ "$lora_ota_only_skip_count" -gt 0 ]; then
|
||||
echo "Skipping ${lora_ota_only_skip_count} LoRa-OTA-only target(s) for logging-on pass because logging disables LoRa OTA."
|
||||
fi
|
||||
|
||||
filtered_logging_targets=()
|
||||
for target in "${logging_targets[@]}"; do
|
||||
if is_companion_radio_full_target "$target"; then
|
||||
full_companion_logging_skip_count=$((full_companion_logging_skip_count + 1))
|
||||
elif has_esp32_full_profile "$target"; then
|
||||
full_profile_logging_skip_count=$((full_profile_logging_skip_count + 1))
|
||||
else
|
||||
filtered_logging_targets+=("$target")
|
||||
fi
|
||||
done
|
||||
logging_targets=("${filtered_logging_targets[@]}")
|
||||
if [ "$full_profile_logging_skip_count" -gt 0 ]; then
|
||||
echo "Deferring ${full_profile_logging_skip_count} ESP32 target(s) to the unified FULL/fallback pass; their separate standard logging artifacts would be redundant."
|
||||
fi
|
||||
if [ "$full_companion_logging_skip_count" -gt 0 ]; then
|
||||
echo "Skipping ${full_companion_logging_skip_count} Full Companion target(s) for the separate logging-on pass; each Full image provides persistent runtime logging through either a dedicated CDC port or its input-capable single-TTY terminal."
|
||||
fi
|
||||
|
||||
for target in "${logging_targets[@]}"; do
|
||||
if is_logging_size_constrained_target "$target"; then
|
||||
constrained_logging_targets+=("$target")
|
||||
fi
|
||||
done
|
||||
mapfile -t logging_targets < <(filter_out_logging_size_constrained_targets "${logging_targets[@]}")
|
||||
logging_target_count=$((${#logging_targets[@]} + ${#constrained_logging_targets[@]}))
|
||||
|
||||
if [ "$logging_target_count" -gt 0 ]; then
|
||||
echo "Profile 2/3: building ${logging_target_count} standard target(s) with logging on and MQTT bridge off."
|
||||
echo "Logging-on artifacts use filename form: name-logging-version"
|
||||
else
|
||||
echo "No non-Bluetooth targets remain for logging-on pass."
|
||||
fi
|
||||
|
||||
if [ ${#logging_targets[@]} -gt 0 ]; then
|
||||
MESHDEBUG_OVERRIDE="on"
|
||||
PACKET_LOGGING_OVERRIDE="on"
|
||||
MQTT_BRIDGE_OVERRIDE="off"
|
||||
FIRMWARE_FILENAME_INFIX="logging"
|
||||
run_logged_build_targets "${logging_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
fi
|
||||
|
||||
if [ ${#constrained_logging_targets[@]} -gt 0 ]; then
|
||||
echo "Building ${#constrained_logging_targets[@]} size-constrained STM32 target(s) with packet logging on and MESH_DEBUG off to fit flash."
|
||||
MESHDEBUG_OVERRIDE="off"
|
||||
PACKET_LOGGING_OVERRIDE="on"
|
||||
MQTT_BRIDGE_OVERRIDE="off"
|
||||
FIRMWARE_FILENAME_INFIX="logging"
|
||||
run_logged_build_targets "${constrained_logging_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
fi
|
||||
|
||||
run_full_esp32_profile "Profile 3/3 unified FULL" "unified" "${targets[@]}"
|
||||
run_full_esp32_profile "FULL unified pass" "unified" "${targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
run_full_esp32_profile "Profile 3/3 logging fallback" "fallback" "${targets[@]}"
|
||||
run_full_esp32_profile "FULL logging fallback pass" "fallback" "${targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
@@ -4863,7 +4960,7 @@ main() {
|
||||
echo "Skipping separate debug and MQTT prompts; FULL everything enables USB logging and WiFi MQTT where the hardware supports it."
|
||||
elif is_automatic_profile_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
if is_logging_matrix_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
echo "Skipping debug and MQTT prompts; this action builds standard, logging, and unified FULL profiles automatically."
|
||||
echo "Skipping debug and MQTT prompts; this action builds standard artifacts with merged runtime USB logging and unified FULL profiles automatically."
|
||||
elif is_full_esp32_logging_command "${SELECTED_COMMAND_ARGS[0]}"; then
|
||||
echo "Skipping debug and MQTT prompts; this action builds only logging fallbacks for FULL targets without WiFi MQTT."
|
||||
else
|
||||
|
||||
+5
-5
@@ -431,8 +431,8 @@ meaning can change when the service reorders or adds presets.
|
||||
|
||||
| Build profile | WiFi/MQTT behavior |
|
||||
|---|---|
|
||||
| Standard | Uses the selected target's role. 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. |
|
||||
| Logging | Enables USB/debug packet logging and disables the MQTT bridge. CommonCLI roles persist `get/set usb.logging`; logging output itself is not a direct MQTT uplink. |
|
||||
| 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. |
|
||||
@@ -445,9 +445,9 @@ persistent MQTT discovery state leaves insufficient internal-DRAM margin at 254.
|
||||
|
||||
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 a
|
||||
separate standard logging image or non-MQTT FULL twin for a role covered by the
|
||||
unified image. All FULL profiles include LoRa OTA, WebConfig where supported,
|
||||
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. All FULL profiles include LoRa OTA, WebConfig where supported,
|
||||
up to 254 neighbors, and expanded dual-OTA partitions. Target-specific
|
||||
internal-DRAM limits still apply.
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
full: "Full Companion transports",
|
||||
ble: "Bluetooth LE",
|
||||
usb: "USB",
|
||||
"usb-ble": "USB + Bluetooth LE",
|
||||
wifi: "Wi-Fi",
|
||||
serial: "Serial / UART",
|
||||
ethernet: "Ethernet",
|
||||
@@ -109,6 +110,7 @@
|
||||
// Exact aliases of the unsuffixed Heltec V4 USB/BLE recipes.
|
||||
"heltec_v4_companion_radio_usb_femon",
|
||||
"heltec_v4_companion_radio_ble_femon",
|
||||
"ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_full",
|
||||
]);
|
||||
|
||||
let pickerInstanceCount = 0;
|
||||
@@ -273,6 +275,7 @@
|
||||
const value = String(tail).toLowerCase().replace(/^[_-]+/, "");
|
||||
if (role === "companion") {
|
||||
if (/^full(?:$|[_-])/.test(value)) return "full";
|
||||
if (/^usb_ble(?:$|[_-])/.test(value)) return "usb-ble";
|
||||
if (/^ble(?:$|[_-])/.test(value)) return "ble";
|
||||
if (/^usb(?:$|[_-])/.test(value)) return "usb";
|
||||
if (/^wifi(?:$|[_-])/.test(value)) return "wifi";
|
||||
@@ -291,7 +294,7 @@
|
||||
let value = String(tail);
|
||||
if (role === "companion") {
|
||||
value = value.replace(
|
||||
/^(?:full|ble|usb|wifi|serial|ethernet)(?=$|[_-])/i,
|
||||
/^(?:full|usb_ble|ble|usb|wifi|serial|ethernet)(?=$|[_-])/i,
|
||||
" "
|
||||
);
|
||||
// Power saving and controllable FEM gain are persisted settings. Legacy
|
||||
@@ -414,13 +417,69 @@
|
||||
}).map(function (profile) {
|
||||
return profile.hardware + "\n" + profile.variant;
|
||||
}));
|
||||
const combinedUsbBleKeys = new Set((profiles || []).filter(
|
||||
function (profile) {
|
||||
return profile.role === "companion" && profile.mode === "usb-ble";
|
||||
}
|
||||
).map(function (profile) {
|
||||
return profile.hardware + "\n" + profile.variant;
|
||||
}));
|
||||
const terminalReplacementKeys = new Set((profiles || []).filter(
|
||||
function (profile) {
|
||||
return profile.role === "companion" &&
|
||||
(profile.mode === "full" || profile.mode === "usb" ||
|
||||
profile.mode === "usb-ble");
|
||||
}
|
||||
).map(function (profile) {
|
||||
return profile.hardware + "\n" + profile.variant;
|
||||
}));
|
||||
const targets = new Set((profiles || []).map(function (profile) {
|
||||
return String(profile.target || "").toLowerCase();
|
||||
}));
|
||||
const mergedRs232Targets = {
|
||||
heltec_t096_repeater_bridge_rs232: "heltec_t096_repeater",
|
||||
heltec_t096_repeater_bridge_rs232_lora_ota_no_external_sensors:
|
||||
"heltec_t096_repeater_lora_ota_no_external_sensors",
|
||||
rak_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors:
|
||||
"rak_4631_repeater_lora_ota_no_external_sensors",
|
||||
rak_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors:
|
||||
"rak_4631_repeater_lora_ota_no_external_sensors",
|
||||
rak_4631_repeater_bridge_rs232_serial1: "rak_4631_repeater",
|
||||
rak_4631_repeater_bridge_rs232_serial2: "rak_4631_repeater",
|
||||
promicro_repeater_bridge_rs232_serial1: "promicro_repeater",
|
||||
heltec_t114_without_display_repeater_bridge_rs232:
|
||||
"heltec_t114_without_display_repeater",
|
||||
heltec_t114_repeater_bridge_rs232: "heltec_t114_repeater",
|
||||
rak_3112_repeater_bridge_rs232: "rak_3112_repeater",
|
||||
rak_11310_repeater_bridge_rs232: "rak_11310_repeater",
|
||||
waveshare_rp2040_lora_repeater_bridge_rs232:
|
||||
"waveshare_rp2040_lora_repeater",
|
||||
solarxiao_30s_repeater_bridge_rs232: "solarxiao_30s_repeater",
|
||||
solarxiao_33s_repeater_bridge_rs232: "solarxiao_33s_repeater",
|
||||
heltec_v3_repeater_bridge_rs232: "heltec_v3_repeater",
|
||||
heltec_wsl3_repeater_bridge_rs232: "heltec_wsl3_repeater",
|
||||
lilygo_tlora_v2_1_1_6_repeater_bridge_rs232:
|
||||
"lilygo_tlora_v2_1_1_6_repeater",
|
||||
};
|
||||
|
||||
return (profiles || []).filter(function (profile) {
|
||||
const key = profile.hardware + "\n" + profile.variant;
|
||||
const replacedAttachedTransport = profile.role === "companion" &&
|
||||
(profile.mode === "usb" || profile.mode === "ble" ||
|
||||
profile.mode === "wifi");
|
||||
return !(replacedAttachedTransport && fullKeys.has(key));
|
||||
profile.mode === "wifi" || profile.mode === "serial" ||
|
||||
profile.mode === "ethernet" || profile.mode === "usb-ble");
|
||||
const replacedByUsbBle = profile.role === "companion" &&
|
||||
(profile.mode === "usb" || profile.mode === "ble") &&
|
||||
combinedUsbBleKeys.has(key);
|
||||
const replacedTerminal = profile.role === "terminal" &&
|
||||
terminalReplacementKeys.has(key);
|
||||
const rs232Replacement = mergedRs232Targets[
|
||||
String(profile.target || "").toLowerCase()
|
||||
];
|
||||
const replacedRs232 = Boolean(rs232Replacement &&
|
||||
targets.has(rs232Replacement));
|
||||
return !(replacedAttachedTransport && fullKeys.has(key)) &&
|
||||
!replacedByUsbBle && !replacedTerminal && !replacedRs232;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -440,6 +499,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyMergedStandardUsbLoggingCapabilities(profiles) {
|
||||
return (profiles || []).map(function (profile) {
|
||||
const safeRole = [
|
||||
"companion", "repeater", "room", "sensor", "terminal",
|
||||
].includes(profile.role);
|
||||
const unsafeCompanionMode = profile.role === "companion" &&
|
||||
(profile.mode === "ble" || profile.mode === "full");
|
||||
if (profile.logging !== "none" || profile.ota !== "none" ||
|
||||
!safeRole || unsafeCompanionMode) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
profile.logging = "usb-runtime";
|
||||
profile.loggingModes = ["none", "usb"];
|
||||
return profile;
|
||||
});
|
||||
}
|
||||
|
||||
function hardwareFamilyFor(hardware, hardwareNames) {
|
||||
const value = String(hardware || "");
|
||||
const lowerValue = value.toLowerCase();
|
||||
@@ -539,8 +616,10 @@
|
||||
}).filter(function (profile) {
|
||||
return !isHiddenLegacyProfile(profile);
|
||||
});
|
||||
const profiles = omitTransportsReplacedByFull(
|
||||
applyFullCompanionCapabilities(visibleProfiles)
|
||||
const profiles = applyMergedStandardUsbLoggingCapabilities(
|
||||
omitTransportsReplacedByFull(
|
||||
applyFullCompanionCapabilities(visibleProfiles)
|
||||
)
|
||||
).sort(function (a, b) {
|
||||
return a.target.localeCompare(b.target, undefined, {
|
||||
numeric: true,
|
||||
@@ -738,7 +817,11 @@
|
||||
"With no saved SSID, the setup AP stays available for 30 minutes after each boot, then Wi-Fi powers off automatically until reboot or an explicit start webconfig command. A configured Wi-Fi mode keeps reconnecting instead."
|
||||
);
|
||||
} else if (profile.logging === "usb-runtime") {
|
||||
if (profile.dedicatedUsbLogging) {
|
||||
if (!isFullCompanion(profile)) {
|
||||
extra.push(
|
||||
"This ordinary image includes USB logging. Use get usb.logging and set usb.logging off|on to select and save normal or logging operation."
|
||||
);
|
||||
} else if (profile.dedicatedUsbLogging) {
|
||||
extra.push(
|
||||
"Full Companion starts with USB logging off and only interface 00. Use get usb.logging, or set usb.logging on reboot to add interface 02; set usb.logging off reboot removes it again."
|
||||
);
|
||||
@@ -1118,6 +1201,8 @@
|
||||
parseFirmwareAsset: parseFirmwareAsset,
|
||||
parseTargetProfile: parseTargetProfile,
|
||||
applyFullCompanionCapabilities: applyFullCompanionCapabilities,
|
||||
applyMergedStandardUsbLoggingCapabilities:
|
||||
applyMergedStandardUsbLoggingCapabilities,
|
||||
applyDualCdcFullCompanionCapabilities: applyFullCompanionCapabilities,
|
||||
applyNrf52FullCompanionCapabilities:
|
||||
applyFullCompanionCapabilities,
|
||||
|
||||
@@ -49,7 +49,7 @@ an old or failed artifact without a verified sidecar.
|
||||
| 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 |
|
||||
| Serial, USB, BLE, or WiFi companion | Uses the companion protocol; any serial diagnostics are target-specific |
|
||||
| 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 |
|
||||
|
||||
@@ -65,8 +65,8 @@ retain 50 because their MQTT discovery tables are constrained by internal DRAM.
|
||||
|
||||
| Build/profile | Command availability |
|
||||
|---|---|
|
||||
| Standard non-MQTT repeater or room server | Keeps the normal role CLI. 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. |
|
||||
| Standard logging | Logging does not remove commands by itself. It has the same CLI as the selected role/profile and adds compiled logging behavior. CommonCLI roles persist `get/set usb.logging`. Roles covered by a Full image with runtime logging are not duplicated here. |
|
||||
| Standard non-MQTT repeater or room server | Keeps the normal role CLI and, where USB is a safe plaintext console, embeds debug/packet logging behind persistent `get/set usb.logging`. 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 a 254-entry neighbor table. |
|
||||
| Internal-flash nRF52 repeater auto pair | `full-ota` retains the board's external-sensor drivers; `reduced-ota` omits the declared optional sensors to leave additional internal-flash staging room. RAK3401 and RAK4631 reduced builds retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors at a measured 4,808-byte flash cost. Both artifacts carry the same stable OTA target identity and are checked for `ota ...` and `retry.preset`; RAK artifacts also verify the retained monitor drivers. |
|
||||
| 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. |
|
||||
@@ -74,10 +74,11 @@ retain 50 because their MQTT discovery tables are constrained by internal DRAM.
|
||||
| FULL ESP32 logging fallback | Uses the matching non-MQTT target only when no WiFi MQTT sibling exists, with debug and packet logging enabled and the complete command surface supported by that role and hardware. Its persistent USB gate also covers output-off operation, avoiding a second FULL ESP-NOW image. |
|
||||
| Dual-CDC Full Companion | nRF52 and qualified native-USB ESP32-S3 Full images use one physical USB connection. Fresh installs expose only interface `00` for framed Companion/terminal/mOTA traffic. Enabling logging and rebooting adds interface `02` for plaintext logs. They also provide BLE and source-only LoRa OTA; ESP32 additionally provides WiFi. `get/set usb.logging` persistently controls whether the logging interface is present. |
|
||||
| Single-TTY Full Companion | ESP32 Full images without dual CDC start with framed Companion on their one TTY. `set usb.logging on` switches it to an input-capable plaintext logging terminal; `set usb.logging off` replies and then restores framed Companion automatically. BLE, WiFi, and source-only LoRa OTA remain available. |
|
||||
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. RAK3401 and RAK4631 profiles retain the four common INA I2C voltage/current monitors. GPS-preserving RAK nRF52 OTA profiles also retain their GPS commands and provider. The RAK4631 Serial1 RS232 bridge remains GPS-off because both features require Serial1. The legacy target suffix is retained for OTA identity compatibility. |
|
||||
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove 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 GPS uses UART 1. Legacy target suffixes remain stable for OTA identity compatibility. |
|
||||
|
||||
`logging`, `OTA`, and `FULL` describe independent build features. Do not infer
|
||||
that a command is missing merely because `logging` appears in the filename.
|
||||
`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.
|
||||
|
||||
## Canonical bulk-build policy
|
||||
|
||||
@@ -94,7 +95,8 @@ available from a canonical image:
|
||||
persisted `radio.rxgain on|off` setting; the G3 alias changed only the
|
||||
advertised default name.
|
||||
- When a board has a Full Companion, that one artifact replaces its
|
||||
separate USB, BLE, ordinary WiFi, and USB packet-logging Companion artifacts.
|
||||
separate USB, BLE, ordinary WiFi, hardware-serial, Ethernet Companion, and
|
||||
USB packet-logging Companion artifacts.
|
||||
It provides BLE and source-only LoRa OTA; ESP32 also provides ordinary WiFi.
|
||||
Dual-CDC builds keep framed traffic on interface `00` and add logging on
|
||||
interface `02` after a reboot. Single-TTY builds switch interface `00` into
|
||||
@@ -108,6 +110,18 @@ available from a canonical image:
|
||||
available as explicit build targets, but are not canonical release artifacts
|
||||
when the qualified Full image fits.
|
||||
|
||||
- Standalone Terminal Chat is omitted when the same exact hardware has either
|
||||
Full Companion or a USB Companion, because their local text terminal supplies
|
||||
the same role. Heltec E290 and T190 similarly publish one combined USB + BLE
|
||||
Companion. RAK4631 repeater and room-server Ethernet builds stay separate;
|
||||
only the RAK4631 Ethernet Companion is folded into Full Companion.
|
||||
- Matching RS-232 bridge roles are compiled into the normal repeater and
|
||||
selected at runtime with `bridge.enabled`, `bridge.baud`, and `bridge.uart`.
|
||||
Historical bridge names remain directly buildable but are omitted from bulk
|
||||
releases. Wio-E5 remains separate because its normal image has only 916
|
||||
bytes free, while the combined image exceeds the fixed 240 KiB application
|
||||
partition by 2,192 bytes.
|
||||
|
||||
The old aliases still work with `build-firmware` and
|
||||
`build-matching-firmwares`. Dedicated repeater LoRa OTA receiver images are not
|
||||
collapsed; they retain their exact storage, bootloader, role, and target
|
||||
|
||||
@@ -11,10 +11,10 @@ own interface, so those build roles are not represented here.
|
||||
|
||||
Build columns mean:
|
||||
|
||||
- **Standard** - the ordinary non-MQTT artifact, without the `logging` or
|
||||
explicit `ota` filename marker.
|
||||
- **Logging** - the ordinary non-MQTT `-logging-` artifact. Logging does not
|
||||
remove commands by itself.
|
||||
- **Standard** - the ordinary non-MQTT artifact, without an explicit `ota`
|
||||
filename marker. Safe plaintext-USB roles embed runtime USB logging.
|
||||
- **Logging** - the legacy `-logging-` profile, now represented by the same
|
||||
ordinary artifact and retained as a comparison column only.
|
||||
- **LoRa OTA** - the explicit `-ota-` repeater or repeater-bridge artifact. Its
|
||||
optional external-sensor drivers are removed, but onboard GPS is retained.
|
||||
- **FULL unified** - the expanded-partition ESP32 artifact with LoRa OTA, the
|
||||
@@ -72,7 +72,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Statistics | [`set telemetry.gps`; `get/set/send telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data; other roles can return empty data | Yes | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial |
|
||||
| Logging | [`get/set usb.logging`; unified FULL `get/set logging.output`](cli_commands.md#control-live-usb-logging) | Logging artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; Full Companion uses either a reboot-controlled second CDC or an input-capable single-TTY logging terminal | No | Yes | No |
|
||||
| Logging | [`get/set usb.logging`; unified FULL `get/set logging.output`](cli_commands.md#control-live-usb-logging) | Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; Full Companion uses either a reboot-controlled second CDC or an input-capable single-TTY logging terminal | Yes | Yes | No |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes |
|
||||
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply | Yes | Yes | Yes |
|
||||
| Radio | [`tempradio ...`; `normalradio`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes |
|
||||
@@ -155,6 +155,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Bridge | [`get/set bridge.delay`](cli_commands.md#add-a-delay-to-packets-routed-through-this-bridge) | Compiled bridge | Feature | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.source`](cli_commands.md#view-or-change-the-source-of-packets-bridged-to-the-external-interface) | Compiled bridge | Feature | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.baud`](cli_commands.md#view-or-change-the-speed-of-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.uart`](cli_commands.md#view-or-change-the-uart-used-by-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.channel`](cli_commands.md#view-or-change-the-channel-used-for-bridging-espnow-only) | ESP-NOW is ESP32 only | No | No | No |
|
||||
| Bridge | [`get/set bridge.secret`](cli_commands.md#set-the-esp-now-secret) | ESP-NOW is ESP32 only | No | No | No |
|
||||
| Board | [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 bootloader metadata | Yes | Yes | Yes |
|
||||
@@ -235,7 +236,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Statistics | [`set telemetry.gps`; `get/set/send telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS setting requires a provider; remote access requires administrator | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial | Serial | Serial |
|
||||
| Logging | [`get/set usb.logging`; unified FULL `get/set logging.output`](cli_commands.md#control-live-usb-logging) | Logging artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; Full Companion uses either a reboot-controlled second CDC or an input-capable single-TTY logging terminal | No | Yes | No | No | Yes |
|
||||
| Logging | [`get/set usb.logging`; unified FULL `get/set logging.output`](cli_commands.md#control-live-usb-logging) | Ordinary safe-USB artifacts; CommonCLI USB gate is persistent; unified ESP32 FULL selects off/USB/WiFi/both; Full Companion uses either a reboot-controlled second CDC or an input-capable single-TTY logging terminal | Yes | Yes | No | No | Yes |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply | Yes | Yes | Yes | Yes | Yes |
|
||||
| Radio | [`tempradio ...`; `normalradio`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
@@ -318,6 +319,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Bridge | [`get/set bridge.delay`](cli_commands.md#add-a-delay-to-packets-routed-through-this-bridge) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.source`](cli_commands.md#view-or-change-the-source-of-packets-bridged-to-the-external-interface) | Compiled bridge | Feature | Feature | Feature | Yes | Feature |
|
||||
| Bridge | [`get/set bridge.baud`](cli_commands.md#view-or-change-the-speed-of-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature | No | Feature |
|
||||
| Bridge | [`get/set bridge.uart`](cli_commands.md#view-or-change-the-uart-used-by-the-bridge-rs-232-only) | RS-232 bridge | Feature | Feature | Feature | No | Feature |
|
||||
| Bridge | [`get/set bridge.channel`](cli_commands.md#view-or-change-the-channel-used-for-bridging-espnow-only) | ESP-NOW bridge | No | No | No | Feature | Feature |
|
||||
| Bridge | [`get/set bridge.secret`](cli_commands.md#set-the-esp-now-secret) | ESP-NOW bridge | No | No | No | Feature | Feature |
|
||||
| Board | [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 only | No | No | No | No | No |
|
||||
|
||||
+41
-6
@@ -562,6 +562,23 @@ set flag bit 0.
|
||||
|
||||
---
|
||||
|
||||
## Set Companion display rotation
|
||||
|
||||
SSD1306 Full Companion builds support a persisted runtime orientation:
|
||||
|
||||
```text
|
||||
get display.rotation
|
||||
set display.rotation 0
|
||||
set display.rotation 90
|
||||
set display.rotation 180
|
||||
set display.rotation 270
|
||||
```
|
||||
|
||||
The values are clockwise degrees. `0` clears the override and restores the
|
||||
board's compiled default. Unsupported display drivers return an error.
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Builds compiled with `MESH_PACKET_LOGGING` emit one `RAW:` line for every
|
||||
@@ -576,8 +593,9 @@ timing, hash, type, route, and payload information. Frames that cannot be
|
||||
decoded still emit their `RAW:` line. Transmitted packets emit the decoded TX
|
||||
summary.
|
||||
|
||||
Ordinary `-logging-` artifacts keep packet logging separate from LoRa OTA.
|
||||
Use the separately named `-ota-` artifact when LoRa OTA is required. A
|
||||
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. Use the separately named `-ota-` artifact when LoRa OTA is 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.
|
||||
@@ -594,10 +612,11 @@ set usb.logging on reboot
|
||||
set usb.logging off reboot
|
||||
```
|
||||
|
||||
These commands are compiled into logging artifacts and every Full Companion.
|
||||
They control live USB debug and packet output. CommonCLI roles save the setting
|
||||
in `/com_prefs`, so it survives reboot; their first boot defaults to on. Full
|
||||
Companion starts off on a fresh installation.
|
||||
These commands are compiled into ordinary USB-loggable artifacts and every
|
||||
Full Companion. They control live USB debug and packet output. CommonCLI roles
|
||||
save the setting in `/com_prefs`, so it survives reboot; their first boot
|
||||
defaults to on. Full Companion and ordinary USB Companion start off on a fresh
|
||||
installation so diagnostics cannot corrupt framed traffic.
|
||||
|
||||
On Full Companion these lines belong to its text terminal, not `meshcli`'s
|
||||
Binary `get/set` parameter namespace. Open interface `00`, send
|
||||
@@ -3446,6 +3465,22 @@ Requires WiFi connected and the MQTT bridge running.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the UART used by the bridge (RS-232 only)
|
||||
**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 GPS.
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the channel used for bridging (ESPNow only)
|
||||
**Usage:**
|
||||
- `get bridge.channel`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Companion Protocol
|
||||
|
||||
- **Last Updated**: 2026-08-18
|
||||
- **Protocol Version**: 13 (`FIRMWARE_VER_CODE`)
|
||||
- **Last Updated**: 2026-08-26
|
||||
- **Protocol Version**: 14 (`FIRMWARE_VER_CODE`)
|
||||
|
||||
> The command and response catalogs track
|
||||
> `examples/companion_radio/MyMesh.cpp`. Applications should negotiate the
|
||||
@@ -60,6 +60,22 @@ MeshCore Companion devices expose a BLE service with the following UUIDs:
|
||||
- **RX Characteristic** (App -> Firmware): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||
- **TX Characteristic** (Firmware -> App): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||
|
||||
An nRF52 Full Companion also exposes a separate LoRa mOTA source service. It
|
||||
does not replace or multiplex the normal Companion UART service:
|
||||
|
||||
- **mOTA Service**: `14518FC2-7E7A-4D84-8CAE-6664B0234CF2`
|
||||
- **Device Request** (notify): `2BFAA1EE-7030-459A-B65A-E7CFD5B09735`
|
||||
- **Host Response** (write with response): `ACF38A51-DD58-4DCE-917F-0B1135E41B1A`
|
||||
|
||||
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](#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.
|
||||
|
||||
### Connection Steps
|
||||
|
||||
1. **Scan for Devices**
|
||||
@@ -170,7 +186,7 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi
|
||||
|
||||
## Commands
|
||||
|
||||
The first byte selects the command. This is the current protocol-v13 command
|
||||
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 |
|
||||
@@ -222,12 +238,14 @@ catalog; bytes `0x2C`-`0x31` are parked and `0x35` is unused.
|
||||
| `0x44` / `0x45` | `CMD_GET_RADIO_RXGAIN` / `CMD_SET_RADIO_RXGAIN` | Read or set the radio chip's boosted receive-gain mode. |
|
||||
| `0x46` / `0x47` | `CMD_GET_WIFI_POWER_SAVE` / `CMD_SET_WIFI_POWER_SAVE` | Read or set ESP32 Companion WiFi modem sleep. |
|
||||
| `0x48` / `0x49` | `CMD_GET_BLUETOOTH_NAME` / `CMD_SET_BLUETOOTH_NAME` | Read or set the independent Bluetooth device name. |
|
||||
| `0x4A` | `CMD_EXEC_LOCAL_OTA_CONTROL` | Run one bounded local TempRadio or OTA command on a Full Companion. |
|
||||
| `0x4B` | `CMD_BLE_MOTA_SOURCE` | Query, start, or stop an nRF52 Full Companion's Bluetooth-backed LoRa mOTA source. |
|
||||
|
||||
The sections below detail the most common frames. Refer to the source named
|
||||
above for command bodies that are not expanded here.
|
||||
|
||||
The additive hardware-setting commands `0x42`-`0x47` do not change any existing
|
||||
version-13 frame layout. Clients should probe the command they need and treat
|
||||
legacy frame layout. Clients should probe the command they need and treat
|
||||
`ERR_CODE_UNSUPPORTED_CMD` as feature absence.
|
||||
|
||||
Both gain-command pairs can be used over the normal binary Companion
|
||||
@@ -273,6 +291,79 @@ override and restores `MeshCore-<advert name>`. A successful SET replies with
|
||||
control characters, malformed UTF-8, or oversized values return
|
||||
`ERR_CODE_ILLEGAL_ARG`; a storage failure returns `ERR_CODE_BAD_STATE`.
|
||||
|
||||
### Bluetooth LoRa mOTA source
|
||||
|
||||
Protocol v14 lets a phone use an nRF52 Full Companion 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. Full Companion accepts only these local command families:
|
||||
|
||||
```text
|
||||
tempradio <freq_kHz>,<bw_kHz>,<sf>,<cr>,<minutes>
|
||||
normalradio
|
||||
ota ...
|
||||
```
|
||||
|
||||
`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 Full Companion 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):
|
||||
|
||||
```text
|
||||
00 action flags offered_le16 advertised_le16 source_packets_sent_le32
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```text
|
||||
device -> host: 'M' 'S' op args... xor(op || args)
|
||||
host -> device: 'm' 's' op status payload... xor(all prior bytes)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 1. App Start
|
||||
|
||||
**Purpose**: Initialize communication with the device. Must be sent first after connection.
|
||||
@@ -305,7 +396,7 @@ Byte 1: Highest companion protocol version understood by the app
|
||||
|
||||
**Example** (hex):
|
||||
```
|
||||
16 0D
|
||||
16 0E
|
||||
```
|
||||
|
||||
**Response**: `PACKET_DEVICE_INFO` (0x0D) with device information
|
||||
|
||||
@@ -12,8 +12,10 @@ firmware as an mOTA image.
|
||||
| BLE Binary Companion | Yes | Yes |
|
||||
| USB ASCII terminal | Yes | Yes |
|
||||
| Dedicated USB plaintext logging | Qualified native-USB ESP32-S3 profiles | Yes |
|
||||
| Host-backed LoRa mOTA source | WiFi TCP 5001 | Exclusive USB mode |
|
||||
| Host-backed LoRa mOTA source | WiFi TCP 5001 | Exclusive USB mode or encrypted BLE |
|
||||
| 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 |
|
||||
| LoRa self-update | No | No |
|
||||
|
||||
## Build and install
|
||||
@@ -63,8 +65,11 @@ 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,
|
||||
and USB-only packet-logging release artifacts whenever the exact board supports
|
||||
the combined profile. Dual-CDC builds separate framed traffic and logs;
|
||||
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. Dual-CDC builds separate framed traffic and logs;
|
||||
single-TTY builds make those modes mutually exclusive. In WebConfig, use the
|
||||
**FEM RX boost** switch. From the text terminal (USB, or TCP 5002 on ESP32), use:
|
||||
|
||||
@@ -85,6 +90,22 @@ 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:
|
||||
|
||||
```text
|
||||
get display.rotation
|
||||
set display.rotation 90
|
||||
set display.rotation 180
|
||||
set display.rotation 270
|
||||
set display.rotation 0
|
||||
```
|
||||
|
||||
`0` resets the screen to that board's compiled default orientation.
|
||||
|
||||
Heltec E290 and T190 use one `usb_ble` Companion artifact for simultaneous USB
|
||||
and BLE rather than publishing separate USB-only and BLE-only images.
|
||||
|
||||
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:
|
||||
|
||||
@@ -219,6 +240,7 @@ itself.
|
||||
| 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
|
||||
@@ -356,6 +378,10 @@ only one may own the terminal at a time. Entering USB terminal mode closes an
|
||||
active TCP terminal session. 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
|
||||
@@ -459,6 +485,50 @@ While mOTA mode owns USB:
|
||||
|
||||
No manual mode token or modified `motatool` build is required.
|
||||
|
||||
## nRF52 Bluetooth mOTA source
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
python3 tools/ble_mota/ble_mota_seeder.py \
|
||||
--device MeshCore-MyCompanion \
|
||||
--dir ./motas \
|
||||
--local 'tempradio 909.950,250,5,5,120'
|
||||
```
|
||||
|
||||
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](./companion_protocol.md#bluetooth-lora-mota-source).
|
||||
|
||||
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.
|
||||
|
||||
## Serve mOTA images manually
|
||||
|
||||
First put the destination, required relays, controller, and source on the same
|
||||
@@ -514,6 +584,10 @@ 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:
|
||||
|
||||
```text
|
||||
|
||||
+23
-5
@@ -140,8 +140,12 @@ from the published firmware assets.
|
||||
| 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, Bluetooth, USB, Wi-Fi, serial, or Ethernet transports.
|
||||
Repeaters may offer standard, ESP-NOW bridge, RS-232 bridge, Ethernet, or MQTT
|
||||
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 `set bridge.enabled on` after configuring `bridge.uart` and
|
||||
`bridge.baud`. The Wio-E5 remains the capacity exception and offers a separate
|
||||
RS-232 image. Repeaters may also offer separate ESP-NOW, Ethernet, or MQTT
|
||||
observer modes.
|
||||
|
||||
## FULL versus standard
|
||||
@@ -169,9 +173,23 @@ available as an override. A saved SSID switches to the normal indefinite
|
||||
reconnect behavior instead.
|
||||
|
||||
Full Companion profiles use one binary for USB, BLE, ordinary Wi-Fi on ESP32,
|
||||
source-only LoRa OTA, and optional USB packet logging. The picker therefore
|
||||
omits separate attached-transport and USB-logging choices whenever the exact
|
||||
Full recipe exists. Fresh installs default to logging off.
|
||||
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.
|
||||
|
||||
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 publish a combined USB + BLE Companion. 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.
|
||||
|
||||
Ordinary non-OTA roles also use one artifact for normal operation and USB
|
||||
logging. Select the saved mode with `set usb.logging off|on`; no `-logging-`
|
||||
artifact is emitted. KISS, BLE-only Companion, and constrained LoRa-OTA
|
||||
receiver images retain their protocol/partition contracts and do not inherit
|
||||
plaintext USB logging.
|
||||
|
||||
Dual-CDC nRF52 and qualified native-USB ESP32-S3 builds keep Binary Companion
|
||||
on interface `00`; `set usb.logging on reboot` adds plaintext interface `02`.
|
||||
|
||||
@@ -21,10 +21,10 @@ example and is disabled unless the endpoint is started with `--allow-reboot`.
|
||||
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. The specialized Wio-E5
|
||||
RS232 bridge image omits it because the RS232 application already fills its
|
||||
fixed 240 KiB application partition; use the normal Wio-E5 repeater image when
|
||||
the USB/MQTT host service is needed.
|
||||
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.
|
||||
|
||||
## Run alongside meshcoretomqtt
|
||||
|
||||
|
||||
+17
-2
@@ -90,8 +90,9 @@ environmental sensors while retaining board-native features such as displays, bu
|
||||
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 add 4,808
|
||||
bytes over the otherwise reduced image. The RAK3401 OTA repeater retains RAK12501 support in sensor slot A;
|
||||
slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The RAK4631 OTA repeater retains GPS except for
|
||||
its Serial1 RS232 bridge, which uses the same UART. Selected nRF52 boards with matched external
|
||||
slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. The RAK4631 OTA repeater retains GPS and defaults
|
||||
its runtime RS-232 bridge to Serial2; Serial1 and GPS use the same UART and must not be enabled together.
|
||||
Selected nRF52 boards with matched external
|
||||
QSPI application and bootloader support can instead make the normal full-sensor
|
||||
repeater install-capable; those targets do not need to reserve internal flash
|
||||
for the downloaded container. SolarXiao 30S and 33S use this matched external-QSPI
|
||||
@@ -156,6 +157,10 @@ An nRF52 `companion_radio_full` starts in USB Binary mode. Use
|
||||
`+++MESHCORE-TERM-STOP`. When `motatool serve --serial` opens the port, its
|
||||
automatic `ota folder on` command selects exclusive mOTA mode; stopping the
|
||||
tool or disconnecting resets USB to Binary. BLE remains available throughout.
|
||||
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](./companion_radio_full.md#nrf52-bluetooth-mota-source).
|
||||
|
||||
For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:
|
||||
|
||||
@@ -356,6 +361,16 @@ Replace `/dev/ttyACM0` with the USB serial device of the source companion select
|
||||
`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:
|
||||
|
||||
```bash
|
||||
python3 tools/ble_mota/ble_mota_seeder.py \
|
||||
--device MeshCore-MyCompanion \
|
||||
--dir ./motas \
|
||||
--local 'tempradio 909.950,250,5,5,120'
|
||||
```
|
||||
|
||||
Leave this command running until the destination finishes downloading.
|
||||
|
||||
### 3. Find and download the update
|
||||
|
||||
+22
-10
@@ -133,8 +133,8 @@ matched external QSPI staging, so their ordinary full-sensor repeater is already
|
||||
redundant lean sibling is generated. Integrated GPS and other
|
||||
board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The RAK3401
|
||||
OTA repeater also retains RAK12501 GPS support; install that module in sensor slot A because slot D conflicts
|
||||
with the RAK13302 radio's BUSY/DIO1 lines. RAK4631 OTA repeaters retain GPS except for the Serial1 RS232
|
||||
bridge, whose bridge and GPS would contend for the same UART.
|
||||
with the RAK13302 radio's BUSY/DIO1 lines. RAK4631 OTA repeaters retain GPS and default the runtime RS-232
|
||||
bridge to UART 2; do not enable GPS and an explicitly selected UART 1 bridge at the same time.
|
||||
ESP32 siblings retain the compact browser WiFi updater and use the full
|
||||
254-entry neighbor table. RP2040 and STM32 targets are not offered because
|
||||
those platforms do not yet have a safe bootloader/apply path.
|
||||
@@ -161,8 +161,9 @@ 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;
|
||||
the old separate standard-logging and non-MQTT FULL twins are skipped for
|
||||
covered ESP32 roles. MQTT observers and ESP-NOW bridges are emitted only with
|
||||
ordinary non-OTA roles compile runtime USB logging into their canonical image,
|
||||
so separate standard-logging artifacts are not emitted. 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.
|
||||
@@ -686,13 +687,15 @@ verify everything). The serve side (`OtaManager`) keeps a lightweight registry o
|
||||
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 same host-folder link is also a **pull destination** (the reverse direction): `ota pull <mid8> folder`
|
||||
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.
|
||||
The phone-oriented BLE link is deliberately source-only and does not register
|
||||
a folder destination.
|
||||
|
||||
### 10.1 The `MotaSource` abstraction (`OtaSource.h`)
|
||||
|
||||
@@ -717,7 +720,8 @@ blocks) and streams payload blocks from the source on demand; proofs are generat
|
||||
### 10.2 The `mota-seeder` transport (`MotaSeederProto.h`)
|
||||
|
||||
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) or, on an ESP32 WiFi companion or FULL ESP32 role, over **WiFi (TCP)**. 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`](https://github.com/vk496/motatool) (`motatool serve --serial <port>` /
|
||||
`--tcp <host[:port]>`, which also builds + verifies + inspects `.mota`). The device only emits request frames *while
|
||||
@@ -761,6 +765,10 @@ deployment runner temporarily uses `0.3` on managed relays.
|
||||
work**: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and `mota-seeder`
|
||||
request/response transport. 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
|
||||
@@ -784,10 +792,14 @@ disappearing. Operators should split a large chain or use a higher-capacity/SD s
|
||||
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`). A future framed link such as **BLE GATT** (an
|
||||
Android phone relaying a folder) could carry the same ops with no magic/checksum at all - a request
|
||||
characteristic write delivers `op + args`, the reply notifies `status + payload`. `motatool` reflects this
|
||||
split: a transport-free `SeederCore` (the catalog logic) under a swappable framing/transport layer.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ the GPS in sensor slot A. Slot D's reset/PPS lines conflict with the RAK13302
|
||||
radio's BUSY/DIO1 lines.
|
||||
|
||||
The RAK4631 internal-flash OTA repeater likewise retains GPS. A RAK12501 can
|
||||
use sensor slot A or D; a RAK12500 can use slot A or C. The RAK4631 Serial1
|
||||
RS232 OTA bridge is the exception because the bridge owns the GPS UART. The
|
||||
Serial2 RS232 OTA bridge retains GPS on Serial1.
|
||||
use sensor slot A or D; a RAK12500 can use slot A or C. Its runtime RS-232
|
||||
bridge defaults to Serial2 so GPS can retain Serial1. If you explicitly select
|
||||
Serial1 with `set bridge.uart 1`, turn GPS off before enabling the bridge.
|
||||
|
||||
Selected nRF52 repeaters with dedicated external QSPI can now stage the
|
||||
complete package off-chip, so their normal full-sensor repeater build can
|
||||
@@ -394,14 +394,17 @@ 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**, or over **WiFi** if it is
|
||||
an ESP32 WiFi companion or FULL ESP32 node:
|
||||
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.
|
||||
|
||||
@@ -107,13 +107,21 @@ GPS idle behavior; it does not change LoRa RXPS or WiFi modem sleep.
|
||||
get usb.logging
|
||||
set usb.logging {on|off} [reboot]
|
||||
```
|
||||
Shows or changes persistent live USB debug and packet output in a Companion
|
||||
logging artifact or Full Companion. Full starts off on a fresh install.
|
||||
Shows or changes persistent live USB debug and packet output in an ordinary
|
||||
USB-loggable Companion or Full Companion. USB Companion and Full start off on
|
||||
a fresh install with logging disabled to protect framed traffic.
|
||||
Dual-CDC Full changes its interface count after a reboot and keeps Companion on
|
||||
interface `00`. Single-TTY Full needs no reboot: logging uses the active text
|
||||
terminal, which continues accepting `set usb.logging off` and automatically
|
||||
restores Binary Companion after that reply.
|
||||
|
||||
```
|
||||
reboot
|
||||
```
|
||||
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
|
||||
get radio.rxps.config
|
||||
|
||||
@@ -71,5 +71,10 @@ public:
|
||||
int channel_idx = -1,
|
||||
const char* channel_name = nullptr) = 0;
|
||||
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
||||
virtual bool supportsDisplayRotation() const { return false; }
|
||||
virtual bool setDisplayRotationDegrees(uint16_t degrees) {
|
||||
(void)degrees;
|
||||
return false;
|
||||
}
|
||||
virtual void loop() = 0;
|
||||
};
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
#ifndef COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
#define COMPANION_FEATURE_USB_MOTA_SOURCE 0
|
||||
#endif
|
||||
#ifndef COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
#define COMPANION_FEATURE_BLE_MOTA_SOURCE 0
|
||||
#endif
|
||||
#ifndef COMPANION_FEATURE_DEDICATED_USB_LOGGING
|
||||
#define COMPANION_FEATURE_DEDICATED_USB_LOGGING 0
|
||||
#endif
|
||||
@@ -70,6 +73,12 @@
|
||||
#error "COMPANION_FEATURE_USB_MOTA_SOURCE requires nRF52 USB folder seeding"
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE \
|
||||
&& !(defined(COMPANION_RADIO_FULL) && defined(NRF52_PLATFORM) \
|
||||
&& defined(BLE_PIN_CODE) && defined(ENABLE_OTA))
|
||||
#error "COMPANION_FEATURE_BLE_MOTA_SOURCE requires an nRF52 Full Companion with BLE and OTA"
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS && !defined(ESP32_PLATFORM)
|
||||
#error "COMPANION_FEATURE_MEMORY_DIAGNOSTICS requires ESP32"
|
||||
#endif
|
||||
|
||||
@@ -355,6 +355,11 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d
|
||||
if (file.available() >= (int)sizeof(_prefs.bluetooth_name)) {
|
||||
file.read((uint8_t *)_prefs.bluetooth_name,
|
||||
sizeof(_prefs.bluetooth_name)); // 141
|
||||
if (file.available()
|
||||
>= (int)sizeof(_prefs.display_rotation_degrees)) {
|
||||
file.read((uint8_t *)&_prefs.display_rotation_degrees,
|
||||
sizeof(_prefs.display_rotation_degrees));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,6 +439,10 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
|
||||
== sizeof(_prefs.usb_logging_enabled); // 140
|
||||
success = success && file.write((uint8_t *)_prefs.bluetooth_name,
|
||||
sizeof(_prefs.bluetooth_name)) == sizeof(_prefs.bluetooth_name); // 141
|
||||
success = success && file.write(
|
||||
(uint8_t *)&_prefs.display_rotation_degrees,
|
||||
sizeof(_prefs.display_rotation_degrees))
|
||||
== sizeof(_prefs.display_rotation_degrees);
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
success = file.commit(success);
|
||||
|
||||
@@ -1386,7 +1386,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_clock_sync(radio, _clock_sync_millis, rtc, _clock_sync_acl, sensors,
|
||||
_prefs.airtime_factor),
|
||||
#endif
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
|
||||
_serial(NULL), _mota_source_control(NULL),
|
||||
telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
|
||||
_iter_started = false;
|
||||
_cli_rescue = false;
|
||||
#ifdef ENABLE_USB_INTERFACE
|
||||
@@ -1412,9 +1413,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
command_radio_cr = 0;
|
||||
command_radio_repeat = 0;
|
||||
command_radio_apply_deadline = 0;
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
_usb_logging_reboot_at = 0;
|
||||
#endif
|
||||
_scheduled_reboot_at = 0;
|
||||
#if COMPANION_FEATURE_TEMP_RADIO
|
||||
_temp_radio_set_at = 0;
|
||||
_temp_radio_revert_at = 0;
|
||||
@@ -1477,10 +1476,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.powersaving_policy_version = 0;
|
||||
_prefs.wifi_enabled = 1;
|
||||
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
// Keep Full Companion's primary stream exclusively framed on a fresh
|
||||
// install. Dual-CDC builds can add a diagnostics port; single-TTY builds
|
||||
// switch the primary stream into the text terminal before emitting logs.
|
||||
_prefs.display_rotation_degrees = 0;
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
// Keep a USB Companion's primary stream exclusively framed on a fresh
|
||||
// install. Full dual-CDC builds can add a diagnostics port; single-TTY
|
||||
// builds switch the primary stream into the text terminal before logs.
|
||||
_prefs.usb_logging_enabled = 0;
|
||||
#else
|
||||
_prefs.usb_logging_enabled = 1;
|
||||
@@ -1567,6 +1567,14 @@ void MyMesh::begin(bool has_display, bool radio_available) {
|
||||
if (bluetooth_name_repaired) {
|
||||
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
|
||||
}
|
||||
const bool display_rotation_repaired =
|
||||
_prefs.display_rotation_degrees != 0
|
||||
&& _prefs.display_rotation_degrees != 90
|
||||
&& _prefs.display_rotation_degrees != 180
|
||||
&& _prefs.display_rotation_degrees != 270;
|
||||
if (display_rotation_repaired) {
|
||||
_prefs.display_rotation_degrees = 0;
|
||||
}
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
||||
@@ -1601,7 +1609,8 @@ void MyMesh::begin(bool has_display, bool radio_available) {
|
||||
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
|
||||
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
|
||||
&_prefs.rx_ps_sleep_us);
|
||||
if (power_saving_default_migrated || bluetooth_name_repaired) {
|
||||
if (power_saving_default_migrated || bluetooth_name_repaired
|
||||
|| display_rotation_repaired) {
|
||||
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
}
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
@@ -1864,6 +1873,57 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
if (!command || !reply || reply_size == 0) return false;
|
||||
while (*command == ' ') command++;
|
||||
|
||||
if (strcmp(command, "get display.rotation") == 0) {
|
||||
if (_ui == NULL || !_ui->supportsDisplayRotation()) {
|
||||
snprintf(reply, reply_size, "Error: display rotation is unsupported");
|
||||
} else if (_prefs.display_rotation_degrees == 0) {
|
||||
snprintf(reply, reply_size, "display.rotation default");
|
||||
} else {
|
||||
snprintf(reply, reply_size, "display.rotation %u",
|
||||
(unsigned)_prefs.display_rotation_degrees);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strncmp(command, "set display.rotation", 20) == 0
|
||||
&& (command[20] == 0 || command[20] == ' '
|
||||
|| command[20] == '\t')) {
|
||||
const char* value = command + 20;
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
char* end = NULL;
|
||||
const unsigned long degrees = strtoul(value, &end, 10);
|
||||
while (end != NULL && (*end == ' ' || *end == '\t')) end++;
|
||||
const bool valid = value[0] != 0 && end != NULL && *end == 0
|
||||
&& (degrees == 0 || degrees == 90 || degrees == 180
|
||||
|| degrees == 270);
|
||||
if (!valid) {
|
||||
snprintf(reply, reply_size,
|
||||
"Error: use set display.rotation <0|90|180|270>");
|
||||
} else if (_ui == NULL || !_ui->supportsDisplayRotation()) {
|
||||
snprintf(reply, reply_size, "Error: display rotation is unsupported");
|
||||
} else {
|
||||
const uint16_t previous = _prefs.display_rotation_degrees;
|
||||
if (!_ui->setDisplayRotationDegrees((uint16_t)degrees)) {
|
||||
snprintf(reply, reply_size, "Error: display rotation failed");
|
||||
} else {
|
||||
_prefs.display_rotation_degrees = (uint16_t)degrees;
|
||||
if (!savePrefs()) {
|
||||
_prefs.display_rotation_degrees = previous;
|
||||
_ui->setDisplayRotationDegrees(previous);
|
||||
snprintf(reply, reply_size,
|
||||
"Error: display rotation changed but save failed");
|
||||
} else if (degrees == 0) {
|
||||
snprintf(reply, reply_size,
|
||||
"OK - display rotation reset to board default");
|
||||
} else {
|
||||
snprintf(reply, reply_size, "OK - display rotation %lu",
|
||||
degrees);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get bluetooth.name") == 0
|
||||
|| strcmp(command, "get ble.name") == 0) {
|
||||
formatBluetoothNameStatus(reply, reply_size);
|
||||
@@ -2074,11 +2134,10 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
|
||||
if (strncmp(command, "tempradio ", 10) == 0) {
|
||||
float freq = 0.0f, bw = 0.0f;
|
||||
int sf = 0, cr = 0;
|
||||
unsigned long timeout_mins = 0;
|
||||
char extra = 0;
|
||||
if (sscanf(command + 10, "%f,%f,%d,%d,%lu%c",
|
||||
&freq, &bw, &sf, &cr, &timeout_mins, &extra) != 5
|
||||
uint8_t sf = 0, cr = 0;
|
||||
uint32_t timeout_mins = 0;
|
||||
if (!mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
command + 10, freq, bw, sf, cr, timeout_mins)
|
||||
|| !isfinite(freq) || !isfinite(bw)
|
||||
|| freq < 150.0f || freq > 2500.0f
|
||||
|| !isFullCompanionBandwidth(bw)
|
||||
@@ -2088,8 +2147,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
"ERR usage: tempradio freq,bw,sf,cr,minutes (minutes 1-10080)");
|
||||
return true;
|
||||
}
|
||||
scheduleTempRadio(freq, bw, (uint8_t)sf, (uint8_t)cr,
|
||||
(uint32_t)timeout_mins, reply, reply_size);
|
||||
scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2448,17 +2506,16 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
}
|
||||
if (strcmp(key, "radio") == 0) {
|
||||
float freq, bw;
|
||||
int sf, cr;
|
||||
char extra;
|
||||
if (sscanf(value, "%f,%f,%d,%d%c", &freq, &bw, &sf, &cr, &extra) != 4
|
||||
uint8_t sf, cr;
|
||||
if (!mesh::cli::parseRadioTupleStrict(value, freq, bw, sf, cr)
|
||||
|| !isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f
|
||||
|| bw < 7.0f || bw > 500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8) {
|
||||
strcpy(reply, "Error: radio must be freq,bw,sf,cr");
|
||||
} else {
|
||||
_prefs.freq = freq;
|
||||
_prefs.bw = bw;
|
||||
_prefs.sf = static_cast<uint8_t>(sf);
|
||||
_prefs.cr = static_cast<uint8_t>(cr);
|
||||
_prefs.sf = sf;
|
||||
_prefs.cr = cr;
|
||||
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
|
||||
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
|
||||
&_prefs.rx_ps_sleep_us);
|
||||
@@ -3825,6 +3882,85 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
writeOKFrame();
|
||||
}
|
||||
}
|
||||
} else if (cmd_frame[0]
|
||||
== mesh::companion::CMD_EXEC_LOCAL_OTA_CONTROL) {
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
const size_t command_len = len - 1;
|
||||
if (!mesh::companion::isBleOtaControlCommandAllowed(
|
||||
&cmd_frame[1], command_len)) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
} else {
|
||||
char command[MAX_FRAME_SIZE] = {0};
|
||||
memcpy(command, &cmd_frame[1], command_len);
|
||||
char reply[MAX_FRAME_SIZE] = {0};
|
||||
if (!handleLocalControlCommand(command, reply, sizeof(reply))) {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
} else {
|
||||
out_frame[0] = RESP_CODE_OK;
|
||||
const size_t reply_len = strnlen(reply, MAX_FRAME_SIZE - 2);
|
||||
out_frame[1] = static_cast<uint8_t>(reply_len);
|
||||
memcpy(&out_frame[2], reply, reply_len);
|
||||
_serial->writeFrame(out_frame, 2 + reply_len);
|
||||
}
|
||||
}
|
||||
#else
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
#endif
|
||||
} else if (cmd_frame[0] == mesh::companion::CMD_BLE_MOTA_SOURCE) {
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
if (len != 2) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
} else if (_mota_source_control == NULL) {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
} else {
|
||||
const mesh::companion::MotaSourceAction action =
|
||||
static_cast<mesh::companion::MotaSourceAction>(cmd_frame[1]);
|
||||
char control_reply[96] = {0};
|
||||
bool action_ok = true;
|
||||
if (action == mesh::companion::MotaSourceAction::Start) {
|
||||
action_ok = _mota_source_control->start(control_reply,
|
||||
sizeof(control_reply));
|
||||
} else if (action == mesh::companion::MotaSourceAction::Stop) {
|
||||
action_ok = _mota_source_control->stop(control_reply,
|
||||
sizeof(control_reply));
|
||||
} else if (action != mesh::companion::MotaSourceAction::Status) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!action_ok) {
|
||||
writeErrFrame(ERR_CODE_BAD_STATE);
|
||||
} else {
|
||||
const mesh::companion::MotaSourceStatus status =
|
||||
_mota_source_control->status();
|
||||
uint8_t flags = 0;
|
||||
if (status.channel_ready) {
|
||||
flags |= mesh::companion::MOTA_SOURCE_FLAG_CHANNEL_READY;
|
||||
}
|
||||
if (status.attached) {
|
||||
flags |= mesh::companion::MOTA_SOURCE_FLAG_ATTACHED;
|
||||
}
|
||||
if (status.another_link_active) {
|
||||
flags |=
|
||||
mesh::companion::MOTA_SOURCE_FLAG_ANOTHER_LINK_ACTIVE;
|
||||
}
|
||||
out_frame[0] = RESP_CODE_OK;
|
||||
out_frame[1] = cmd_frame[1];
|
||||
out_frame[2] = flags;
|
||||
out_frame[3] = static_cast<uint8_t>(status.offered & 0xFF);
|
||||
out_frame[4] = static_cast<uint8_t>(status.offered >> 8);
|
||||
out_frame[5] = static_cast<uint8_t>(status.advertised & 0xFF);
|
||||
out_frame[6] = static_cast<uint8_t>(status.advertised >> 8);
|
||||
out_frame[7] = static_cast<uint8_t>(status.packets_sent & 0xFF);
|
||||
out_frame[8] = static_cast<uint8_t>((status.packets_sent >> 8) & 0xFF);
|
||||
out_frame[9] = static_cast<uint8_t>((status.packets_sent >> 16) & 0xFF);
|
||||
out_frame[10] = static_cast<uint8_t>((status.packets_sent >> 24) & 0xFF);
|
||||
_serial->writeFrame(out_frame, 11);
|
||||
}
|
||||
}
|
||||
#else
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
#endif
|
||||
} else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) {
|
||||
// FUTURE use: uint8_t reserved = cmd_frame[1];
|
||||
uint8_t *pub_key = &cmd_frame[2];
|
||||
@@ -5313,7 +5449,7 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().printf(
|
||||
" OK - USB logging %s (saved); rebooting to change USB interfaces\r\n",
|
||||
enabled ? "on" : "off");
|
||||
_usb_logging_reboot_at = futureMillis(1000);
|
||||
_scheduled_reboot_at = futureMillis(1000);
|
||||
} else {
|
||||
terminalOutput().printf(
|
||||
" OK - USB logging %s (saved); reboot required to change USB interfaces\r\n",
|
||||
@@ -5413,11 +5549,16 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
} else {
|
||||
terminalOutput().printf(" ERROR: unknown setting: %s\r\n", config);
|
||||
}
|
||||
} else if (strcmp(command, "reboot") == 0) {
|
||||
terminalOutput().print(" OK - rebooting in 1 second\r\n");
|
||||
_scheduled_reboot_at = futureMillis(1000);
|
||||
} else if (strcmp(command, "ver") == 0) {
|
||||
terminalOutput().printf("Companion %s (protocol %u, build %s)\r\n",
|
||||
FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE);
|
||||
} else if (strcmp(command, "help") == 0) {
|
||||
terminalOutput().print("Commands:\r\n");
|
||||
terminalOutput().print(" get display.rotation\r\n");
|
||||
terminalOutput().print(" set display.rotation <0|90|180|270>\r\n");
|
||||
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
|
||||
terminalOutput().print(" get bluetooth.name\r\n");
|
||||
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
|
||||
@@ -5476,6 +5617,7 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().print(" ota {status|ls|announce|folder|config|...}\r\n");
|
||||
#endif
|
||||
#endif
|
||||
terminalOutput().print(" reboot\r\n");
|
||||
terminalOutput().print(" ver\r\n");
|
||||
if (_terminal_mode) {
|
||||
terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n");
|
||||
@@ -5700,14 +5842,12 @@ void MyMesh::checkSerialInterface() {
|
||||
}
|
||||
|
||||
void MyMesh::loop() {
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (_usb_logging_reboot_at != 0
|
||||
&& millisHasNowPassed(_usb_logging_reboot_at)) {
|
||||
_usb_logging_reboot_at = 0;
|
||||
if (_scheduled_reboot_at != 0
|
||||
&& millisHasNowPassed(_scheduled_reboot_at)) {
|
||||
_scheduled_reboot_at = 0;
|
||||
board.reboot();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if COMPANION_FEATURE_TEMP_RADIO
|
||||
serviceTempRadio();
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "CompanionFeatures.h"
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
#define FIRMWARE_VER_CODE 13
|
||||
#define FIRMWARE_VER_CODE 14
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||
@@ -40,6 +40,7 @@
|
||||
#include <RTClib.h>
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/BaseSerialInterface.h>
|
||||
#include <helpers/CompanionMotaControl.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/LogicalMessageCache.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
@@ -137,6 +138,9 @@ public:
|
||||
CompanionNodePrefs *getNodePrefs();
|
||||
uint32_t getBLEPin();
|
||||
int getOfflineQueueCapacity() const;
|
||||
void setMotaSourceControl(mesh::companion::MotaSourceControl* control) {
|
||||
_mota_source_control = control;
|
||||
}
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
||||
void serviceMQTT(const char* wifi_ssid, const char* wifi_password);
|
||||
@@ -397,6 +401,7 @@ private:
|
||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||
uint32_t pending_req; // pending _BINARY_REQ
|
||||
BaseSerialInterface *_serial;
|
||||
mesh::companion::MotaSourceControl* _mota_source_control;
|
||||
AbstractUITask* _ui;
|
||||
|
||||
ContactsIterator _iter;
|
||||
@@ -437,9 +442,9 @@ private:
|
||||
uint8_t command_radio_cr;
|
||||
uint8_t command_radio_repeat;
|
||||
unsigned long command_radio_apply_deadline;
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
unsigned long _usb_logging_reboot_at;
|
||||
#endif
|
||||
// Deferred so USB/TCP terminals can transmit the acknowledgement before
|
||||
// the transport disappears. Also used by USB interface changes.
|
||||
unsigned long _scheduled_reboot_at;
|
||||
#if COMPANION_FEATURE_TEMP_RADIO
|
||||
unsigned long _temp_radio_set_at;
|
||||
unsigned long _temp_radio_revert_at;
|
||||
|
||||
@@ -57,6 +57,7 @@ struct CompanionNodePrefs { // persisted to file
|
||||
uint8_t usb_logging_enabled; // live USB packet/debug output
|
||||
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE];
|
||||
// exact BLE name; empty uses BLE_NAME_PREFIX + node_name
|
||||
uint16_t display_rotation_degrees; // 0=board default; otherwise 90/180/270
|
||||
|
||||
// Keep the upstream repeat API while retaining the existing binary prefs
|
||||
// layout used by this branch.
|
||||
|
||||
@@ -147,6 +147,116 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
#endif
|
||||
);
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
#include <helpers/ota/MotaSourceSerial.h>
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
|
||||
class Nrf52BleMotaSourceControl : public mesh::companion::MotaSourceControl {
|
||||
public:
|
||||
Nrf52BleMotaSourceControl()
|
||||
: _source(bluetooth_interface.motaStream(),
|
||||
mesh::ota::MotaStreamWritePolicy::NoFlush, 3000),
|
||||
_packets_sent_at_start(0), _last_packets_sent(0) {}
|
||||
|
||||
bool start(char* reply, size_t reply_size) override {
|
||||
if (!reply || reply_size == 0) return false;
|
||||
if (!bluetooth_interface.isMotaChannelReady()) {
|
||||
snprintf(reply, reply_size,
|
||||
"ERR subscribe to the Bluetooth mOTA request characteristic first");
|
||||
return false;
|
||||
}
|
||||
|
||||
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
||||
if (context.folder_active
|
||||
&& context.folderLink() != mesh::ota::OtaContext::FOLDER_LINK_BLE) {
|
||||
snprintf(reply, reply_size, "ERR mOTA source already uses %s",
|
||||
mesh::ota::OtaContext::folderLinkName(context.folderLink()));
|
||||
return false;
|
||||
}
|
||||
|
||||
_packets_sent_at_start = context.manager.packetsSent();
|
||||
_last_packets_sent = 0;
|
||||
bluetooth_interface.setMotaStreamActive(true);
|
||||
if (!context.attach_folder_source(
|
||||
&_source, mesh::ota::OtaContext::FOLDER_LINK_BLE, "ble",
|
||||
reply, reply_size)) {
|
||||
bluetooth_interface.setMotaStreamActive(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
context.manager.announce();
|
||||
mesh::usbLoggingPort().println("Bluetooth mOTA source attached");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool stop(char* reply, size_t reply_size) override {
|
||||
if (!reply || reply_size == 0) return false;
|
||||
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
||||
bluetooth_interface.setMotaStreamActive(false);
|
||||
if (context.folder_active
|
||||
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE) {
|
||||
context.detach_folder();
|
||||
context.manager.announce();
|
||||
mesh::usbLoggingPort().println("Bluetooth mOTA source detached");
|
||||
}
|
||||
_last_packets_sent = context.manager.packetsSent()
|
||||
- _packets_sent_at_start;
|
||||
snprintf(reply, reply_size, "OK Bluetooth mOTA source stopped");
|
||||
return true;
|
||||
}
|
||||
|
||||
mesh::companion::MotaSourceStatus status() const override {
|
||||
const mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
||||
mesh::companion::MotaSourceStatus result;
|
||||
result.channel_ready = bluetooth_interface.isMotaChannelReady();
|
||||
result.attached = context.folder_active
|
||||
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE
|
||||
&& bluetooth_interface.isMotaStreamActive();
|
||||
result.another_link_active = context.folder_active
|
||||
&& context.folderLink() != mesh::ota::OtaContext::FOLDER_LINK_BLE;
|
||||
if (result.attached) {
|
||||
context.folderSourceStats(result.offered, result.advertised);
|
||||
result.packets_sent = context.manager.packetsSent()
|
||||
- _packets_sent_at_start;
|
||||
} else {
|
||||
result.packets_sent = _last_packets_sent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
||||
const bool owns_folder = context.folder_active
|
||||
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE;
|
||||
if (!owns_folder) {
|
||||
if (bluetooth_interface.isMotaStreamActive()) {
|
||||
bluetooth_interface.setMotaStreamActive(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (bluetooth_interface.isMotaChannelReady()
|
||||
&& bluetooth_interface.isMotaStreamActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bluetooth_interface.setMotaStreamActive(false);
|
||||
context.detach_folder();
|
||||
context.manager.announce();
|
||||
_last_packets_sent = context.manager.packetsSent()
|
||||
- _packets_sent_at_start;
|
||||
mesh::usbLoggingPort().println(
|
||||
"Bluetooth mOTA source disconnected and was detached");
|
||||
}
|
||||
|
||||
private:
|
||||
mesh::ota::SerialMotaSource _source;
|
||||
uint32_t _packets_sent_at_start;
|
||||
uint32_t _last_packets_sent;
|
||||
};
|
||||
|
||||
static Nrf52BleMotaSourceControl ble_mota_source_control;
|
||||
#endif
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
#ifdef RECOVERABLE_EXTERNAL_RADIO
|
||||
@@ -463,7 +573,7 @@ static void serviceUsbTerminal() {
|
||||
// logs. `set usb.logging off` remains available here and returns this TTY to
|
||||
// Binary Companion after its command reply, even on USB-UART bridges that
|
||||
// cannot detect a host disconnect.
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (!mesh::hasDedicatedUsbLoggingPort()) {
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
if (!the_mesh.isTerminalMode()) {
|
||||
@@ -522,7 +632,7 @@ static void serviceUsbTerminal() {
|
||||
Serial.print("\r\n");
|
||||
the_mesh.handleTerminalCommand(usb_terminal_line);
|
||||
clearUsbTerminalLine();
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (usb_logging_terminal_mode
|
||||
&& !mesh::isUsbLoggingEnabled()) {
|
||||
leaveUsbTerminalMode(true);
|
||||
@@ -1188,6 +1298,10 @@ void setup() {
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
the_mesh.setMotaSourceControl(&ble_mota_source_control);
|
||||
#endif
|
||||
|
||||
// nRF52 cannot decide whether to add its optional logging CDC interface
|
||||
// until the saved Companion preferences above are available. ESP32 already
|
||||
// fixed its descriptor from the early NVS mirror, so this is harmless there.
|
||||
@@ -1279,7 +1393,7 @@ void setup() {
|
||||
usb_serial_interface.setConnectedCheck([]() { return (bool)Serial; });
|
||||
#endif
|
||||
interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface);
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (!mesh::hasDedicatedUsbLoggingPort()
|
||||
&& mesh::isUsbLoggingEnabled()) {
|
||||
// Apply a saved single-TTY logging preference before the dispatcher can
|
||||
@@ -1344,6 +1458,9 @@ void loop() {
|
||||
serviceUsbTerminal();
|
||||
#endif
|
||||
interface_manager.loop();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
ble_mota_source_control.loop();
|
||||
#endif
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
|
||||
|
||||
@@ -854,6 +854,9 @@ public:
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
|
||||
@@ -107,6 +107,12 @@ public:
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
int getPreviewCount() const;
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool supportsDisplayRotation() const override {
|
||||
return _display != NULL && _display->supportsRotation();
|
||||
}
|
||||
bool setDisplayRotationDegrees(uint16_t degrees) override {
|
||||
return _display != NULL && _display->setRotationDegrees(degrees);
|
||||
}
|
||||
bool isButtonPressed() const;
|
||||
|
||||
bool isBuzzerQuiet() {
|
||||
|
||||
@@ -46,6 +46,9 @@ static const uint8_t meshcore_logo [] PROGMEM = {
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
clearMsgPreview();
|
||||
_node_prefs = node_prefs;
|
||||
|
||||
@@ -74,6 +74,12 @@ public:
|
||||
void begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs);
|
||||
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool supportsDisplayRotation() const override {
|
||||
return _display != NULL && _display->supportsRotation();
|
||||
}
|
||||
bool setDisplayRotationDegrees(uint16_t degrees) override {
|
||||
return _display != NULL && _display->setRotationDegrees(degrees);
|
||||
}
|
||||
void clearMsgPreview();
|
||||
|
||||
// from AbstractUITask
|
||||
|
||||
@@ -437,6 +437,9 @@ public:
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_cached_batt_mv = getBattMilliVolts();
|
||||
|
||||
|
||||
@@ -89,6 +89,12 @@ public:
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
uint16_t getCachedBattMV() const { return _cached_batt_mv; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool supportsDisplayRotation() const override {
|
||||
return _display != NULL && _display->supportsRotation();
|
||||
}
|
||||
bool setDisplayRotationDegrees(uint16_t degrees) override {
|
||||
return _display != NULL && _display->setRotationDegrees(degrees);
|
||||
}
|
||||
bool isButtonPressed() const;
|
||||
|
||||
bool isBuzzerQuiet() {
|
||||
|
||||
@@ -664,7 +664,7 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
|
||||
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
|
||||
reply_data[8] = 0; // features
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
reply_data[8] |= 0x01; // is bridge, type UART
|
||||
if (_prefs.bridge_enabled) reply_data[8] |= 0x01; // is bridge, type UART
|
||||
#elif WITH_ESPNOW_BRIDGE
|
||||
reply_data[8] |= 0x03; // is bridge, type ESP-NOW
|
||||
#endif
|
||||
@@ -3161,7 +3161,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#if defined(WITH_MQTT_BRIDGE)
|
||||
, mqtt_bridge(nullptr)
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
|
||||
, bridge(nullptr)
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
, bridge(&_prefs, _mgr, &rtc)
|
||||
#endif
|
||||
@@ -3335,11 +3335,21 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
&_prefs.rx_ps_sleep_us);
|
||||
|
||||
// bridge defaults
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
||||
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
_prefs.bridge_enabled = 0; // normal repeater until explicitly enabled
|
||||
#else
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
#endif
|
||||
_prefs.bridge_delay = 500; // milliseconds
|
||||
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
|
||||
_prefs.bridge_baud = 115200; // baud rate
|
||||
_prefs.bridge_channel = 1; // channel 1
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs.bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
#else
|
||||
_prefs.bridge_uart = 0;
|
||||
#endif
|
||||
|
||||
StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret));
|
||||
|
||||
@@ -3470,6 +3480,11 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
node_info.repeat_when_nonzero = false;
|
||||
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (!bridge) {
|
||||
bridge = createRS232Bridge();
|
||||
}
|
||||
#endif
|
||||
AbstractBridge* active_bridge = activeBridge();
|
||||
if (active_bridge) {
|
||||
|
||||
@@ -493,7 +493,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#if defined(WITH_MQTT_BRIDGE)
|
||||
MQTTBridge* mqtt_bridge;
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
RS232Bridge bridge;
|
||||
RS232Bridge* bridge;
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge bridge;
|
||||
#endif
|
||||
@@ -501,6 +501,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
AbstractBridge* activeBridge() {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
return mqtt_bridge;
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
return bridge;
|
||||
#else
|
||||
return &bridge;
|
||||
#endif
|
||||
@@ -508,6 +510,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
const AbstractBridge* activeBridge() const {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
return mqtt_bridge;
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
return bridge;
|
||||
#else
|
||||
return &bridge;
|
||||
#endif
|
||||
@@ -975,6 +979,21 @@ public:
|
||||
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
||||
|
||||
#if defined(WITH_BRIDGE)
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
RS232Bridge* createRS232Bridge() {
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
if (_prefs.bridge_uart == WITH_RS232_BRIDGE_ALT_UART) {
|
||||
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE_ALT,
|
||||
WITH_RS232_BRIDGE_ALT_RX,
|
||||
WITH_RS232_BRIDGE_ALT_TX, _mgr, getRTCClock());
|
||||
}
|
||||
#endif
|
||||
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE,
|
||||
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX,
|
||||
_mgr, getRTCClock());
|
||||
}
|
||||
#endif
|
||||
|
||||
void setBridgeState(bool enable) override {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (!mqtt_bridge) {
|
||||
@@ -990,9 +1009,24 @@ public:
|
||||
getRTCClock(), &self_id);
|
||||
if (!mqtt_bridge) return;
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (enable && !bridge) {
|
||||
bridge = createRS232Bridge();
|
||||
if (!bridge) return;
|
||||
}
|
||||
#endif
|
||||
AbstractBridge* active_bridge = activeBridge();
|
||||
if (!active_bridge || enable == active_bridge->isRunning()) return;
|
||||
if (!active_bridge) return;
|
||||
if (enable == active_bridge->isRunning()) {
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (!enable) {
|
||||
delete bridge;
|
||||
bridge = nullptr;
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (enable)
|
||||
{
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -1014,6 +1048,10 @@ public:
|
||||
else
|
||||
{
|
||||
active_bridge->end();
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
delete bridge;
|
||||
bridge = nullptr;
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_alerter.setBridge(nullptr);
|
||||
#endif
|
||||
@@ -1030,6 +1068,12 @@ public:
|
||||
}
|
||||
#endif
|
||||
active_bridge->end();
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
delete bridge;
|
||||
bridge = createRS232Bridge();
|
||||
if (bridge) bridge->begin();
|
||||
return;
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Set device metadata before restarting bridge (same as in begin())
|
||||
char device_id[65];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
@@ -72,6 +75,7 @@ struct NodePrefs { // persisted to file
|
||||
float freq;
|
||||
int8_t tx_power_dbm;
|
||||
uint8_t unused[3];
|
||||
uint8_t usb_logging_enabled;
|
||||
};
|
||||
|
||||
class MyMesh : public BaseChatMesh, ContactVisitor {
|
||||
@@ -293,6 +297,7 @@ public:
|
||||
strcpy(_prefs.node_name, "NONAME");
|
||||
_prefs.freq = LORA_FREQ;
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.usb_logging_enabled = 1;
|
||||
|
||||
command[0] = 0;
|
||||
curr_recipient = NULL;
|
||||
@@ -357,6 +362,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
_prefs.usb_logging_enabled = constrain(_prefs.usb_logging_enabled, 0, 1);
|
||||
mesh::setUsbLoggingEnabled(_prefs.usb_logging_enabled != 0);
|
||||
#endif
|
||||
|
||||
loadContacts();
|
||||
_public = addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
|
||||
}
|
||||
@@ -493,9 +503,28 @@ public:
|
||||
}
|
||||
} else if (memcmp(command, "import ", 7) == 0) {
|
||||
importCard(&command[7]);
|
||||
} else if (strcmp(command, "get usb.logging") == 0) {
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
Serial.printf(" usb.logging %s\n",
|
||||
mesh::isUsbLoggingEnabled() ? "on" : "off");
|
||||
#else
|
||||
Serial.println(" ERROR: USB logging is unavailable");
|
||||
#endif
|
||||
} else if (memcmp(command, "set ", 4) == 0) {
|
||||
const char* config = &command[4];
|
||||
if (memcmp(config, "af ", 3) == 0) {
|
||||
if (strcmp(config, "usb.logging on") == 0
|
||||
|| strcmp(config, "usb.logging off") == 0) {
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
const bool enabled = strcmp(config, "usb.logging on") == 0;
|
||||
_prefs.usb_logging_enabled = enabled ? 1 : 0;
|
||||
mesh::setUsbLoggingEnabled(enabled);
|
||||
savePrefs();
|
||||
Serial.printf(" OK - USB logging %s (saved)\n",
|
||||
enabled ? "on" : "off");
|
||||
#else
|
||||
Serial.println(" ERROR: USB logging is unavailable");
|
||||
#endif
|
||||
} else if (memcmp(config, "af ", 3) == 0) {
|
||||
_prefs.airtime_factor = atof(&config[3]);
|
||||
savePrefs();
|
||||
Serial.println(" OK");
|
||||
@@ -527,6 +556,8 @@ public:
|
||||
} else if (memcmp(command, "help", 4) == 0) {
|
||||
Serial.println("Commands:");
|
||||
Serial.println(" set {name|lat|lon|freq|tx|af} {value}");
|
||||
Serial.println(" get usb.logging");
|
||||
Serial.println(" set usb.logging {on|off}");
|
||||
Serial.println(" card");
|
||||
Serial.println(" import {biz card}");
|
||||
Serial.println(" clock");
|
||||
@@ -578,6 +609,9 @@ void halt() {
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
#if MESH_PACKET_LOGGING
|
||||
mesh::serialLogBegin();
|
||||
#endif
|
||||
|
||||
board.begin();
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// MeshCore BLE mOTA seeder service. The device notifies one framed seeder
|
||||
// request on REQUEST; the paired host writes the framed response to RESPONSE.
|
||||
// Both characteristics require an encrypted MITM-authenticated BLE link.
|
||||
static constexpr char BLE_MOTA_SERVICE_UUID[] =
|
||||
"14518fc2-7e7a-4d84-8cae-6664b0234cf2";
|
||||
static constexpr char BLE_MOTA_REQUEST_UUID[] =
|
||||
"2bfaa1ee-7030-459a-b65a-e7cfd5b09735";
|
||||
static constexpr char BLE_MOTA_RESPONSE_UUID[] =
|
||||
"acf38a51-dd58-4dce-917f-0b1135e41b1a";
|
||||
|
||||
static constexpr uint16_t BLE_MOTA_REQUEST_MAX = 11;
|
||||
static constexpr uint16_t BLE_MOTA_RESPONSE_MAX = 197;
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// A single-producer/single-consumer Stream adapter for the nRF52 BLE mOTA
|
||||
// GATT service. The BLE callback appends host response fragments while the
|
||||
// mesh loop synchronously consumes them through SerialMotaSource. Device
|
||||
// requests travel in the other direction through the bounded send callback.
|
||||
//
|
||||
// One seeder response is at most 197 bytes. Keeping 256 bytes here holds a
|
||||
// complete response while still failing closed on duplicate or injected data.
|
||||
class BleMotaStream : public Stream {
|
||||
public:
|
||||
static constexpr uint16_t RX_CAPACITY = 256;
|
||||
using SendCallback = size_t (*)(void* context, const uint8_t* data,
|
||||
size_t length);
|
||||
|
||||
BleMotaStream() = default;
|
||||
|
||||
void setSender(SendCallback callback, void* context) {
|
||||
_send_context = context;
|
||||
_send = callback;
|
||||
}
|
||||
|
||||
void setActive(bool active) {
|
||||
_active.store(false, std::memory_order_release);
|
||||
if (active) {
|
||||
clear();
|
||||
} else {
|
||||
invalidateWriters();
|
||||
}
|
||||
_overflowed.store(false, std::memory_order_release);
|
||||
_active.store(active, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool isActive() const {
|
||||
return _active.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool overflowed() const {
|
||||
return _overflowed.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// Called only by the BLE response-characteristic callback. A fragment is
|
||||
// accepted atomically or rejected in full; a partial frame is never queued.
|
||||
bool pushRx(const uint8_t* data, size_t length) {
|
||||
if (!isActive() || data == nullptr || length == 0
|
||||
|| length >= RX_CAPACITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t head_state = _head_state.load(std::memory_order_acquire);
|
||||
const uint16_t head = static_cast<uint16_t>(head_state);
|
||||
const uint16_t tail = _tail.load(std::memory_order_acquire);
|
||||
const uint16_t used = head >= tail ? head - tail
|
||||
: RX_CAPACITY - (tail - head);
|
||||
const uint16_t free_bytes = RX_CAPACITY - used - 1;
|
||||
if (length > free_bytes) {
|
||||
_overflowed.store(true, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cursor = head;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
_rx[cursor] = data[i];
|
||||
cursor = static_cast<uint16_t>((cursor + 1) % RX_CAPACITY);
|
||||
}
|
||||
const uint32_t next_state = (head_state & 0xFFFF0000UL) | cursor;
|
||||
uint32_t expected = head_state;
|
||||
// setActive() changes the generation, and a new session also resets the
|
||||
// head. The CAS ensures a callback which overlapped either transition
|
||||
// cannot publish stale bytes into the next source session, even when both
|
||||
// sessions happened to use the same ring index.
|
||||
return isActive()
|
||||
&& _head_state.compare_exchange_strong(
|
||||
expected, next_state, std::memory_order_release,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int available() override {
|
||||
if (!isActive()) return 0;
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
return head >= tail ? head - tail : RX_CAPACITY - (tail - head);
|
||||
}
|
||||
|
||||
int read() override {
|
||||
if (!isActive()) return -1;
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
if (tail == head) return -1;
|
||||
const uint8_t value = _rx[tail];
|
||||
_tail.store(static_cast<uint16_t>((tail + 1) % RX_CAPACITY),
|
||||
std::memory_order_release);
|
||||
return value;
|
||||
}
|
||||
|
||||
int peek() override {
|
||||
if (!isActive()) return -1;
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
return tail == head ? -1 : _rx[tail];
|
||||
}
|
||||
|
||||
void flush() override {}
|
||||
|
||||
size_t write(uint8_t value) override {
|
||||
return write(&value, 1);
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* data, size_t length) override {
|
||||
if (!isActive() || data == nullptr || length == 0 || _send == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return _send(_send_context, data, length);
|
||||
}
|
||||
|
||||
using Print::write;
|
||||
|
||||
private:
|
||||
static uint16_t nextGeneration(uint32_t state) {
|
||||
return static_cast<uint16_t>(state >> 16) + 1;
|
||||
}
|
||||
|
||||
void invalidateWriters() {
|
||||
const uint32_t previous = _head_state.load(std::memory_order_relaxed);
|
||||
const uint32_t next =
|
||||
(static_cast<uint32_t>(nextGeneration(previous)) << 16)
|
||||
| static_cast<uint16_t>(previous);
|
||||
_head_state.store(next, std::memory_order_release);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
const uint32_t previous = _head_state.load(std::memory_order_relaxed);
|
||||
_head_state.store(
|
||||
static_cast<uint32_t>(nextGeneration(previous)) << 16,
|
||||
std::memory_order_release);
|
||||
_tail.store(0, std::memory_order_release);
|
||||
}
|
||||
|
||||
uint8_t _rx[RX_CAPACITY] = {};
|
||||
// High 16 bits are a source-session generation; low 16 bits are the ring
|
||||
// head. Packing both into one lock-free nRF52840 atomic prevents an ABA
|
||||
// commit across clear()/restart.
|
||||
std::atomic<uint32_t> _head_state{0};
|
||||
std::atomic<uint16_t> _tail{0};
|
||||
std::atomic<bool> _active{false};
|
||||
std::atomic<bool> _overflowed{false};
|
||||
SendCallback _send = nullptr;
|
||||
void* _send_context = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -588,5 +588,98 @@ inline bool parseIntegerStrict(const char* text, int32_t& result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool splitCommaFieldsStrict(const char* text, char* storage,
|
||||
size_t storage_size,
|
||||
const char** fields,
|
||||
size_t field_count) {
|
||||
if (text == nullptr || storage == nullptr || storage_size == 0
|
||||
|| fields == nullptr || field_count == 0) {
|
||||
return false;
|
||||
}
|
||||
const size_t length = strlen(text);
|
||||
if (length == 0 || length >= storage_size) return false;
|
||||
memcpy(storage, text, length + 1);
|
||||
|
||||
char* cursor = storage;
|
||||
for (size_t i = 0; i < field_count; ++i) {
|
||||
fields[i] = cursor;
|
||||
char* separator = strchr(cursor, ',');
|
||||
if (i + 1 == field_count) {
|
||||
if (separator != nullptr) return false;
|
||||
} else {
|
||||
if (separator == nullptr) return false;
|
||||
*separator = 0;
|
||||
cursor = separator + 1;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse the radio tuple syntax without libc's optional float-scanf support.
|
||||
// In particular, newlib-nano nRF52 builds commonly compile `%f` but return no
|
||||
// conversions unless the much larger _scanf_float implementation is linked.
|
||||
inline bool parseRadioTupleStrict(const char* text, float& frequency,
|
||||
float& bandwidth, uint8_t& spreading_factor,
|
||||
uint8_t& coding_rate) {
|
||||
char storage[96] = {0};
|
||||
const char* fields[4] = {nullptr};
|
||||
if (!splitCommaFieldsStrict(text, storage, sizeof(storage), fields, 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float parsed_frequency = 0.0f;
|
||||
float parsed_bandwidth = 0.0f;
|
||||
int32_t parsed_sf = 0;
|
||||
int32_t parsed_cr = 0;
|
||||
if (!parseDecimalStrict(fields[0], parsed_frequency)
|
||||
|| !parseDecimalStrict(fields[1], parsed_bandwidth)
|
||||
|| !parseIntegerStrict(fields[2], parsed_sf)
|
||||
|| !parseIntegerStrict(fields[3], parsed_cr)
|
||||
|| parsed_sf < 0 || parsed_sf > 255
|
||||
|| parsed_cr < 0 || parsed_cr > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency = parsed_frequency;
|
||||
bandwidth = parsed_bandwidth;
|
||||
spreading_factor = static_cast<uint8_t>(parsed_sf);
|
||||
coding_rate = static_cast<uint8_t>(parsed_cr);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool parseTemporaryRadioTupleStrict(
|
||||
const char* text, float& frequency, float& bandwidth,
|
||||
uint8_t& spreading_factor, uint8_t& coding_rate,
|
||||
uint32_t& timeout_minutes) {
|
||||
char storage[96] = {0};
|
||||
const char* fields[5] = {nullptr};
|
||||
if (!splitCommaFieldsStrict(text, storage, sizeof(storage), fields, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float parsed_frequency = 0.0f;
|
||||
float parsed_bandwidth = 0.0f;
|
||||
int32_t parsed_sf = 0;
|
||||
int32_t parsed_cr = 0;
|
||||
int32_t parsed_timeout = 0;
|
||||
if (!parseDecimalStrict(fields[0], parsed_frequency)
|
||||
|| !parseDecimalStrict(fields[1], parsed_bandwidth)
|
||||
|| !parseIntegerStrict(fields[2], parsed_sf)
|
||||
|| !parseIntegerStrict(fields[3], parsed_cr)
|
||||
|| !parseIntegerStrict(fields[4], parsed_timeout)
|
||||
|| parsed_sf < 0 || parsed_sf > 255
|
||||
|| parsed_cr < 0 || parsed_cr > 255
|
||||
|| parsed_timeout < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency = parsed_frequency;
|
||||
bandwidth = parsed_bandwidth;
|
||||
spreading_factor = static_cast<uint8_t>(parsed_sf);
|
||||
coding_rate = static_cast<uint8_t>(parsed_cr);
|
||||
timeout_minutes = static_cast<uint32_t>(parsed_timeout);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace mesh
|
||||
|
||||
@@ -768,6 +768,11 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
_prefs->system_watchdog_enabled = 1;
|
||||
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
||||
_prefs->usb_logging_enabled = 1;
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
#else
|
||||
_prefs->bridge_uart = 0;
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool node_prefs_needs_migration = false;
|
||||
@@ -898,6 +903,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
File file = fs->open(filename);
|
||||
#endif
|
||||
if (file) {
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
bool has_runtime_bridge_uart = false;
|
||||
#endif
|
||||
// Every supported layout contains the fixed 290-byte common core. Reject
|
||||
// a truncated in-place write before it can leave strings unterminated or
|
||||
// feed partial radio values into startup. loadPrefs() rewrites the image.
|
||||
@@ -1257,6 +1265,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->usb_logging_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->usb_logging_enabled,
|
||||
sizeof(_prefs->usb_logging_enabled));
|
||||
if (file.available() >= (int)sizeof(_prefs->bridge_uart)) {
|
||||
file.read((uint8_t *)&_prefs->bridge_uart,
|
||||
sizeof(_prefs->bridge_uart));
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
has_runtime_bridge_uart = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (file.available() > 0) {
|
||||
@@ -1314,11 +1329,32 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); // LOOP_DETECT_OFF..LOOP_DETECT_STRICT
|
||||
|
||||
// sanitise bad bridge pref values
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
if (!has_runtime_bridge_uart) {
|
||||
// Pre-merge normal repeaters persisted bridge_enabled=1 even though no
|
||||
// bridge was compiled. Fail safe on the first merged boot instead of
|
||||
// unexpectedly claiming a UART; the user can explicitly enable it.
|
||||
_prefs->bridge_enabled = 0;
|
||||
_com_prefs_needs_upgrade = true;
|
||||
}
|
||||
#endif
|
||||
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
|
||||
_prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
|
||||
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
|
||||
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
|
||||
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
|
||||
#endif
|
||||
) {
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
_com_prefs_needs_upgrade = true;
|
||||
}
|
||||
#else
|
||||
_prefs->bridge_uart = 0;
|
||||
#endif
|
||||
|
||||
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
|
||||
_prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255);
|
||||
@@ -1538,6 +1574,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->extra_sf); // 856
|
||||
WRITE_COMMON_PREFS(&prefs->radio_fem_txgain); // 860
|
||||
WRITE_COMMON_PREFS(&prefs->usb_logging_enabled); // 861
|
||||
WRITE_COMMON_PREFS(&prefs->bridge_uart); // 862
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -1690,7 +1727,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf)); // 856
|
||||
file.write((uint8_t *)&_prefs->radio_fem_txgain, sizeof(_prefs->radio_fem_txgain)); // 860
|
||||
file.write((uint8_t *)&_prefs->usb_logging_enabled, sizeof(_prefs->usb_logging_enabled)); // 861
|
||||
// next: 862
|
||||
file.write((uint8_t *)&_prefs->bridge_uart, sizeof(_prefs->bridge_uart)); // 862
|
||||
// next: 863
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (!file.commit()) {
|
||||
@@ -2538,7 +2576,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
strcpy(reply, "gps_interval must be 0..86400 seconds");
|
||||
} else if (!valid_gps_toggle) {
|
||||
strcpy(reply, "gps must be 0 or 1");
|
||||
} else if (_sensors->setSettingValue(key, value)) {
|
||||
}
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
else if (is_gps_toggle && strcmp(value, "1") == 0
|
||||
&& _prefs->bridge_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "turn the RS232 bridge off or select another UART first");
|
||||
}
|
||||
#endif
|
||||
else if (_sensors->setSettingValue(key, value)) {
|
||||
if (is_gps_interval) {
|
||||
_prefs->gps_interval = gps_interval;
|
||||
savePrefs();
|
||||
@@ -2579,6 +2625,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
handleRegionCmd(command, reply);
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
} else if (memcmp(command, "gps on", 6) == 0) {
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
if (_prefs->bridge_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "turn the RS232 bridge off or select another UART first");
|
||||
} else
|
||||
#endif
|
||||
if (_sensors->setSettingValue("gps", "1")) {
|
||||
_prefs->gps_enabled = 1;
|
||||
savePrefs();
|
||||
@@ -4019,10 +4071,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
#ifdef WITH_BRIDGE
|
||||
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
|
||||
_prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
|
||||
_callbacks->setBridgeState(_prefs->bridge_enabled);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
const bool enable = memcmp(&config[15], "on", 2) == 0;
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
if (enable && _prefs->gps_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "Error: turn GPS off or select another UART first");
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
_prefs->bridge_enabled = enable;
|
||||
_callbacks->setBridgeState(_prefs->bridge_enabled);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
|
||||
int delay = _atoi(&config[13]);
|
||||
if (delay >= 0 && delay <= 10000) {
|
||||
@@ -4059,6 +4120,33 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
|
||||
}
|
||||
} else if (memcmp(config, "bridge.uart ", 12) == 0) {
|
||||
const int uart = atoi(&config[12]);
|
||||
const bool supported = uart == WITH_RS232_BRIDGE_UART
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
|| uart == WITH_RS232_BRIDGE_ALT_UART
|
||||
#endif
|
||||
;
|
||||
if (!supported) {
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
sprintf(reply, "Error: UART must be %d or %d",
|
||||
WITH_RS232_BRIDGE_UART, WITH_RS232_BRIDGE_ALT_UART);
|
||||
#else
|
||||
sprintf(reply, "Error: UART is fixed at %d", WITH_RS232_BRIDGE_UART);
|
||||
#endif
|
||||
}
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
else if (uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART
|
||||
&& _prefs->gps_enabled && _prefs->bridge_enabled) {
|
||||
strcpy(reply, "Error: turn GPS or the bridge off first");
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
_prefs->bridge_uart = (uint8_t)uart;
|
||||
_callbacks->restartBridge();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
|
||||
@@ -4453,6 +4541,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
} else if (memcmp(config, "bridge.baud", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
|
||||
} else if (memcmp(config, "bridge.uart", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_uart);
|
||||
#endif
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
} else if (memcmp(config, "bridge.channel", 14) == 0) {
|
||||
|
||||
+10
-2
@@ -25,6 +25,10 @@
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_RS232_BRIDGE_UART)
|
||||
#define WITH_RS232_BRIDGE_UART 1
|
||||
#endif
|
||||
|
||||
#define ADVERT_LOC_NONE 0
|
||||
#define ADVERT_LOC_SHARE 1
|
||||
#define ADVERT_LOC_PREFS 2
|
||||
@@ -169,9 +173,12 @@ public:
|
||||
uint8_t extra_sf[4] = {};
|
||||
uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain; persisted at /com_prefs offset 860
|
||||
// Runtime USB packet output gate. Appended at /com_prefs offset 861 so
|
||||
// older images remain readable and logging builds keep their historical
|
||||
// enabled-at-first-boot behavior.
|
||||
// older images remain readable and merged standard builds keep the former
|
||||
// logging artifact's enabled-at-first-boot behavior.
|
||||
uint8_t usb_logging_enabled = 1;
|
||||
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
|
||||
// /com_prefs offset 862; single-UART builds keep their compiled port here.
|
||||
uint8_t bridge_uart = 0;
|
||||
uint8_t retry_preset = 0;
|
||||
uint8_t direct_retry_attempts = 0;
|
||||
uint16_t direct_retry_base_ms = 0;
|
||||
@@ -267,6 +274,7 @@ private:
|
||||
def("delay", _parent->bridge_delay);
|
||||
def("src", _parent->bridge_pkt_src);
|
||||
def("baud", _parent->bridge_baud);
|
||||
def("uart", _parent->bridge_uart);
|
||||
def("ch", _parent->bridge_channel);
|
||||
def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret));
|
||||
def("usb_log", _parent->usb_logging_enabled);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace companion {
|
||||
|
||||
static constexpr uint8_t CMD_EXEC_LOCAL_OTA_CONTROL = 0x4A;
|
||||
static constexpr uint8_t CMD_BLE_MOTA_SOURCE = 0x4B;
|
||||
|
||||
enum class MotaSourceAction : uint8_t {
|
||||
Status = 0,
|
||||
Start = 1,
|
||||
Stop = 2,
|
||||
};
|
||||
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_CHANNEL_READY = 0x01;
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_ATTACHED = 0x02;
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_ANOTHER_LINK_ACTIVE = 0x04;
|
||||
|
||||
struct MotaSourceStatus {
|
||||
bool channel_ready = false;
|
||||
bool attached = false;
|
||||
bool another_link_active = false;
|
||||
uint16_t offered = 0;
|
||||
uint16_t advertised = 0;
|
||||
uint32_t packets_sent = 0;
|
||||
};
|
||||
|
||||
class MotaSourceControl {
|
||||
public:
|
||||
virtual ~MotaSourceControl() = default;
|
||||
virtual bool start(char* reply, size_t reply_size) = 0;
|
||||
virtual bool stop(char* reply, size_t reply_size) = 0;
|
||||
virtual MotaSourceStatus status() const = 0;
|
||||
};
|
||||
|
||||
// Bluetooth may expose only the local commands needed to coordinate a LoRa
|
||||
// mOTA session. The command arrives as one length-delimited Companion frame,
|
||||
// so reject embedded NUL/control bytes and USB folder ownership commands.
|
||||
inline bool isBleOtaControlCommandAllowed(const uint8_t* command,
|
||||
size_t length) {
|
||||
if (command == nullptr || length == 0 || length > 174) return false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (command[i] < 0x20 || command[i] > 0x7E) return false;
|
||||
switch (command[i]) {
|
||||
case ';':
|
||||
case '&':
|
||||
case '|':
|
||||
case '`':
|
||||
case '$':
|
||||
case '\\':
|
||||
case '\'':
|
||||
case '"':
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto exact = [command, length](const char* text, size_t text_length) {
|
||||
if (length != text_length) return false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (command[i] != static_cast<uint8_t>(text[i])) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto token = [command, length](const char* text, size_t text_length) {
|
||||
if (length < text_length) return false;
|
||||
for (size_t i = 0; i < text_length; ++i) {
|
||||
if (command[i] != static_cast<uint8_t>(text[i])) return false;
|
||||
}
|
||||
return length == text_length || command[text_length] == ' ';
|
||||
};
|
||||
|
||||
if (exact("normalradio", 11)) return true;
|
||||
// Bare `tempradio` is a read-only status query. Keep the parameterized
|
||||
// form strict, but let a BLE controller prove that cleanup completed.
|
||||
if (exact("tempradio", 9)) return true;
|
||||
if (token("tempradio", 9)) return length > 10;
|
||||
if (!token("ota", 3)) return false;
|
||||
if (token("ota folder", 10)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace companion
|
||||
} // namespace mesh
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
// Full Companion owns its primary USB stream for framed traffic until saved
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
// A USB Companion owns its primary stream for framed traffic until saved
|
||||
// preferences are loaded. Starting disabled prevents early boot diagnostics
|
||||
// from corrupting that stream before single-TTY builds can enter terminal mode.
|
||||
static std::atomic<bool> usb_logging_enabled{false};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// Logging artifacts compile at least one of these two diagnostics. Keep the
|
||||
// command surface out of ordinary images while providing one runtime gate for
|
||||
// every diagnostic category that writes to the USB Serial stream.
|
||||
// Canonical USB-capable images compile at least one of these two diagnostics.
|
||||
// One runtime gate covers every diagnostic category that writes to the USB
|
||||
// Serial stream, so a separate logging firmware is unnecessary.
|
||||
#if defined(ARDUINO) && \
|
||||
((defined(MESH_DEBUG) && MESH_DEBUG) || \
|
||||
(defined(MESH_PACKET_LOGGING) && MESH_PACKET_LOGGING))
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Ordinary logging images start enabled. Every Full Companion starts with
|
||||
// logging disabled and restores its saved choice after preferences load.
|
||||
// Ordinary merged images start enabled. USB/Full Companion starts disabled to
|
||||
// protect framed traffic and restores its saved choice after preferences load.
|
||||
bool isUsbLoggingEnabled();
|
||||
void setUsbLoggingEnabled(bool enabled);
|
||||
|
||||
|
||||
@@ -251,6 +251,12 @@ inline bool isFirmwareReserved(uint8_t pin) {
|
||||
#ifdef WITH_RS232_BRIDGE_TX
|
||||
WITH_RS232_BRIDGE_TX,
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE_ALT_RX
|
||||
WITH_RS232_BRIDGE_ALT_RX,
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE_ALT_TX
|
||||
WITH_RS232_BRIDGE_ALT_TX,
|
||||
#endif
|
||||
#ifdef PIN_SERIAL_RX
|
||||
PIN_SERIAL_RX,
|
||||
#endif
|
||||
|
||||
@@ -5,30 +5,28 @@
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
|
||||
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
|
||||
: BridgeBase(prefs, mgr, rtc), _serial(&serial) {}
|
||||
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
|
||||
int16_t tx_pin, mesh::PacketManager *mgr,
|
||||
mesh::RTCClock *rtc)
|
||||
: BridgeBase(prefs, mgr, rtc), _serial(&serial), _rx_pin(rx_pin),
|
||||
_tx_pin(tx_pin) {}
|
||||
|
||||
void RS232Bridge::begin() {
|
||||
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
|
||||
#if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
|
||||
#error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
|
||||
#endif
|
||||
|
||||
#if defined(ESP32)
|
||||
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
// Tested with RAK_4631 and T114
|
||||
// The Adafruit Uart object may already be active on its variant defaults.
|
||||
// Stop it before changing pins or the EasyDMA instance can retain the old
|
||||
// pin selection and silently receive nothing.
|
||||
mesh::bridge::prepareNrfUart(*((Uart *)_serial),
|
||||
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
mesh::bridge::prepareNrfUart(*((Uart *)_serial), _rx_pin, _tx_pin);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
|
||||
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
|
||||
((SerialUART *)_serial)->setRX(_rx_pin);
|
||||
((SerialUART *)_serial)->setTX(_tx_pin);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
((HardwareSerial *)_serial)->setRx(WITH_RS232_BRIDGE_RX);
|
||||
((HardwareSerial *)_serial)->setTx(WITH_RS232_BRIDGE_TX);
|
||||
((HardwareSerial *)_serial)->setRx(_rx_pin);
|
||||
((HardwareSerial *)_serial)->setTx(_tx_pin);
|
||||
#else
|
||||
#error RS232Bridge was not tested on the current platform
|
||||
#endif
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* - Fletcher-16 checksum for data integrity verification
|
||||
* - Magic header for packet synchronization and frame alignment
|
||||
* - Duplicate packet detection using SimpleMeshTables tracking
|
||||
* - Configurable RX/TX pins via build defines
|
||||
* - Configurable UART and RX/TX pins selected by the owning runtime
|
||||
* - Fixed baud rate at 115200 for consistent timing
|
||||
*
|
||||
* Packet Structure:
|
||||
@@ -34,8 +34,7 @@
|
||||
*
|
||||
* Configuration:
|
||||
* - Define WITH_RS232_BRIDGE to enable this bridge
|
||||
* - Define WITH_RS232_BRIDGE_RX with the RX pin number
|
||||
* - Define WITH_RS232_BRIDGE_TX with the TX pin number
|
||||
* - The owning role supplies the selected UART and pins to the constructor
|
||||
*
|
||||
* Platform Support:
|
||||
* Different platforms require different pin configuration methods:
|
||||
@@ -54,12 +53,12 @@ public:
|
||||
* @param mgr PacketManager for allocating and queuing packets
|
||||
* @param rtc RTCClock for timestamping debug messages
|
||||
*/
|
||||
RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin, int16_t tx_pin,
|
||||
mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
|
||||
/**
|
||||
* Initializes the RS232 bridge
|
||||
*
|
||||
* - Validates that RX/TX pins are defined
|
||||
* - Configures UART pins based on target platform
|
||||
* - Sets baud rate to 115200 for consistent communication
|
||||
* - Platform-specific pin configuration methods are used
|
||||
@@ -137,6 +136,8 @@ private:
|
||||
|
||||
/** Hardware serial port interface */
|
||||
Stream *_serial;
|
||||
int16_t _rx_pin;
|
||||
int16_t _tx_pin;
|
||||
|
||||
/** Buffer for building received packets */
|
||||
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
|
||||
|
||||
@@ -67,9 +67,16 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
BLESecurity sec;
|
||||
#if defined(CONFIG_NIMBLE_ENABLED)
|
||||
sec.setPassKey(true, pin_code);
|
||||
// A passkey alone does not provide MITM protection when the controller's
|
||||
// default capability is NoInputNoOutput: the peers can silently fall back
|
||||
// to Just Works and create an unauthenticated bond. The Companion displays
|
||||
// (or otherwise publishes) this PIN for entry on the phone/host, so declare
|
||||
// the peripheral as DisplayOnly and force the passkey-entry association.
|
||||
sec.setCapability(ESP_IO_CAP_OUT);
|
||||
sec.setAuthenticationMode(true, true, true);
|
||||
#else
|
||||
sec.setStaticPIN(pin_code);
|
||||
sec.setCapability(ESP_IO_CAP_OUT);
|
||||
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ void SerialBLEInterface::onConnect(uint16_t connection_handle) {
|
||||
instance->_isDeviceConnected = false;
|
||||
instance->_security_timer.start(millis());
|
||||
instance->clearBuffers();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
instance->setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +55,46 @@ void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason
|
||||
instance->_isDeviceConnected = false;
|
||||
instance->_security_timer.cancel();
|
||||
instance->clearBuffers();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
instance->setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
void SerialBLEInterface::onMotaResponse(
|
||||
uint16_t conn_handle, BLECharacteristic* characteristic,
|
||||
uint8_t* data, uint16_t length) {
|
||||
if (!instance || characteristic != &instance->_mota_response
|
||||
|| instance->_conn_handle != conn_handle || !instance->isConnected()
|
||||
|| !instance->_mota_stream.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (length == 0 || length > mesh::ota::BLE_MOTA_RESPONSE_MAX
|
||||
|| !instance->_mota_stream.pushRx(data, length)) {
|
||||
// A response that cannot fit intact would make the byte stream ambiguous.
|
||||
// Disable the link so the current transaction times out and the main loop
|
||||
// detaches it instead of consuming a partial or injected frame.
|
||||
instance->setMotaStreamActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::sendMotaRequest(void* context,
|
||||
const uint8_t* data,
|
||||
size_t length) {
|
||||
SerialBLEInterface* self = static_cast<SerialBLEInterface*>(context);
|
||||
if (!self || !data || length == 0
|
||||
|| length > mesh::ota::BLE_MOTA_REQUEST_MAX
|
||||
|| !self->isMotaChannelReady() || !self->_mota_stream.isActive()) {
|
||||
return 0;
|
||||
}
|
||||
return self->_mota_request.notify(self->_conn_handle, data, length)
|
||||
? length : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
|
||||
if (instance) {
|
||||
@@ -272,6 +311,38 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
bleuart.begin();
|
||||
bleuart.setRxCallback(onBleUartRX);
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
_mota_stream.setSender(sendMotaRequest, this);
|
||||
_mota_stream.setActive(false);
|
||||
|
||||
_mota_service.setPermission(SECMODE_ENC_WITH_MITM,
|
||||
SECMODE_ENC_WITH_MITM);
|
||||
if (_mota_service.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA service begin failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_mota_request.setProperties(CHR_PROPS_NOTIFY);
|
||||
_mota_request.setPermission(SECMODE_ENC_WITH_MITM,
|
||||
SECMODE_NO_ACCESS);
|
||||
_mota_request.setMaxLen(mesh::ota::BLE_MOTA_REQUEST_MAX);
|
||||
_mota_request.setUserDescriptor("mOTA device request");
|
||||
if (_mota_request.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA request characteristic begin failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_mota_response.setProperties(CHR_PROPS_WRITE);
|
||||
_mota_response.setPermission(SECMODE_NO_ACCESS,
|
||||
SECMODE_ENC_WITH_MITM);
|
||||
_mota_response.setMaxLen(mesh::ota::BLE_MOTA_RESPONSE_MAX);
|
||||
_mota_response.setUserDescriptor("mOTA host response");
|
||||
_mota_response.setWriteCallback(onMotaResponse);
|
||||
if (_mota_response.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA response characteristic begin failed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Register DFU on the main BLE stack so paired clients can discover it
|
||||
@@ -390,6 +461,9 @@ void SerialBLEInterface::recoverStalledTx(const char* cause) {
|
||||
_last_retry_attempt = 0;
|
||||
_tx_stall_watchdog.reset();
|
||||
bleuart.flush();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
setMotaStreamActive(false);
|
||||
#endif
|
||||
_tx_disconnect_recovery.begin();
|
||||
serviceTxRecovery((uint32_t)millis());
|
||||
}
|
||||
@@ -442,6 +516,9 @@ void SerialBLEInterface::disable() {
|
||||
disconnect();
|
||||
_security_timer.cancel();
|
||||
_last_health_check = 0;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
@@ -610,6 +687,13 @@ bool SerialBLEInterface::isConnected() const {
|
||||
Bluefruit.connected() > 0;
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
bool SerialBLEInterface::isMotaChannelReady() {
|
||||
return isConnected() && _conn_handle != BLE_CONN_HANDLE_INVALID
|
||||
&& _mota_request.notifyEnabled(_conn_handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SerialBLEInterface::isReadBusy() const {
|
||||
return (recv_queue_len > 0);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "SecuritySessionTimer.h"
|
||||
#include <bluefruit.h>
|
||||
#include <atomic>
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
#include "../BleMotaProtocol.h"
|
||||
#include "../BleMotaStream.h"
|
||||
#endif
|
||||
|
||||
#ifndef BLE_TX_POWER
|
||||
#define BLE_TX_POWER 4
|
||||
@@ -14,6 +18,14 @@
|
||||
class SerialBLEInterface : public BaseSerialInterface {
|
||||
BLEDfu bledfu;
|
||||
BLEUart bleuart;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
BLEService _mota_service = BLEService(mesh::ota::BLE_MOTA_SERVICE_UUID);
|
||||
BLECharacteristic _mota_request =
|
||||
BLECharacteristic(mesh::ota::BLE_MOTA_REQUEST_UUID);
|
||||
BLECharacteristic _mota_response =
|
||||
BLECharacteristic(mesh::ota::BLE_MOTA_RESPONSE_UUID);
|
||||
mesh::ota::BleMotaStream _mota_stream;
|
||||
#endif
|
||||
bool _isEnabled;
|
||||
bool _isDeviceConnected;
|
||||
uint16_t _conn_handle;
|
||||
@@ -56,6 +68,13 @@ class SerialBLEInterface : public BaseSerialInterface {
|
||||
static void onPairingComplete(uint16_t connection_handle, uint8_t auth_status);
|
||||
static void onBLEEvent(ble_evt_t* evt);
|
||||
static void onBleUartRX(uint16_t conn_handle);
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
static void onMotaResponse(uint16_t conn_handle,
|
||||
BLECharacteristic* characteristic,
|
||||
uint8_t* data, uint16_t length);
|
||||
static size_t sendMotaRequest(void* context, const uint8_t* data,
|
||||
size_t length);
|
||||
#endif
|
||||
|
||||
public:
|
||||
SerialBLEInterface() {
|
||||
@@ -91,6 +110,12 @@ public:
|
||||
}
|
||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
Stream& motaStream() { return _mota_stream; }
|
||||
bool isMotaChannelReady();
|
||||
bool isMotaStreamActive() const { return _mota_stream.isActive(); }
|
||||
void setMotaStreamActive(bool active) { _mota_stream.setActive(active); }
|
||||
#endif
|
||||
};
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
|
||||
@@ -11,6 +11,7 @@ bool FolderMotaStore::readByteT(uint8_t& b) const {
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c >= 0) { b = (uint8_t)c; return true; }
|
||||
delay(1); // let BLE/WiFi callbacks deliver the response
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ bool FolderMotaStore::txn(uint8_t op, const uint8_t* args, uint16_t arglen,
|
||||
uint32_t t0 = millis(); bool got = false; uint8_t prev = 0;
|
||||
while ((millis() - t0) < _to) { // scan for response magic 'm' 's' (tolerate noise)
|
||||
int c = _io.read();
|
||||
if (c < 0) continue;
|
||||
if (c < 0) { delay(1); continue; }
|
||||
if (prev == MOTA_SEEDER_RSP_MAGIC0 && (uint8_t)c == MOTA_SEEDER_RSP_MAGIC1) { got = true; break; }
|
||||
prev = (uint8_t)c;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ bool SerialMotaSource::readByteT(uint8_t& b) {
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c >= 0) { b = (uint8_t)c; return true; }
|
||||
delay(1); // let BLE/WiFi callbacks deliver the response
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ bool SerialMotaSource::txn(uint8_t op, const uint8_t* args, uint8_t arglen,
|
||||
uint8_t prev = 0;
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c < 0) continue;
|
||||
if (c < 0) { delay(1); continue; }
|
||||
if (prev == MOTA_SEEDER_RSP_MAGIC0 && (uint8_t)c == MOTA_SEEDER_RSP_MAGIC1) { got = true; break; }
|
||||
prev = (uint8_t)c;
|
||||
}
|
||||
|
||||
@@ -321,8 +321,17 @@ struct OtaContext {
|
||||
FOLDER_LINK_NONE = 0,
|
||||
FOLDER_LINK_SERIAL,
|
||||
FOLDER_LINK_TCP,
|
||||
FOLDER_LINK_BLE,
|
||||
};
|
||||
FolderLink folderLink() const { return _folder_link; }
|
||||
static const char* folderLinkName(FolderLink link) {
|
||||
switch (link) {
|
||||
case FOLDER_LINK_SERIAL: return "serial";
|
||||
case FOLDER_LINK_TCP: return "tcp";
|
||||
case FOLDER_LINK_BLE: return "ble";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
bool folderSourceStats(uint16_t& offered, uint16_t& advertised) const {
|
||||
return manager.sourceStats(_folder_source, offered, advertised);
|
||||
}
|
||||
@@ -337,7 +346,7 @@ struct OtaContext {
|
||||
}
|
||||
if (folder_active && _folder_link != link) {
|
||||
snprintf(msg, cap, "ERR folder already attached via %s",
|
||||
_folder_link == FOLDER_LINK_TCP ? "tcp" : "serial");
|
||||
folderLinkName(_folder_link));
|
||||
return false;
|
||||
}
|
||||
if (folder_active && _folder_source == source) {
|
||||
|
||||
@@ -447,6 +447,7 @@ public:
|
||||
FetchError fetchError() const { return _fetch_error; }
|
||||
uint32_t blocksHave() const { return _have; }
|
||||
uint32_t blocksTotal() const { return _fbc; }
|
||||
uint32_t packetsSent() const { return _packets_sent; }
|
||||
uint8_t fetchPipelineWidth() const { return _pipeline_width; }
|
||||
static constexpr uint8_t fetchPipelineCapacity() { return OTA_FETCH_PIPELINE; }
|
||||
uint32_t fetchRetryTimeoutMs() const;
|
||||
@@ -482,7 +483,9 @@ private:
|
||||
bool expandCatalog();
|
||||
|
||||
bool emit(const uint8_t* b, uint16_t n, bool flood) {
|
||||
return _send && n && _send(_ctx, b, n, flood);
|
||||
const bool sent = _send && n && _send(_ctx, b, n, flood);
|
||||
if (sent) _packets_sent++;
|
||||
return sent;
|
||||
}
|
||||
void handleAdv(const uint8_t* m, uint16_t n); // beacon -> sources table (+ query if interested)
|
||||
void handleQuery(const uint8_t* m, uint16_t n); // serve: reply OTA_HAVE catalog
|
||||
@@ -655,6 +658,7 @@ private:
|
||||
uint32_t _radio_packet_airtime_ms = 0; // measured for MAX_TRANS_UNIT at active SF/BW
|
||||
uint16_t _tx_spacing_permille = 2000; // 1/duty-cycle; default airtime factor 1 => 50% TX
|
||||
uint8_t _observed_path_transmissions = 0; // source + relays; 0 falls back to configured max_hops+1
|
||||
uint32_t _packets_sent = 0; // OTA packets accepted by the radio adapter (wrap-safe)
|
||||
// multi-fragment manifest reassembly (a signed v2 manifest exceeds one packet)
|
||||
uint8_t _mf_buf[OTA_MF_MAXFRAG * OTA_MF_FRAG]; // sized to the fragment cap so no valid manifest is silently dropped
|
||||
uint16_t _mf_retries = 0; // GET_MANIFEST retries while WANT_MANIFEST (give up after a cap)
|
||||
|
||||
@@ -15,6 +15,7 @@ class DisplayDriver {
|
||||
int _w, _h;
|
||||
protected:
|
||||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
||||
void setDimensions(int w, int h) { _w = w; _h = h; }
|
||||
|
||||
static size_t trimLastUTF8Codepoint(char* str, size_t length) {
|
||||
if (length == 0) return 0;
|
||||
@@ -30,6 +31,11 @@ public:
|
||||
int height() const { return _h; }
|
||||
|
||||
virtual bool isOn() = 0;
|
||||
virtual bool supportsRotation() const { return false; }
|
||||
virtual bool setRotationDegrees(uint16_t degrees) {
|
||||
(void)degrees;
|
||||
return false;
|
||||
}
|
||||
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
|
||||
virtual void forceFullRefresh() {} // next refresh will be full for eink
|
||||
virtual void turnOn() = 0;
|
||||
|
||||
@@ -22,10 +22,34 @@ bool SSD1306Display::begin() {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
_isOn = true;
|
||||
}
|
||||
#ifdef DISPLAY_ROTATION
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
#endif
|
||||
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
const bool started = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS,
|
||||
true, false)
|
||||
&& i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
if (started) setRotationDegrees(0);
|
||||
return started;
|
||||
}
|
||||
|
||||
bool SSD1306Display::setRotationDegrees(uint16_t degrees) {
|
||||
uint8_t rotation;
|
||||
switch (degrees) {
|
||||
case 0:
|
||||
#ifdef DISPLAY_ROTATION
|
||||
rotation = DISPLAY_ROTATION & 3;
|
||||
#else
|
||||
rotation = 0;
|
||||
#endif
|
||||
break;
|
||||
case 90: rotation = 1; break;
|
||||
case 180: rotation = 2; break;
|
||||
case 270: rotation = 3; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
display.setRotation(rotation);
|
||||
setDimensions(display.width(), display.height());
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSD1306Display::turnOn() {
|
||||
|
||||
@@ -32,6 +32,8 @@ public:
|
||||
bool begin();
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
bool supportsRotation() const override { return true; }
|
||||
bool setRotationDegrees(uint16_t degrees) override;
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
|
||||
@@ -51,6 +51,7 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_companion_status_response` | `src/helpers/CompanionStatusResponse.h` | request-tag correlation and minimum status-response length, including rejection of the three-entry ACL payload that previously masqueraded as status |
|
||||
| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp`, `src/helpers/MultiSerialInterface.h` | terminal/seeder control-sequence recognition and passthrough ownership; queued/atomic USB output under backpressure and short writes; partial-frame busy state; requester-affine replies, locked contact streams, and Bluetooth-only pairing routing |
|
||||
| `test_ble_tx_stall_watchdog` | `src/helpers/BleTxStallWatchdog.h` | exact BLE fragment progress; blocked-reply timeout; rollover-safe elapsed time; disconnect recovery retry and completion |
|
||||
| `test_ble_mota_control` | `src/helpers/BleMotaStream.h`, `CompanionMotaControl.h` | encrypted mOTA channel ring buffering, overflow fail-closed behavior, request gating, and strict rejection of injected or USB-ownership control commands |
|
||||
| `test_atomic_file_writer` | `src/helpers/AtomicFileWriter.h` | verified temporary-file commit; short-write, readback, validation, and rename failures; preservation of the live file and stale-temp cleanup |
|
||||
| `test_cad_timing` | `src/helpers/radiolib/CadTiming.h`, `LR2021SideDetectorConfig.h`, `RadioAirtime.h` | Cascade and slow-profile CAD deadlines; invalid airtime handling; bounded LR2021 side-detector parsing and LDRO recomputation |
|
||||
| `test_companion_node_prefs` | `examples/companion_radio/NodePrefs.h` | independent device power saving, RXPS, Wi-Fi, and FEM preferences; one-time migration of the regressed power-saving default |
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helpers/BleMotaStream.h"
|
||||
#include "helpers/CompanionMotaControl.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
bool allowed(const char* text) {
|
||||
return mesh::companion::isBleOtaControlCommandAllowed(
|
||||
reinterpret_cast<const uint8_t*>(text), std::strlen(text));
|
||||
}
|
||||
|
||||
TEST(BleMotaControl, AllowsOnlyBoundedOtaSessionCommands) {
|
||||
EXPECT_TRUE(allowed("tempradio"));
|
||||
EXPECT_TRUE(allowed("tempradio 915,250,5,5,10"));
|
||||
EXPECT_TRUE(allowed("normalradio"));
|
||||
EXPECT_TRUE(allowed("ota"));
|
||||
EXPECT_TRUE(allowed("ota status"));
|
||||
EXPECT_TRUE(allowed("ota neighbors"));
|
||||
|
||||
EXPECT_FALSE(allowed("tempradio "));
|
||||
EXPECT_FALSE(allowed("normalradio now"));
|
||||
EXPECT_FALSE(allowed("otafolder status"));
|
||||
EXPECT_FALSE(allowed("ota folder on"));
|
||||
EXPECT_FALSE(allowed("ota folder off"));
|
||||
EXPECT_FALSE(allowed("reboot"));
|
||||
}
|
||||
|
||||
TEST(BleMotaControl, RejectsCommandInjectionBytes) {
|
||||
const uint8_t embedded_nul[] = {'o', 't', 'a', ' ', 's', 't', 0, 'a'};
|
||||
const uint8_t newline[] = {'o', 't', 'a', ' ', 's', 't', '\n', 'x'};
|
||||
const uint8_t carriage_return[] = {'n', 'o', 'r', 'm', 'a', 'l', 'r',
|
||||
'a', 'd', 'i', 'o', '\r'};
|
||||
const uint8_t shell_chain[] = {'o', 't', 'a', ' ', 's', 't', 'a', 't',
|
||||
'u', 's', ';', 'r', 'e', 'b', 'o', 'o', 't'};
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
embedded_nul, sizeof(embedded_nul)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
newline, sizeof(newline)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
carriage_return, sizeof(carriage_return)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
shell_chain, sizeof(shell_chain)));
|
||||
}
|
||||
|
||||
struct SendCapture {
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
size_t captureSend(void* context, const uint8_t* data, size_t length) {
|
||||
auto* capture = static_cast<SendCapture*>(context);
|
||||
capture->bytes.assign(data, data + length);
|
||||
return length;
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, IsInactiveAndEmptyByDefault) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
const uint8_t byte = 7;
|
||||
EXPECT_FALSE(stream.isActive());
|
||||
EXPECT_FALSE(stream.pushRx(&byte, 1));
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
EXPECT_EQ(stream.write(&byte, 1), 0u);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, CarriesFragmentedResponseAcrossRingWrap) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
|
||||
std::vector<uint8_t> first(200);
|
||||
for (size_t i = 0; i < first.size(); ++i) first[i] = i;
|
||||
ASSERT_TRUE(stream.pushRx(first.data(), first.size()));
|
||||
for (size_t i = 0; i < 190; ++i) {
|
||||
ASSERT_EQ(stream.read(), first[i]);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> second(100);
|
||||
for (size_t i = 0; i < second.size(); ++i) second[i] = 200 + i;
|
||||
ASSERT_TRUE(stream.pushRx(second.data(), second.size()));
|
||||
EXPECT_EQ(stream.available(), 110);
|
||||
for (size_t i = 190; i < first.size(); ++i) {
|
||||
ASSERT_EQ(stream.read(), first[i]);
|
||||
}
|
||||
for (uint8_t value : second) ASSERT_EQ(stream.read(), value);
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, RejectsOverflowWithoutQueuingPartialFragment) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
std::vector<uint8_t> first(240, 0x11);
|
||||
std::vector<uint8_t> overflow(20, 0x22);
|
||||
ASSERT_TRUE(stream.pushRx(first.data(), first.size()));
|
||||
EXPECT_FALSE(stream.pushRx(overflow.data(), overflow.size()));
|
||||
EXPECT_TRUE(stream.overflowed());
|
||||
EXPECT_EQ(stream.available(), 240);
|
||||
while (stream.available()) EXPECT_EQ(stream.read(), 0x11);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, SendsRequestsOnlyWhileActiveAndClearsOnStop) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
SendCapture capture;
|
||||
stream.setSender(captureSend, &capture);
|
||||
stream.setActive(true);
|
||||
const uint8_t request[] = {'M', 'S', 1, 1};
|
||||
EXPECT_EQ(stream.write(request, sizeof(request)), sizeof(request));
|
||||
EXPECT_EQ(capture.bytes,
|
||||
std::vector<uint8_t>(request, request + sizeof(request)));
|
||||
|
||||
const uint8_t response[] = {'m', 's', 1, 0, 1, 1};
|
||||
ASSERT_TRUE(stream.pushRx(response, sizeof(response)));
|
||||
stream.setActive(false);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
EXPECT_EQ(stream.write(request, sizeof(request)), 0u);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, DisconnectAndRestartDiscardEveryPartialOldResponse) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
|
||||
const uint8_t partial_old_response[] = {'m', 's', 3, 0, 0xAA, 0xBB};
|
||||
ASSERT_TRUE(stream.pushRx(partial_old_response,
|
||||
sizeof(partial_old_response)));
|
||||
ASSERT_EQ(stream.available(), (int)sizeof(partial_old_response));
|
||||
|
||||
// A BLE loss can happen between response fragments. Starting the next
|
||||
// source session must never splice those stale bytes into its first frame.
|
||||
stream.setActive(false);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
stream.setActive(true);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
|
||||
const uint8_t fresh_response[] = {'m', 's', 1, 0, 1, 1};
|
||||
ASSERT_TRUE(stream.pushRx(fresh_response, sizeof(fresh_response)));
|
||||
for (uint8_t expected : fresh_response) {
|
||||
EXPECT_EQ(stream.read(), expected);
|
||||
}
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -9,6 +9,9 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$OPTION3_BUILD_WORKERS" -eq 1 ] \
|
||||
|| fail "logging matrix permits concurrent PlatformIO target builds"
|
||||
|
||||
# Full targets are synthesized from an ordinary transport environment. Check
|
||||
# the resolved PlatformIO configuration so board-specific display, GPS, input,
|
||||
# and BLE constraints cannot silently disappear through that inheritance.
|
||||
@@ -53,6 +56,45 @@ require(m8_usb, "lib_deps", "GxEPD2 @ 1.6.9")
|
||||
reject(m8_usb, "lib_deps", "GxEPD2 @ 1.6.2")
|
||||
|
||||
require("ThinkNode_M5_companion_radio_wifi", "build_flags", "UI_RECENT_LIST_SIZE=9")
|
||||
require("ThinkNode_M5_companion_radio_full", "build_flags", "SERIAL_TX=43")
|
||||
require("ThinkNode_M2_companion_radio_full", "build_flags", "SERIAL_RX=44")
|
||||
require("Xiao_S3_WIO_companion_radio_full", "build_flags", "SERIAL_TX=D6")
|
||||
require("ThinkNode_M7_companion_radio_full", "build_flags", "ETHERNET_USE_CH390")
|
||||
require("ThinkNode_M7_companion_radio_full", "build_src_filter", "helpers/ethernet/ch390")
|
||||
require("ThinkNode_M7_companion_radio_full", "lib_deps", "ESP32-CH390")
|
||||
require("RAK_4631_companion_radio_full", "build_flags", "ETHERNET_USE_RAK13800")
|
||||
require("RAK_4631_companion_radio_full", "build_src_filter", "helpers/ethernet/RAK13800")
|
||||
require("RAK_4631_companion_radio_full", "lib_deps", "RAK13800-W5100S")
|
||||
require("Heltec_E290_companion_usb_ble", "build_flags", "ENABLE_USB_INTERFACE")
|
||||
require("Heltec_E290_companion_usb_ble", "build_flags", "BLE_PIN_CODE=123456")
|
||||
require("Heltec_T190_companion_radio_usb_ble_", "build_flags", "ENABLE_USB_INTERFACE")
|
||||
require("Heltec_T190_companion_radio_usb_ble_", "build_flags", "BLE_PIN_CODE=123456")
|
||||
|
||||
for env_name in (
|
||||
"Heltec_t114_without_display_repeater",
|
||||
"Heltec_t114_repeater",
|
||||
"Heltec_t114_repeater_lora_ota_no_external_sensors",
|
||||
"RAK_3112_repeater",
|
||||
"RAK_11310_repeater",
|
||||
"ProMicro_repeater",
|
||||
"waveshare_rp2040_lora_repeater",
|
||||
"solarxiao_30S_repeater",
|
||||
"solarxiao_33S_repeater",
|
||||
"Heltec_v3_repeater",
|
||||
"Heltec_WSL3_repeater",
|
||||
"Heltec_t096_repeater",
|
||||
"Heltec_t096_repeater_lora_ota_no_external_sensors",
|
||||
"RAK_4631_repeater",
|
||||
"RAK_4631_repeater_lora_ota_no_external_sensors",
|
||||
"LilyGo_TLora_V2_1_1_6_repeater",
|
||||
):
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE=")
|
||||
require(env_name, "build_flags", "RS232_BRIDGE_MERGED=1")
|
||||
require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
|
||||
|
||||
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_ALT=Serial1")
|
||||
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_UART=2")
|
||||
reject("wio-e5_repeater", "build_flags", "WITH_RS232_BRIDGE=")
|
||||
|
||||
for env_name in (
|
||||
"t1000e_companion_radio_usb",
|
||||
@@ -133,6 +175,63 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \
|
||||
|| fail "non-repeater nRF52 target incorrectly scheduled two artifacts"
|
||||
|
||||
# Ordinary USB-loggable roles compile their historical logging profile into
|
||||
# the canonical artifact. OTA receivers, KISS, and BLE keep their distinct
|
||||
# stream/partition contracts. A standard ESP32 artifact still embeds logging
|
||||
# when the same target also has an expanded FULL profile.
|
||||
PIO_ENV_PLATFORM_BY_NAME[wio-e5-mini_repeater]=STM32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_kiss_modem]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_companion_radio_ble]=NRF52_PLATFORM
|
||||
uses_merged_standard_usb_logging nrf_sensor \
|
||||
|| fail "ordinary nRF52 sensor omitted merged USB logging"
|
||||
uses_merged_standard_usb_logging wio-e5-mini_repeater \
|
||||
|| fail "size-constrained STM32 target omitted merged packet logging"
|
||||
if uses_merged_standard_usb_logging nrf_repeater_lora_ota_no_external_sensors; then
|
||||
fail "LoRa OTA receiver incorrectly merged USB logging"
|
||||
fi
|
||||
if uses_merged_standard_usb_logging nrf_kiss_modem; then
|
||||
fail "KISS target incorrectly merged plaintext USB logging"
|
||||
fi
|
||||
if uses_merged_standard_usb_logging nrf_companion_radio_ble; then
|
||||
fail "BLE Companion incorrectly merged USB logging"
|
||||
fi
|
||||
uses_merged_standard_usb_logging esp_repeater \
|
||||
|| fail "standard ESP32 target omitted logging because FULL also exists"
|
||||
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
MESHDEBUG_OVERRIDE=""
|
||||
PACKET_LOGGING_OVERRIDE=""
|
||||
DISABLE_DEBUG=0
|
||||
apply_merged_standard_usb_logging_profile nrf_sensor
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_PACKET_LOGGING=1* ]] \
|
||||
|| fail "merged profile omitted packet logging"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_DEBUG=1* ]] \
|
||||
|| fail "merged profile omitted mesh diagnostics"
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
apply_merged_standard_usb_logging_profile wio-e5-mini_repeater
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_PACKET_LOGGING=1* ]] \
|
||||
|| fail "constrained merged profile omitted packet logging"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" != *-DMESH_DEBUG=1* ]] \
|
||||
|| fail "constrained merged profile enabled oversized mesh diagnostics"
|
||||
if declare -f run_logging_matrix_build_targets \
|
||||
| grep -q 'FIRMWARE_FILENAME_INFIX="logging"'; then
|
||||
fail "logging matrix still emits a separate standard logging artifact"
|
||||
fi
|
||||
if declare -f get_firmware_filename | grep -q 'filename_infix="logging"'; then
|
||||
fail "explicit packet logging still renames the ordinary artifact"
|
||||
fi
|
||||
FIRMWARE_FILENAME_INFIX=""
|
||||
ESP32_FULL_BUILD=0
|
||||
PACKET_LOGGING_OVERRIDE="on"
|
||||
MQTT_BRIDGE_OVERRIDE="off"
|
||||
[ "$(get_firmware_filename nrf_sensor vtest)" = "nrf_sensor-vtest" ] \
|
||||
|| fail "packet logging still creates a separately named artifact"
|
||||
PACKET_LOGGING_OVERRIDE=""
|
||||
MQTT_BRIDGE_OVERRIDE=""
|
||||
if declare -f run_logging_matrix_build_targets | grep -q 'Profile 2'; then
|
||||
fail "logging matrix still labels a separate Profile 2"
|
||||
fi
|
||||
|
||||
calls=()
|
||||
build_firmware() {
|
||||
calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$FIRMWARE_OUTPUT_ENV_NAME")
|
||||
@@ -220,6 +319,7 @@ SUPPORTED_PIO_ENVS=(
|
||||
Station_G2_companion_radio_ble
|
||||
Station_G2_companion_radio_wifi
|
||||
Station_G2_companion_radio_full
|
||||
Station_G2_terminal_chat
|
||||
)
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
PIO_ENV_PLATFORM_BY_NAME[$env_name]=ESP32_PLATFORM
|
||||
@@ -233,6 +333,87 @@ mapfile -t companion_release_targets < <(
|
||||
[ "${companion_release_targets[0]}" = Station_G2_companion_radio_full ] \
|
||||
|| fail "Station G2 release selected a non-Full Companion artifact"
|
||||
|
||||
# Full Companion's text terminal also replaces a separate Terminal Chat image
|
||||
# in bulk builds. Cover direct ESP32/nRF52 names and the canonical Heltec names
|
||||
# whose hardware/revision suffixes differ from their Terminal Chat targets.
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_3401_terminal_chat]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_3401_companion_radio_full]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_2_v4_3_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_tft_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_tft_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_tracker_v2_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_tracker_v2_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[PicoW_terminal_chat]=RP2040_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[PicoW_companion_radio_usb]=RP2040_PLATFORM
|
||||
PIO_ENV_BOARD_BY_NAME[PicoW_terminal_chat]=rpipicow
|
||||
PIO_ENV_BOARD_BY_NAME[PicoW_companion_radio_usb]=rpipicow
|
||||
|
||||
[ "$(get_terminal_chat_full_companion_replacement RAK_3401_terminal_chat)" \
|
||||
= RAK_3401_companion_radio_full ] \
|
||||
|| fail "nRF52 Terminal Chat did not map to Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_v4_terminal_chat)" \
|
||||
= heltec_v4_2_v4_3_companion_radio_full_femon ] \
|
||||
|| fail "Heltec V4 Terminal Chat did not map to its canonical Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_v4_tft_terminal_chat)" \
|
||||
= heltec_v4_tft_companion_radio_full_femon ] \
|
||||
|| fail "Heltec V4 TFT Terminal Chat did not map to Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_tracker_v2_terminal_chat)" \
|
||||
= heltec_tracker_v2_companion_radio_full_femon ] \
|
||||
|| fail "Heltec Tracker V2 Terminal Chat did not map to Full Companion"
|
||||
is_redundant_bulk_build_target Station_G2_terminal_chat \
|
||||
|| fail "ESP32 Terminal Chat remained in bulk builds beside Full Companion"
|
||||
is_redundant_bulk_build_target RAK_3401_terminal_chat \
|
||||
|| fail "nRF52 Terminal Chat remained in bulk builds beside Full Companion"
|
||||
[ "$(get_terminal_chat_companion_replacement PicoW_terminal_chat)" \
|
||||
= PicoW_companion_radio_usb ] \
|
||||
|| fail "RP2040 Terminal Chat did not map to USB Companion"
|
||||
is_redundant_bulk_build_target PicoW_terminal_chat \
|
||||
|| fail "Terminal Chat remained beside its matching USB Companion"
|
||||
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_4631_companion_radio_ethernet]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_4631_companion_radio_full]=NRF52_PLATFORM
|
||||
[ "$(get_nrf52_full_companion_replacement \
|
||||
RAK_4631_companion_radio_ethernet)" = RAK_4631_companion_radio_full ] \
|
||||
|| fail "RAK4631 Ethernet Companion did not map to Full Companion"
|
||||
PIO_ENV_PLATFORM_BY_NAME[ThinkNode_M2_companion_radio_serial]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[ThinkNode_M2_companion_radio_full]=ESP32_PLATFORM
|
||||
[ "$(get_esp32_full_companion_replacement \
|
||||
ThinkNode_M2_companion_radio_serial)" = ThinkNode_M2_companion_radio_full ] \
|
||||
|| fail "serial Companion did not map to Full Companion"
|
||||
|
||||
[ "$(get_combined_usb_ble_companion_replacement \
|
||||
Heltec_E290_companion_usb)" = Heltec_E290_companion_usb_ble ] \
|
||||
|| fail "E290 USB Companion did not map to USB+BLE Companion"
|
||||
[ "$(get_combined_usb_ble_companion_replacement \
|
||||
Heltec_T190_companion_radio_ble_)" \
|
||||
= Heltec_T190_companion_radio_usb_ble_ ] \
|
||||
|| fail "T190 BLE Companion did not map to USB+BLE Companion"
|
||||
|
||||
if is_redundant_bulk_build_target RAK_4631_repeater_ethernet \
|
||||
|| is_redundant_bulk_build_target RAK_4631_room_server_ethernet; then
|
||||
fail "RAK4631 repeater/room Ethernet artifact was incorrectly merged"
|
||||
fi
|
||||
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
RAK_4631_repeater_bridge_rs232_serial1)" = RAK_4631_repeater ] \
|
||||
|| fail "RAK4631 Serial1 bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors)" \
|
||||
= RAK_4631_repeater_lora_ota_no_external_sensors ] \
|
||||
|| fail "RAK4631 Serial2 OTA bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
Heltec_t114_repeater_bridge_rs232)" = Heltec_t114_repeater ] \
|
||||
|| fail "T114 RS232 bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
Heltec_t096_repeater_bridge_rs232)" = Heltec_t096_repeater ] \
|
||||
|| fail "T096 RS232 bridge did not map to merged repeater"
|
||||
is_redundant_bulk_build_target Heltec_v3_repeater_bridge_rs232 \
|
||||
|| fail "merged RS232 bridge remained in canonical bulk builds"
|
||||
if is_redundant_bulk_build_target wio-e5-repeater_bridge_rs232; then
|
||||
fail "capacity-constrained Wio-E5 RS232 bridge was incorrectly merged"
|
||||
fi
|
||||
|
||||
# A WiFi base's final -UENABLE_OTA must not win over the Full Companion's
|
||||
# source-only OTA overlay. That mismatch compiles out both the TCP terminal and
|
||||
# seeder while still leaving a superficially valid binary.
|
||||
|
||||
@@ -414,6 +414,70 @@ TEST(CLICommandUtils, RejectsInvalidOrOverflowingIntegers) {
|
||||
EXPECT_EQ(123, value);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, ParsesRadioTuplesWithoutFloatScanf) {
|
||||
float frequency = 0.0f;
|
||||
float bandwidth = 0.0f;
|
||||
uint8_t spreading_factor = 0;
|
||||
uint8_t coding_rate = 0;
|
||||
uint32_t timeout_minutes = 0;
|
||||
|
||||
EXPECT_TRUE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FLOAT_EQ(910.525f, frequency);
|
||||
EXPECT_FLOAT_EQ(62.5f, bandwidth);
|
||||
EXPECT_EQ(7, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
|
||||
EXPECT_TRUE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
" 909.950 , 250 , 5 , 5 , 120 ", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FLOAT_EQ(909.950f, frequency);
|
||||
EXPECT_FLOAT_EQ(250.0f, bandwidth);
|
||||
EXPECT_EQ(5, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
EXPECT_EQ(120u, timeout_minutes);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, RejectsMalformedRadioTuples) {
|
||||
float frequency = 123.0f;
|
||||
float bandwidth = 456.0f;
|
||||
uint8_t spreading_factor = 7;
|
||||
uint8_t coding_rate = 5;
|
||||
uint32_t timeout_minutes = 60;
|
||||
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
nullptr, frequency, bandwidth, spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7,5,1", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525x,62.5,7,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,256,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,1,99", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,1x", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,2147483648", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
|
||||
EXPECT_FLOAT_EQ(123.0f, frequency);
|
||||
EXPECT_FLOAT_EQ(456.0f, bandwidth);
|
||||
EXPECT_EQ(7, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
EXPECT_EQ(60u, timeout_minutes);
|
||||
}
|
||||
|
||||
TEST(RadioPowerLimits, LeavesUnspecifiedBackendRangeToDriver) {
|
||||
EXPECT_EQ(INT8_MIN, mesh::minLoRaTxPowerForFrequency(915.0f));
|
||||
EXPECT_EQ(INT8_MAX, mesh::maxLoRaTxPowerForFrequency(915.0f));
|
||||
|
||||
@@ -217,6 +217,7 @@ TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
saved.radio_fem_rxgain = 0;
|
||||
saved.radio_fem_txgain = 1;
|
||||
saved.usb_logging_enabled = 0;
|
||||
saved.bridge_uart = 2;
|
||||
|
||||
MockPrintStream output;
|
||||
ASSERT_TRUE(saved.saveSerial(output));
|
||||
@@ -225,17 +226,20 @@ TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("usb_log:0"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("uart:2"));
|
||||
|
||||
MockInputStream input(serialised.c_str());
|
||||
NodePrefs loaded;
|
||||
loaded.radio_fem_rxgain = 1;
|
||||
loaded.radio_fem_txgain = 0;
|
||||
loaded.usb_logging_enabled = 1;
|
||||
loaded.bridge_uart = 1;
|
||||
|
||||
ASSERT_TRUE(loaded.loadSerial(input));
|
||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
||||
EXPECT_EQ(0, loaded.usb_logging_enabled);
|
||||
EXPECT_EQ(2, loaded.bridge_uart);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,31 @@ assert(catalog.rows.some(function (item) {
|
||||
return item.target === "RAK_4631_companion_radio_usb-logging";
|
||||
}));
|
||||
|
||||
const rakSensor = profile("RAK_4631_sensor");
|
||||
assert.strictEqual(rakSensor.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(rakSensor.loggingModes, ["none", "usb"]);
|
||||
|
||||
const standardBesideFull = picker.applyMergedStandardUsbLoggingCapabilities([
|
||||
Object.assign(picker.parseTargetProfile("Example_repeater"), {
|
||||
hardware: "Example",
|
||||
role: "repeater",
|
||||
variant: "default",
|
||||
feature: "standard",
|
||||
logging: "none",
|
||||
ota: "none",
|
||||
}),
|
||||
Object.assign(picker.parseTargetProfile("Example_repeater-full-logging-ota"), {
|
||||
hardware: "Example",
|
||||
role: "repeater",
|
||||
variant: "default",
|
||||
feature: "full",
|
||||
logging: "usb",
|
||||
ota: "lora-receiver",
|
||||
}),
|
||||
]);
|
||||
assert.strictEqual(standardBesideFull[0].logging, "usb-runtime");
|
||||
assert.deepStrictEqual(standardBesideFull[0].loggingModes, ["none", "usb"]);
|
||||
|
||||
const v4Full = profile("heltec_v4_2_v4_3_companion_radio_full_femon");
|
||||
assert.strictEqual(v4Full.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(v4Full.loggingModes, ["none", "usb"]);
|
||||
@@ -192,6 +217,58 @@ assert.strictEqual(v4Full.dedicatedUsbLogging, true);
|
||||
}
|
||||
);
|
||||
|
||||
const combinedUsbBle = [
|
||||
picker.parseTargetProfile("Heltec_E290_companion_usb"),
|
||||
picker.parseTargetProfile("Heltec_E290_companion_ble"),
|
||||
picker.parseTargetProfile("Heltec_E290_companion_usb_ble"),
|
||||
];
|
||||
combinedUsbBle.forEach(function (item) {
|
||||
item.installKinds = ["bin"];
|
||||
});
|
||||
assert.strictEqual(combinedUsbBle[2].mode, "usb-ble");
|
||||
assert.strictEqual(combinedUsbBle[2].variant, "default");
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(combinedUsbBle).map(function (item) {
|
||||
return item.target;
|
||||
}),
|
||||
["Heltec_E290_companion_usb_ble"]
|
||||
);
|
||||
|
||||
const fullWithWiredTransports = [
|
||||
"ThinkNode_M7_companion_radio_full",
|
||||
"ThinkNode_M7_companion_radio_usb",
|
||||
"ThinkNode_M7_companion_radio_ble",
|
||||
"ThinkNode_M7_companion_radio_wifi",
|
||||
"ThinkNode_M7_companion_radio_serial",
|
||||
"ThinkNode_M7_companion_radio_ethernet",
|
||||
"ThinkNode_M7_terminal_chat",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["bin"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(fullWithWiredTransports).map(
|
||||
function (item) { return item.target; }
|
||||
),
|
||||
["ThinkNode_M7_companion_radio_full"]
|
||||
);
|
||||
|
||||
const terminalWithUsb = [
|
||||
"PicoW_terminal_chat",
|
||||
"PicoW_companion_radio_usb",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["uf2"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(terminalWithUsb).map(
|
||||
function (item) { return item.target; }
|
||||
),
|
||||
["PicoW_companion_radio_usb"]
|
||||
);
|
||||
|
||||
const companionBle = picker.parseTargetProfile(
|
||||
"Heltec_t096_companion_radio_ble_ps_femon"
|
||||
);
|
||||
@@ -323,10 +400,23 @@ assert.deepStrictEqual(
|
||||
const standardRepeater = profile("Station_G2_repeater");
|
||||
assert.strictEqual(standardRepeater.hardwareFamily, "Station_G2");
|
||||
assert.strictEqual(standardRepeater.role, "repeater");
|
||||
assert.strictEqual(standardRepeater.logging, "none");
|
||||
assert.strictEqual(standardRepeater.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(standardRepeater.loggingModes, ["none", "usb"]);
|
||||
assert.strictEqual(standardRepeater.ota, "none");
|
||||
assert.strictEqual(standardRepeater.mode, "standard");
|
||||
assert.strictEqual(standardRepeater.feature, "standard");
|
||||
|
||||
const mergedLoggingProfiles = picker.applyMergedStandardUsbLoggingCapabilities([
|
||||
Object.assign(picker.parseTargetProfile("PicoW_room_server"), {
|
||||
ota: "none",
|
||||
}),
|
||||
Object.assign(picker.parseTargetProfile("PicoW_kiss_modem"), {
|
||||
ota: "none",
|
||||
}),
|
||||
]);
|
||||
assert.strictEqual(mergedLoggingProfiles[0].logging, "usb-runtime");
|
||||
assert.deepStrictEqual(mergedLoggingProfiles[0].loggingModes, ["none", "usb"]);
|
||||
assert.strictEqual(mergedLoggingProfiles[1].logging, "none");
|
||||
assert.strictEqual(
|
||||
picker.canonicalAsset(standardRepeater.files, "merged-bin").name,
|
||||
"Station_G2_repeater-" + family + "-merged.bin"
|
||||
@@ -390,6 +480,34 @@ assert.strictEqual(wio.hardware, "wio-e5");
|
||||
assert.strictEqual(wio.role, "repeater");
|
||||
assert.strictEqual(wio.mode, "rs232");
|
||||
|
||||
const mergedRs232 = [
|
||||
"RAK_4631_repeater",
|
||||
"RAK_4631_repeater_bridge_rs232_serial1",
|
||||
"RAK_4631_repeater_bridge_rs232_serial2",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["uf2"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(mergedRs232).map(function (item) {
|
||||
return item.target;
|
||||
}),
|
||||
["RAK_4631_repeater"]
|
||||
);
|
||||
const constrainedRs232 = [
|
||||
"wio-e5_repeater",
|
||||
"wio-e5-repeater_bridge_rs232",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["hex"],
|
||||
});
|
||||
});
|
||||
assert.strictEqual(
|
||||
picker.omitTransportsReplacedByFull(constrainedRs232).length,
|
||||
2
|
||||
);
|
||||
|
||||
const matches = catalog.profiles.filter(function (item) {
|
||||
return picker.profileMatches(item, {
|
||||
hardware: "Station_G2",
|
||||
@@ -397,10 +515,12 @@ const matches = catalog.profiles.filter(function (item) {
|
||||
logging: "usb",
|
||||
});
|
||||
});
|
||||
assert.strictEqual(matches.length, 1);
|
||||
assert.strictEqual(
|
||||
matches[0].target,
|
||||
"Station_G2_repeater_observer_mqtt-full-usb-wifi"
|
||||
assert.deepStrictEqual(
|
||||
matches.map(function (item) { return item.target; }).sort(),
|
||||
[
|
||||
"Station_G2_repeater",
|
||||
"Station_G2_repeater_observer_mqtt-full-usb-wifi",
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
|
||||
@@ -1396,6 +1396,24 @@ static bool gated_capture_send(void* ctx, const uint8_t* msg, uint16_t len, bool
|
||||
capture->items.emplace_back(msg, msg + len);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(OtaMetrics, CountsOnlyPacketsAcceptedByTheRadioAdapter) {
|
||||
OtaManager manager;
|
||||
GatedCapture sent;
|
||||
manager.begin(0, gated_capture_send, &sent);
|
||||
|
||||
EXPECT_EQ(manager.packetsSent(), 0u);
|
||||
manager.announce();
|
||||
EXPECT_EQ(manager.packetsSent(), 0u);
|
||||
EXPECT_TRUE(sent.items.empty());
|
||||
|
||||
sent.accept = true;
|
||||
manager.announce();
|
||||
manager.announce();
|
||||
EXPECT_EQ(manager.packetsSent(), 2u);
|
||||
EXPECT_EQ(sent.items.size(), 2u);
|
||||
}
|
||||
|
||||
// Drive the bus to quiescence: deliver queued messages; when idle, advance the client's clock (monotonic
|
||||
// across calls, so a jittered query scheduled in a prior pump still comes due) and call loop() (fires the
|
||||
// scheduled catalog query / block re-requests). Two idle ticks in a row = quiescent.
|
||||
|
||||
Executable
+571
@@ -0,0 +1,571 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reference BLE controller/seeder for an nRF52 Full Companion.
|
||||
|
||||
This is primarily for Linux testing (including a Raspberry Pi Zero). A phone
|
||||
app can implement the same documented GATT and Companion frames.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BLEAK_IMPORT_ERROR: ImportError | None = None
|
||||
try:
|
||||
from bleak import BleakClient, BleakScanner
|
||||
from bleak.exc import BleakError
|
||||
except ImportError as exc: # pragma: no cover - depends on the test host
|
||||
BLEAK_IMPORT_ERROR = exc
|
||||
BleakClient = None # type: ignore[assignment,misc]
|
||||
BleakScanner = None # type: ignore[assignment,misc]
|
||||
|
||||
class BleakError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
NUS_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" # host -> Companion
|
||||
NUS_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" # Companion -> host
|
||||
MOTA_REQUEST_UUID = "2bfaa1ee-7030-459a-b65a-e7cfd5b09735"
|
||||
MOTA_RESPONSE_UUID = "acf38a51-dd58-4dce-917f-0b1135e41b1a"
|
||||
|
||||
CMD_EXEC_LOCAL_OTA_CONTROL = 0x4A
|
||||
CMD_BLE_MOTA_SOURCE = 0x4B
|
||||
MOTA_ACTION_STATUS = 0
|
||||
MOTA_ACTION_START = 1
|
||||
MOTA_ACTION_STOP = 2
|
||||
|
||||
RESP_OK = 0
|
||||
RESP_ERR = 1
|
||||
MOTA_FLAG_CHANNEL_READY = 0x01
|
||||
MOTA_FLAG_ATTACHED = 0x02
|
||||
MOTA_FLAG_ANOTHER_LINK_ACTIVE = 0x04
|
||||
|
||||
OP_COUNT = 0x01
|
||||
OP_DESCRIBE = 0x02
|
||||
OP_READ = 0x03
|
||||
STATUS_OK = 0
|
||||
STATUS_ERR = 1
|
||||
MOTA_READ_MAX = 192
|
||||
MOTA_DESC_WIRE = 38
|
||||
MOTA_HEADER_LEN = 8
|
||||
MOTA_MANIFEST_LEN = 197
|
||||
MOTA_TRAILER = b"vk496"
|
||||
|
||||
|
||||
def xor_bytes(data: bytes, seed: int = 0) -> int:
|
||||
result = seed
|
||||
for value in data:
|
||||
result ^= value
|
||||
return result
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MotaFile:
|
||||
path: Path
|
||||
size: int
|
||||
descriptor: bytes
|
||||
|
||||
@staticmethod
|
||||
def load(path: Path) -> "MotaFile":
|
||||
size = path.stat().st_size
|
||||
if size > 0xFFFFFFFF:
|
||||
raise ValueError("container is too large for the mOTA protocol")
|
||||
if size < MOTA_HEADER_LEN + MOTA_MANIFEST_LEN + len(MOTA_TRAILER):
|
||||
raise ValueError("container is too short")
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(MOTA_HEADER_LEN)
|
||||
manifest = stream.read(MOTA_MANIFEST_LEN)
|
||||
stream.seek(-len(MOTA_TRAILER), 2)
|
||||
trailer = stream.read(len(MOTA_TRAILER))
|
||||
if header[:4] != b"mOTA":
|
||||
raise ValueError("bad mOTA magic")
|
||||
if struct.unpack_from("<I", header, 4)[0] != size:
|
||||
raise ValueError("declared size does not match file size")
|
||||
if trailer != MOTA_TRAILER:
|
||||
raise ValueError("bad mOTA trailer")
|
||||
|
||||
flags = manifest[1]
|
||||
target_id = struct.unpack_from("<I", manifest, 3)[0]
|
||||
firmware_version = struct.unpack_from("<I", manifest, 7)[0]
|
||||
payload_size = struct.unpack_from("<I", manifest, 15)[0]
|
||||
block_size_log2 = manifest[19]
|
||||
if not 1 <= block_size_log2 <= 24 or payload_size == 0:
|
||||
raise ValueError("bad block geometry")
|
||||
block_size = 1 << block_size_log2
|
||||
block_count = (payload_size + block_size - 1) // block_size
|
||||
leaves_offset = MOTA_HEADER_LEN + MOTA_MANIFEST_LEN
|
||||
payload_offset = leaves_offset + block_count * 4
|
||||
if payload_offset + payload_size + len(MOTA_TRAILER) != size:
|
||||
raise ValueError("container geometry does not match file size")
|
||||
|
||||
descriptor = bytearray(MOTA_DESC_WIRE)
|
||||
descriptor[0:4] = manifest[20:24]
|
||||
struct.pack_into("<I", descriptor, 4, target_id)
|
||||
struct.pack_into("<I", descriptor, 8, firmware_version)
|
||||
descriptor[12] = manifest[56]
|
||||
descriptor[13] = flags
|
||||
struct.pack_into("<I", descriptor, 14, size)
|
||||
struct.pack_into("<I", descriptor, 18, leaves_offset)
|
||||
struct.pack_into("<I", descriptor, 22, block_count)
|
||||
struct.pack_into("<I", descriptor, 26, payload_offset)
|
||||
struct.pack_into("<I", descriptor, 30, payload_size)
|
||||
descriptor[34] = block_size_log2
|
||||
return MotaFile(path=path, size=size, descriptor=bytes(descriptor))
|
||||
|
||||
|
||||
class Catalog:
|
||||
def __init__(self, files: list[MotaFile], verbose: bool) -> None:
|
||||
self.files = files
|
||||
self.verbose = verbose
|
||||
|
||||
@staticmethod
|
||||
def scan(directory: Path, recursive: bool, motatool: str,
|
||||
verbose: bool) -> "Catalog":
|
||||
pattern = "**/*.mota" if recursive else "*.mota"
|
||||
paths = sorted(path for path in directory.glob(pattern) if path.is_file())
|
||||
if not paths:
|
||||
raise ValueError(f"no .mota files found under {directory}")
|
||||
if len(paths) > 255:
|
||||
raise ValueError("the Bluetooth mOTA catalog is limited to 255 files")
|
||||
|
||||
executable = shutil.which(motatool)
|
||||
if executable is None:
|
||||
raise ValueError(
|
||||
f"{motatool!r} was not found; install motatool before serving"
|
||||
)
|
||||
files: list[MotaFile] = []
|
||||
for path in paths:
|
||||
verified = subprocess.run(
|
||||
[executable, "verify", str(path)],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
check=False,
|
||||
)
|
||||
if verified.returncode != 0:
|
||||
raise ValueError(
|
||||
f"motatool verification failed for {path}:\n"
|
||||
+ verified.stdout
|
||||
)
|
||||
try:
|
||||
files.append(MotaFile.load(path))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise ValueError(f"cannot serve {path}: {exc}") from exc
|
||||
return Catalog(files, verbose)
|
||||
|
||||
def handle_request(self, frame: bytes) -> bytes | None:
|
||||
if len(frame) < 4 or frame[:2] != b"MS":
|
||||
return None
|
||||
op = frame[2]
|
||||
args = frame[3:-1]
|
||||
if frame[-1] != xor_bytes(args, op):
|
||||
return None
|
||||
|
||||
status = STATUS_ERR
|
||||
payload = b""
|
||||
if op == OP_COUNT and not args:
|
||||
status = STATUS_OK
|
||||
payload = bytes([len(self.files)])
|
||||
elif op == OP_DESCRIBE and len(args) == 1:
|
||||
index = args[0]
|
||||
if index < len(self.files):
|
||||
status = STATUS_OK
|
||||
payload = self.files[index].descriptor
|
||||
elif op == OP_READ and len(args) == 7:
|
||||
index = args[0]
|
||||
offset = struct.unpack_from("<I", args, 1)[0]
|
||||
length = struct.unpack_from("<H", args, 5)[0]
|
||||
if index < len(self.files) and length <= MOTA_READ_MAX:
|
||||
mota = self.files[index]
|
||||
end = offset + length
|
||||
if end >= offset and end <= mota.size:
|
||||
try:
|
||||
if mota.path.stat().st_size == mota.size:
|
||||
with mota.path.open("rb") as stream:
|
||||
stream.seek(offset)
|
||||
payload = stream.read(length)
|
||||
if len(payload) == length:
|
||||
status = STATUS_OK
|
||||
except OSError:
|
||||
payload = b""
|
||||
|
||||
response = bytearray(b"ms")
|
||||
response.extend((op, status))
|
||||
response.extend(payload)
|
||||
response.append(xor_bytes(response))
|
||||
if self.verbose:
|
||||
if op == OP_COUNT:
|
||||
detail = f"count={len(self.files)}"
|
||||
elif op == OP_DESCRIBE and args:
|
||||
detail = f"index={args[0]}"
|
||||
elif op == OP_READ and len(args) == 7:
|
||||
detail = f"index={args[0]} offset={struct.unpack_from('<I', args, 1)[0]}"
|
||||
else:
|
||||
detail = "invalid"
|
||||
print(f"mOTA op=0x{op:02x} {detail} status={status}")
|
||||
return bytes(response)
|
||||
|
||||
|
||||
class BleSession:
|
||||
def __init__(self, client: BleakClient, catalog: Catalog) -> None:
|
||||
self.client = client
|
||||
self.catalog = catalog
|
||||
self.mota_requests: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
self.companion_chunks: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
self.mota_response_chunk_size = 20
|
||||
|
||||
async def negotiate_mtu(self) -> None:
|
||||
"""Use the negotiated ATT payload when the backend can expose it.
|
||||
|
||||
BlueZ otherwise reports Bleak's conservative 23-byte default and each
|
||||
192-byte mOTA read takes ten acknowledged writes. Its backend provides
|
||||
the acquisition hook referenced by Bleak's own warning. Other backends
|
||||
already expose ``mtu_size`` directly. Failure is only a performance
|
||||
issue, so retain the universally safe 20-byte ATT payload fallback.
|
||||
"""
|
||||
mtu_size = 23
|
||||
backend = getattr(self.client, "_backend", None)
|
||||
acquire_mtu = getattr(backend, "_acquire_mtu", None)
|
||||
if callable(acquire_mtu):
|
||||
try:
|
||||
await acquire_mtu()
|
||||
candidate = getattr(backend, "_mtu_size", None)
|
||||
if isinstance(candidate, int) and candidate >= 23:
|
||||
mtu_size = candidate
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
candidate = self.client.mtu_size
|
||||
if isinstance(candidate, int) and candidate >= 23:
|
||||
mtu_size = candidate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.mota_response_chunk_size = min(
|
||||
MOTA_READ_MAX + 5, mtu_size - 3
|
||||
)
|
||||
if self.catalog.verbose:
|
||||
print(
|
||||
f"BLE MTU {mtu_size}; mOTA response chunks "
|
||||
f"{self.mota_response_chunk_size} bytes"
|
||||
)
|
||||
|
||||
def on_mota_request(self, _characteristic: object, data: bytearray) -> None:
|
||||
self.mota_requests.put_nowait(bytes(data))
|
||||
|
||||
def on_companion_data(self, _characteristic: object,
|
||||
data: bytearray) -> None:
|
||||
self.companion_chunks.put_nowait(bytes(data))
|
||||
|
||||
async def serve(self, stop: asyncio.Event) -> None:
|
||||
while not stop.is_set() and self.client.is_connected:
|
||||
try:
|
||||
request = await asyncio.wait_for(self.mota_requests.get(), 0.5)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
response = self.catalog.handle_request(request)
|
||||
if response is None:
|
||||
continue
|
||||
chunk_size = self.mota_response_chunk_size
|
||||
for offset in range(0, len(response), chunk_size):
|
||||
await self.client.write_gatt_char(
|
||||
MOTA_RESPONSE_UUID,
|
||||
response[offset : offset + chunk_size],
|
||||
response=True,
|
||||
)
|
||||
|
||||
async def companion_command(self, frame: bytes, expected_length,
|
||||
timeout: float = 8.0) -> bytes:
|
||||
while not self.companion_chunks.empty():
|
||||
self.companion_chunks.get_nowait()
|
||||
await self.client.write_gatt_char(NUS_RX_UUID, frame, response=True)
|
||||
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
result = bytearray()
|
||||
while True:
|
||||
remaining = deadline - asyncio.get_running_loop().time()
|
||||
if remaining <= 0:
|
||||
raise asyncio.TimeoutError
|
||||
chunk = await asyncio.wait_for(
|
||||
self.companion_chunks.get(), remaining
|
||||
)
|
||||
# Companion push frames have their own atomic BLE notification and
|
||||
# use codes 0x80-0xff. They can arrive at any time, including while
|
||||
# a command reply is pending, so do not splice one into the reply.
|
||||
if not result and chunk and chunk[0] >= 0x80:
|
||||
continue
|
||||
result.extend(chunk)
|
||||
wanted = expected_length(result)
|
||||
if wanted is None:
|
||||
continue
|
||||
if len(result) != wanted:
|
||||
raise RuntimeError(
|
||||
f"malformed Companion response length {len(result)}/{wanted}"
|
||||
)
|
||||
return bytes(result)
|
||||
|
||||
async def local_control(self, command: str) -> str:
|
||||
encoded = command.encode("ascii")
|
||||
if not 1 <= len(encoded) <= 174:
|
||||
raise ValueError("local command must be 1-174 ASCII bytes")
|
||||
response = await self.companion_command(
|
||||
bytes([CMD_EXEC_LOCAL_OTA_CONTROL]) + encoded,
|
||||
lambda data: (
|
||||
2 if data and data[0] == RESP_ERR
|
||||
else 2 + data[1] if len(data) >= 2 and data[0] == RESP_OK
|
||||
else 1 if data and data[0] not in (RESP_OK, RESP_ERR)
|
||||
else None
|
||||
),
|
||||
)
|
||||
if not response:
|
||||
raise RuntimeError("empty Companion response")
|
||||
if response[0] == RESP_ERR:
|
||||
code = response[1] if len(response) > 1 else -1
|
||||
raise RuntimeError(f"Companion rejected local command (error {code})")
|
||||
if response[0] != RESP_OK:
|
||||
raise RuntimeError(f"unexpected Companion response 0x{response[0]:02x}")
|
||||
return response[2:].decode("ascii", errors="replace")
|
||||
|
||||
async def source_action(
|
||||
self, action: int
|
||||
) -> tuple[int, int, int, int | None]:
|
||||
response = await self.companion_command(
|
||||
bytes([CMD_BLE_MOTA_SOURCE, action]),
|
||||
lambda data: (
|
||||
2 if data and data[0] == RESP_ERR
|
||||
else 11 if len(data) > 7 and data[0] == RESP_OK
|
||||
else 7 if len(data) == 7 and data[0] == RESP_OK
|
||||
else None if data and data[0] == RESP_OK
|
||||
else 1 if data
|
||||
else None
|
||||
),
|
||||
timeout=30.0,
|
||||
)
|
||||
return parse_source_status(response, action)
|
||||
|
||||
|
||||
def parse_source_status(
|
||||
response: bytes, expected_action: int
|
||||
) -> tuple[int, int, int, int | None]:
|
||||
if not response:
|
||||
raise RuntimeError("empty Companion response")
|
||||
if response[0] == RESP_ERR:
|
||||
code = response[1] if len(response) > 1 else -1
|
||||
raise RuntimeError(f"BLE mOTA source action failed (error {code})")
|
||||
if (
|
||||
len(response) not in (7, 11)
|
||||
or response[0] != RESP_OK
|
||||
or response[1] != expected_action
|
||||
):
|
||||
raise RuntimeError(f"malformed source status: {response.hex()}")
|
||||
flags = response[2]
|
||||
offered = struct.unpack_from("<H", response, 3)[0]
|
||||
advertised = struct.unpack_from("<H", response, 5)[0]
|
||||
packets_sent = (
|
||||
struct.unpack_from("<I", response, 7)[0]
|
||||
if len(response) == 11
|
||||
else None
|
||||
)
|
||||
return flags, offered, advertised, packets_sent
|
||||
|
||||
|
||||
async def resolve_device(device: str):
|
||||
wanted = device.casefold()
|
||||
found = await BleakScanner.find_device_by_filter(
|
||||
lambda candidate, _advertisement: (
|
||||
candidate.address.casefold() == wanted
|
||||
or (candidate.name is not None and candidate.name == device)
|
||||
),
|
||||
timeout=10.0,
|
||||
)
|
||||
if found is None:
|
||||
raise RuntimeError(f"Bluetooth device {device!r} was not found")
|
||||
return found
|
||||
|
||||
|
||||
def describe_status(
|
||||
flags: int,
|
||||
offered: int,
|
||||
advertised: int,
|
||||
packets_sent: int | None,
|
||||
) -> str:
|
||||
states = [
|
||||
"channel-ready" if flags & MOTA_FLAG_CHANNEL_READY else "channel-not-ready",
|
||||
"attached" if flags & MOTA_FLAG_ATTACHED else "detached",
|
||||
]
|
||||
if flags & MOTA_FLAG_ANOTHER_LINK_ACTIVE:
|
||||
states.append("another-source-link-active")
|
||||
packet_detail = (
|
||||
f"; {packets_sent} LoRa packets sent"
|
||||
if packets_sent is not None
|
||||
else "; LoRa packet count unavailable (legacy firmware)"
|
||||
)
|
||||
return (
|
||||
f"{', '.join(states)}; advertising {advertised}/{offered} files"
|
||||
f"{packet_detail}"
|
||||
)
|
||||
|
||||
|
||||
async def run(args: argparse.Namespace) -> None:
|
||||
if BLEAK_IMPORT_ERROR is not None:
|
||||
raise RuntimeError(
|
||||
"bleak is required; install it in an isolated environment, for "
|
||||
"example with 'pipx runpip <environment> install bleak'"
|
||||
) from BLEAK_IMPORT_ERROR
|
||||
if args.directory is not None:
|
||||
catalog = Catalog.scan(
|
||||
args.directory, args.recursive, args.motatool, args.verbose
|
||||
)
|
||||
else:
|
||||
catalog = Catalog([], args.verbose)
|
||||
|
||||
device = await resolve_device(args.device)
|
||||
stop = asyncio.Event()
|
||||
loop = asyncio.get_running_loop()
|
||||
for signal_name in (signal.SIGINT, signal.SIGTERM):
|
||||
try:
|
||||
loop.add_signal_handler(signal_name, stop.set)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def disconnected(_client: BleakClient) -> None:
|
||||
loop.call_soon_threadsafe(stop.set)
|
||||
|
||||
async with BleakClient(
|
||||
device, disconnected_callback=disconnected, pair=args.pair
|
||||
) as client:
|
||||
session = BleSession(client, catalog)
|
||||
await session.negotiate_mtu()
|
||||
await client.start_notify(NUS_TX_UUID, session.on_companion_data)
|
||||
needs_mota_channel = (
|
||||
args.directory is not None or args.source == "start"
|
||||
)
|
||||
serve_task: asyncio.Task[None] | None = None
|
||||
if needs_mota_channel:
|
||||
await client.start_notify(MOTA_REQUEST_UUID, session.on_mota_request)
|
||||
serve_task = asyncio.create_task(session.serve(stop))
|
||||
source_started = False
|
||||
temp_radio_requested = False
|
||||
try:
|
||||
for command in args.local:
|
||||
print(await session.local_control(command))
|
||||
if command.startswith("tempradio "):
|
||||
temp_radio_requested = True
|
||||
|
||||
action = args.source
|
||||
if action is None and args.directory is not None:
|
||||
action = "start"
|
||||
if action is not None:
|
||||
action_code = {
|
||||
"status": MOTA_ACTION_STATUS,
|
||||
"start": MOTA_ACTION_START,
|
||||
"stop": MOTA_ACTION_STOP,
|
||||
}[action]
|
||||
flags, offered, advertised, packets_sent = (
|
||||
await session.source_action(action_code)
|
||||
)
|
||||
print(describe_status(
|
||||
flags, offered, advertised, packets_sent
|
||||
))
|
||||
source_started = action == "start" and bool(
|
||||
flags & MOTA_FLAG_ATTACHED
|
||||
)
|
||||
|
||||
if source_started:
|
||||
if args.seconds > 0:
|
||||
try:
|
||||
await asyncio.wait_for(stop.wait(), args.seconds)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
else:
|
||||
print("Serving over Bluetooth; press Ctrl-C to stop")
|
||||
await stop.wait()
|
||||
finally:
|
||||
if client.is_connected and source_started:
|
||||
try:
|
||||
flags, offered, advertised, packets_sent = (
|
||||
await session.source_action(MOTA_ACTION_STOP)
|
||||
)
|
||||
print(describe_status(
|
||||
flags, offered, advertised, packets_sent
|
||||
))
|
||||
except Exception as exc: # best effort during disconnect
|
||||
print(f"warning: could not stop BLE source cleanly: {exc}",
|
||||
file=sys.stderr)
|
||||
if (client.is_connected and temp_radio_requested
|
||||
and not args.leave_temp_radio):
|
||||
try:
|
||||
print(await session.local_control("normalradio"))
|
||||
except Exception as exc: # best effort during disconnect
|
||||
print(f"warning: could not restore normal radio: {exc}",
|
||||
file=sys.stderr)
|
||||
stop.set()
|
||||
if serve_task is not None:
|
||||
await serve_task
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Control and serve LoRa mOTA files through an nRF52 Full "
|
||||
"Companion's encrypted Bluetooth link"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device", required=True,
|
||||
help="BLE address or exact advertised MeshCore device name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir", dest="directory", type=Path,
|
||||
help="folder of verified .mota files to serve",
|
||||
)
|
||||
parser.add_argument("--recursive", action="store_true")
|
||||
parser.add_argument("--motatool", default="motatool")
|
||||
parser.add_argument(
|
||||
"--local", action="append", default=[], metavar="COMMAND",
|
||||
help="run an allowed tempradio, normalradio, or ota command",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source", choices=("status", "start", "stop"),
|
||||
help="query or change BLE source state; --dir defaults to start",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seconds", type=float, default=0,
|
||||
help="stop serving after this many seconds (default: until Ctrl-C)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pair", action="store_true",
|
||||
help="request pairing before accessing the MITM-protected service",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--leave-temp-radio", action="store_true",
|
||||
help="do not send normalradio when this process exits",
|
||||
)
|
||||
parser.add_argument("--verbose", action="store_true")
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
if args.seconds < 0:
|
||||
raise SystemExit("--seconds cannot be negative")
|
||||
if args.source == "start" and args.directory is None:
|
||||
raise SystemExit("--source start requires --dir")
|
||||
try:
|
||||
asyncio.run(run(args))
|
||||
except (BleakError, OSError, RuntimeError, ValueError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import unittest
|
||||
|
||||
import ble_mota_seeder as seeder
|
||||
|
||||
|
||||
class SourceStatusTests(unittest.TestCase):
|
||||
def test_parses_current_status_with_packet_count(self):
|
||||
response = bytes.fromhex("0001030200020078563412")
|
||||
self.assertEqual(
|
||||
seeder.parse_source_status(response, seeder.MOTA_ACTION_START),
|
||||
(3, 2, 2, 0x12345678),
|
||||
)
|
||||
|
||||
def test_accepts_legacy_status_without_packet_count(self):
|
||||
response = bytes.fromhex("00010302000200")
|
||||
self.assertEqual(
|
||||
seeder.parse_source_status(response, seeder.MOTA_ACTION_START),
|
||||
(3, 2, 2, None),
|
||||
)
|
||||
|
||||
def test_rejects_wrong_action_and_malformed_lengths(self):
|
||||
current = bytes.fromhex("0001030200020078563412")
|
||||
with self.assertRaisesRegex(RuntimeError, "malformed source status"):
|
||||
seeder.parse_source_status(current, seeder.MOTA_ACTION_STOP)
|
||||
with self.assertRaisesRegex(RuntimeError, "malformed source status"):
|
||||
seeder.parse_source_status(current[:8], seeder.MOTA_ACTION_START)
|
||||
|
||||
def test_reports_firmware_error(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "error 4"):
|
||||
seeder.parse_source_status(
|
||||
bytes((seeder.RESP_ERR, 4)), seeder.MOTA_ACTION_START
|
||||
)
|
||||
|
||||
|
||||
class SourceActionTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_waits_for_and_parses_current_status(self):
|
||||
response = bytes.fromhex("0001030200020078563412")
|
||||
session = object.__new__(seeder.BleSession)
|
||||
|
||||
async def companion_command(frame, expected_length, timeout):
|
||||
self.assertEqual(
|
||||
frame,
|
||||
bytes((seeder.CMD_BLE_MOTA_SOURCE, seeder.MOTA_ACTION_START)),
|
||||
)
|
||||
self.assertEqual(timeout, 30.0)
|
||||
self.assertIsNone(expected_length(bytes((seeder.RESP_OK,))))
|
||||
self.assertEqual(expected_length(response), 11)
|
||||
return response
|
||||
|
||||
session.companion_command = companion_command
|
||||
self.assertEqual(
|
||||
await session.source_action(seeder.MOTA_ACTION_START),
|
||||
(3, 2, 2, 0x12345678),
|
||||
)
|
||||
|
||||
async def test_accepts_legacy_seven_byte_status(self):
|
||||
response = bytes.fromhex("00000300000000")
|
||||
session = object.__new__(seeder.BleSession)
|
||||
|
||||
async def companion_command(_frame, expected_length, timeout):
|
||||
self.assertEqual(timeout, 30.0)
|
||||
self.assertEqual(expected_length(response), 7)
|
||||
return response
|
||||
|
||||
session.companion_command = companion_command
|
||||
self.assertEqual(
|
||||
await session.source_action(seeder.MOTA_ACTION_STATUS),
|
||||
(3, 0, 0, None),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,6 +10,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -2403,9 +2404,27 @@ class Rak3401KnownUnsafeReleaseTests(unittest.TestCase):
|
||||
class MotatoolIntegrationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.motatool = os.environ.get("MOTATOOL_TEST_BIN")
|
||||
candidates = [
|
||||
os.environ.get("MOTATOOL_TEST_BIN"),
|
||||
shutil.which("motatool"),
|
||||
str(
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "motatool"
|
||||
/ "target"
|
||||
/ "release"
|
||||
/ "motatool"
|
||||
),
|
||||
]
|
||||
cls.motatool = next(
|
||||
(candidate for candidate in candidates
|
||||
if candidate and Path(candidate).is_file()),
|
||||
None,
|
||||
)
|
||||
if not cls.motatool or not Path(cls.motatool).is_file():
|
||||
raise unittest.SkipTest("set MOTATOOL_TEST_BIN to run motatool integration tests")
|
||||
raise unittest.SkipTest(
|
||||
"install/build motatool or set MOTATOOL_TEST_BIN to run "
|
||||
"motatool integration tests"
|
||||
)
|
||||
subprocess.run([cls.motatool, "--version"], check=True, capture_output=True)
|
||||
|
||||
def test_raw_esp32_zip_becomes_full_mota(self) -> None:
|
||||
|
||||
+12
-5
@@ -88,8 +88,11 @@ def test_full_esp32_profile_unifies_usb_logging_and_wifi_mqtt():
|
||||
|
||||
matrix = build.split("run_logging_matrix_build_targets()", 1)[1]
|
||||
matrix = matrix.split("run_build_targets()", 1)[0]
|
||||
assert 'is_companion_radio_full_target "$target"' in matrix
|
||||
assert "input-capable single-TTY terminal" in matrix
|
||||
assert 'uses_merged_standard_usb_logging "$target"' in matrix
|
||||
assert 'run_full_esp32_profile "FULL unified pass"' in matrix
|
||||
assert 'run_full_esp32_profile "FULL logging fallback pass"' in matrix
|
||||
assert "Single-TTY boards first switch CDC 0" in mqtt_gate
|
||||
assert "input-capable terminal" in mqtt_gate
|
||||
|
||||
|
||||
def test_espnow_tx_power_matches_cli_callback_contract():
|
||||
@@ -136,10 +139,14 @@ def test_flash_constrained_stm32_repeaters_pin_the_size_qualified_toolchain():
|
||||
root / "examples/simple_repeater/MyMesh.cpp"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "#if MESH_PACKET_LOGGING_COMPACT" in dispatcher
|
||||
assert "Utils::printHex(logging_port, raw, len);" in dispatcher
|
||||
assert 'line.printf("T");' in dispatcher
|
||||
assert "line.hex(raw, len);" in dispatcher
|
||||
assert "line.flush(usbLoggingPort(), false);" in dispatcher
|
||||
assert "!MESH_PACKET_LOGGING_COMPACT" in dispatcher
|
||||
assert "#if MESH_PACKET_LOGGING_COMPACT" in repeater
|
||||
assert "mesh::Utils::printHex(logging_port, raw, len);" in repeater
|
||||
assert 'line.printf("R");' in repeater
|
||||
assert "line.hex(raw, len);" in repeater
|
||||
assert "line.flush(mesh::usbLoggingPort(), false);" in repeater
|
||||
|
||||
|
||||
def test_usb_companion_profiles_enable_the_usb_transport():
|
||||
@@ -186,7 +193,7 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
|
||||
redundant = build.split("is_redundant_bulk_build_target()", 1)[1]
|
||||
redundant = redundant.split("resolve_logging_matrix_firmwares()", 1)[0]
|
||||
assert "is_runtime_setting_alias_target" in redundant
|
||||
assert "is_companion_transport_replaced_by_full" in redundant
|
||||
assert "is_firmware_role_replaced_by_canonical_artifact" in redundant
|
||||
|
||||
logging_matrix = build.split("resolve_logging_matrix_firmwares()", 1)[1]
|
||||
logging_matrix = logging_matrix.split("resolve_companion_firmwares()", 1)[0]
|
||||
|
||||
@@ -79,6 +79,12 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
|
||||
[env:Heltec_E290_companion_usb_ble]
|
||||
extends = env:Heltec_E290_companion_usb
|
||||
build_flags =
|
||||
${env:Heltec_E290_companion_usb.build_flags}
|
||||
-D BLE_PIN_CODE=123456
|
||||
|
||||
[env:Heltec_E290_repeater]
|
||||
extends = Heltec_E290_base
|
||||
build_flags =
|
||||
|
||||
@@ -63,6 +63,7 @@ upload_protocol = nrfutil
|
||||
[env:Heltec_t096_repeater]
|
||||
extends = Heltec_t096
|
||||
build_src_filter = ${Heltec_t096.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
build_flags =
|
||||
@@ -74,6 +75,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D POWERSAVING_DEBUG=1
|
||||
@@ -82,6 +88,7 @@ build_flags =
|
||||
[env:Heltec_t096_repeater_lora_ota_no_external_sensors]
|
||||
extends = Heltec_t096
|
||||
build_src_filter = ${Heltec_t096.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${Heltec_t096.build_flags}
|
||||
@@ -92,6 +99,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
lib_deps =
|
||||
${nrf52_lora_ota.lib_deps}
|
||||
${nrf52_no_external_sensors_keep_gps.lib_deps}
|
||||
@@ -109,6 +121,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
; -D BRIDGE_DEBUG=1
|
||||
@@ -131,6 +144,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
build_src_filter = ${Heltec_t096.build_src_filter}
|
||||
|
||||
@@ -50,6 +50,7 @@ upload_protocol = nrfutil
|
||||
[env:Heltec_t114_without_display_repeater]
|
||||
extends = Heltec_t114
|
||||
build_src_filter = ${Heltec_t114.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${Heltec_t114.build_flags}
|
||||
@@ -58,6 +59,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
@@ -71,6 +77,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
; -D BRIDGE_DEBUG=1
|
||||
@@ -163,6 +170,7 @@ upload_protocol = nrfutil
|
||||
[env:Heltec_t114_repeater]
|
||||
extends = Heltec_t114_with_display
|
||||
build_src_filter = ${Heltec_t114_with_display.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
build_flags =
|
||||
@@ -174,6 +182,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D POWERSAVING_DEBUG=1
|
||||
@@ -182,6 +195,7 @@ build_flags =
|
||||
[env:Heltec_t114_repeater_lora_ota_no_external_sensors]
|
||||
extends = Heltec_t114_with_display
|
||||
build_src_filter = ${Heltec_t114_with_display.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${Heltec_t114_with_display.build_flags}
|
||||
@@ -192,6 +206,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
lib_deps =
|
||||
${nrf52_lora_ota.lib_deps}
|
||||
${nrf52_no_external_sensors_keep_gps.lib_deps}
|
||||
@@ -207,6 +226,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=10
|
||||
; -D BRIDGE_DEBUG=1
|
||||
|
||||
@@ -83,6 +83,12 @@ lib_deps =
|
||||
${Heltec_T190_base.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:Heltec_T190_companion_radio_usb_ble_]
|
||||
extends = env:Heltec_T190_companion_radio_usb_
|
||||
build_flags =
|
||||
${env:Heltec_T190_companion_radio_usb_.build_flags}
|
||||
-D BLE_PIN_CODE=123456
|
||||
|
||||
[env:Heltec_T190_repeater_]
|
||||
extends = Heltec_T190_base
|
||||
build_flags =
|
||||
|
||||
@@ -48,6 +48,11 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
extra_scripts =
|
||||
@@ -57,6 +62,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ota/*.cpp>
|
||||
+<helpers/ota/detools/detools.c>
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
-<helpers/bridges/MQTTBridge.cpp>
|
||||
-<helpers/MQTTMessageBuilder.cpp>
|
||||
@@ -77,6 +83,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
; -D BRIDGE_DEBUG=1
|
||||
@@ -391,9 +398,15 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
-<helpers/bridges/MQTTBridge.cpp>
|
||||
-<helpers/MQTTMessageBuilder.cpp>
|
||||
@@ -413,6 +426,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
; -D BRIDGE_DEBUG=1
|
||||
|
||||
@@ -56,6 +56,7 @@ extends = LilyGo_TLora_V2_1_1_6
|
||||
; upload handler instead of ElegantOTA on this ultra-tight 4 MB target.
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${LilyGo_TLora_V2_1_1_6.build_flags}
|
||||
@@ -67,6 +68,11 @@ build_flags =
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D FLOOD_CHANNEL_SCOPE_SLOTS=4 ; ultra-tight classic ESP32: three wildcards plus one exact channel
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=34
|
||||
-D WITH_RS232_BRIDGE_TX=25
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D CORE_DEBUG_LEVEL=3
|
||||
@@ -296,6 +302,7 @@ build_flags =
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; classic ESP32 DRAM cannot fit the default 255 with bridge logging
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=34
|
||||
-D WITH_RS232_BRIDGE_TX=25
|
||||
-D DISABLE_LORA_OTA=1 ; RS232 bridge plus LoRa OTA overflows classic ESP32 DRAM
|
||||
|
||||
@@ -42,6 +42,7 @@ build_src_filter = ${Promicro.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
build_flags =
|
||||
${Promicro.build_flags}
|
||||
-D ADVERT_NAME='"ProMicro Repeater"'
|
||||
@@ -50,6 +51,12 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
-D WITH_RS232_BRIDGE_GPS_CONFLICT_UART=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
lib_deps = ${Promicro.lib_deps}
|
||||
@@ -71,6 +78,7 @@ build_flags =
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-UENV_INCLUDE_GPS
|
||||
|
||||
@@ -44,9 +44,15 @@ build_flags = ${rak11310.build_flags}
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=8
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${rak11310.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
[env:RAK_11310_repeater_bridge_rs232]
|
||||
@@ -58,6 +64,7 @@ build_flags = ${rak11310.build_flags}
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=8
|
||||
; -D BRIDGE_DEBUG=1
|
||||
|
||||
@@ -47,9 +47,15 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${rak3112.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${rak3112.lib_deps}
|
||||
@@ -66,6 +72,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=5
|
||||
-D WITH_RS232_BRIDGE_TX=6
|
||||
; -D BRIDGE_DEBUG=1
|
||||
|
||||
@@ -55,12 +55,23 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX
|
||||
-D WITH_RS232_BRIDGE_ALT=Serial1
|
||||
-D WITH_RS232_BRIDGE_ALT_UART=1
|
||||
-D WITH_RS232_BRIDGE_ALT_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_ALT_TX=PIN_SERIAL1_TX
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
-D WITH_RS232_BRIDGE_GPS_CONFLICT_UART=1
|
||||
; -D OTA_DEBUG=1 ; bring-up: trace OTA fetch (REQ / block / page-flush) over Serial
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D POWERSAVING_DEBUG=1
|
||||
build_src_filter = ${rak4631.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
; Shared sources remain inherited, but ENABLE_OTA is explicitly undefined for this oversized role.
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
@@ -104,11 +115,22 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX
|
||||
-D WITH_RS232_BRIDGE_ALT=Serial1
|
||||
-D WITH_RS232_BRIDGE_ALT_UART=1
|
||||
-D WITH_RS232_BRIDGE_ALT_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_ALT_TX=PIN_SERIAL1_TX
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
-D WITH_RS232_BRIDGE_GPS_CONFLICT_UART=1
|
||||
build_src_filter = ${nrf52_lora_ota.build_src_filter}
|
||||
+<../variants/rak4631>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${nrf52_lora_ota.lib_deps}
|
||||
@@ -150,6 +172,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-UENV_INCLUDE_GPS
|
||||
@@ -176,6 +199,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
build_src_filter = ${rak4631.build_src_filter}
|
||||
@@ -200,6 +224,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX
|
||||
-UENV_INCLUDE_GPS
|
||||
@@ -226,6 +251,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX
|
||||
build_src_filter = ${rak4631.build_src_filter}
|
||||
@@ -332,6 +358,20 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
|
||||
|
||||
[env:RAK_4631_companion_radio_full]
|
||||
extends = env:RAK_4631_companion_radio_usb
|
||||
build_flags =
|
||||
${env:RAK_4631_companion_radio_usb.build_flags}
|
||||
-D ETHERNET_ENABLED=1
|
||||
-D ETHERNET_USE_RAK13800
|
||||
-D ETHERNET_CLASS=RAK13800EthernetInterface
|
||||
build_src_filter = ${env:RAK_4631_companion_radio_usb.build_src_filter}
|
||||
+<helpers/ethernet/*.cpp>
|
||||
+<helpers/ethernet/RAK13800/>
|
||||
lib_deps =
|
||||
${env:RAK_4631_companion_radio_usb.lib_deps}
|
||||
https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
|
||||
|
||||
|
||||
[env:RAK_4631_companion_radio_ble]
|
||||
extends = rak4631
|
||||
|
||||
@@ -217,6 +217,13 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
end2endzone/NonBlockingRTTTL@^1.3.0
|
||||
|
||||
[env:ThinkNode_M2_companion_radio_full]
|
||||
extends = env:ThinkNode_M2_companion_radio_wifi
|
||||
build_flags =
|
||||
${env:ThinkNode_M2_companion_radio_wifi.build_flags}
|
||||
-D SERIAL_TX=43
|
||||
-D SERIAL_RX=44
|
||||
|
||||
[env:ThinkNode_M2_kiss_modem]
|
||||
extends = ThinkNode_M2
|
||||
build_flags = ${ThinkNode_M2.build_flags}
|
||||
|
||||
@@ -233,6 +233,13 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
end2endzone/NonBlockingRTTTL@^1.3.0
|
||||
|
||||
[env:ThinkNode_M5_companion_radio_full]
|
||||
extends = env:ThinkNode_M5_companion_radio_wifi
|
||||
build_flags =
|
||||
${env:ThinkNode_M5_companion_radio_wifi.build_flags}
|
||||
-D SERIAL_TX=43
|
||||
-D SERIAL_RX=44
|
||||
|
||||
[env:ThinkNode_M5_kiss_modem]
|
||||
extends = ThinkNode_M5
|
||||
build_src_filter = ${ThinkNode_M5.build_src_filter}
|
||||
|
||||
@@ -175,6 +175,17 @@ lib_deps = ${ThinkNode_M7.lib_deps}
|
||||
${ThinkNode_M7_ethernet.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:ThinkNode_M7_companion_radio_full]
|
||||
extends = env:ThinkNode_M7_companion_radio_wifi
|
||||
build_flags =
|
||||
${env:ThinkNode_M7_companion_radio_wifi.build_flags}
|
||||
${ThinkNode_M7_ethernet.build_flags}
|
||||
build_src_filter = ${env:ThinkNode_M7_companion_radio_wifi.build_src_filter}
|
||||
${ThinkNode_M7_ethernet.build_src_filter}
|
||||
lib_deps =
|
||||
${env:ThinkNode_M7_companion_radio_wifi.lib_deps}
|
||||
${ThinkNode_M7_ethernet.lib_deps}
|
||||
|
||||
[env:ThinkNode_M7_kiss_modem]
|
||||
extends = ThinkNode_M7
|
||||
build_src_filter = ${ThinkNode_M7.build_src_filter}
|
||||
|
||||
@@ -43,9 +43,15 @@ build_flags = ${waveshare_rp2040_lora.build_flags}
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=8
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${waveshare_rp2040_lora.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
[env:waveshare_rp2040_lora_repeater_bridge_rs232]
|
||||
@@ -57,6 +63,7 @@ build_flags = ${waveshare_rp2040_lora.build_flags}
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=9
|
||||
-D WITH_RS232_BRIDGE_TX=8
|
||||
; -D BRIDGE_DEBUG=1
|
||||
|
||||
@@ -78,6 +78,7 @@ build_flags = ${lora_e5.build_flags}
|
||||
-D FLOOD_CHANNEL_SCOPE_SLOTS=7
|
||||
-D ENABLE_HWSERIAL2
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
-D WITH_RS232_BRIDGE_RX=PA3
|
||||
-D WITH_RS232_BRIDGE_TX=PA2
|
||||
build_src_filter = ${lora_e5.build_src_filter}
|
||||
|
||||
@@ -213,7 +213,14 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-D BRIDGE_MAX_BAUD=500000
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
build_src_filter = ${solarxiao.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
|
||||
[env:solarxiao_33S_repeater]
|
||||
@@ -236,9 +243,11 @@ build_flags =
|
||||
${env:solarxiao_30S_repeater.build_flags}
|
||||
-D ADVERT_NAME='"SolarXiao 30S RS232"'
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-D BRIDGE_MAX_BAUD=500000
|
||||
-D RS232_BRIDGE_DEFAULT_ON=1
|
||||
build_src_filter = ${env:solarxiao_30S_repeater.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
|
||||
|
||||
@@ -313,6 +313,8 @@ build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
|
||||
build_flags =
|
||||
${env:Xiao_S3_WIO_companion_radio_wifi.build_flags}
|
||||
${esp32_s3_dual_cdc_full.build_flags}
|
||||
-D SERIAL_TX=D6
|
||||
-D SERIAL_RX=D7
|
||||
|
||||
[env:Xiao_S3_WIO_sensor]
|
||||
extends = Xiao_S3_WIO
|
||||
|
||||
Reference in New Issue
Block a user