mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-29 06:08:18 +00:00
fix: harden firmware builds and runtime reliability
This commit is contained in:
@@ -100,8 +100,8 @@ Commands:
|
||||
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. Supported Full Companion targets provide an optional dedicated logging USB port.
|
||||
build-companion-firmwares-logging-matrix: Build canonical Companion targets in each applicable standard, MQTT, and expanded FULL profile. Dual-CDC Full Companion replaces separate USB, BLE, WiFi, and USB-logging artifacts where supported.
|
||||
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-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>.
|
||||
@@ -414,6 +414,79 @@ for section, options in data:
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"
|
||||
done
|
||||
|
||||
# Some qualified boards historically published only BLE, or BLE plus USB,
|
||||
# even though the same recipe has enough flash and RAM for every Companion
|
||||
# transport on that platform. Build these measured profiles from the BLE
|
||||
# recipe so the radio, display, GPS, and sensor wiring stays exact. ESP32
|
||||
# adds USB and WiFi below; nRF52 adds native USB. Legacy transport names
|
||||
# remain explicit-build aliases and canonical releases use the Full target.
|
||||
local -a qualified_esp32_full_companion_bases=(
|
||||
M5Stack_Unit_C6L_companion_radio_ble
|
||||
Heltec_Wireless_Tracker_companion_radio_ble
|
||||
LilyGo_T3S3_sx1276_companion_radio_ble
|
||||
Heltec_ct62_companion_radio_ble
|
||||
Meshadventurer_sx1262_companion_radio_ble
|
||||
Meshadventurer_sx1268_companion_radio_ble
|
||||
Heltec_Wireless_Paper_companion_radio_ble
|
||||
Heltec_E213_companion_radio_ble
|
||||
Xiao_S3_companion_radio_ble
|
||||
LilyGo_TETH_Elite_sx1262_companion_radio_ble
|
||||
LilyGo_T3S3_sx1262_companion_radio_ble
|
||||
LilyGo_TDeck_companion_radio_ble
|
||||
Ebyte_EoRa-S3_companion_radio_ble
|
||||
Tbeam_SX1262_companion_radio_ble
|
||||
Tbeam_SX1276_companion_radio_ble
|
||||
T_Beam_S3_Supreme_SX1262_companion_radio_ble
|
||||
)
|
||||
local -a qualified_nrf52_full_companion_bases=(
|
||||
GAT562_Mesh_Watch13_companion_radio_ble
|
||||
LilyGo_T-Echo-Lite_companion_radio_ble
|
||||
LilyGo_T_Impulse_Plus_companion_radio_ble
|
||||
WioTrackerL1Eink_companion_radio_ble
|
||||
)
|
||||
|
||||
for env_name in "${qualified_esp32_full_companion_bases[@]}"; do
|
||||
full_env=${env_name/companion_radio_ble/companion_radio_full}
|
||||
if [ -n "${PIO_ENV_PLATFORM_BY_NAME[$full_env]+x}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" != "ESP32_PLATFORM" ]; then
|
||||
echo "Qualified Full Companion base is missing or not ESP32: ${env_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
SUPPORTED_PIO_ENVS+=("$full_env")
|
||||
PIO_ENV_PLATFORM_BY_NAME["$full_env"]="ESP32_PLATFORM"
|
||||
PIO_ENV_BOARD_BY_NAME["$full_env"]="${PIO_ENV_BOARD_BY_NAME[$env_name]}"
|
||||
PIO_ENV_MQTT_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$full_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"
|
||||
done
|
||||
|
||||
for env_name in "${qualified_nrf52_full_companion_bases[@]}"; do
|
||||
full_env=${env_name/companion_radio_ble/companion_radio_full}
|
||||
if [ -n "${PIO_ENV_PLATFORM_BY_NAME[$full_env]+x}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" != "NRF52_PLATFORM" ]; then
|
||||
echo "Qualified Full Companion base is missing or not nRF52: ${env_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
SUPPORTED_PIO_ENVS+=("$full_env")
|
||||
PIO_ENV_PLATFORM_BY_NAME["$full_env"]="NRF52_PLATFORM"
|
||||
PIO_ENV_BOARD_BY_NAME["$full_env"]="${PIO_ENV_BOARD_BY_NAME[$env_name]}"
|
||||
PIO_ENV_MQTT_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$full_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1605,14 +1678,13 @@ print_release_firmware_targets() {
|
||||
get-companion-firmwares-to-build)
|
||||
get_pio_envs_ending_with_string "_companion_radio_usb"
|
||||
get_pio_envs_ending_with_string "_companion_radio_ble"
|
||||
# A dual-CDC Full Companion supplies every ordinary attached transport,
|
||||
# a separate USB logging interface, and source-only mOTA in one image. It
|
||||
# replaces separate transport releases without becoming an OTA target.
|
||||
# Full Companion supplies every ordinary attached transport and
|
||||
# source-only mOTA in one image. Dual-CDC boards use a separate logging
|
||||
# port; single-TTY boards switch that port between Binary Companion and
|
||||
# an input-capable plaintext logging terminal.
|
||||
local env_name
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if is_companion_radio_full_target "$env_name" \
|
||||
&& { is_nrf52_companion_radio_full_target "$env_name" \
|
||||
|| is_esp32_dual_cdc_companion_radio_full_target "$env_name"; } \
|
||||
&& ! is_redundant_bulk_build_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
@@ -2067,8 +2139,19 @@ normalize_resolved_targets_for_mqtt() {
|
||||
}
|
||||
|
||||
disable_debug_flags() {
|
||||
local env_name=${1:-}
|
||||
local usb_logging_undefs="-UMESH_DEBUG -UMESH_PACKET_LOGGING"
|
||||
|
||||
# Full Companion always carries diagnostics behind its saved runtime gate.
|
||||
# PlatformIO groups -U flags after -D flags, so emitting these undefines here
|
||||
# would override apply_companion_radio_full_profile() regardless of the
|
||||
# apparent order in PLATFORMIO_BUILD_FLAGS.
|
||||
if [ -n "$env_name" ] && is_companion_radio_full_target "$env_name"; then
|
||||
usb_logging_undefs=""
|
||||
fi
|
||||
|
||||
if [ "$DISABLE_DEBUG" == "1" ]; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UMESH_PACKET_LOGGING -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -DCFG_DEBUG=0 -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} ${usb_logging_undefs} -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -DCFG_DEBUG=0 -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -2084,12 +2167,21 @@ apply_mqtt_bridge_override() {
|
||||
}
|
||||
|
||||
apply_debug_overrides() {
|
||||
local env_name=${1:-}
|
||||
local preserve_full_companion_logging=0
|
||||
|
||||
if [ -n "$env_name" ] && is_companion_radio_full_target "$env_name"; then
|
||||
preserve_full_companion_logging=1
|
||||
fi
|
||||
|
||||
case "${MESHDEBUG_OVERRIDE,,}" in
|
||||
on)
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_DEBUG=1"
|
||||
;;
|
||||
off)
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG"
|
||||
if [ "$preserve_full_companion_logging" -eq 0 ]; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -2098,7 +2190,9 @@ apply_debug_overrides() {
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_PACKET_LOGGING=1"
|
||||
;;
|
||||
off)
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_PACKET_LOGGING"
|
||||
if [ "$preserve_full_companion_logging" -eq 0 ]; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_PACKET_LOGGING"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -2112,15 +2206,10 @@ apply_debug_overrides() {
|
||||
disable_usb_logging_for_mqtt() {
|
||||
local env_name=$1
|
||||
|
||||
# Full Companion may enable diagnostics only when it has a dedicated second
|
||||
# CDC interface. Plaintext on its primary framed stream corrupts Companion
|
||||
# traffic.
|
||||
# Full Companion always compiles diagnostics behind a saved runtime gate.
|
||||
# Dual-CDC boards write them to CDC 1. Single-TTY boards first switch CDC 0
|
||||
# into an input-capable terminal so plaintext cannot mix with framed traffic.
|
||||
if is_companion_radio_full_target "$env_name"; then
|
||||
if is_nrf52_companion_radio_full_target "$env_name" \
|
||||
|| is_esp32_dual_cdc_companion_radio_full_target "$env_name"; then
|
||||
return 0
|
||||
fi
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UMESH_PACKET_LOGGING -UMQTT_DEBUG -UMQTT_MEMORY_DEBUG"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -2217,7 +2306,7 @@ requires_esp32_companion_full_ota_fallback() {
|
||||
# internal DRAM. Keep their ordinary high-capacity image unchanged and emit
|
||||
# a separately named FULL OTA image with measured-safe capacities.
|
||||
case "${1,,}" in
|
||||
heltec_v2_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi|meshadventurer_sx1262_companion_radio_usb|meshadventurer_sx1268_companion_radio_usb) return 0 ;;
|
||||
heltec_v2_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -2451,6 +2540,7 @@ declare_build_capability_contract() {
|
||||
record_build_expectation "companion.usb" "+++MESHCORE-TERM-START"
|
||||
record_build_expectation "companion.bluetooth" \
|
||||
"Companion: starting Bluetooth"
|
||||
record_build_expectation "companion.usb_logging" "get usb.logging"
|
||||
if is_nrf52_companion_radio_full_target "$env_name" \
|
||||
|| is_esp32_dual_cdc_companion_radio_full_target "$env_name"; then
|
||||
record_build_expectation "companion.dedicated_usb_logging" \
|
||||
@@ -2885,7 +2975,7 @@ apply_companion_radio_full_profile() {
|
||||
# folder transport. Full also restores WebConfig when a constrained legacy
|
||||
# WiFi sibling disabled it only to fit its smaller application partition.
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1 -DDISABLE_LORA_OTA=1 -DWEBCONFIG_DISABLED=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -UWEBCONFIG_DISABLED -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -UWEBCONFIG_DISABLED -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1"
|
||||
|
||||
if is_nrf52_companion_radio_full_target "$env_name"; then
|
||||
# CDC 0 starts as Binary Companion. `motatool serve --serial` switches it
|
||||
@@ -2920,19 +3010,43 @@ apply_companion_radio_full_profile() {
|
||||
# mode when WiFi is unavailable and serve the same folder protocol there.
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWIFI_OTA_SEEDER=1 -DCOMPANION_FEATURE_NETWORK_TERMINAL=1 -DCOMPANION_FEATURE_MEMORY_DIAGNOSTICS=1"
|
||||
|
||||
# Qualified BLE-based Full recipes did not previously need WiFi credentials.
|
||||
# Supply the same first-boot setup placeholders used by ordinary WiFi
|
||||
# Companion recipes; saved credentials and WebConfig replace them at runtime.
|
||||
if ! pio_env_option_contains "$pio_env_name" build_flags "WIFI_SSID"; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWIFI_SSID='\"WIFI_SSID\"' -DWIFI_PWD='\"Password\"'"
|
||||
fi
|
||||
|
||||
# BLE + WiFi exhaust internal DRAM on these high-capacity ESP32 recipes. Use
|
||||
# measured-safe tables for FULL OTA without changing ordinary USB/BLE/WiFi
|
||||
# companion builds.
|
||||
if requires_esp32_companion_full_ota_fallback "$pio_env_name"; then
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=512 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
|
||||
record_build_reduction \
|
||||
"companion.capacity limited to 100 contacts, 8 channels, and 16 queued frames by measured internal DRAM"
|
||||
case "${env_name,,}" in
|
||||
meshadventurer_sx1262_companion_radio_full|\
|
||||
meshadventurer_sx1268_companion_radio_full)
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=128"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=30 -DOFFLINE_QUEUE_SIZE=64"
|
||||
record_build_reduction \
|
||||
"companion.capacity limited to 160 contacts, 30 channels, and 64 queued frames by measured internal DRAM"
|
||||
;;
|
||||
*)
|
||||
if requires_esp32_companion_full_ota_fallback "$pio_env_name"; then
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=512 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
|
||||
record_build_reduction \
|
||||
"companion.capacity limited to 100 contacts, 8 channels, and 16 queued frames by measured internal DRAM"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# A few BLE recipes list only their BLE implementation instead of the full
|
||||
# ESP32 helper directory. Add the WiFi transport explicitly in that case.
|
||||
if ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/*.cpp" \
|
||||
&& ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/SerialWifiInterface.cpp"; then
|
||||
append_platformio_build_src_filter "+<helpers/esp32/SerialWifiInterface.cpp>"
|
||||
fi
|
||||
|
||||
# A few WiFi recipes list only their WiFi implementation instead of the
|
||||
# helpers/esp32 wildcard used by newer boards. Add the BLE implementation
|
||||
# explicitly when the inherited source filter does not already include it.
|
||||
# WiFi recipes can have the inverse narrow filter. Preserve the existing
|
||||
# guard so both kinds of synthesized Full target receive both transports.
|
||||
if ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/*.cpp" \
|
||||
&& ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/SerialBLEInterface.cpp"; then
|
||||
append_platformio_build_src_filter "+<helpers/esp32/SerialBLEInterface.cpp>"
|
||||
@@ -3371,8 +3485,8 @@ build_firmware() {
|
||||
fi
|
||||
|
||||
export PLATFORMIO_BUILD_FLAGS="${original_platformio_build_flags} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_BUILD_EPOCH=${firmware_build_epoch} -DFIRMWARE_VERSION='\"${embedded_version_string}\"' -DOTA_VARIANT='\"${env_name}\"'${mota_target_flag}"
|
||||
disable_debug_flags
|
||||
apply_debug_overrides
|
||||
disable_debug_flags "$env_name"
|
||||
apply_debug_overrides "$env_name"
|
||||
apply_mqtt_bridge_override
|
||||
disable_usb_logging_for_mqtt "$env_name"
|
||||
apply_lora_ota_override "$env_name"
|
||||
@@ -3564,7 +3678,7 @@ get_nrf52_full_companion_replacement() {
|
||||
printf '%s\n' "$full_env"
|
||||
}
|
||||
|
||||
get_esp32_dual_cdc_full_companion_replacement() {
|
||||
get_esp32_full_companion_replacement() {
|
||||
local source_env=$1
|
||||
local env_name=${source_env,,}
|
||||
local full_env=""
|
||||
@@ -3611,13 +3725,13 @@ get_esp32_dual_cdc_full_companion_replacement() {
|
||||
;;
|
||||
esac
|
||||
|
||||
is_esp32_dual_cdc_companion_radio_full_target "$full_env" || return 1
|
||||
is_esp32_companion_radio_full_target "$full_env" || return 1
|
||||
printf '%s\n' "$full_env"
|
||||
}
|
||||
|
||||
get_full_companion_replacement() {
|
||||
get_nrf52_full_companion_replacement "$1" 2>/dev/null \
|
||||
|| get_esp32_dual_cdc_full_companion_replacement "$1"
|
||||
|| get_esp32_full_companion_replacement "$1"
|
||||
}
|
||||
|
||||
is_companion_transport_replaced_by_full() {
|
||||
@@ -3647,9 +3761,9 @@ is_redundant_bulk_build_target() {
|
||||
}
|
||||
|
||||
resolve_logging_matrix_firmwares() {
|
||||
# Dual-CDC Full Companion replaces normal transport and USB-logging artifacts.
|
||||
# It exposes framed Companion traffic and plaintext logging as separate CDC
|
||||
# interfaces over one physical USB connection.
|
||||
# Full Companion replaces normal transport and USB-logging artifacts. It
|
||||
# either exposes separate framed/logging CDC interfaces or safely switches a
|
||||
# single TTY between Binary Companion and the plaintext logging terminal.
|
||||
resolve_all_firmwares
|
||||
}
|
||||
|
||||
@@ -4477,7 +4591,7 @@ run_logging_matrix_build_targets() {
|
||||
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; supported Full images can add a dedicated logging USB port after it is enabled and rebooted, while other Full images keep their single USB stream protocol-safe."
|
||||
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
|
||||
|
||||
@@ -403,9 +403,14 @@
|
||||
].includes(target);
|
||||
}
|
||||
|
||||
function omitTransportsReplacedByDualCdcFull(profiles) {
|
||||
function isFullCompanion(profile) {
|
||||
return Boolean(profile && profile.role === "companion" &&
|
||||
profile.mode === "full");
|
||||
}
|
||||
|
||||
function omitTransportsReplacedByFull(profiles) {
|
||||
const fullKeys = new Set((profiles || []).filter(function (profile) {
|
||||
return isDualCdcFullCompanion(profile);
|
||||
return isFullCompanion(profile);
|
||||
}).map(function (profile) {
|
||||
return profile.hardware + "\n" + profile.variant;
|
||||
}));
|
||||
@@ -419,16 +424,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyDualCdcFullCompanionCapabilities(profiles) {
|
||||
function applyFullCompanionCapabilities(profiles) {
|
||||
return (profiles || []).map(function (profile) {
|
||||
if (!isDualCdcFullCompanion(profile)) return profile;
|
||||
if (!isFullCompanion(profile)) return profile;
|
||||
|
||||
// Supported Full Companion images always expose framed Companion traffic
|
||||
// on interface 00. Logging defaults off; enabling it and rebooting adds
|
||||
// the independent plaintext CDC interface 02.
|
||||
// Every Full Companion has a runtime USB logging mode. Dual-CDC targets
|
||||
// add an independent plaintext interface after reboot; single-TTY
|
||||
// targets switch their existing port between framed and plaintext modes.
|
||||
profile.logging = "usb-runtime";
|
||||
profile.loggingModes = ["none", "usb"];
|
||||
profile.dedicatedUsbLogging = true;
|
||||
if (isDualCdcFullCompanion(profile)) {
|
||||
profile.dedicatedUsbLogging = true;
|
||||
}
|
||||
return profile;
|
||||
});
|
||||
}
|
||||
@@ -532,8 +539,8 @@
|
||||
}).filter(function (profile) {
|
||||
return !isHiddenLegacyProfile(profile);
|
||||
});
|
||||
const profiles = omitTransportsReplacedByDualCdcFull(
|
||||
applyDualCdcFullCompanionCapabilities(visibleProfiles)
|
||||
const profiles = omitTransportsReplacedByFull(
|
||||
applyFullCompanionCapabilities(visibleProfiles)
|
||||
).sort(function (a, b) {
|
||||
return a.target.localeCompare(b.target, undefined, {
|
||||
numeric: true,
|
||||
@@ -731,13 +738,17 @@
|
||||
"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") {
|
||||
extra.push(
|
||||
"Full Companion starts with USB logging off and only interface 00. Use get usb.logging, or set usb.logging on reboot to save logging on and reboot when needed; set usb.logging off reboot removes interface 02 again."
|
||||
);
|
||||
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."
|
||||
);
|
||||
extra.push(
|
||||
"Interface 00 always carries Companion/terminal/mOTA traffic. After logging is enabled and the node reboots, interface 02 carries plaintext logs. Match services by USB interface number instead of assuming tty or COM numbering."
|
||||
);
|
||||
} else {
|
||||
extra.push(
|
||||
"Full Companion starts with USB logging off and Binary Companion on its single TTY. Enter the text terminal and use set usb.logging on for plaintext logs; that TTY still accepts set usb.logging off and automatically returns to Binary Companion after the reply."
|
||||
);
|
||||
}
|
||||
}
|
||||
return common.concat(byKind[kind] || [], extra);
|
||||
@@ -1106,15 +1117,16 @@
|
||||
flattenReleaseAssets: flattenReleaseAssets,
|
||||
parseFirmwareAsset: parseFirmwareAsset,
|
||||
parseTargetProfile: parseTargetProfile,
|
||||
applyDualCdcFullCompanionCapabilities:
|
||||
applyDualCdcFullCompanionCapabilities,
|
||||
applyFullCompanionCapabilities: applyFullCompanionCapabilities,
|
||||
applyDualCdcFullCompanionCapabilities: applyFullCompanionCapabilities,
|
||||
applyNrf52FullCompanionCapabilities:
|
||||
applyDualCdcFullCompanionCapabilities,
|
||||
applyFullCompanionCapabilities,
|
||||
canonicalHardware: canonicalHardware,
|
||||
omitTransportsReplacedByFull: omitTransportsReplacedByFull,
|
||||
omitTransportsReplacedByDualCdcFull:
|
||||
omitTransportsReplacedByDualCdcFull,
|
||||
omitTransportsReplacedByFull,
|
||||
omitNrf52TransportsReplacedByFull:
|
||||
omitTransportsReplacedByDualCdcFull,
|
||||
omitTransportsReplacedByFull,
|
||||
hardwareFamilyFor: hardwareFamilyFor,
|
||||
humanizeHardwareVariant: humanizeHardwareVariant,
|
||||
buildCatalog: buildCatalog,
|
||||
|
||||
+12
-13
@@ -66,13 +66,14 @@ 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`. ESP32 roles covered by unified FULL and nRF52 Companions covered by dual-CDC Full Companion are not duplicated here. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| FULL ESP32 USB + WiFi | Uses the matching MQTT target with packet logging on, verbose debug off, and the complete command surface supported by that role and hardware. `get/set logging.output off\|usb\|wifi\|both` selects and persists the active output paths. |
|
||||
| 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. |
|
||||
|
||||
`logging`, `OTA`, and `FULL` describe independent build features. Do not infer
|
||||
@@ -92,16 +93,13 @@ available from a canonical image:
|
||||
replaced by their ordinary Station target. G2 boosted receive gain is the
|
||||
persisted `radio.rxgain on|off` setting; the G3 alias changed only the
|
||||
advertised default name.
|
||||
- When a board has a dual-CDC Full Companion, that one artifact replaces its
|
||||
- When a board has a Full Companion, that one artifact replaces its
|
||||
separate USB, BLE, ordinary WiFi, and USB packet-logging Companion artifacts.
|
||||
Current support includes nRF52 Full Companion plus qualified native-USB
|
||||
ESP32-S3 Full targets: Heltec V4, T-Beam 1W, Station G2/G3, XIAO S3 WIO,
|
||||
Heltec Tracker V2, Meshnology W12, and Nibble Screen/Zero Connect. RAK3112
|
||||
and Heltec RC32 keep separate transport and logging artifacts pending live
|
||||
hardware validation.
|
||||
It provides BLE plus an always-present interface `00` for framed
|
||||
Companion/terminal/mOTA traffic. Enabling logging and rebooting adds
|
||||
interface `02` for plaintext logs.
|
||||
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
|
||||
an input-capable logging terminal and restore Binary Companion when that
|
||||
terminal session ends.
|
||||
Its LoRa OTA support is source-only: it can serve a host file to another node
|
||||
but has no staging store and cannot update itself over LoRa.
|
||||
Installing an ESP32 Full Companion may require one merged-image erase/flash
|
||||
@@ -113,9 +111,10 @@ available from a canonical image:
|
||||
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
|
||||
identity contracts. ESP32 boards without the tested dual-CDC Full profile keep
|
||||
USB, BLE, WiFi, and Full Companion images separate because Full changes
|
||||
partitions, RAM use, active transports, and power behavior.
|
||||
identity contracts. Companion boards keep transport-specific canonical images
|
||||
only when no exact Full recipe has passed the combined flash/RAM qualification.
|
||||
Dual CDC is not required: a qualified single-TTY Full image safely makes Binary
|
||||
Companion and plaintext USB logging mutually exclusive.
|
||||
|
||||
## Complete CLI policy
|
||||
|
||||
|
||||
@@ -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; dual-CDC Full Companion adds/removes its second USB port after reboot | No | Yes | No |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -235,7 +235,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; dual-CDC Full Companion adds/removes its second USB port after reboot | No | Yes | No | No | Yes |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
+15
-7
@@ -594,9 +594,10 @@ set usb.logging on reboot
|
||||
set usb.logging off reboot
|
||||
```
|
||||
|
||||
These commands are compiled into logging artifacts and control their live USB
|
||||
debug and packet output. CommonCLI roles save the setting in `/com_prefs`, so
|
||||
it survives reboot; their first boot defaults to on.
|
||||
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.
|
||||
|
||||
On Full Companion these lines belong to its text terminal, not `meshcli`'s
|
||||
Binary `get/set` parameter namespace. Open interface `00`, send
|
||||
@@ -612,11 +613,18 @@ without the optional `reboot` argument saves the choice and reports that a
|
||||
reboot is required when the USB interface count must change. The exact
|
||||
`set usb.logging on reboot` and `set usb.logging off reboot` forms save the
|
||||
choice, send their reply, and reboot one second later only when needed. This
|
||||
behavior includes nRF52 and qualified native-USB ESP32-S3 Full images.
|
||||
behavior includes nRF52 and dual-CDC native-USB ESP32-S3 Full images.
|
||||
|
||||
Turning USB logging off does not disable CLI replies or Companion protocol
|
||||
frames. It also does not change the node-storage capture controlled by `log
|
||||
start` and `log stop`.
|
||||
On a single-TTY ESP32 Full Companion, enter the USB text terminal and use
|
||||
`set usb.logging on` to turn that TTY into a logging-repeater-style plaintext
|
||||
stream. It remains an input-capable CLI, so `set usb.logging off` works on the
|
||||
same TTY and automatically restores Binary Companion after its reply. No
|
||||
reboot is needed because the USB interface count does not change.
|
||||
|
||||
Turning USB logging off does not disable CLI replies. Dual-CDC builds keep
|
||||
Companion frames active on interface `00`; single-TTY builds resume frames when
|
||||
their terminal session ends. This setting does not change the node-storage
|
||||
capture controlled by `log start` and `log stop`.
|
||||
|
||||
Unified ESP32 FULL builds add one saved selector for both output paths:
|
||||
|
||||
|
||||
@@ -15,11 +15,16 @@ A reboot clears it.
|
||||
| RP2040 | 256 |
|
||||
| STM32 | 16 |
|
||||
| Known constrained classic ESP32 target override | 128 |
|
||||
| Meshadventurer Full Companion | 64 |
|
||||
| Constrained Full ESP32 fallback | 16 |
|
||||
|
||||
An explicit target `OFFLINE_QUEUE_SIZE` overrides the platform default. The
|
||||
Heltec V2 and TLora V2 Full Companion profiles, for example, use 16 frames so
|
||||
their combined WiFi, BLE, and LoRa mOTA image retains enough internal DRAM.
|
||||
Meshadventurer SX1262 and SX1268 Full Companion use 64 frames together with 160
|
||||
contacts and 30 group channels; their ordinary transport-specific images keep
|
||||
128 frames and 40 channels.
|
||||
|
||||
Standard, logging, MQTT, and Cascade build overlays retain the selected target
|
||||
capacity; they do not silently shrink the queue.
|
||||
|
||||
|
||||
@@ -18,11 +18,22 @@ firmware as an mOTA image.
|
||||
|
||||
## Build and install
|
||||
|
||||
The target is synthesized by `build.sh` only when matching transport recipes
|
||||
exist for the exact board variant:
|
||||
The target is synthesized by `build.sh` only for an exact board recipe that has
|
||||
passed the combined-transport size check:
|
||||
|
||||
- ESP32 requires matching WiFi, USB, and BLE Companion environments.
|
||||
- nRF52 requires matching USB and BLE Companion environments.
|
||||
- The normal automatic path requires matching WiFi, USB, and BLE recipes on
|
||||
ESP32, or matching USB and BLE recipes on nRF52.
|
||||
- A measured qualification list also promotes an exact BLE recipe when the
|
||||
same board can safely add its platform's remaining transports. It never
|
||||
substitutes the pin map or peripherals from another board.
|
||||
|
||||
The measured ESP32 additions are M5Stack Unit C6L, Heltec Wireless Tracker,
|
||||
Wireless Paper, E213, and CT62; LilyGo T3S3 SX1262/SX1276, T-Deck, TETH Elite,
|
||||
classic T-Beam SX1262/SX1276, and T-Beam S3 Supreme; Ebyte EoRa-S3;
|
||||
Meshadventurer SX1262/SX1268; and XIAO S3. The measured nRF52 additions are
|
||||
GAT562 Mesh Watch13, LilyGo T-Echo Lite, LilyGo T-Impulse Plus, and Wio Tracker
|
||||
L1 E-Ink. Their old transport-specific names remain available for explicit
|
||||
compatibility builds, but the Full image is the canonical release artifact.
|
||||
|
||||
List the available targets:
|
||||
|
||||
@@ -51,12 +62,10 @@ bash build.sh build-full-companion-firmwares \
|
||||
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. Dual-CDC Full Companion replaces separate USB, BLE,
|
||||
ordinary WiFi, and USB-only packet-logging release artifacts whenever the
|
||||
exact board supports the combined profile. This includes nRF52 Full Companion
|
||||
and the qualified native-USB ESP32-S3 profiles listed below. One physical USB
|
||||
connection can enumerate separate Companion and logging serial ports, so
|
||||
plaintext logs cannot corrupt binary frames. In WebConfig, use the
|
||||
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;
|
||||
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:
|
||||
|
||||
```text
|
||||
@@ -176,7 +185,11 @@ flash because this source-only role does not install updates into a second app
|
||||
slot. Flash the generated `-merged.bin` when first installing this partition
|
||||
layout. Other boards with 8 MB or more retain dual application partitions.
|
||||
Heltec V2 and TLora V2 use 100 contacts, 8 group channels, and a 16-frame offline
|
||||
queue in this combined profile because of internal DRAM limits.
|
||||
queue in this combined profile because of internal DRAM limits. Meshadventurer
|
||||
SX1262 and SX1268 retain 160 contacts, use 30 group channels, and use a
|
||||
64-frame queue. That is the smallest measured reduction which cleared their
|
||||
classic ESP32 internal-DRAM link limit; their ordinary transport-specific
|
||||
images retain 160 contacts, 40 channels, and 128 queued frames.
|
||||
|
||||
Full Companions normally retain 256 pending Companion message frames. ESP32
|
||||
boards with configured PSRAM retain 512 and allocate that queue from PSRAM
|
||||
@@ -268,11 +281,21 @@ The terminal supports Companion chat commands, including `channels`,
|
||||
`login <admin-password>` and `cmd <remote-command>`, and routed
|
||||
`trace [recipient-name-or-prefix]`, plus local `ota`, `tempradio`, and
|
||||
`normalradio` controls. ESP32 builds also provide local
|
||||
WiFi credential, status, WebConfig, CLI-tab, and power-save controls. Logging
|
||||
artifacts additionally provide `get/set usb.logging`; turning it off
|
||||
suppresses live USB diagnostics without disabling Companion frames or terminal
|
||||
replies. Dual-CDC Full Companion saves this setting and applies it only to its
|
||||
dedicated logging port. It starts off on a fresh installation.
|
||||
WiFi credential, status, WebConfig, CLI-tab, and power-save controls. Every
|
||||
Full Companion provides persistent `get/set usb.logging` and starts with
|
||||
logging off on a fresh installation.
|
||||
|
||||
### Single USB serial port
|
||||
|
||||
On an ESP32 Full Companion without dual CDC, interface `00` has two exclusive
|
||||
modes. It starts as framed Binary Companion. Enter its text terminal with
|
||||
`+++MESHCORE-TERM-START`, then run `set usb.logging on`; the same TTY emits
|
||||
plaintext packet/debug logs and continues accepting CLI commands, including
|
||||
`set usb.logging off`. Turning it off sends the command reply and then returns
|
||||
that TTY to Binary Companion automatically, including on USB-UART bridges that
|
||||
cannot detect a cable disconnect. A saved logging-on preference boots directly
|
||||
into this input-capable logging terminal. BLE and Wi-Fi Companion remain
|
||||
available while USB is logging.
|
||||
|
||||
### Dual USB serial ports
|
||||
|
||||
@@ -307,14 +330,13 @@ depending on a particular COM number. The nRF52 bootloader temporarily exposes
|
||||
its normal DFU serial interface during an update. Qualified S3 boards
|
||||
temporarily expose the ESP32-S3 ROM USB-JTAG serial port during a wired flash.
|
||||
|
||||
Qualified ESP32-S3 targets are Heltec V4, T-Beam 1W, Station G2/G3, XIAO S3
|
||||
Dual-CDC ESP32-S3 targets are Heltec V4, T-Beam 1W, Station G2/G3, XIAO S3
|
||||
WIO, Heltec Tracker V2, Meshnology W12, and Nibble Screen/Zero Connect. The
|
||||
base Heltec V4 profile has completed live two-interface, ROM-flashing, and
|
||||
logging-off one-interface validation. RAK3112 and Heltec RC32 retain separate
|
||||
transport and logging images pending hardware validation. Boards whose
|
||||
connector terminates at an external USB-UART bridge also keep their
|
||||
transport-specific images: firmware cannot add a second USB interface to that
|
||||
bridge chip.
|
||||
logging-off one-interface validation. Full recipes with only one usable TTY
|
||||
still replace separate transport images; they use the exclusive terminal/log
|
||||
mode above because firmware cannot add a second interface to a USB-UART bridge
|
||||
or a single-port USB peripheral.
|
||||
|
||||
Every ESP32-S3 Full Companion image uses DIO flash mode, including the RAK3112
|
||||
and RC32 profiles that do not expose dual CDC. The S3 ROM supports DIO while
|
||||
|
||||
+19
-15
@@ -168,22 +168,26 @@ next reboot or power cycle. An explicit administrator `start webconfig` remains
|
||||
available as an override. A saved SSID switches to the normal indefinite
|
||||
reconnect behavior instead.
|
||||
|
||||
Current dual-CDC Full Companion profiles also use one binary for normal
|
||||
attached Companion use and USB packet logging. This includes nRF52 and
|
||||
qualified native-USB ESP32-S3 Full images. Fresh installs default to logging off and expose only
|
||||
interface `00` for Binary Companion, terminal, and mOTA source traffic. Enabling
|
||||
logging and rebooting adds interface `02` for plaintext logs. The picker
|
||||
therefore omits older separate USB, BLE, ordinary WiFi, and USB-logging
|
||||
Companion choices when the matching Full artifact exists. Use
|
||||
`set usb.logging on reboot` or `set usb.logging off reboot` to persist the
|
||||
choice and apply the corresponding USB interface count.
|
||||
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.
|
||||
|
||||
Qualified ESP32-S3 hardware currently includes Heltec V4, T-Beam 1W, Station
|
||||
G2/G3, XIAO S3 WIO, Heltec Tracker V2, Meshnology W12, and Nibble Screen/Zero
|
||||
Connect layouts. The base Heltec V4 profile has completed live two-interface,
|
||||
ROM-flashing, and logging-off one-interface validation. RAK3112 and Heltec RC32
|
||||
retain separate transport and logging images pending hardware validation, as do
|
||||
Heltec V3/WSL3, ThinkNode M2/M5/M7/M9, classic ESP32, and ESP32-C3 targets.
|
||||
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`.
|
||||
Single-TTY ESP32 builds instead use `set usb.logging on` to switch that TTY to
|
||||
an input-capable plaintext logging terminal. `set usb.logging off` stops the
|
||||
logs and returns the TTY to Binary Companion after its reply. BLE and Wi-Fi
|
||||
remain usable while the USB TTY is logging.
|
||||
|
||||
The dual-CDC ESP32-S3 subset includes Heltec V4, T-Beam 1W, Station G2/G3,
|
||||
XIAO S3 WIO, Heltec Tracker V2, Meshnology W12, and Nibble Screen/Zero Connect
|
||||
layouts. Existing single-TTY Full recipes include RAK3112, Heltec RC32,
|
||||
Heltec V3/WSL3, ThinkNode M2/M5/M7/M9, Heltec V2, LilyGo T-LoRa V2.1.1.6,
|
||||
and XIAO C3. Additional qualified single-TTY ESP32 profiles include M5Stack
|
||||
Unit C6L, Heltec Wireless Tracker/Paper/E213/CT62, LilyGo T3S3
|
||||
SX1262/SX1276, T-Deck, TETH Elite, classic T-Beam SX1262/SX1276, T-Beam S3
|
||||
Supreme, Ebyte EoRa-S3, Meshadventurer SX1262/SX1268, and XIAO S3.
|
||||
|
||||
## Installation methods
|
||||
|
||||
|
||||
@@ -112,13 +112,12 @@ The "reconstructed image" referenced by the manifest is the full `BODY || EndF`
|
||||
|
||||
### ESP32 application-slot profiles
|
||||
|
||||
ESP32 companion firmware is exempt from the portable-slot limit. USB and WiFi companion artifacts retain
|
||||
LoRa OTA and carry `-ota-` in their filenames so they can seed a host folder over serial or TCP; they keep
|
||||
their target partition table rather than using the FULL profile. A small set of high-capacity, non-PSRAM classic ESP32
|
||||
companions cannot combine their configured contact, group-channel, and offline-queue capacities with LoRa
|
||||
OTA in internal DRAM. Their normal artifacts remain unchanged, and option 3 emits a
|
||||
`-full-logging-ota-` fallback with 100 contacts, 8 group channels, a 16-frame offline queue, and a saved
|
||||
USB-logging on/off gate. MQTT
|
||||
ESP32 Companion firmware is exempt from the portable-slot limit. When an exact
|
||||
Full recipe exists, one expanded-partition image supplies USB, BLE, WiFi,
|
||||
source-only LoRa OTA, and persistent USB logging instead of separate transport
|
||||
artifacts. A small set of high-capacity, non-PSRAM classic ESP32 companions use
|
||||
100 contacts, 8 group channels, and a 16-frame offline queue in that combined
|
||||
image to preserve internal-DRAM headroom. MQTT
|
||||
observers and ESP-NOW bridges always use FULL builds because fitting them into the legacy slot would require
|
||||
removing CLI and role features. Except for those FULL roles and the ESP32-C6 case below, non-companion ESP32
|
||||
artifacts, including room, sensor, and repeater roles, must fit the legacy slot from `0x10000` up to
|
||||
|
||||
@@ -107,11 +107,12 @@ 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 live USB debug and packet output in a Companion logging
|
||||
artifact. The setting is persistent. Dual-CDC Full Companion defaults off;
|
||||
changing its USB interface count requires a reboot, and the optional exact
|
||||
`reboot` argument performs that reboot after sending the reply. Companion
|
||||
protocol frames and terminal replies remain enabled on interface `00`.
|
||||
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.
|
||||
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.
|
||||
|
||||
```
|
||||
get radio.rxps
|
||||
|
||||
@@ -1273,8 +1273,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
|
||||
}
|
||||
int i = 0;
|
||||
out_frame[i++] = PUSH_CODE_CONTROL_DATA;
|
||||
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
|
||||
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
|
||||
out_frame[i++] = (int8_t)(packet->getSNR() * 4);
|
||||
out_frame[i++] = (int8_t)packet->getRSSI();
|
||||
out_frame[i++] = packet->path_len;
|
||||
memcpy(&out_frame[i], packet->payload, packet->payload_len);
|
||||
i += packet->payload_len;
|
||||
@@ -1293,8 +1293,8 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||
}
|
||||
int i = 0;
|
||||
out_frame[i++] = PUSH_CODE_RAW_DATA;
|
||||
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
|
||||
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
|
||||
out_frame[i++] = (int8_t)(packet->getSNR() * 4);
|
||||
out_frame[i++] = (int8_t)packet->getRSSI();
|
||||
out_frame[i++] = 0xFF; // reserved (possibly path_len in future)
|
||||
memcpy(&out_frame[i], packet->payload, packet->payload_len);
|
||||
i += packet->payload_len;
|
||||
@@ -1477,9 +1477,10 @@ 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(MESH_DUAL_CDC_LOGGING)
|
||||
// Keep the primary CDC stream exclusively framed unless the owner opts in
|
||||
// to the separate plaintext logging interface and reboots.
|
||||
#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.usb_logging_enabled = 0;
|
||||
#else
|
||||
_prefs.usb_logging_enabled = 1;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <Mesh.h>
|
||||
#include "MyMesh.h"
|
||||
#include "CompanionWiFi.h"
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
|
||||
#ifdef ESP32_PLATFORM
|
||||
#include "esp_bt.h"
|
||||
@@ -291,6 +294,7 @@ static char usb_terminal_line[MAX_TRANS_UNIT * 2 + 32];
|
||||
static size_t usb_terminal_line_len = 0;
|
||||
static bool usb_terminal_discard_line = false;
|
||||
static bool usb_terminal_disconnect_armed = false;
|
||||
static bool usb_logging_terminal_mode = false;
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
static bool usb_mota_mode = false;
|
||||
static char usb_mota_line[32];
|
||||
@@ -345,9 +349,15 @@ static void enterUsbTerminalMode() {
|
||||
clearUsbTerminalLine();
|
||||
usb_terminal_discard_line = false;
|
||||
usb_terminal_disconnect_armed = isUsbTerminalDataConnected();
|
||||
usb_logging_terminal_mode = false;
|
||||
the_mesh.enterTerminalMode();
|
||||
}
|
||||
|
||||
static void enterUsbLoggingTerminalMode() {
|
||||
enterUsbTerminalMode();
|
||||
usb_logging_terminal_mode = true;
|
||||
}
|
||||
|
||||
static void leaveUsbTerminalMode(bool acknowledge) {
|
||||
if (acknowledge) {
|
||||
Serial.print("\r\nOK - Binary mode\r\n");
|
||||
@@ -357,6 +367,7 @@ static void leaveUsbTerminalMode(bool acknowledge) {
|
||||
clearUsbTerminalLine();
|
||||
usb_terminal_discard_line = false;
|
||||
usb_terminal_disconnect_armed = false;
|
||||
usb_logging_terminal_mode = false;
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
@@ -445,6 +456,26 @@ static void serviceUsbTerminal() {
|
||||
serviceUsbMota();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// A saved logging-on preference makes the one available TTY behave like a
|
||||
// logging repeater: plaintext diagnostics plus an input-capable CLI. Put the
|
||||
// Companion interface into passthrough before it can mix framed traffic with
|
||||
// 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::hasDedicatedUsbLoggingPort()) {
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
if (!the_mesh.isTerminalMode()) {
|
||||
enterUsbLoggingTerminalMode();
|
||||
return;
|
||||
}
|
||||
usb_logging_terminal_mode = true;
|
||||
} else if (usb_logging_terminal_mode && the_mesh.isTerminalMode()) {
|
||||
leaveUsbTerminalMode(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!the_mesh.isTerminalMode()) {
|
||||
if (usb_serial_interface.takeControlSequence()) {
|
||||
@@ -491,6 +522,13 @@ static void serviceUsbTerminal() {
|
||||
Serial.print("\r\n");
|
||||
the_mesh.handleTerminalCommand(usb_terminal_line);
|
||||
clearUsbTerminalLine();
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
if (usb_logging_terminal_mode
|
||||
&& !mesh::isUsbLoggingEnabled()) {
|
||||
leaveUsbTerminalMode(true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Serial.print("> ");
|
||||
return; // service at most one command per mesh loop
|
||||
}
|
||||
@@ -1025,6 +1063,9 @@ void halt() {
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
#if MESH_PACKET_LOGGING
|
||||
mesh::serialLogBegin();
|
||||
#endif
|
||||
mesh::beginUsbLoggingPort();
|
||||
board.begin();
|
||||
|
||||
@@ -1238,6 +1279,14 @@ 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::hasDedicatedUsbLoggingPort()
|
||||
&& mesh::isUsbLoggingEnabled()) {
|
||||
// Apply a saved single-TTY logging preference before the dispatcher can
|
||||
// emit its first framed Companion response on this interface.
|
||||
enterUsbLoggingTerminalMode();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// add ethernet interface
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
#include <stdlib.h> // for qsort()
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/ClockSyncUtils.h>
|
||||
#include <helpers/DatagramPayloadLimits.h>
|
||||
#include <helpers/FloodFilterPolicy.h>
|
||||
#include <helpers/RegionNameUtils.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
|
||||
#endif
|
||||
@@ -96,6 +100,14 @@ extern "C" caddr_t _sbrk(int increment);
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
// createDatagram() rejects a reply beyond this plaintext limit after the MAC
|
||||
// and worst-case cipher padding are added. Exporters must stay within it or no
|
||||
// reply is transmitted.
|
||||
static constexpr size_t MAX_ANON_REPLY_LEN =
|
||||
DatagramPayloadLimits::maxPlaintext(
|
||||
MAX_PACKET_PAYLOAD, CIPHER_MAC_SIZE, CIPHER_BLOCK_SIZE);
|
||||
static_assert(MAX_ANON_REPLY_LEN >= 8, "anonymous reply prefix must fit");
|
||||
|
||||
#define ANON_REQ_TYPE_REGIONS 0x01
|
||||
#define ANON_REQ_TYPE_OWNER 0x02
|
||||
#define ANON_REQ_TYPE_BASIC 0x03 // just remote clock
|
||||
@@ -472,7 +484,8 @@ static void formatFixed3(char* dest, size_t dest_len, float value) {
|
||||
snprintf(dest, dest_len, "%ld.%03ld", whole, decimals);
|
||||
}
|
||||
|
||||
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) {
|
||||
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr,
|
||||
int16_t rssi) {
|
||||
#if MAX_NEIGHBOURS // check if neighbours enabled
|
||||
// find existing neighbour, else use least recently updated
|
||||
uint32_t oldest_timestamp = 0xFFFFFFFF;
|
||||
@@ -496,6 +509,11 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
|
||||
neighbour->advert_timestamp = timestamp;
|
||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
neighbour->snr = (int8_t)(snr * 4);
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
neighbour->rssi = rssi;
|
||||
#else
|
||||
(void)rssi;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -606,7 +624,9 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
|
||||
|
||||
return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); // reply length
|
||||
return 8 + region_map.exportNamesTo(
|
||||
(char *)&reply_data[8], MAX_ANON_REPLY_LEN - 8,
|
||||
REGION_DENY_FLOOD); // reply length
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1026,12 +1046,16 @@ const char *MyMesh::getLogDateTime() {
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Stream& logging_port = mesh::usbLoggingPort();
|
||||
logging_port.print(getLogDateTime());
|
||||
logging_port.print(" RAW: ");
|
||||
mesh::Utils::printHex(logging_port, raw, len);
|
||||
logging_port.println();
|
||||
mesh::SerialLogLine<> line;
|
||||
#if MESH_PACKET_LOGGING_COMPACT
|
||||
line.printf("R");
|
||||
line.hex(raw, len);
|
||||
line.flush(mesh::usbLoggingPort(), false);
|
||||
#else
|
||||
line.printf("%s RAW: ", getLogDateTime());
|
||||
line.hex(raw, len);
|
||||
line.flush(mesh::usbLoggingPort());
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2584,7 +2608,7 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32
|
||||
if (packet->getPathHashCount() == 0 && !isShare(packet)) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
|
||||
putNeighbour(id, timestamp, packet->getSNR());
|
||||
putNeighbour(id, timestamp, packet->getSNR(), packet->getRSSI());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2612,7 +2636,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) {
|
||||
int oi = i - NEIGHBOR_DISCOVER_PEER_BASE;
|
||||
if (type == PAYLOAD_TYPE_RESPONSE && oi >= 0 && oi < neighbor_discover_count) {
|
||||
handleNeighborDiscoverResponse(oi, data, len);
|
||||
handleNeighborDiscoverResponse(
|
||||
oi, data, len, packet->getSNR(), packet->getRSSI());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2628,7 +2653,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) {
|
||||
for (int oi = 0; oi < neighbor_discover_count; oi++) {
|
||||
if (client->id.matches(neighbor_discover[oi].id)
|
||||
&& handleNeighborDiscoverResponse(oi, data, len)) {
|
||||
&& handleNeighborDiscoverResponse(
|
||||
oi, data, len, packet->getSNR(), packet->getRSSI())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3103,7 +3129,8 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
if (id.matches(self_id)) {
|
||||
return;
|
||||
}
|
||||
putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR());
|
||||
putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR(),
|
||||
packet->getRSSI());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11884,6 +11911,7 @@ bool MyMesh::completeNeighborDiscoverEntry() {
|
||||
entry.status == ND_RESPONDED ? "responded"
|
||||
: (entry.status == ND_SEND_FAILED ? "send_failed" : "timeout")
|
||||
};
|
||||
measured.rssi = entry.rssi;
|
||||
size_t added = MQTTMessageBuilder::measureNeighborsMessageEntry(measured);
|
||||
if (neighbor_discover_publish_count > 0) added++; // array comma
|
||||
|
||||
@@ -11902,7 +11930,9 @@ bool MyMesh::completeNeighborDiscoverEntry() {
|
||||
|
||||
// Match a RESPONSE against the pending overlay entry by tag; copy its scope
|
||||
// string (payload after the 8-byte {tag}{clock} header) into the entry.
|
||||
bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len) {
|
||||
bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx,
|
||||
const uint8_t* data, size_t len,
|
||||
float snr, int16_t rssi) {
|
||||
if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count) return false;
|
||||
NeighborDiscoverEntry& entry = neighbor_discover[overlay_idx];
|
||||
if (entry.status != ND_PENDING || len < 8) return false;
|
||||
@@ -11919,19 +11949,24 @@ bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data
|
||||
entry.scopes[scope_len] = 0;
|
||||
entry.status = ND_RESPONDED;
|
||||
// A zero-hop reply is proof we heard this neighbour now, so re-stamp both the
|
||||
// snapshot and the live table; a stamp taken before time sync heals here.
|
||||
// snapshot and live table with this packet's measurements. A stamp taken
|
||||
// before time sync also heals here.
|
||||
entry.heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
touchNeighbourHeard(entry.id, entry.heard_timestamp);
|
||||
entry.snr = (int8_t)(snr * 4);
|
||||
entry.rssi = rssi;
|
||||
touchNeighbourHeard(entry.id, entry.heard_timestamp, snr, rssi);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh a live neighbour's heard time only: a scope reply carries no advert
|
||||
// timestamp or SNR to update.
|
||||
void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp) {
|
||||
void MyMesh::touchNeighbourHeard(const mesh::Identity& id,
|
||||
uint32_t heard_timestamp, float snr,
|
||||
int16_t rssi) {
|
||||
#if MAX_NEIGHBOURS
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
if (id.matches(neighbours[i].id)) {
|
||||
neighbours[i].heard_timestamp = heard_timestamp;
|
||||
neighbours[i].snr = (int8_t)(snr * 4);
|
||||
neighbours[i].rssi = rssi;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -12054,6 +12089,7 @@ void MyMesh::finishNeighborDiscover() {
|
||||
mesh::Utils::toHex(hex, entry.id.pub_key, PUB_KEY_SIZE);
|
||||
entries[i].pubkey_hex = hex;
|
||||
entries[i].snr = entry.snr / 4.0f;
|
||||
entries[i].rssi = entry.rssi;
|
||||
bool heard_known = neighborHeardAgeUsable(entry.heard_timestamp, now_secs);
|
||||
entries[i].heard_unknown = !heard_known;
|
||||
entries[i].heard_secs_ago = heard_known ? (now_secs - entry.heard_timestamp) : 0;
|
||||
@@ -12197,6 +12233,7 @@ bool MyMesh::startNeighborDiscover(char* reply) {
|
||||
entry.id = neighbours[i].id;
|
||||
entry.heard_timestamp = neighbours[i].heard_timestamp;
|
||||
entry.snr = neighbours[i].snr;
|
||||
entry.rssi = neighbours[i].rssi;
|
||||
entry.scopes[0] = 0;
|
||||
entry.tag = 0;
|
||||
entry.status = ND_UNSENT;
|
||||
|
||||
@@ -131,6 +131,9 @@ struct NeighbourInfo {
|
||||
uint32_t advert_timestamp;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4, user should divide to get float value
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
int16_t rssi; // dBm from the last packet heard from this neighbour
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
@@ -539,6 +542,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
mesh::Identity id; // immutable snapshot: neighbour table can change mid-pass
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4
|
||||
int16_t rssi; // dBm from the last packet heard from this neighbour
|
||||
uint32_t tag; // anon-regions request tag we're waiting on
|
||||
char scopes[96]; // scope names from the response
|
||||
uint8_t status; // NeighborDiscoverStatus
|
||||
@@ -569,8 +573,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool startNeighborDiscover(char* reply);
|
||||
void loopNeighborDiscover();
|
||||
void finishNeighborDiscover();
|
||||
bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len);
|
||||
void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp);
|
||||
bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data,
|
||||
size_t len, float snr, int16_t rssi);
|
||||
void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp,
|
||||
float snr, int16_t rssi);
|
||||
void getLocalScopes(char* buf, size_t len);
|
||||
// Overlay peer indices are offset by this base so onPeerDataRecv can tell a
|
||||
// discovery response apart from a normal ACL-client index.
|
||||
@@ -606,7 +612,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const;
|
||||
bool handleClientPathCommand(ClientInfo* sender, char* command, char* reply);
|
||||
bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const;
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr,
|
||||
int16_t rssi);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
|
||||
#include "MyMesh.h"
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -69,6 +72,9 @@ static unsigned long userBtnDownAt = 0;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
#if MESH_PACKET_LOGGING
|
||||
mesh::serialLogBegin();
|
||||
#endif
|
||||
delay(1000);
|
||||
|
||||
board.begin();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/radiolib/RxBoostedGainDefaults.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
|
||||
#include <helpers/FloodFilterPolicy.h>
|
||||
#endif
|
||||
@@ -265,12 +268,10 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
// Logging builds prefer backpressure over silently losing a packet record.
|
||||
Stream& logging_port = mesh::usbLoggingPort();
|
||||
logging_port.print(getLogDateTime());
|
||||
logging_port.print(" RAW: ");
|
||||
mesh::Utils::printHex(logging_port, raw, len);
|
||||
logging_port.println();
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("%s RAW: ", getLogDateTime());
|
||||
line.hex(raw, len);
|
||||
line.flush(mesh::usbLoggingPort());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -770,7 +771,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) {
|
||||
int oi = i - NEIGHBOR_DISCOVER_PEER_BASE;
|
||||
if (type == PAYLOAD_TYPE_RESPONSE && oi >= 0 && oi < neighbor_discover_count) {
|
||||
handleNeighborDiscoverResponse(oi, data, len);
|
||||
handleNeighborDiscoverResponse(
|
||||
oi, data, len, packet->getSNR(), packet->getRSSI());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -786,7 +788,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) {
|
||||
for (int oi = 0; oi < neighbor_discover_count; oi++) {
|
||||
if (client->id.matches(neighbor_discover[oi].id)
|
||||
&& handleNeighborDiscoverResponse(oi, data, len)) {
|
||||
&& handleNeighborDiscoverResponse(
|
||||
oi, data, len, packet->getSNR(), packet->getRSSI())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1051,7 +1054,7 @@ void MyMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
if (packet->getPathHashCount() == 0 && !is_share) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) {
|
||||
putNeighbour(id, timestamp, packet->getSNR());
|
||||
putNeighbour(id, timestamp, packet->getSNR(), packet->getRSSI());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1066,7 +1069,8 @@ void MyMesh::onGroupPacketRecv(mesh::Packet* packet) {
|
||||
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
|
||||
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
|
||||
|
||||
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) {
|
||||
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr,
|
||||
int16_t rssi) {
|
||||
// find existing neighbour, else use least recently updated
|
||||
uint32_t oldest_timestamp = 0xFFFFFFFF;
|
||||
NeighbourInfo *neighbour = &neighbours[0];
|
||||
@@ -1089,6 +1093,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
|
||||
neighbour->advert_timestamp = timestamp;
|
||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
neighbour->snr = (int8_t)(snr * 4);
|
||||
neighbour->rssi = rssi;
|
||||
}
|
||||
|
||||
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
@@ -1120,7 +1125,8 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
if (id.matches(self_id)) {
|
||||
return;
|
||||
}
|
||||
putNeighbour(id, getRTCClock()->getCurrentTime(), packet->getSNR());
|
||||
putNeighbour(id, getRTCClock()->getCurrentTime(), packet->getSNR(),
|
||||
packet->getRSSI());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2712,6 +2718,7 @@ bool MyMesh::completeNeighborDiscoverEntry() {
|
||||
entry.status == ND_RESPONDED ? "responded"
|
||||
: (entry.status == ND_SEND_FAILED ? "send_failed" : "timeout")
|
||||
};
|
||||
measured.rssi = entry.rssi;
|
||||
size_t added = MQTTMessageBuilder::measureNeighborsMessageEntry(measured);
|
||||
if (neighbor_discover_publish_count > 0) added++; // array comma
|
||||
|
||||
@@ -2730,7 +2737,9 @@ bool MyMesh::completeNeighborDiscoverEntry() {
|
||||
|
||||
// Match a RESPONSE against the pending overlay entry by tag; copy its scope
|
||||
// string (payload after the 8-byte {tag}{clock} header) into the entry.
|
||||
bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len) {
|
||||
bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx,
|
||||
const uint8_t* data, size_t len,
|
||||
float snr, int16_t rssi) {
|
||||
if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count) return false;
|
||||
NeighborDiscoverEntry& entry = neighbor_discover[overlay_idx];
|
||||
if (entry.status != ND_PENDING || len < 8) return false;
|
||||
@@ -2747,18 +2756,23 @@ bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data
|
||||
entry.scopes[scope_len] = 0;
|
||||
entry.status = ND_RESPONDED;
|
||||
// A zero-hop reply is proof we heard this neighbour now, so re-stamp both the
|
||||
// snapshot and the live table; a stamp taken before time sync heals here.
|
||||
// snapshot and live table with this packet's measurements. A stamp taken
|
||||
// before time sync also heals here.
|
||||
entry.heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
touchNeighbourHeard(entry.id, entry.heard_timestamp);
|
||||
entry.snr = (int8_t)(snr * 4);
|
||||
entry.rssi = rssi;
|
||||
touchNeighbourHeard(entry.id, entry.heard_timestamp, snr, rssi);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh a live neighbour's heard time only: a scope reply carries no advert
|
||||
// timestamp or SNR to update.
|
||||
void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp) {
|
||||
void MyMesh::touchNeighbourHeard(const mesh::Identity& id,
|
||||
uint32_t heard_timestamp, float snr,
|
||||
int16_t rssi) {
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
if (id.matches(neighbours[i].id)) {
|
||||
neighbours[i].heard_timestamp = heard_timestamp;
|
||||
neighbours[i].snr = (int8_t)(snr * 4);
|
||||
neighbours[i].rssi = rssi;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2880,6 +2894,7 @@ void MyMesh::finishNeighborDiscover() {
|
||||
mesh::Utils::toHex(hex, entry.id.pub_key, PUB_KEY_SIZE);
|
||||
entries[i].pubkey_hex = hex;
|
||||
entries[i].snr = entry.snr / 4.0f;
|
||||
entries[i].rssi = entry.rssi;
|
||||
bool heard_known = neighborHeardAgeUsable(entry.heard_timestamp, now_secs);
|
||||
entries[i].heard_unknown = !heard_known;
|
||||
entries[i].heard_secs_ago = heard_known ? (now_secs - entry.heard_timestamp) : 0;
|
||||
@@ -3023,6 +3038,7 @@ bool MyMesh::startNeighborDiscover(char* reply) {
|
||||
entry.id = neighbours[i].id;
|
||||
entry.heard_timestamp = neighbours[i].heard_timestamp;
|
||||
entry.snr = neighbours[i].snr;
|
||||
entry.rssi = neighbours[i].rssi;
|
||||
entry.scopes[0] = 0;
|
||||
entry.tag = 0;
|
||||
entry.status = ND_UNSENT;
|
||||
|
||||
@@ -145,6 +145,7 @@ struct NeighbourInfo {
|
||||
uint32_t advert_timestamp;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4, user should divide to get float value
|
||||
int16_t rssi; // dBm from the last packet heard from this neighbour
|
||||
};
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
|
||||
@@ -218,6 +219,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
|
||||
mesh::Identity id;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr;
|
||||
int16_t rssi;
|
||||
uint32_t tag;
|
||||
char scopes[96];
|
||||
uint8_t status;
|
||||
@@ -239,7 +241,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
|
||||
char self_default_scope_buf[31];
|
||||
char neighbor_discover_origin[32];
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr,
|
||||
int16_t rssi);
|
||||
void sendNodeDiscoverReq();
|
||||
mesh::Packet* sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag);
|
||||
bool cancelNeighborDiscoverRequest();
|
||||
@@ -250,8 +253,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
|
||||
bool startNeighborDiscover(char* reply);
|
||||
void loopNeighborDiscover();
|
||||
void finishNeighborDiscover();
|
||||
bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len);
|
||||
void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp);
|
||||
bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data,
|
||||
size_t len, float snr, int16_t rssi);
|
||||
void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp,
|
||||
float snr, int16_t rssi);
|
||||
void getLocalScopes(char* buf, size_t len);
|
||||
static const int NEIGHBOR_DISCOVER_PEER_BASE = 1000;
|
||||
static const unsigned long NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS = 29000;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
|
||||
#include "MyMesh.h"
|
||||
|
||||
@@ -37,6 +40,9 @@ unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled)
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
#if MESH_PACKET_LOGGING
|
||||
mesh::serialLogBegin();
|
||||
#endif
|
||||
delay(1000);
|
||||
|
||||
board.begin();
|
||||
|
||||
@@ -435,8 +435,17 @@ void PsychicMqttClient::connect()
|
||||
}
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_start(_client));
|
||||
ESP_LOGI(TAG, "MQTT client started.");
|
||||
esp_err_t start_result = esp_mqtt_client_start(_client);
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(start_result);
|
||||
if (start_result == ESP_OK)
|
||||
{
|
||||
_started = true;
|
||||
ESP_LOGI(TAG, "MQTT client started.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "MQTT client failed to start: %s", esp_err_to_name(start_result));
|
||||
}
|
||||
}
|
||||
|
||||
void PsychicMqttClient::reconnect()
|
||||
@@ -491,9 +500,39 @@ void PsychicMqttClient::disconnect()
|
||||
}
|
||||
|
||||
esp_mqtt_client_stop(_client);
|
||||
_started = false;
|
||||
ESP_LOGI(TAG, "MQTT client stopped.");
|
||||
}
|
||||
|
||||
void PsychicMqttClient::softDisconnect(unsigned long timeout_ms)
|
||||
{
|
||||
if (_client == nullptr)
|
||||
{
|
||||
ESP_LOGW(TAG, "MQTT client not started.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Disconnecting MQTT transport (client task retained).");
|
||||
_stopMqttClient = false;
|
||||
esp_mqtt_client_disconnect(_client);
|
||||
|
||||
unsigned long waited = 0;
|
||||
while (!_stopMqttClient && waited < timeout_ms)
|
||||
{
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
waited += 10;
|
||||
}
|
||||
if (!_stopMqttClient)
|
||||
{
|
||||
ESP_LOGW(TAG, "softDisconnect: no DISCONNECTED event in %lums", timeout_ms);
|
||||
}
|
||||
}
|
||||
|
||||
void PsychicMqttClient::forceStop()
|
||||
{
|
||||
if (_client == nullptr)
|
||||
@@ -508,6 +547,7 @@ void PsychicMqttClient::forceStop()
|
||||
}
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(_client));
|
||||
_connected = false;
|
||||
_started = false;
|
||||
ESP_LOGI(TAG, "MQTT client forcefully stopped.");
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,12 @@ public:
|
||||
*/
|
||||
void disconnect();
|
||||
|
||||
/** Close the transport while retaining the esp-mqtt client task. */
|
||||
void softDisconnect(unsigned long timeout_ms = 5000);
|
||||
|
||||
/** True after a successful start and before a stop. */
|
||||
bool isStarted() const { return _started; }
|
||||
|
||||
/**
|
||||
* @brief Forcefully stops the MQTT client and disconnects from the server.
|
||||
* This does not trigger the onDisconnect callbacks.
|
||||
@@ -478,6 +484,7 @@ private:
|
||||
bool _connected = false;
|
||||
bool _stopMqttClient = false;
|
||||
bool _config_dirty = true;
|
||||
bool _started = false;
|
||||
|
||||
// Runtime cap on the esp-mqtt outbox for QoS 0 async publishes (bytes).
|
||||
// 0 = disabled. Enforced in publish(); not an esp-mqtt config field.
|
||||
|
||||
@@ -140,10 +140,10 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
"selected non-logging utilities. Unified expanded-partition FULL builds "
|
||||
"provide USB packet logging and direct WiFi MQTT in one image with a "
|
||||
"persistent off/USB/WiFi/both selector. Host software can consume the "
|
||||
"USB serial log and publish it separately. Dual-CDC Full Companion "
|
||||
"defaults to its single framed USB interface; enabling logging and "
|
||||
"rebooting adds a separate plaintext interface, replacing older "
|
||||
"USB-logging images on nRF52 and qualified native-USB ESP32-S3. "
|
||||
"USB serial log and publish it separately. Full Companion replaces "
|
||||
"older USB-logging images. Dual-CDC builds add a separate plaintext "
|
||||
"interface after reboot; single-TTY builds use an input-capable "
|
||||
"logging terminal on their existing port. "
|
||||
"Open Release notes for role, hardware, installation, and partition "
|
||||
"requirements."
|
||||
)
|
||||
@@ -287,6 +287,13 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
notes = common.normalize_esp32_dual_cdc_full_companion_metadata(
|
||||
firmware, notes
|
||||
)
|
||||
elif device_type == "esp32" and any(
|
||||
"companion_radio_full" in identity.lower()
|
||||
for identity in identities
|
||||
):
|
||||
notes = common.normalize_esp32_single_tty_full_companion_metadata(
|
||||
firmware, notes
|
||||
)
|
||||
if any("-logging" in identity.lower() for identity in identities):
|
||||
logging_note = (
|
||||
"LOGGING USE - This USB packet-logging build is for a "
|
||||
|
||||
@@ -400,10 +400,58 @@ def normalize_esp32_dual_cdc_full_companion_metadata(
|
||||
return "\n\n".join(paragraphs)
|
||||
|
||||
|
||||
def release_identity_has_nrf52_package(
|
||||
release_files: dict[str, list[Path]], identity: str
|
||||
) -> bool:
|
||||
return any(path.suffix.lower() == ".zip" for path in release_files[identity])
|
||||
def normalize_esp32_single_tty_full_companion_metadata(
|
||||
firmware: dict, notes: str
|
||||
) -> str:
|
||||
"""Describe an ESP32 Full Companion whose USB path has one TTY."""
|
||||
firmware["title"] = "Full Companion"
|
||||
firmware["subTitle"] = (
|
||||
"USB Companion/logging + BLE + Wi-Fi + LoRa OTA source"
|
||||
)
|
||||
profile = (
|
||||
"PROFILE - Single-TTY ESP32 Full Companion: the USB TTY defaults to "
|
||||
"Binary Companion on a fresh install. BLE, Wi-Fi Companion on TCP "
|
||||
"5000, WebConfig, TCP mOTA seeding on 5001, and the text terminal on "
|
||||
"5002 remain available. Enter the USB text terminal and use set "
|
||||
"usb.logging on to turn that TTY into an input-capable plaintext "
|
||||
"packet/debug stream. Use set usb.logging off to stop diagnostics; "
|
||||
"after its reply, the TTY returns to Binary Companion automatically. "
|
||||
"The saved logging choice is restored at boot."
|
||||
)
|
||||
logging_use = (
|
||||
"LOGGING USE - USB logging and Binary Companion deliberately do not "
|
||||
"share the single TTY at the same time. While logging is on, the TTY "
|
||||
"continues to accept text CLI commands, including set usb.logging "
|
||||
"off. Use BLE or Wi-Fi Companion while the USB TTY is logging."
|
||||
)
|
||||
selection = (
|
||||
"SELECTION - One Full image for this exact hardware layout replaces "
|
||||
"separate USB, BLE, ordinary Wi-Fi, and USB-logging images."
|
||||
)
|
||||
|
||||
paragraphs: list[str] = []
|
||||
profile_added = False
|
||||
selection_added = False
|
||||
for paragraph in notes.split("\n\n"):
|
||||
if paragraph.startswith("PROFILE "):
|
||||
if not profile_added:
|
||||
paragraphs.append(profile)
|
||||
profile_added = True
|
||||
continue
|
||||
if paragraph.startswith("LOGGING USE "):
|
||||
continue
|
||||
if paragraph.startswith("SELECTION "):
|
||||
if not selection_added:
|
||||
paragraphs.append(selection)
|
||||
selection_added = True
|
||||
continue
|
||||
paragraphs.append(paragraph)
|
||||
if not profile_added:
|
||||
paragraphs.append(profile)
|
||||
if not selection_added:
|
||||
paragraphs.append(selection)
|
||||
paragraphs.append(logging_use)
|
||||
return "\n\n".join(paragraphs)
|
||||
|
||||
|
||||
def canonical_full_identity_for_transport(identity: str) -> str | None:
|
||||
@@ -421,16 +469,10 @@ def canonical_full_identity_for_transport(identity: str) -> str | None:
|
||||
return canonical_runtime_identity(full_identity)
|
||||
|
||||
|
||||
def release_identity_is_dual_cdc_full(
|
||||
def release_identity_is_full_companion(
|
||||
release_files: dict[str, list[Path]], identity: str
|
||||
) -> bool:
|
||||
return (
|
||||
identity in release_files and
|
||||
(
|
||||
release_identity_has_nrf52_package(release_files, identity) or
|
||||
ESP32_DUAL_CDC_FULL_RE.match(identity) is not None
|
||||
)
|
||||
)
|
||||
return identity in release_files and "_companion_radio_full" in identity.lower()
|
||||
|
||||
|
||||
def resolve_release_identity(
|
||||
@@ -441,25 +483,25 @@ def resolve_release_identity(
|
||||
if candidate in release_files:
|
||||
return candidate, False
|
||||
|
||||
# Dual-CDC Full Companion replaces the old USB-only logging artifact because
|
||||
# its second CDC interface carries plaintext logs. Limit this fallback to a
|
||||
# native nRF52 Full package or a qualified native-USB ESP32-S3 identity.
|
||||
# Full Companion replaces the old USB-only logging artifact. Dual-CDC
|
||||
# hardware uses its second interface; single-TTY hardware safely switches
|
||||
# its primary port into an input-capable plaintext logging terminal.
|
||||
for candidate in candidates:
|
||||
if not candidate.endswith("-logging"):
|
||||
continue
|
||||
logging_base = candidate.removesuffix("-logging")
|
||||
full_identity = canonical_full_identity_for_transport(logging_base)
|
||||
if full_identity is not None and release_identity_is_dual_cdc_full(
|
||||
if full_identity is not None and release_identity_is_full_companion(
|
||||
release_files, full_identity
|
||||
):
|
||||
return full_identity, False
|
||||
|
||||
# Canonical dual-CDC Full Companion replaces separate USB, BLE, and ordinary
|
||||
# Wi-Fi artifacts. Other ESP32 transport-specific images remain separate.
|
||||
# Full is source-only for LoRa OTA and needs no staging/self-install slot.
|
||||
# Canonical Full Companion replaces separate USB, BLE, and ordinary Wi-Fi
|
||||
# artifacts. Full is source-only for LoRa OTA and needs no target-side
|
||||
# staging/self-install slot.
|
||||
for candidate in candidates:
|
||||
full_identity = canonical_full_identity_for_transport(candidate)
|
||||
if full_identity is not None and release_identity_is_dual_cdc_full(
|
||||
if full_identity is not None and release_identity_is_full_companion(
|
||||
release_files, full_identity
|
||||
):
|
||||
return full_identity, False
|
||||
@@ -733,12 +775,11 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
"BW62.5 / CR5 preset. This catalog contains standard builds, Full "
|
||||
"Companion builds, lean LoRa-OTA builds, and expanded-partition FULL "
|
||||
"USB + Wi-Fi observer and ESP-NOW bridge builds. Unified observers "
|
||||
"provide persistent off/USB/WiFi/both output selection. Dual-CDC Full "
|
||||
"Companion replaces separate BLE, USB, ordinary Wi-Fi, and USB-logging "
|
||||
"choices with one image. It defaults to one framed USB port; enabling "
|
||||
"logging and rebooting adds the separate plaintext port. "
|
||||
"This applies to nRF52 Full and qualified native-USB ESP32-S3 Full; "
|
||||
"USB-UART bridge variants remain separate. Open Release notes for role, "
|
||||
"provide persistent off/USB/WiFi/both output selection. Full Companion "
|
||||
"replaces separate BLE, USB, ordinary Wi-Fi, and USB-logging choices "
|
||||
"with one image. Dual-CDC builds can add a separate plaintext port; "
|
||||
"single-TTY builds switch that port into an input-capable logging "
|
||||
"terminal. Open Release notes for role, "
|
||||
"hardware, installation, and partition requirements."
|
||||
)
|
||||
|
||||
@@ -840,6 +881,13 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
notes = normalize_esp32_dual_cdc_full_companion_metadata(
|
||||
firmware, notes
|
||||
)
|
||||
elif device_type == "esp32" and any(
|
||||
"companion_radio_full" in identity.lower()
|
||||
for identity in resolved_identities
|
||||
):
|
||||
notes = normalize_esp32_single_tty_full_companion_metadata(
|
||||
firmware, notes
|
||||
)
|
||||
|
||||
firmware["version"] = {version_key: {"notes": notes, "files": files}}
|
||||
updated_entries += 1
|
||||
|
||||
@@ -224,6 +224,10 @@ board_build.core = earlephilhower
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D RP2040_PLATFORM
|
||||
; Arduino-Pico ships board-specific helper libraries alongside its core.
|
||||
; Deep dependency discovery can mistake iLabs_Hearth for an application
|
||||
; dependency even on boards without its required ESP32-C6 coprocessor.
|
||||
lib_ignore = iLabs Hearth
|
||||
|
||||
; ----------------- STM32 ----------------------
|
||||
|
||||
|
||||
+32
-28
@@ -2,6 +2,7 @@
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <Arduino.h>
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
@@ -165,8 +166,14 @@ bool Dispatcher::startOutboundTransmit() {
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
if (isUsbLoggingEnabled()) {
|
||||
logPacketStart("TX", outbound, len);
|
||||
logPacketEnd(outbound);
|
||||
#if MESH_PACKET_LOGGING_COMPACT
|
||||
SerialLogLine<> line;
|
||||
line.printf("T");
|
||||
line.hex(raw, len);
|
||||
line.flush(usbLoggingPort(), false);
|
||||
#else
|
||||
logPacketLine("TX", outbound, len, false, 0.0f, 0);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
@@ -207,26 +214,31 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
|
||||
}
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
void Dispatcher::logPacketStart(const char* direction, const Packet* packet, int len) {
|
||||
Stream& logging_port = usbLoggingPort();
|
||||
logging_port.print(getLogDateTime());
|
||||
logging_port.printf(": %s, len=%d (type=%d, route=%s, payload_len=%d)",
|
||||
direction, len, packet->getPayloadType(),
|
||||
packet->isRouteDirect() ? "D" : "F",
|
||||
packet->payload_len);
|
||||
}
|
||||
void Dispatcher::logPacketLine(const char* direction, const Packet* packet,
|
||||
int len, bool include_rx_metrics, float score,
|
||||
uint32_t air_time) {
|
||||
SerialLogLine<256> line;
|
||||
line.printf("%s: %s, len=%d (type=%d, route=%s, payload_len=%d)",
|
||||
getLogDateTime(), direction, len, packet->getPayloadType(),
|
||||
packet->isRouteDirect() ? "D" : "F", packet->payload_len);
|
||||
if (include_rx_metrics) {
|
||||
line.printf(" SNR=%d RSSI=%d score=%d time=%u", (int)packet->getSNR(),
|
||||
(int)packet->getRSSI(), (int)(score * 1000),
|
||||
(unsigned)air_time);
|
||||
uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(packet_hash);
|
||||
line.printf(" hash=");
|
||||
line.hex(packet_hash, MAX_HASH_SIZE);
|
||||
}
|
||||
|
||||
void Dispatcher::logPacketEnd(const Packet* packet) {
|
||||
const uint8_t type = packet->getPayloadType();
|
||||
if (packet->payload_len >= 2
|
||||
&& (type == PAYLOAD_TYPE_PATH || type == PAYLOAD_TYPE_REQ
|
||||
|| type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_TXT_MSG)) {
|
||||
usbLoggingPort().printf(" [%02X -> %02X]\n",
|
||||
(uint32_t)packet->payload[1],
|
||||
(uint32_t)packet->payload[0]);
|
||||
} else {
|
||||
usbLoggingPort().write((uint8_t)'\n');
|
||||
line.printf(" [%02X -> %02X]", (uint32_t)packet->payload[1],
|
||||
(uint32_t)packet->payload[0]);
|
||||
}
|
||||
line.flush(usbLoggingPort());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -567,6 +579,7 @@ void Dispatcher::checkRecv() {
|
||||
} else {
|
||||
if (tryParsePacket(pkt, raw, len)) {
|
||||
pkt->_snr = snr * 4.0f;
|
||||
pkt->_rssi = (int16_t)rssi;
|
||||
score = _radio->packetScore(snr, len);
|
||||
air_time = _radio->getEstAirtimeFor(len);
|
||||
rx_air_time += air_time;
|
||||
@@ -580,19 +593,9 @@ void Dispatcher::checkRecv() {
|
||||
}
|
||||
}
|
||||
if (pkt) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
#if MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT
|
||||
if (isUsbLoggingEnabled()) {
|
||||
logPacketStart("RX", pkt, pkt->getRawLength());
|
||||
Stream& logging_port = usbLoggingPort();
|
||||
logging_port.printf(" SNR=%d RSSI=%d score=%d time=%d",
|
||||
(int)pkt->getSNR(), (int)rssi,
|
||||
(int)(score * 1000), air_time);
|
||||
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
logging_port.print(" hash=");
|
||||
mesh::Utils::printHex(logging_port, packet_hash, MAX_HASH_SIZE);
|
||||
logPacketEnd(pkt);
|
||||
logPacketLine("RX", pkt, pkt->getRawLength(), true, score, air_time);
|
||||
}
|
||||
#endif
|
||||
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
|
||||
@@ -724,6 +727,7 @@ Packet* Dispatcher::obtainNewPacket() {
|
||||
_err_flags |= ERR_EVENT_FULL;
|
||||
} else {
|
||||
pkt->payload_len = pkt->path_len = 0;
|
||||
pkt->_rssi = 0;
|
||||
pkt->_snr = 0;
|
||||
pkt->tx_cr = 0;
|
||||
pkt->flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
|
||||
|
||||
+3
-3
@@ -382,9 +382,9 @@ public:
|
||||
|
||||
private:
|
||||
#if MESH_PACKET_LOGGING
|
||||
void logPacketStart(const char* direction, const Packet* packet, int len)
|
||||
__attribute__((noinline));
|
||||
void logPacketEnd(const Packet* packet) __attribute__((noinline));
|
||||
void logPacketLine(const char* direction, const Packet* packet, int len,
|
||||
bool include_rx_metrics, float score,
|
||||
uint32_t air_time) __attribute__((noinline));
|
||||
#endif
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
|
||||
@@ -8,6 +8,8 @@ Packet::Packet() {
|
||||
header = 0;
|
||||
path_len = 0;
|
||||
payload_len = 0;
|
||||
_rssi = 0;
|
||||
_snr = 0;
|
||||
tx_cr = 0;
|
||||
flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
|
||||
}
|
||||
@@ -68,6 +70,8 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
|
||||
if (src == NULL || len < 2) return false; // header + path_len
|
||||
|
||||
uint8_t i = 0;
|
||||
_rssi = 0;
|
||||
_snr = 0;
|
||||
tx_cr = 0;
|
||||
flood_retry_policy = FLOOD_RETRY_POLICY_DEFAULT;
|
||||
header = src[i++];
|
||||
|
||||
@@ -55,6 +55,7 @@ public:
|
||||
uint16_t transport_codes[2];
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
uint8_t payload[MAX_PACKET_PAYLOAD];
|
||||
int16_t _rssi; // dBm, captured with _snr when this packet is received
|
||||
int8_t _snr;
|
||||
uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized
|
||||
uint8_t flood_retry_policy; // volatile receive-policy result; not serialized
|
||||
@@ -99,6 +100,7 @@ public:
|
||||
bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
|
||||
|
||||
float getSNR() const { return ((float)_snr) / 4.0f; }
|
||||
int16_t getRSSI() const { return _rssi; }
|
||||
|
||||
/**
|
||||
* \returns the encoded/wire format length of this packet
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public:
|
||||
|
||||
/**
|
||||
* \brief Prints the hexadecimal representation of 'src' bytes of given length, to Stream 's'.
|
||||
*/
|
||||
*/
|
||||
static void printHex(Stream& s, const uint8_t* src, size_t len);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace AlertFaultPolicy {
|
||||
|
||||
static const uint32_t kCheckIntervalMs = 5000UL;
|
||||
static const uint16_t kMinIntervalMinutes = 60;
|
||||
static const uint32_t kMsPerMinute = 60000UL;
|
||||
|
||||
enum class State : uint8_t { OK, FIRING };
|
||||
|
||||
struct Fault {
|
||||
State state;
|
||||
uint32_t fired_at_ms;
|
||||
uint32_t last_outage_started_ms;
|
||||
};
|
||||
|
||||
struct OutageSnapshot {
|
||||
bool down;
|
||||
uint32_t started_ms;
|
||||
uint8_t reason;
|
||||
};
|
||||
|
||||
enum class Action : uint8_t { None, FireDown, FireRecovered };
|
||||
|
||||
struct TickResult {
|
||||
Action action;
|
||||
uint32_t duration_ms;
|
||||
};
|
||||
|
||||
static inline OutageSnapshot fromStartMs(uint32_t started_ms) {
|
||||
OutageSnapshot snapshot{};
|
||||
snapshot.down = started_ms != 0;
|
||||
snapshot.started_ms = started_ms;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Pack the cross-task outage state into one atomic word. The explicit down bit
|
||||
// means an outage beginning at millis()==0 remains representable.
|
||||
static const uint64_t kOutageDownBit = 1ULL << 40;
|
||||
|
||||
static inline uint64_t packOutageSnapshot(OutageSnapshot snapshot) {
|
||||
if (!snapshot.down) {
|
||||
snapshot.started_ms = 0;
|
||||
snapshot.reason = 0;
|
||||
}
|
||||
uint64_t value = (uint64_t)snapshot.started_ms;
|
||||
value |= (uint64_t)snapshot.reason << 32;
|
||||
if (snapshot.down) value |= kOutageDownBit;
|
||||
return value;
|
||||
}
|
||||
|
||||
static inline OutageSnapshot unpackOutageSnapshot(uint64_t value) {
|
||||
OutageSnapshot snapshot{};
|
||||
snapshot.started_ms = (uint32_t)value;
|
||||
snapshot.reason = (uint8_t)(value >> 32);
|
||||
snapshot.down = (value & kOutageDownBit) != 0;
|
||||
if (!snapshot.down) {
|
||||
snapshot.started_ms = 0;
|
||||
snapshot.reason = 0;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
static inline OutageSnapshot applyWifiStatus(uint32_t now, bool connected,
|
||||
OutageSnapshot current,
|
||||
bool initialized) {
|
||||
if (connected) {
|
||||
if (!initialized || current.down) return OutageSnapshot{};
|
||||
return current;
|
||||
}
|
||||
if (!initialized || !current.down) {
|
||||
OutageSnapshot snapshot{};
|
||||
snapshot.down = true;
|
||||
snapshot.started_ms = now;
|
||||
snapshot.reason = current.reason;
|
||||
return snapshot;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static inline OutageSnapshot applyWifiGotIp(OutageSnapshot) {
|
||||
return OutageSnapshot{};
|
||||
}
|
||||
|
||||
// Preserve the first outage start and first useful reason. Reconnect attempts
|
||||
// may produce later local disconnect events which must not rewrite either.
|
||||
static inline OutageSnapshot applyWifiDisconnectEvent(
|
||||
uint32_t now, uint8_t reason, OutageSnapshot current) {
|
||||
if (!current.down) {
|
||||
OutageSnapshot snapshot{};
|
||||
snapshot.down = true;
|
||||
snapshot.started_ms = now;
|
||||
snapshot.reason = reason;
|
||||
return snapshot;
|
||||
}
|
||||
if (current.reason == 0 && reason != 0) current.reason = reason;
|
||||
return current;
|
||||
}
|
||||
|
||||
static inline uint32_t elapsedMs(uint32_t now, uint32_t then) {
|
||||
return now - then;
|
||||
}
|
||||
|
||||
static inline bool checkDue(uint32_t now, uint32_t next_check_ms) {
|
||||
return (int32_t)(now - next_check_ms) >= 0;
|
||||
}
|
||||
|
||||
static inline uint32_t nextCheckMs(uint32_t now) {
|
||||
return now + kCheckIntervalMs;
|
||||
}
|
||||
|
||||
static inline uint32_t minIntervalMs(uint16_t configured_minutes) {
|
||||
uint16_t minutes = configured_minutes < kMinIntervalMinutes
|
||||
? kMinIntervalMinutes
|
||||
: configured_minutes;
|
||||
return (uint32_t)minutes * kMsPerMinute;
|
||||
}
|
||||
|
||||
static inline uint32_t thresholdMs(uint16_t minutes) {
|
||||
return (uint32_t)minutes * kMsPerMinute;
|
||||
}
|
||||
|
||||
static inline uint32_t downDurationMs(uint32_t now,
|
||||
const OutageSnapshot& snapshot) {
|
||||
return snapshot.down ? elapsedMs(now, snapshot.started_ms) : 0;
|
||||
}
|
||||
|
||||
static inline bool rateLimitAllows(uint32_t now, uint32_t fired_at_ms,
|
||||
uint32_t min_interval_ms) {
|
||||
return fired_at_ms == 0 ||
|
||||
elapsedMs(now, fired_at_ms) >= min_interval_ms;
|
||||
}
|
||||
|
||||
static inline TickResult tick(const Fault& fault, uint32_t now,
|
||||
const OutageSnapshot& snapshot,
|
||||
uint32_t threshold_ms,
|
||||
uint32_t min_interval_ms) {
|
||||
TickResult result = {Action::None, 0};
|
||||
if (fault.state == State::OK) {
|
||||
const uint32_t down_ms = downDurationMs(now, snapshot);
|
||||
if (snapshot.down && down_ms >= threshold_ms &&
|
||||
rateLimitAllows(now, fault.fired_at_ms, min_interval_ms)) {
|
||||
result.action = Action::FireDown;
|
||||
result.duration_ms = down_ms;
|
||||
}
|
||||
} else if (!snapshot.down) {
|
||||
result.action = Action::FireRecovered;
|
||||
result.duration_ms = elapsedMs(now, fault.last_outage_started_ms);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static inline void commitDown(Fault& fault, uint32_t now,
|
||||
uint32_t outage_start_ms) {
|
||||
fault.state = State::FIRING;
|
||||
fault.fired_at_ms = now;
|
||||
fault.last_outage_started_ms = outage_start_ms;
|
||||
}
|
||||
|
||||
static inline void commitRecovered(Fault& fault) {
|
||||
fault.state = State::OK;
|
||||
}
|
||||
|
||||
static inline void reset(Fault& fault) {
|
||||
fault.state = State::OK;
|
||||
fault.fired_at_ms = 0;
|
||||
}
|
||||
|
||||
static inline void rearmIfDisabled(Fault& fault) {
|
||||
if (fault.state == State::FIRING) fault.state = State::OK;
|
||||
}
|
||||
|
||||
static inline void formatAge(uint32_t age_ms, char* out, size_t out_size) {
|
||||
if (!out || out_size == 0) return;
|
||||
uint32_t seconds = age_ms / 1000U;
|
||||
uint32_t hours = seconds / 3600U;
|
||||
uint32_t minutes = (seconds % 3600U) / 60U;
|
||||
if (hours > 0) {
|
||||
snprintf(out, out_size, "%uh%um", (unsigned)hours, (unsigned)minutes);
|
||||
} else {
|
||||
snprintf(out, out_size, "%um", (unsigned)minutes);
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool formatWifiAlert(char* out, size_t out_size,
|
||||
const TickResult& result,
|
||||
const OutageSnapshot& snapshot) {
|
||||
if (!out || out_size == 0) return false;
|
||||
char age[16];
|
||||
formatAge(result.duration_ms, age, sizeof(age));
|
||||
if (result.action == Action::FireDown) {
|
||||
if (snapshot.reason != 0) {
|
||||
snprintf(out, out_size, "WiFi down %s (reason %u)", age,
|
||||
(unsigned)snapshot.reason);
|
||||
} else {
|
||||
snprintf(out, out_size, "WiFi down %s", age);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (result.action == Action::FireRecovered) {
|
||||
snprintf(out, out_size, "WiFi recovered after %s", age);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline void formatMqttDown(char* out, size_t out_size, int slot_1based,
|
||||
const char* preset_name,
|
||||
uint32_t duration_ms) {
|
||||
if (!out || out_size == 0) return;
|
||||
char age[16];
|
||||
formatAge(duration_ms, age, sizeof(age));
|
||||
snprintf(out, out_size, "MQTT slot %d (%s) down %s", slot_1based,
|
||||
preset_name ? preset_name : "?", age);
|
||||
}
|
||||
|
||||
static inline void formatMqttRecovered(char* out, size_t out_size,
|
||||
int slot_1based,
|
||||
const char* preset_name,
|
||||
uint32_t duration_ms) {
|
||||
if (!out || out_size == 0) return;
|
||||
char age[16];
|
||||
formatAge(duration_ms, age, sizeof(age));
|
||||
snprintf(out, out_size, "MQTT slot %d (%s) recovered after %s", slot_1based,
|
||||
preset_name ? preset_name : "?", age);
|
||||
}
|
||||
|
||||
} // namespace AlertFaultPolicy
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <Packet.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#include "AlertFaultPolicy.h"
|
||||
#endif
|
||||
|
||||
// Header layout for PAYLOAD_TYPE_GRP_TXT before encryption:
|
||||
// [0..3] timestamp (uint32_t LE) - also helps make packet_hash unique
|
||||
@@ -129,11 +132,9 @@ bool AlertReporter::resolveChannel(mesh::GroupChannel& out) const {
|
||||
void AlertReporter::onConfigChanged() {
|
||||
// Reset transient state so a config change re-arms the edge detector.
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_wifi.state = OK;
|
||||
_wifi.fired_at_ms = 0;
|
||||
AlertFaultPolicy::reset(_wifi);
|
||||
for (size_t i = 0; i < sizeof(_mqtt) / sizeof(_mqtt[0]); i++) {
|
||||
_mqtt[i].state = OK;
|
||||
_mqtt[i].fired_at_ms = 0;
|
||||
AlertFaultPolicy::reset(_mqtt[i]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -194,122 +195,78 @@ bool AlertReporter::sendText(const char* text) {
|
||||
return sendChannel(text);
|
||||
}
|
||||
|
||||
void AlertReporter::formatAge(unsigned long age_ms, char* out, size_t out_size) const {
|
||||
unsigned long secs = age_ms / 1000UL;
|
||||
unsigned long h = secs / 3600UL;
|
||||
unsigned long m = (secs % 3600UL) / 60UL;
|
||||
if (h > 0) {
|
||||
snprintf(out, out_size, "%luh%lum", h, m);
|
||||
} else {
|
||||
snprintf(out, out_size, "%lum", m);
|
||||
}
|
||||
}
|
||||
|
||||
void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
if (!_prefs || !_obs || !_obs->alert_enabled) return;
|
||||
if (!_mesh) return;
|
||||
|
||||
// Throttle: ~5 s cadence. The thresholds are minutes-scale so this is fine.
|
||||
if ((long)(now_ms - _next_check_ms) < 0) return;
|
||||
_next_check_ms = now_ms + 5000UL;
|
||||
const uint32_t now = (uint32_t)now_ms;
|
||||
if (!AlertFaultPolicy::checkDue(now, (uint32_t)_next_check_ms)) return;
|
||||
_next_check_ms = AlertFaultPolicy::nextCheckMs(now);
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Clamp to a 60-minute floor regardless of what's in NodePrefs. The CLI
|
||||
// already enforces this on set, but a stale prefs file or future field
|
||||
// tweak shouldn't be able to drag the floor below 1 hour and let a
|
||||
// flapping link spam the mesh.
|
||||
//
|
||||
// The rate limiter only applies between two real sends: fired_at_ms == 0
|
||||
// means "never fired since boot/config change", and treating it as a send
|
||||
// at millis()==0 would suppress every first alert until uptime reaches
|
||||
// min_interval (observed as a 30-minute alert.mqtt threshold not reporting
|
||||
// until 60 minutes after a reboot).
|
||||
uint16_t cfg_min = _obs->alert_min_interval_min;
|
||||
if (cfg_min < 60) cfg_min = 60;
|
||||
unsigned long min_interval_ms = (unsigned long)cfg_min * 60000UL;
|
||||
const uint32_t min_interval_ms =
|
||||
AlertFaultPolicy::minIntervalMs(_obs->alert_min_interval_min);
|
||||
|
||||
// -------- WiFi fault --------
|
||||
if (_obs->alert_wifi_minutes > 0) {
|
||||
unsigned long wifi_disc_ms = MQTTBridge::getLastWifiDisconnectTime();
|
||||
unsigned long wifi_conn_ms = MQTTBridge::getWifiConnectedAtMillis();
|
||||
bool wifi_down = (wifi_disc_ms != 0 && wifi_conn_ms == 0);
|
||||
unsigned long down_ms = wifi_down ? (now_ms - wifi_disc_ms) : 0;
|
||||
unsigned long thresh_ms = (unsigned long)_obs->alert_wifi_minutes * 60000UL;
|
||||
|
||||
if (_wifi.state == OK) {
|
||||
if (wifi_down && down_ms >= thresh_ms &&
|
||||
(_wifi.fired_at_ms == 0 || (now_ms - _wifi.fired_at_ms) >= min_interval_ms)) {
|
||||
char age[16];
|
||||
formatAge(down_ms, age, sizeof(age));
|
||||
uint8_t reason = MQTTBridge::getLastWifiDisconnectReason();
|
||||
if (_bridge != nullptr) {
|
||||
const AlertFaultPolicy::OutageSnapshot snapshot =
|
||||
_bridge->getWifiOutageSnapshot();
|
||||
AlertFaultPolicy::TickResult result = AlertFaultPolicy::tick(
|
||||
_wifi, now, snapshot,
|
||||
AlertFaultPolicy::thresholdMs(_obs->alert_wifi_minutes),
|
||||
min_interval_ms);
|
||||
if (result.action == AlertFaultPolicy::Action::FireDown) {
|
||||
char text[80];
|
||||
if (reason != 0) {
|
||||
snprintf(text, sizeof(text), "WiFi down %s (reason %u)", age, (unsigned)reason);
|
||||
} else {
|
||||
snprintf(text, sizeof(text), "WiFi down %s", age);
|
||||
}
|
||||
AlertFaultPolicy::formatWifiAlert(text, sizeof(text), result, snapshot);
|
||||
if (sendChannel(text)) {
|
||||
_wifi.state = FIRING;
|
||||
_wifi.fired_at_ms = now_ms;
|
||||
_wifi.last_outage_started_ms = wifi_disc_ms;
|
||||
AlertFaultPolicy::commitDown(_wifi, now, snapshot.started_ms);
|
||||
}
|
||||
}
|
||||
} else { // FIRING
|
||||
if (!wifi_down) {
|
||||
unsigned long total = (wifi_conn_ms != 0 && _wifi.last_outage_started_ms != 0)
|
||||
? (wifi_conn_ms - _wifi.last_outage_started_ms) : 0;
|
||||
char age[16];
|
||||
formatAge(total, age, sizeof(age));
|
||||
} else if (result.action == AlertFaultPolicy::Action::FireRecovered) {
|
||||
char text[80];
|
||||
snprintf(text, sizeof(text), "WiFi recovered after %s", age);
|
||||
if (sendChannel(text)) _wifi.state = OK;
|
||||
AlertFaultPolicy::formatWifiAlert(text, sizeof(text), result, snapshot);
|
||||
sendChannel(text);
|
||||
AlertFaultPolicy::commitRecovered(_wifi);
|
||||
}
|
||||
}
|
||||
} else if (_wifi.state == FIRING) {
|
||||
_wifi.state = OK; // threshold disabled mid-fault: silently re-arm
|
||||
} else {
|
||||
AlertFaultPolicy::rearmIfDisabled(_wifi);
|
||||
}
|
||||
|
||||
// -------- MQTT slot faults --------
|
||||
if (_obs->alert_mqtt_minutes > 0 && _bridge != nullptr) {
|
||||
int n = MQTTBridge::getRuntimeSlotCount();
|
||||
if (n > (int)(sizeof(_mqtt) / sizeof(_mqtt[0]))) n = (int)(sizeof(_mqtt) / sizeof(_mqtt[0]));
|
||||
unsigned long thresh_ms = (unsigned long)_obs->alert_mqtt_minutes * 60000UL;
|
||||
const uint32_t threshold_ms =
|
||||
AlertFaultPolicy::thresholdMs(_obs->alert_mqtt_minutes);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
Fault& f = _mqtt[i];
|
||||
AlertFaultPolicy::Fault& fault = _mqtt[i];
|
||||
if (!_bridge->isSlotEnabledAndAttempted(i)) {
|
||||
if (f.state == FIRING) f.state = OK; // slot disabled mid-fault
|
||||
AlertFaultPolicy::rearmIfDisabled(fault);
|
||||
continue;
|
||||
}
|
||||
unsigned long outage_start = _bridge->getSlotCurrentOutageStartMs(i);
|
||||
bool down = (outage_start != 0);
|
||||
unsigned long down_ms = down ? (now_ms - outage_start) : 0;
|
||||
|
||||
if (f.state == OK) {
|
||||
if (down && down_ms >= thresh_ms &&
|
||||
(f.fired_at_ms == 0 || (now_ms - f.fired_at_ms) >= min_interval_ms)) {
|
||||
char age[16];
|
||||
formatAge(down_ms, age, sizeof(age));
|
||||
char text[100];
|
||||
snprintf(text, sizeof(text), "MQTT slot %d (%s) down %s",
|
||||
i + 1, _bridge->getSlotPresetName(i), age);
|
||||
if (sendChannel(text)) {
|
||||
f.state = FIRING;
|
||||
f.fired_at_ms = now_ms;
|
||||
f.last_outage_started_ms = outage_start;
|
||||
}
|
||||
}
|
||||
} else { // FIRING
|
||||
if (!down) {
|
||||
unsigned long total = (f.last_outage_started_ms != 0)
|
||||
? (now_ms - f.last_outage_started_ms) : 0;
|
||||
char age[16];
|
||||
formatAge(total, age, sizeof(age));
|
||||
char text[100];
|
||||
snprintf(text, sizeof(text), "MQTT slot %d (%s) recovered after %s",
|
||||
i + 1, _bridge->getSlotPresetName(i), age);
|
||||
if (sendChannel(text)) f.state = OK;
|
||||
const uint32_t outage_start =
|
||||
(uint32_t)_bridge->getSlotCurrentOutageStartMs(i);
|
||||
const AlertFaultPolicy::OutageSnapshot snapshot =
|
||||
AlertFaultPolicy::fromStartMs(outage_start);
|
||||
AlertFaultPolicy::TickResult result = AlertFaultPolicy::tick(
|
||||
fault, now, snapshot, threshold_ms, min_interval_ms);
|
||||
if (result.action == AlertFaultPolicy::Action::FireDown) {
|
||||
char text[100];
|
||||
AlertFaultPolicy::formatMqttDown(
|
||||
text, sizeof(text), i + 1, _bridge->getSlotPresetName(i),
|
||||
result.duration_ms);
|
||||
if (sendChannel(text)) {
|
||||
AlertFaultPolicy::commitDown(fault, now, outage_start);
|
||||
}
|
||||
} else if (result.action == AlertFaultPolicy::Action::FireRecovered) {
|
||||
char text[100];
|
||||
AlertFaultPolicy::formatMqttRecovered(
|
||||
text, sizeof(text), i + 1, _bridge->getSlotPresetName(i),
|
||||
result.duration_ms);
|
||||
sendChannel(text);
|
||||
AlertFaultPolicy::commitRecovered(fault);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#include "bridges/MQTTBridge.h"
|
||||
#include "AlertFaultPolicy.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
@@ -89,14 +90,6 @@ public:
|
||||
private:
|
||||
bool resolveChannel(mesh::GroupChannel& out) const;
|
||||
bool sendChannel(const char* text);
|
||||
void formatAge(unsigned long age_ms, char* out, size_t out_size) const;
|
||||
|
||||
enum FaultState { OK, FIRING };
|
||||
struct Fault {
|
||||
FaultState state;
|
||||
unsigned long fired_at_ms; // millis() when we last sent a "down" alert
|
||||
unsigned long last_outage_started_ms; // remembered so the recovered msg can quote duration
|
||||
};
|
||||
|
||||
NodePrefs* _prefs;
|
||||
MQTTPrefs* _obs;
|
||||
@@ -104,8 +97,8 @@ private:
|
||||
CommonCLICallbacks* _callbacks;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
MQTTBridge* _bridge;
|
||||
Fault _wifi;
|
||||
Fault _mqtt[RUNTIME_MQTT_SLOTS];
|
||||
AlertFaultPolicy::Fault _wifi;
|
||||
AlertFaultPolicy::Fault _mqtt[RUNTIME_MQTT_SLOTS];
|
||||
#endif
|
||||
unsigned long _next_check_ms;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
namespace DatagramPayloadLimits {
|
||||
|
||||
// Encrypted datagrams append a MAC and round the plaintext up to a whole
|
||||
// cipher block. Reserving block_size - 1 bytes covers the worst-case padding.
|
||||
static constexpr size_t maxPlaintext(size_t packet_payload_size,
|
||||
size_t mac_size,
|
||||
size_t block_size) {
|
||||
return block_size > 0 &&
|
||||
packet_payload_size >= mac_size + block_size - 1
|
||||
? packet_payload_size - mac_size - (block_size - 1)
|
||||
: 0;
|
||||
}
|
||||
|
||||
} // namespace DatagramPayloadLimits
|
||||
@@ -17,6 +17,7 @@ static const uint8_t kMaxFailuresAtMaxBackoff = 3;
|
||||
static const uint32_t kDefaultJwtLifetimeSecs = 86400UL;
|
||||
static const uint32_t kMaxJwtStaggerSecs = 300UL;
|
||||
static const uint32_t kMinimumValidEpoch = 1000000000UL;
|
||||
static const uint32_t kJwtReconnectSafetyMarginSecs = 60UL;
|
||||
static const uint32_t kJwtClockThreshold = 1735689600UL; // 2025-01-01 UTC
|
||||
// A wall clock at or past this instant was set from a real source (NTP or an
|
||||
// admin); anything earlier is the firmware's unset-clock default (1715770351,
|
||||
@@ -157,6 +158,23 @@ static inline bool tokenNeedsRenewal(bool time_synced, uint32_t current_time,
|
||||
return current_time >= token_expires_at - renewal_buffer_secs;
|
||||
}
|
||||
|
||||
// Reuse credentials only when their validity is known to outlast the next
|
||||
// handshake. Uncertain credentials are refreshed before reconnecting.
|
||||
static inline bool canReuseJwtForReconnect(bool time_synced, bool has_token,
|
||||
bool force_mint,
|
||||
uint32_t current_time,
|
||||
uint32_t token_expires_at,
|
||||
uint32_t renewal_buffer_secs) {
|
||||
const uint32_t floor_secs =
|
||||
renewal_buffer_secs > UINT32_MAX - kJwtReconnectSafetyMarginSecs
|
||||
? UINT32_MAX
|
||||
: renewal_buffer_secs + kJwtReconnectSafetyMarginSecs;
|
||||
return time_synced && has_token && !force_mint &&
|
||||
token_expires_at >= kMinimumValidEpoch &&
|
||||
current_time < token_expires_at &&
|
||||
(token_expires_at - current_time) > floor_secs;
|
||||
}
|
||||
|
||||
static inline bool renewalAttemptAllowed(uint32_t now, uint32_t last_attempt) {
|
||||
return elapsedMs(now, last_attempt) >= kRenewalThrottleMs;
|
||||
}
|
||||
@@ -196,4 +214,40 @@ static inline SlotActivation classifySlotActivation(int slot, const bool* enable
|
||||
: SlotActivation::OverActiveCap;
|
||||
}
|
||||
|
||||
enum class StaleTokenAction : uint8_t {
|
||||
Defer,
|
||||
Reconnect,
|
||||
Bounce,
|
||||
KeepAlive,
|
||||
};
|
||||
|
||||
// A failed mint must not reconnect with credentials that a clock correction
|
||||
// just invalidated. A live session only needs a bounce when its broker enforces
|
||||
// token expiration on the existing connection.
|
||||
static inline StaleTokenAction classifyStaleToken(bool minted, bool connected,
|
||||
bool broker_enforces_exp) {
|
||||
if (!minted) return StaleTokenAction::Defer;
|
||||
if (!connected) return StaleTokenAction::Reconnect;
|
||||
return broker_enforces_exp ? StaleTokenAction::Bounce
|
||||
: StaleTokenAction::KeepAlive;
|
||||
}
|
||||
|
||||
enum class ClockSource : uint8_t {
|
||||
None,
|
||||
System,
|
||||
Rtc,
|
||||
};
|
||||
|
||||
// Prefer a plausible system clock, then an RTC. Server validation may not use
|
||||
// a local clock as evidence that the requested NTP host answered.
|
||||
static inline ClockSource chooseFallbackClock(bool validating_server,
|
||||
uint32_t system_time,
|
||||
uint32_t rtc_time,
|
||||
uint32_t min_valid_epoch) {
|
||||
if (validating_server) return ClockSource::None;
|
||||
if (system_time >= min_valid_epoch) return ClockSource::System;
|
||||
if (rtc_time >= min_valid_epoch) return ClockSource::Rtc;
|
||||
return ClockSource::None;
|
||||
}
|
||||
|
||||
} // namespace MQTTConnectionPolicy
|
||||
|
||||
@@ -205,6 +205,7 @@ int MQTTMessageBuilder::buildPacketJSON(
|
||||
// Routing path (direct packets only): pass raw hop bytes to buildPacketMessage,
|
||||
// which emits them as an array of lowercase hex hop tokens.
|
||||
bool has_path = packet->isRouteDirect() && packet->getPathHashCount() > 0;
|
||||
const bool has_rx_metrics = !is_tx && packet->getRSSI() < 0;
|
||||
|
||||
return buildPacketMessage(
|
||||
doc,
|
||||
@@ -215,8 +216,8 @@ int MQTTMessageBuilder::buildPacketJSON(
|
||||
packet_type, route_str,
|
||||
packet->payload_len,
|
||||
raw_hex,
|
||||
12.5f, // SNR - using reasonable default
|
||||
-65, // RSSI - using reasonable default
|
||||
has_rx_metrics ? packet->getSNR() : -999.0f,
|
||||
has_rx_metrics ? packet->getRSSI() : -999,
|
||||
NAN, // score - unknown on this reconstruction-less fallback path
|
||||
hash_str,
|
||||
has_path ? packet->path : nullptr,
|
||||
|
||||
@@ -121,9 +121,6 @@ int MQTTPayloadBuilder::buildPacketMessage(
|
||||
snprintf(len_str, sizeof(len_str), "%d", len);
|
||||
snprintf(packet_type_str, sizeof(packet_type_str), "%d", packet_type);
|
||||
snprintf(payload_len_str, sizeof(payload_len_str), "%d", payload_len);
|
||||
StrHelper::ftoaFixed(snr_str, sizeof(snr_str), snr, 1);
|
||||
snprintf(rssi_str, sizeof(rssi_str), "%d", rssi);
|
||||
|
||||
root["timestamp"] = timestamp;
|
||||
root["hash"] = hash;
|
||||
root["origin"] = origin;
|
||||
@@ -139,8 +136,14 @@ int MQTTPayloadBuilder::buildPacketMessage(
|
||||
root["origin_id"] = origin_id;
|
||||
|
||||
if (direction && strcmp(direction, "rx") == 0) {
|
||||
root["SNR"] = snr_str;
|
||||
root["RSSI"] = rssi_str;
|
||||
if (snr > -900.0f) {
|
||||
StrHelper::ftoaFixed(snr_str, sizeof(snr_str), snr, 1);
|
||||
root["SNR"] = snr_str;
|
||||
}
|
||||
if (rssi > -900) {
|
||||
snprintf(rssi_str, sizeof(rssi_str), "%d", rssi);
|
||||
root["RSSI"] = rssi_str;
|
||||
}
|
||||
if (!isnan(score)) {
|
||||
snprintf(score_str, sizeof(score_str), "%d", static_cast<int>(score * 1000));
|
||||
root["score"] = score_str;
|
||||
@@ -222,6 +225,9 @@ static void addNeighborsMessageEntry(
|
||||
JsonObject nb = arr.add<JsonObject>();
|
||||
nb["pubkey"] = neighbor.pubkey_hex;
|
||||
nb["snr"] = neighbor.snr;
|
||||
if (neighbor.rssi < 0) {
|
||||
nb["rssi"] = neighbor.rssi;
|
||||
}
|
||||
if (neighbor.heard_unknown) {
|
||||
nb["heard_secs_ago"] = nullptr; // age unknown, not zero
|
||||
} else {
|
||||
|
||||
@@ -76,6 +76,7 @@ public:
|
||||
uint32_t heard_secs_ago;
|
||||
const char* scopes;
|
||||
const char* status;
|
||||
int rssi; // dBm; non-negative means no measurement is available
|
||||
// True renders heard_secs_ago as JSON null, for a neighbour whose stored
|
||||
// stamp cannot yield an age. No default initializer: the struct stays an
|
||||
// aggregate for the device toolchain, and a zeroed tail means "age known",
|
||||
|
||||
@@ -54,6 +54,14 @@ struct MQTTPresetDef {
|
||||
// Braces match topic placeholders ({device}/{iata}); never send this string to the broker.
|
||||
static const char MQTT_USERPASS_USERNAME_PUBKEY[] = "{pubkey}";
|
||||
|
||||
// Most brokers enforce JWT expiration on a live connection and need a
|
||||
// proactive credential bounce. Waev keeps an established session alive, so
|
||||
// refreshing credentials in place avoids an unnecessary TLS handshake.
|
||||
static inline bool mqttPresetEnforcesTokenExp(const MQTTPresetDef* preset) {
|
||||
if (!preset || !preset->name) return true;
|
||||
return strcmp(preset->name, "waev") != 0;
|
||||
}
|
||||
|
||||
static inline bool mqttPresetUsesDevicePubkeyUsername(const MQTTPresetDef* preset) {
|
||||
return preset && preset->auth_type == MQTT_AUTH_USERPASS &&
|
||||
preset->userpass_username &&
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// Bounded, clamping printf-append for the fixed-size CLI reply buffers used by
|
||||
@@ -41,3 +42,8 @@ static inline void replyAppendf(char* buf, size_t bufsize, int* pos, const char*
|
||||
*pos += n;
|
||||
if ((size_t)*pos >= bufsize) *pos = (int)bufsize - 1; // clamp truncated append
|
||||
}
|
||||
|
||||
static inline uint32_t mbedtlsErrorMagnitude(int32_t stack_err) {
|
||||
return stack_err < 0 ? (uint32_t)(-(int64_t)stack_err)
|
||||
: (uint32_t)stack_err;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// A USB CDC host can leave a port open without draining it. Some platform
|
||||
// implementations then wait indefinitely inside write(), which stalls the
|
||||
// mesh loop. Packet log lines use this bounded writer so a stalled host costs
|
||||
// a diagnostic line instead of radio service.
|
||||
#ifndef SERIAL_LOG_LINE_MAX
|
||||
#define SERIAL_LOG_LINE_MAX 640
|
||||
#endif
|
||||
|
||||
#ifndef SERIAL_LOG_WRITE_BUDGET_MS
|
||||
#define SERIAL_LOG_WRITE_BUDGET_MS 20
|
||||
#endif
|
||||
|
||||
inline uint32_t& serialLogDroppedCount() {
|
||||
static uint32_t count = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
inline bool& serialLogPortSeen() {
|
||||
static bool seen = false;
|
||||
return seen;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool serialLogEmit(T& out, const char* data, size_t len) {
|
||||
if (out.availableForWrite() <= 0) return false;
|
||||
|
||||
const uint32_t start = millis();
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
int space = out.availableForWrite();
|
||||
if (space <= 0) {
|
||||
if ((uint32_t)(millis() - start) >= SERIAL_LOG_WRITE_BUDGET_MS) {
|
||||
return false;
|
||||
}
|
||||
delay(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t chunk = (size_t)space;
|
||||
if (chunk > len - sent) chunk = len - sent;
|
||||
size_t written = out.write(
|
||||
reinterpret_cast<const uint8_t*>(data + sent), chunk);
|
||||
if (written == 0) return false;
|
||||
sent += written;
|
||||
}
|
||||
serialLogPortSeen() = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <size_t CAP = SERIAL_LOG_LINE_MAX>
|
||||
class SerialLogLine {
|
||||
public:
|
||||
static_assert(CAP >= 3, "SerialLogLine needs room for text and CRLF");
|
||||
|
||||
void printf(const char* format, ...) {
|
||||
size_t room = capacity() - _len;
|
||||
if (room == 0) {
|
||||
_truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int count = vsnprintf(&_buffer[_len], room, format, args);
|
||||
va_end(args);
|
||||
if (count < 0) return;
|
||||
if ((size_t)count >= room) {
|
||||
_len = capacity() - 1;
|
||||
_truncated = true;
|
||||
} else {
|
||||
_len += (size_t)count;
|
||||
}
|
||||
}
|
||||
|
||||
void hex(const uint8_t* source, size_t len) {
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
while (len > 0) {
|
||||
if (_len + 2 > capacity()) {
|
||||
_truncated = true;
|
||||
return;
|
||||
}
|
||||
uint8_t value = *source++;
|
||||
_buffer[_len++] = digits[value >> 4];
|
||||
_buffer[_len++] = digits[value & 0x0F];
|
||||
len--;
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the compact packet logger's LF-only wire format when requested.
|
||||
template <class T>
|
||||
bool flush(T& out, bool use_crlf = true) {
|
||||
bool complete = !_truncated;
|
||||
if (use_crlf) _buffer[_len++] = '\r';
|
||||
_buffer[_len++] = '\n';
|
||||
const size_t line_len = _len;
|
||||
_len = 0;
|
||||
_truncated = false;
|
||||
|
||||
uint32_t& dropped = serialLogDroppedCount();
|
||||
if (dropped > 0) {
|
||||
char marker[24];
|
||||
int count = snprintf(marker, sizeof(marker), "DROP:%u\r\n",
|
||||
(unsigned)dropped);
|
||||
if (count > 0 && serialLogEmit(out, marker, (size_t)count)) dropped = 0;
|
||||
}
|
||||
|
||||
if (!serialLogEmit(out, _buffer, line_len)) complete = false;
|
||||
if (!complete && serialLogPortSeen()) dropped++;
|
||||
return complete;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t capacity() const { return CAP - 2; }
|
||||
|
||||
char _buffer[CAP];
|
||||
size_t _len = 0;
|
||||
bool _truncated = false;
|
||||
};
|
||||
|
||||
inline void serialLogBegin() {
|
||||
#if defined(ESP32_PLATFORM) && defined(ARDUINO_USB_CDC_ON_BOOT) && \
|
||||
(ARDUINO_USB_CDC_ON_BOOT == 1)
|
||||
Serial.setTxTimeoutMs(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
@@ -34,7 +34,14 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
// Full Companion owns its primary USB 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};
|
||||
#else
|
||||
static std::atomic<bool> usb_logging_enabled{true};
|
||||
#endif
|
||||
static std::atomic<bool> usb_logging_preference_known{false};
|
||||
|
||||
class NullUsbLoggingStream : public Stream {
|
||||
@@ -151,6 +158,7 @@ void beginUsbLoggingPort() {
|
||||
// A terminal opening or closing the diagnostics interface must never reboot
|
||||
// the node or put it into the ROM downloader.
|
||||
dedicated_usb_logging_port->enableReboot(false);
|
||||
dedicated_usb_logging_port->setTxTimeoutMs(0);
|
||||
dedicated_usb_logging_port->begin(115200);
|
||||
#elif defined(MESH_NRF52_DUAL_CDC_LOGGING)
|
||||
dedicated_usb_logging_port.begin(115200);
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO)
|
||||
class Stream;
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Ordinary logging images start enabled. Dual-CDC Full Companion starts with
|
||||
// Ordinary logging images start enabled. Every Full Companion starts with
|
||||
// logging disabled and restores its saved choice after preferences load.
|
||||
bool isUsbLoggingEnabled();
|
||||
void setUsbLoggingEnabled(bool enabled);
|
||||
@@ -26,9 +26,8 @@ void setUsbLoggingEnabled(bool enabled);
|
||||
// mirror. Returns false only when that mirror could not be saved.
|
||||
bool saveUsbLoggingBootPreference(bool enabled);
|
||||
|
||||
// Start the optional dedicated USB logging interface. Ordinary builds keep
|
||||
// using Serial. Supported Full Companion builds expose a second CDC ACM
|
||||
// interface so plaintext diagnostics never share the framed Companion stream.
|
||||
// Start the optional dedicated USB logging interface. Ordinary and single-TTY
|
||||
// builds use Serial; dual-CDC Full Companion builds use a second CDC ACM port.
|
||||
void beginUsbLoggingPort();
|
||||
Stream& usbLoggingPort();
|
||||
bool hasDedicatedUsbLoggingPort();
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_sntp.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
@@ -583,7 +584,8 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
|
||||
}
|
||||
// mbedTLS stack error (shown as negative hex per convention)
|
||||
if (slot.last_tls_stack_err != 0) {
|
||||
replyAppendf(buf, bufsize, &pos, ", mbedtls:-0x%04X", (unsigned)(-slot.last_tls_stack_err));
|
||||
replyAppendf(buf, bufsize, &pos, ", mbedtls:-0x%04X",
|
||||
(unsigned)mbedtlsErrorMagnitude(slot.last_tls_stack_err));
|
||||
}
|
||||
// Socket errno
|
||||
if (slot.last_sock_errno != 0) {
|
||||
@@ -688,7 +690,7 @@ MQTTBridge::MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs,
|
||||
_snmp_agent(nullptr),
|
||||
#endif
|
||||
_last_wifi_check(0), _last_wifi_status(WL_DISCONNECTED), _wifi_status_initialized(false),
|
||||
_wifi_disconnected_time(0), _last_wifi_reconnect_attempt(0), _wifi_reconnect_backoff_attempt(0),
|
||||
_wifi_outage_bits{0}, _last_wifi_reconnect_attempt(0), _wifi_reconnect_backoff_attempt(0),
|
||||
_last_slot_reconnect_ms(0)
|
||||
#ifdef ESP_PLATFORM
|
||||
, _packet_queue_handle(nullptr), _mqtt_task_handle(nullptr),
|
||||
@@ -733,6 +735,7 @@ MQTTBridge::MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs,
|
||||
_slots[i].last_log_time = 0;
|
||||
_slots[i].port = 1883;
|
||||
_slot_reconfigure_pending[i] = false;
|
||||
_slot_force_jwt_mint[i] = false;
|
||||
_status_publish_pending[i] = false;
|
||||
}
|
||||
|
||||
@@ -797,17 +800,8 @@ void MQTTBridge::allocateRuntimeBuffers() {
|
||||
_json_scratch_buffer ? "PSRAM" : "stack fallback");
|
||||
#endif
|
||||
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
// Persistent neighbors JSON buffer, heap-allocated on every board: too large to
|
||||
// keep inline in the bridge object the way the non-PSRAM status/packet buffers
|
||||
// are. psram_malloc() falls back to internal DRAM, so this works without PSRAM.
|
||||
// Unlike status/packet there is no stack fallback -- a nullptr simply disables
|
||||
// publishing (requestPublishNeighbors/publishNeighbors both no-op on nullptr).
|
||||
_neighbors_json_buffer = static_cast<char*>(MQTTRuntimeBufferLifecycle::allocateIfMissing(
|
||||
_neighbors_json_buffer, NEIGHBORS_JSON_BUFFER_SIZE, psram_malloc));
|
||||
MQTT_DEBUG_PRINTLN("Neighbors buffer: %s",
|
||||
_neighbors_json_buffer ? "ready" : "unavailable");
|
||||
#endif
|
||||
// The neighbors JSON buffer is allocated lazily by requestPublishNeighbors().
|
||||
// Nodes which never publish neighbors do not spend this memory.
|
||||
}
|
||||
|
||||
void MQTTBridge::releaseRuntimeBuffers() {
|
||||
@@ -824,7 +818,7 @@ void MQTTBridge::releaseRuntimeBuffers() {
|
||||
_json_scratch_doc.clear();
|
||||
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
// Paired with the unconditional allocation in allocateRuntimeBuffers().
|
||||
// Paired with the lazy allocation in requestPublishNeighbors().
|
||||
_neighbors_json_buffer = static_cast<char*>(MQTTRuntimeBufferLifecycle::release(
|
||||
_neighbors_json_buffer, psram_free));
|
||||
_neighbors_publish_len = 0;
|
||||
@@ -993,6 +987,17 @@ void MQTTBridge::begin() {
|
||||
_slots[i].enabled = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Preferences are authoritative across bridge restarts. teardownSlot()
|
||||
// intentionally preserves configuration for reuse, so explicitly clear
|
||||
// fields for a slot that is now disabled.
|
||||
_slots[i].enabled = false;
|
||||
_slots[i].preset = nullptr;
|
||||
_slots[i].host[0] = '\0';
|
||||
_slots[i].username[0] = '\0';
|
||||
_slots[i].password[0] = '\0';
|
||||
_slots[i].audience[0] = '\0';
|
||||
_slots[i].port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,8 +1222,10 @@ void MQTTBridge::LifecycleOps::releaseResources() {
|
||||
if (b->_mqtt_task_handle != nullptr) {
|
||||
vTaskDelete(b->_mqtt_task_handle);
|
||||
}
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) b->teardownSlot(i);
|
||||
b->destroySlotClients();
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
b->teardownSlot(i, /*force=*/true);
|
||||
}
|
||||
b->destroySlotClients(/*force=*/true);
|
||||
}
|
||||
// Clean path (or a task that acked right at the deadline): the MQTT task
|
||||
// already disconnected/deleted its clients on Core 0 and self-terminated, so
|
||||
@@ -1297,16 +1304,23 @@ void MQTTBridge::initializeWiFiInTask() {
|
||||
switch(event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiGotIp(wifiOutage()));
|
||||
_wifi_reconnect_backoff_attempt = 0;
|
||||
// Set flag to trigger NTP sync from loop() instead of doing it here
|
||||
if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
|
||||
s_wifi_disconnect_time = millis();
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: {
|
||||
const uint8_t reason = info.wifi_sta_disconnected.reason;
|
||||
const unsigned long event_time = millis();
|
||||
s_wifi_disconnect_reason = reason;
|
||||
s_wifi_disconnect_time = event_time;
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiDisconnectEvent(
|
||||
(uint32_t)event_time, reason, wifiOutage()));
|
||||
MQTT_DEBUG_PRINTLN("WiFi disconnected: reason %d", s_wifi_disconnect_reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1677,6 +1691,7 @@ bool MQTTBridge::ensureSlotClient(int index) {
|
||||
slot.client->onConnect([this, index](bool sessionPresent) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d connected", index + 1);
|
||||
_slots[index].connected = true;
|
||||
_slot_force_jwt_mint[index] = false;
|
||||
// NOTE: reconnect_backoff / max_backoff_failures are NOT reset here.
|
||||
// A CONNACK alone doesn't prove the link is healthy -- a broker that
|
||||
// accepts and then drops within seconds would reset the ladder every
|
||||
@@ -1723,6 +1738,7 @@ bool MQTTBridge::ensureSlotClient(int index) {
|
||||
_slots[index].last_sock_errno = error.esp_transport_sock_errno;
|
||||
_slots[index].last_error_time = millis();
|
||||
if (error.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
|
||||
_slot_force_jwt_mint[index] = true;
|
||||
// Broker rejected the MQTT CONNECT itself -- not a transport failure.
|
||||
// return code: 1=protocol, 2=client-id rejected, 3=server unavailable,
|
||||
// 4=bad username/password, 5=not authorized. Codes 3/4/5 point at a
|
||||
@@ -1779,11 +1795,13 @@ void MQTTBridge::releaseSlotAuthToken(int index) {
|
||||
slot.last_token_renewal = 0;
|
||||
}
|
||||
|
||||
void MQTTBridge::destroySlotClients() {
|
||||
void MQTTBridge::destroySlotClients(bool force) {
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
MQTTSlot& slot = _slots[i];
|
||||
if (slot.client != nullptr) {
|
||||
if (slot.client->connected()) {
|
||||
if (force) {
|
||||
slot.client->forceStop();
|
||||
} else if (slot.client->connected()) {
|
||||
slot.client->disconnect();
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
@@ -1875,6 +1893,7 @@ bool MQTTBridge::setupSlot(int index) {
|
||||
slot.max_backoff_failures = 0;
|
||||
slot.circuit_breaker_tripped = false;
|
||||
slot.last_reconnect_attempt = 0;
|
||||
_slot_force_jwt_mint[index] = false;
|
||||
}
|
||||
|
||||
bool uses_jwt = (slot.preset && slot.preset->auth_type == MQTT_AUTH_JWT) || slot.audience[0] != '\0';
|
||||
@@ -2043,12 +2062,16 @@ bool MQTTBridge::setupSlot(int index) {
|
||||
// the client object alive so a subsequent setupSlot() can reuse its mbedTLS
|
||||
// context. This is called both on reconfigure (preset change) and at shutdown;
|
||||
// destruction of the underlying client happens once in destroySlotClients().
|
||||
void MQTTBridge::teardownSlot(int index) {
|
||||
void MQTTBridge::teardownSlot(int index, bool force) {
|
||||
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return;
|
||||
MQTTSlot& slot = _slots[index];
|
||||
|
||||
if (slot.client && slot.client->connected()) {
|
||||
slot.client->disconnect();
|
||||
if (slot.client && (force || slot.client->connected())) {
|
||||
if (force) {
|
||||
slot.client->forceStop();
|
||||
} else {
|
||||
slot.client->disconnect();
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
#else
|
||||
@@ -2070,6 +2093,20 @@ void MQTTBridge::teardownSlot(int index) {
|
||||
slot.last_reconnect_attempt = 0;
|
||||
slot.last_log_time = 0;
|
||||
slot.last_deferred_log_ms = 0;
|
||||
_slot_force_jwt_mint[index] = false;
|
||||
}
|
||||
|
||||
void MQTTBridge::reconnectSlotClient(int index) {
|
||||
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return;
|
||||
MQTTSlot& slot = _slots[index];
|
||||
if (slot.client == nullptr) return;
|
||||
|
||||
if (!slot.client->isStarted()) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d start (client was stopped)", index + 1);
|
||||
slot.client->connect();
|
||||
return;
|
||||
}
|
||||
slot.client->reconnect();
|
||||
}
|
||||
|
||||
void MQTTBridge::maintainSlotConnections() {
|
||||
@@ -2211,15 +2248,26 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
|
||||
(time_synced && old_token_expires_at >= 1000000000 &&
|
||||
current_time >= (old_token_expires_at - renewal_buffer));
|
||||
|
||||
if (old_token_expired_or_imminent || !slot.client->connected()) {
|
||||
// Disconnect + reconnect with fresh credentials, reusing existing client
|
||||
// to avoid internal heap leak/fragmentation from destroy/create cycles
|
||||
const bool exp_forces_bounce =
|
||||
old_token_expired_or_imminent &&
|
||||
mqttPresetEnforcesTokenExp(slot.preset);
|
||||
if (!exp_forces_bounce && old_token_expired_or_imminent &&
|
||||
slot.client->connected()) {
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"MQTT%d token renewed, no bounce (broker does not enforce exp)",
|
||||
index + 1);
|
||||
}
|
||||
if (exp_forces_bounce || !slot.client->connected()) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d token renewal: reconnecting with fresh credentials", index + 1);
|
||||
if (slot.client->connected()) {
|
||||
slot.client->disconnect(); // stops the client internally
|
||||
if (slot.client->isStarted()) {
|
||||
// Retain the esp-mqtt task and its stack across the TLS handshake.
|
||||
slot.client->softDisconnect();
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
slot.client->reconnect();
|
||||
} else {
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
slot.client->connect();
|
||||
}
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
slot.client->connect(); // restart stopped client; reconnect() fails silently on a stopped client
|
||||
reconnect_attempted = true;
|
||||
_last_slot_reconnect_ms = now_millis;
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d int_heap=%d at token renewal reconnect", index + 1,
|
||||
@@ -2244,6 +2292,64 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
|
||||
// persistent clients (Phase 1), the mbedTLS context is allocated once at
|
||||
// startup and the preflight is no longer necessary.
|
||||
|
||||
const auto prepareJwtReconnect = [&](bool force_mint, int backoff_level) {
|
||||
const bool has_token = slot.auth_token && slot.auth_token[0] != '\0';
|
||||
const unsigned long expires_at = slot.token_expires_at;
|
||||
const bool remaining_known =
|
||||
time_synced && expires_at >= MQTTConnectionPolicy::kMinimumValidEpoch;
|
||||
const unsigned long remaining_secs =
|
||||
current_time < expires_at ? expires_at - current_time : 0;
|
||||
const bool force_after_refusal = _slot_force_jwt_mint[index];
|
||||
force_mint = force_mint || force_after_refusal;
|
||||
const uint32_t renewal_buffer = MQTTConnectionPolicy::renewalBufferSecs(
|
||||
static_cast<uint32_t>(slotTokenLifetime(index)));
|
||||
const bool reuse_token = MQTTConnectionPolicy::canReuseJwtForReconnect(
|
||||
time_synced, has_token, force_mint,
|
||||
static_cast<uint32_t>(current_time),
|
||||
static_cast<uint32_t>(expires_at), renewal_buffer);
|
||||
|
||||
const char* mint_reason = "none";
|
||||
if (!reuse_token) {
|
||||
if (backoff_level < 0) {
|
||||
mint_reason = "circuit-breaker-probe";
|
||||
} else if (force_after_refusal) {
|
||||
mint_reason = "connection-refused";
|
||||
} else if (!time_synced) {
|
||||
mint_reason = "clock-unsynced";
|
||||
} else if (!has_token) {
|
||||
mint_reason = "empty-token";
|
||||
} else if (expires_at < MQTTConnectionPolicy::kMinimumValidEpoch) {
|
||||
mint_reason = "invalid-expiry";
|
||||
} else if (current_time >= expires_at) {
|
||||
mint_reason = "expired";
|
||||
} else if (remaining_secs <=
|
||||
MQTTConnectionPolicy::kJwtReconnectSafetyMarginSecs) {
|
||||
mint_reason = "safety-margin";
|
||||
} else if (remaining_secs <= renewal_buffer) {
|
||||
mint_reason = "renewal-due";
|
||||
} else {
|
||||
mint_reason = "renewal-imminent";
|
||||
}
|
||||
}
|
||||
|
||||
const char* mint_result = reuse_token ? "REUSED" : "FAILED";
|
||||
if (!reuse_token && createSlotAuthToken(index)) {
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
mint_result = "OK";
|
||||
}
|
||||
char remaining_text[24];
|
||||
if (remaining_known) {
|
||||
snprintf(remaining_text, sizeof(remaining_text), "%lus", remaining_secs);
|
||||
} else {
|
||||
strncpy(remaining_text, "unknown", sizeof(remaining_text));
|
||||
remaining_text[sizeof(remaining_text) - 1] = '\0';
|
||||
}
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"MQTT%d JWT reconnect backoff=%d token=%s mint_reason=%s result=%s remaining=%s",
|
||||
index + 1, backoff_level, reuse_token ? "REUSE" : "MINT",
|
||||
mint_reason, mint_result, remaining_text);
|
||||
};
|
||||
|
||||
// Periodic probe for circuit-breaker-tripped slots (recovery from transient outages)
|
||||
// Attempts a single reconnect every 30 minutes to see if the server has come back
|
||||
if (slot.circuit_breaker_tripped && !reconnect_attempted) {
|
||||
@@ -2260,17 +2366,9 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
|
||||
_radio ? _radio->getRadioState() : -1,
|
||||
(_radio && _radio->getLastRecvMillis() > 0) ? (_ms->getMillis() - _radio->getLastRecvMillis()) : 0);
|
||||
if (slot_uses_jwt) {
|
||||
// Regenerate or refresh token, then reconnect the persistent client.
|
||||
// Reaching the ladder at all means setupSlot() ran, so the client object
|
||||
// and its mbedTLS context are live and no full setup is needed here.
|
||||
if (createSlotAuthToken(index)) {
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d circuit breaker probe (fresh token)", index + 1);
|
||||
}
|
||||
slot.client->reconnect();
|
||||
} else {
|
||||
slot.client->reconnect();
|
||||
prepareJwtReconnect(true, -1);
|
||||
}
|
||||
reconnectSlotClient(index);
|
||||
// If the connect callback fires and sets slot.connected = true,
|
||||
// it will clear circuit_breaker_tripped via the onConnect handler
|
||||
}
|
||||
@@ -2300,22 +2398,12 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
|
||||
reconnect_attempted = true;
|
||||
_last_slot_reconnect_ms = now_millis;
|
||||
if (slot_uses_jwt) {
|
||||
// Always lightweight reconnect on the persistent client. A stale/expired
|
||||
// token is handled by regenerating it in place and updating credentials
|
||||
// - no teardown is needed because the client and its mbedTLS context
|
||||
// persist for the bridge lifetime.
|
||||
if (createSlotAuthToken(index)) {
|
||||
slot.client->setCredentials(_jwt_username, slot.auth_token);
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d reconnect (fresh token, backoff %d)", index + 1, slot.reconnect_backoff);
|
||||
} else {
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d reconnect (token refresh failed, backoff %d)", index + 1, slot.reconnect_backoff);
|
||||
}
|
||||
slot.client->reconnect();
|
||||
prepareJwtReconnect(false, slot.reconnect_backoff);
|
||||
} else {
|
||||
// Non-JWT slots - lightweight reconnect on existing client.
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d reconnect (non-JWT, backoff %d)", index + 1, slot.reconnect_backoff);
|
||||
slot.client->reconnect();
|
||||
}
|
||||
reconnectSlotClient(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2750,11 +2838,17 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
if (!_wifi_status_initialized) {
|
||||
_last_wifi_status = current_wifi_status;
|
||||
_wifi_status_initialized = true;
|
||||
if (current_wifi_status != WL_CONNECTED) {
|
||||
_wifi_disconnected_time = now;
|
||||
}
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, current_wifi_status == WL_CONNECTED, wifiOutage(),
|
||||
false));
|
||||
}
|
||||
if (now - _last_wifi_check <= 10000) {
|
||||
if (current_wifi_status == WL_CONNECTED) {
|
||||
AlertFaultPolicy::OutageSnapshot snapshot = wifiOutage();
|
||||
if (snapshot.down) {
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiGotIp(snapshot));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_last_wifi_check = now;
|
||||
@@ -2762,7 +2856,8 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
if (current_wifi_status == WL_CONNECTED) {
|
||||
if (_last_wifi_status != WL_CONNECTED) {
|
||||
transitioned_to_connected = true;
|
||||
_wifi_disconnected_time = 0;
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, true, wifiOutage(), true));
|
||||
s_wifi_connected_at = now;
|
||||
_wifi_reconnect_backoff_attempt = 0;
|
||||
#ifdef ESP_PLATFORM
|
||||
@@ -2795,8 +2890,12 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
}
|
||||
_last_wifi_status = WL_CONNECTED;
|
||||
} else {
|
||||
if (_last_wifi_status == WL_CONNECTED) {
|
||||
_wifi_disconnected_time = now;
|
||||
const bool last_connected = (_last_wifi_status == WL_CONNECTED);
|
||||
AlertFaultPolicy::OutageSnapshot snapshot =
|
||||
AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, false, wifiOutage(), true);
|
||||
setWifiOutage(snapshot);
|
||||
if (last_connected) {
|
||||
s_wifi_connected_at = 0;
|
||||
// Disconnect all slot clients when WiFi drops
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
@@ -2804,13 +2903,13 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
_slots[i].client->disconnect();
|
||||
}
|
||||
}
|
||||
} else if (_wifi_disconnected_time > 0) {
|
||||
} else if (snapshot.down) {
|
||||
// Backoff ladder + wrap-safe timing live in MQTTConnectionPolicy (Phase 6),
|
||||
// exercised by host tests. Behavior is unchanged: both the link-down
|
||||
// duration and the since-last-attempt interval must clear the current rung
|
||||
// (elapsedMs is the wrap-safe form of the old ULONG_MAX branch).
|
||||
if (_manage_wifi && MQTTConnectionPolicy::wifiReconnectDue(
|
||||
(uint32_t)now, (uint32_t)_wifi_disconnected_time,
|
||||
(uint32_t)now, snapshot.started_ms,
|
||||
(uint32_t)_last_wifi_reconnect_attempt,
|
||||
_wifi_reconnect_backoff_attempt)) {
|
||||
_last_wifi_reconnect_attempt = now;
|
||||
@@ -3718,10 +3817,20 @@ void MQTTBridge::setNeighborsSchedule(NeighborsPhase phase, uint32_t secs_until_
|
||||
}
|
||||
|
||||
void MQTTBridge::requestPublishNeighbors(const char* json, size_t len) {
|
||||
if (!_neighbors_json_buffer || !json || len == 0) return;
|
||||
if (!json || len == 0) return;
|
||||
// Drop a new snapshot while one is still being published (Core 0 clears the
|
||||
// flag when done). Acquire pairs with the task loop's release store.
|
||||
if (_neighbors_publish_pending.load(std::memory_order_acquire)) return;
|
||||
// A discovery can finish after bridge shutdown. Do not allocate a buffer
|
||||
// which has no task left to consume or release it.
|
||||
if (!isRunning()) return;
|
||||
_neighbors_json_buffer = static_cast<char*>(
|
||||
MQTTRuntimeBufferLifecycle::allocateIfMissing(
|
||||
_neighbors_json_buffer, NEIGHBORS_JSON_BUFFER_SIZE, psram_malloc));
|
||||
if (!_neighbors_json_buffer) {
|
||||
MQTT_DEBUG_PRINTLN("Neighbors buffer unavailable, dropping snapshot");
|
||||
return;
|
||||
}
|
||||
if (len >= NEIGHBORS_JSON_BUFFER_SIZE) {
|
||||
len = NEIGHBORS_JSON_BUFFER_SIZE - 1;
|
||||
}
|
||||
@@ -3952,15 +4061,20 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
const int kMaxNtpRetriesPerServer = 2;
|
||||
for (int s = 0; s < server_count && !ntp_ok; s++) {
|
||||
const char* server = servers[s];
|
||||
_ntp_client.setPoolServerName(server);
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
IPAddress resolved_ip;
|
||||
if (!WiFi.hostByName(server, resolved_ip)) {
|
||||
MQTT_DEBUG_PRINTLN("WARNING: DNS resolution failed for %s - NTP sync may fail", server);
|
||||
// NTPClient ignores a failed beginPacket(), while WiFiUDP retains the
|
||||
// prior remote address. Sending here could credit the previous server
|
||||
// to this unresolvable name.
|
||||
MQTT_DEBUG_PRINTLN("NTP: %s does not resolve - skipping", server);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
_ntp_client.setPoolServerName(server);
|
||||
|
||||
for (int attempt = 1; attempt <= kMaxNtpRetriesPerServer && !ntp_ok; attempt++) {
|
||||
if (attempt > 1) {
|
||||
MQTT_DEBUG_PRINTLN("NTP retry %d/%d on %s...", attempt, kMaxNtpRetriesPerServer, server);
|
||||
@@ -3984,23 +4098,55 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
for (int s = 0; s < server_count && !ntp_ok; s++) {
|
||||
const char* server = servers[s];
|
||||
MQTT_DEBUG_PRINTLN("SNTP fallback trying %s...", server);
|
||||
if (sntp_enabled()) sntp_stop();
|
||||
sntp_set_sync_status(SNTP_SYNC_STATUS_RESET);
|
||||
configTime(0, 0, server);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
delay(500);
|
||||
if (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) continue;
|
||||
epochTime = (unsigned long)time(nullptr);
|
||||
if (epochTime >= kMinValidEpoch) {
|
||||
ntp_ok = true;
|
||||
ntp_server_used = server;
|
||||
MQTT_DEBUG_PRINTLN("SNTP fallback succeeded on %s: %lu", server, epochTime);
|
||||
break;
|
||||
} else {
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"SNTP fallback: %s synced an implausible epoch %lu", server,
|
||||
epochTime);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ntp_ok && ntp_server_used) {
|
||||
configTime(0, 0, ntp_server_used);
|
||||
if (!ntp_ok) {
|
||||
const unsigned long system_time = (unsigned long)time(nullptr);
|
||||
const unsigned long rtc_time =
|
||||
_rtc ? (unsigned long)_rtc->getCurrentTime() : 0;
|
||||
const MQTTConnectionPolicy::ClockSource source =
|
||||
MQTTConnectionPolicy::chooseFallbackClock(
|
||||
primary_only, (uint32_t)system_time, (uint32_t)rtc_time,
|
||||
(uint32_t)kMinValidEpoch);
|
||||
if (source != MQTTConnectionPolicy::ClockSource::None) {
|
||||
const bool from_rtc = source == MQTTConnectionPolicy::ClockSource::Rtc;
|
||||
epochTime = from_rtc ? rtc_time : system_time;
|
||||
ntp_ok = true;
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"No NTP server answered; continuing on the existing %s: %lu",
|
||||
from_rtc ? "RTC" : "system clock", epochTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (ntp_ok) {
|
||||
// Make the accepted epoch authoritative before stale-token checks or JWT
|
||||
// creation read time(nullptr). configTime() itself is asynchronous.
|
||||
struct timeval accepted;
|
||||
accepted.tv_sec = (time_t)epochTime;
|
||||
accepted.tv_usec = 0;
|
||||
settimeofday(&accepted, nullptr);
|
||||
|
||||
if (ntp_server_used) configTime(0, 0, ntp_server_used);
|
||||
|
||||
if (_rtc) {
|
||||
_rtc->setCurrentTime(epochTime);
|
||||
@@ -4011,11 +4157,11 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
_last_ntp_sync = millis();
|
||||
sync_in_progress = false;
|
||||
|
||||
MQTT_DEBUG_PRINTLN("Time synced: %lu (via %s)", epochTime, ntp_server_used);
|
||||
MQTT_DEBUG_PRINTLN("Time synced: %lu (via %s)", epochTime,
|
||||
ntp_server_used ? ntp_server_used : "existing clock");
|
||||
|
||||
// If slots are already set up and the time jumped significantly (e.g., SNTP
|
||||
// initially returned stale RTC time, then a later sync corrected it), tear down
|
||||
// and re-setup all JWT-authenticated slots so they get fresh tokens.
|
||||
// If a correction made an existing JWT stale, stage fresh credentials and
|
||||
// choose the reconnect action appropriate to the client and broker state.
|
||||
if (_slots_setup_done && was_ntp_synced) {
|
||||
unsigned long current_time = (unsigned long)time(nullptr);
|
||||
// Every slot, not _max_active_slots: that is a count of positions, never an
|
||||
@@ -4028,14 +4174,31 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
bool slot_jwt = (_slots[i].preset && _slots[i].preset->auth_type == MQTT_AUTH_JWT) ||
|
||||
(!_slots[i].preset && _slots[i].audience[0] != '\0');
|
||||
if (_slots[i].enabled && slot_jwt && _slots[i].client) {
|
||||
// Token created before NTP corrected the clock - refresh credentials
|
||||
// in place and reconnect the persistent client. No teardown needed.
|
||||
if (_slots[i].token_expires_at > 0 && current_time > _slots[i].token_expires_at) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT%d token stale after time correction, re-creating", i + 1);
|
||||
if (createSlotAuthToken(i)) {
|
||||
_slots[i].client->setCredentials(_jwt_username, _slots[i].auth_token);
|
||||
const MQTTConnectionPolicy::StaleTokenAction action =
|
||||
MQTTConnectionPolicy::classifyStaleToken(
|
||||
createSlotAuthToken(i), _slots[i].client->connected(),
|
||||
mqttPresetEnforcesTokenExp(_slots[i].preset));
|
||||
if (action == MQTTConnectionPolicy::StaleTokenAction::Defer) {
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"MQTT%d token refresh failed after time correction; deferring",
|
||||
i + 1);
|
||||
continue;
|
||||
}
|
||||
_slots[i].client->setCredentials(_jwt_username,
|
||||
_slots[i].auth_token);
|
||||
if (action == MQTTConnectionPolicy::StaleTokenAction::Reconnect) {
|
||||
reconnectSlotClient(i);
|
||||
} else if (action == MQTTConnectionPolicy::StaleTokenAction::Bounce) {
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"MQTT%d bouncing for the corrected-clock token", i + 1);
|
||||
_slots[i].client->softDisconnect();
|
||||
_slots[i].client->reconnect();
|
||||
} else {
|
||||
MQTT_DEBUG_PRINTLN(
|
||||
"MQTT%d token re-created without a live-session bounce", i + 1);
|
||||
}
|
||||
_slots[i].client->reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "helpers/MQTTPacketFilter.h"
|
||||
#include "helpers/MQTTPresets.h"
|
||||
#include "helpers/MQTTLifecycle.h"
|
||||
#include "helpers/AlertFaultPolicy.h"
|
||||
#include <atomic>
|
||||
|
||||
#ifdef WITH_SNMP
|
||||
@@ -233,6 +234,10 @@ private:
|
||||
// Pending slot reconfigure: set from CLI (Core 1), processed by MQTT task (Core 0)
|
||||
volatile bool _slot_reconfigure_pending[RUNTIME_MQTT_SLOTS];
|
||||
|
||||
// A broker refusal can invalidate an otherwise clock-valid JWT. The event
|
||||
// callback sets this byte and the bridge task consumes it.
|
||||
volatile bool _slot_force_jwt_mint[RUNTIME_MQTT_SLOTS];
|
||||
|
||||
// Pending on-connect status publish: set from the onConnect callback (which
|
||||
// runs on the esp-mqtt event task, NOT this bridge task), consumed by the MQTT
|
||||
// task (Core 0). publishStatusToSlot() touches the shared status doc/buffer/
|
||||
@@ -416,11 +421,20 @@ private:
|
||||
unsigned long _last_wifi_check;
|
||||
wl_status_t _last_wifi_status;
|
||||
bool _wifi_status_initialized;
|
||||
unsigned long _wifi_disconnected_time; // 0 when connected
|
||||
std::atomic<uint64_t> _wifi_outage_bits;
|
||||
unsigned long _last_wifi_reconnect_attempt;
|
||||
uint8_t _wifi_reconnect_backoff_attempt; // 0..5 -> 15s, 30s, 60s, 120s, 300s; reset on connect
|
||||
unsigned long _last_slot_reconnect_ms; // guards against concurrent TLS handshakes (15 s inter-slot gap)
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot wifiOutage() const {
|
||||
return AlertFaultPolicy::unpackOutageSnapshot(
|
||||
_wifi_outage_bits.load(std::memory_order_acquire));
|
||||
}
|
||||
void setWifiOutage(AlertFaultPolicy::OutageSnapshot snapshot) {
|
||||
_wifi_outage_bits.store(AlertFaultPolicy::packOutageSnapshot(snapshot),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
// Optional pointers for collecting stats internally (set by mesh if available)
|
||||
mesh::Dispatcher* _dispatcher; // For air times and errors
|
||||
mesh::Radio* _radio; // For noise floor
|
||||
@@ -449,7 +463,7 @@ private:
|
||||
bool ensureSlotClient(int index); // Allocate this slot's persistent client + callbacks on first use
|
||||
bool ensureSlotAuthToken(int index); // Allocate this slot's JWT token buffer on first token creation
|
||||
void releaseSlotAuthToken(int index);// Free the token buffer (only with the client -- see MQTTSlot)
|
||||
void destroySlotClients(); // Delete all persistent clients (shutdown only)
|
||||
void destroySlotClients(bool force = false);
|
||||
bool setupSlot(int index); // Configure and connect the slot; false = not activated
|
||||
// Single definition of "this slot holds one of the _max_active_slots positions":
|
||||
// it is enabled and has been through a successful setupSlot(). Startup, the
|
||||
@@ -457,7 +471,8 @@ private:
|
||||
// exceeded by one route while another enforces it.
|
||||
int activatedSlotCount() const;
|
||||
bool canActivateSlot(int index) const;
|
||||
void teardownSlot(int index); // Disconnect the slot's client (keeps the object alive)
|
||||
void teardownSlot(int index, bool force = false);
|
||||
void reconnectSlotClient(int index);
|
||||
void maintainSlotConnections(); // Maintain all slot connections (token renewal, reconnect)
|
||||
void maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced, bool& reconnect_attempted, bool& teardown_attempted);
|
||||
bool createSlotAuthToken(int index); // Create/renew JWT token for a slot
|
||||
@@ -650,6 +665,10 @@ public:
|
||||
|
||||
static unsigned long getWifiConnectedAtMillis();
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot getWifiOutageSnapshot() const {
|
||||
return wifiOutage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-slot outage accessors used by AlertReporter to detect prolonged
|
||||
* MQTT broker outages. Indices are 0..RUNTIME_MQTT_SLOTS-1.
|
||||
|
||||
@@ -80,8 +80,8 @@ uint32_t ESPNOWRadio::getRngSeed() {
|
||||
return millis() + intID(); // TODO: where to get some entropy?
|
||||
}
|
||||
|
||||
void ESPNOWRadio::setTxPower(uint8_t dbm) {
|
||||
esp_wifi_set_max_tx_power(dbm * 4);
|
||||
bool ESPNOWRadio::setTxPower(int8_t dbm) {
|
||||
return esp_wifi_set_max_tx_power(dbm * 4) == ESP_OK;
|
||||
}
|
||||
|
||||
uint32_t ESPNOWRadio::intID() {
|
||||
|
||||
@@ -59,7 +59,7 @@ public:
|
||||
bool isCalibratingNoiseFloor() const { return false; }
|
||||
|
||||
uint32_t intID();
|
||||
void setTxPower(uint8_t dbm);
|
||||
bool setTxPower(int8_t dbm);
|
||||
};
|
||||
|
||||
#if ESPNOW_DEBUG_LOGGING && ARDUINO
|
||||
|
||||
@@ -36,11 +36,20 @@ ColorVal UIColor::corp_blue = 0x001A;
|
||||
|
||||
bool ST7789LCDDisplay::begin() {
|
||||
if (!_isOn) {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
if (_peripher_power) {
|
||||
_peripher_power->claim();
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
delay(100);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
#else
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Im not sure if this is just a t-deck problem or not, if your display is slow try this.
|
||||
@@ -57,7 +66,13 @@ bool ST7789LCDDisplay::begin() {
|
||||
display.setTextColor(ST77XX_WHITE);
|
||||
display.setTextSize(2 * DISPLAY_SCALE_X);
|
||||
display.cp437(true); // Use full 256 char 'Code Page 437' font
|
||||
|
||||
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
}
|
||||
#endif
|
||||
|
||||
_isOn = true;
|
||||
}
|
||||
|
||||
@@ -71,14 +86,20 @@ void ST7789LCDDisplay::turnOn() {
|
||||
void ST7789LCDDisplay::turnOff() {
|
||||
if (_isOn) {
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
#else
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
|
||||
#endif
|
||||
}
|
||||
if (PIN_TFT_RST != -1) {
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
}
|
||||
#ifndef HELTEC_V4_R8_TFT
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
|
||||
}
|
||||
#endif
|
||||
_isOn = false;
|
||||
|
||||
if (_peripher_power) _peripher_power->release();
|
||||
|
||||
@@ -66,6 +66,9 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_power_management` | `src/helpers/PowerManagementUtils.h` | median filtering of a brownout outlier and valid-reading requirements for the boot lock |
|
||||
| `test_rx_power_saving` | `src/helpers/radiolib/RXPowerSaving.h` | level-derived timing, tuple-selected 32/64/128-symbol wire preambles, equivalent SF7/BW500, SF6/BW250, and SF5/BW125 profiles, SF5/BW250 and SF6/BW500 level-8/64 timing, SF5/BW500 level-8/128 timing, SF5/BW62.5 with a 16-symbol timing assumption, automatic retuning, and SX1262 TCXO timing thresholds |
|
||||
| `test_region_names` | `src/helpers/RegionNameUtils.h` | canonical public-region markers while preserving distinct private and differently named regions |
|
||||
| `test_datagram_payload_limits` | `src/helpers/DatagramPayloadLimits.h` | encrypted datagram plaintext ceilings, including the anonymous region-reply prefix and worst-case cipher padding |
|
||||
| `test_serial_packet_log` | `src/helpers/SerialPacketLog.h` | bounded USB packet logging and dropped-line reporting |
|
||||
| `test_alert_fault_policy` | `src/helpers/AlertFaultPolicy.h` | coherent WiFi/MQTT outage edges, durations, rate limits, and formatting |
|
||||
| `test_routing_policy` | `src/helpers/RoutingPolicy.h` | scoped/unscoped flood hop limits and selection of direct, path-return, mirrored-scope, default-scope, or unscoped replies |
|
||||
| `test_rs232_uart` | `src/helpers/bridges/RS232UartUtils.h` | stopping the active UART peripheral before reassigning its pins |
|
||||
| `test_security_session_timer` | `src/helpers/nrf52/SecuritySessionTimer.h` | two-minute security-session expiry, cancellation, restart, and `millis()` rollover |
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
|
||||
#include "helpers/AlertFaultPolicy.h"
|
||||
|
||||
namespace Alert = AlertFaultPolicy;
|
||||
|
||||
namespace {
|
||||
|
||||
Alert::Fault okFault() {
|
||||
Alert::Fault fault{};
|
||||
fault.state = Alert::State::OK;
|
||||
return fault;
|
||||
}
|
||||
|
||||
Alert::OutageSnapshot down(uint32_t start, uint8_t reason = 0) {
|
||||
Alert::OutageSnapshot snapshot{};
|
||||
snapshot.down = true;
|
||||
snapshot.started_ms = start;
|
||||
snapshot.reason = reason;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(AlertFaultPolicy, PreservesFirstOutageReasonAcrossReconnectAttempts) {
|
||||
Alert::OutageSnapshot snapshot{};
|
||||
snapshot = Alert::applyWifiDisconnectEvent(1000, 200, snapshot);
|
||||
snapshot = Alert::applyWifiDisconnectEvent(16000, 8, snapshot);
|
||||
EXPECT_TRUE(snapshot.down);
|
||||
EXPECT_EQ(1000U, snapshot.started_ms);
|
||||
EXPECT_EQ(200U, snapshot.reason);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, GotIpClosesAnOutageBetweenStatusPolls) {
|
||||
Alert::OutageSnapshot snapshot = down(1000, 200);
|
||||
snapshot = Alert::applyWifiGotIp(snapshot);
|
||||
EXPECT_FALSE(snapshot.down);
|
||||
EXPECT_EQ(0U, snapshot.started_ms);
|
||||
EXPECT_EQ(0U, snapshot.reason);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, PackedSnapshotRepresentsOutageAtMillisZero) {
|
||||
std::atomic<uint64_t> value{0};
|
||||
value.store(Alert::packOutageSnapshot(down(0, 200)),
|
||||
std::memory_order_release);
|
||||
Alert::OutageSnapshot snapshot = Alert::unpackOutageSnapshot(
|
||||
value.load(std::memory_order_acquire));
|
||||
EXPECT_TRUE(snapshot.down);
|
||||
EXPECT_EQ(0U, snapshot.started_ms);
|
||||
EXPECT_EQ(200U, snapshot.reason);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, FiresAtThresholdAndFormatsInitiatingReason) {
|
||||
Alert::Fault fault = okFault();
|
||||
const uint32_t threshold = Alert::thresholdMs(30);
|
||||
Alert::OutageSnapshot snapshot = down(1000, 200);
|
||||
Alert::TickResult result = Alert::tick(
|
||||
fault, 1000 + threshold, snapshot, threshold,
|
||||
Alert::minIntervalMs(60));
|
||||
EXPECT_EQ(Alert::Action::FireDown, result.action);
|
||||
|
||||
char text[80];
|
||||
ASSERT_TRUE(Alert::formatWifiAlert(text, sizeof(text), result, snapshot));
|
||||
EXPECT_STREQ("WiFi down 30m (reason 200)", text);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, RecoveryUsesRememberedOutageStart) {
|
||||
Alert::Fault fault = okFault();
|
||||
Alert::commitDown(fault, 1801000U, 1000U);
|
||||
Alert::OutageSnapshot up{};
|
||||
Alert::TickResult result = Alert::tick(
|
||||
fault, 7501000U, up, Alert::thresholdMs(30),
|
||||
Alert::minIntervalMs(60));
|
||||
EXPECT_EQ(Alert::Action::FireRecovered, result.action);
|
||||
EXPECT_EQ(7500000U, result.duration_ms);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, FirstAlertIsNotSuppressedByUptime) {
|
||||
Alert::Fault fault = okFault();
|
||||
const uint32_t threshold = Alert::thresholdMs(1);
|
||||
Alert::TickResult result = Alert::tick(
|
||||
fault, threshold, down(0), threshold, Alert::minIntervalMs(60));
|
||||
EXPECT_EQ(Alert::Action::FireDown, result.action);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, MinimumIntervalHasOneHourFloor) {
|
||||
EXPECT_EQ(3600000U, Alert::minIntervalMs(0));
|
||||
EXPECT_EQ(3600000U, Alert::minIntervalMs(59));
|
||||
EXPECT_EQ(7200000U, Alert::minIntervalMs(120));
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, DurationAndCadenceSurviveMillisRollover) {
|
||||
const uint32_t start = std::numeric_limits<uint32_t>::max() - 999U;
|
||||
EXPECT_EQ(1500U, Alert::elapsedMs(500U, start));
|
||||
EXPECT_TRUE(Alert::checkDue(500U, 400U));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helpers/DatagramPayloadLimits.h"
|
||||
|
||||
TEST(DatagramPayloadLimits, MeshDatagramPlaintextLimitIncludesWorstCasePadding) {
|
||||
EXPECT_EQ(167U, DatagramPayloadLimits::maxPlaintext(184, 2, 16));
|
||||
}
|
||||
|
||||
TEST(DatagramPayloadLimits, RegionReplyLeavesRoomForItsEightBytePrefix) {
|
||||
const size_t reply_limit = DatagramPayloadLimits::maxPlaintext(184, 2, 16);
|
||||
ASSERT_GE(reply_limit, 8U);
|
||||
EXPECT_EQ(159U, reply_limit - 8U);
|
||||
}
|
||||
|
||||
TEST(DatagramPayloadLimits, InvalidGeometryFailsClosed) {
|
||||
EXPECT_EQ(0U, DatagramPayloadLimits::maxPlaintext(184, 2, 0));
|
||||
EXPECT_EQ(0U, DatagramPayloadLimits::maxPlaintext(8, 2, 16));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -179,12 +179,15 @@ assert.strictEqual(v4Full.dedicatedUsbLogging, true);
|
||||
{ installKinds: ["bin"] }
|
||||
);
|
||||
});
|
||||
const classified = picker.applyDualCdcFullCompanionCapabilities(candidates);
|
||||
assert.strictEqual(classified[0].logging, "none");
|
||||
const classified = picker.applyFullCompanionCapabilities(candidates);
|
||||
assert.strictEqual(classified[0].logging, "usb-runtime");
|
||||
assert.deepStrictEqual(classified[0].loggingModes, ["none", "usb"]);
|
||||
assert.strictEqual(classified[0].dedicatedUsbLogging, undefined);
|
||||
assert.strictEqual(
|
||||
picker.omitTransportsReplacedByDualCdcFull(classified).length,
|
||||
candidates.length
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(classified).map(function (item) {
|
||||
return item.target;
|
||||
}),
|
||||
[hardware + "_companion_radio_full"]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -137,6 +137,33 @@ TEST(MQTTConnectionPolicy, SyncedClockRenewsInvalidExpiredOrImminentTokens) {
|
||||
EXPECT_TRUE(Policy::tokenNeedsRenewal(true, expires + 1U, expires, 300U));
|
||||
}
|
||||
|
||||
TEST(MQTTConnectionPolicy, JwtReconnectReusesOnlyProvenValidCredentials) {
|
||||
const uint32_t now = 1735689600U;
|
||||
const uint32_t usable_expiry =
|
||||
now + Policy::kJwtReconnectSafetyMarginSecs + 1U;
|
||||
|
||||
EXPECT_TRUE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now, usable_expiry, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now, Policy::kMinimumValidEpoch - 1U, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now, 0U, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now,
|
||||
now + Policy::kJwtReconnectSafetyMarginSecs, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, false, false, now, usable_expiry, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
false, true, false, now, usable_expiry, 0));
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, true, true, now, usable_expiry, 0));
|
||||
|
||||
EXPECT_FALSE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now, now + 360U, 300U));
|
||||
EXPECT_TRUE(Policy::canReuseJwtForReconnect(
|
||||
true, true, false, now, now + 361U, 300U));
|
||||
}
|
||||
|
||||
TEST(MQTTConnectionPolicy, RenewalThrottleHasExactBoundaryAndHandlesRollover) {
|
||||
EXPECT_FALSE(Policy::renewalAttemptAllowed(59999U, 0U));
|
||||
EXPECT_TRUE(Policy::renewalAttemptAllowed(60000U, 0U));
|
||||
@@ -242,6 +269,48 @@ TEST(SlotActivation, DisabledAndOutOfRangeSlots) {
|
||||
EXPECT_EQ(SlotActivation::Disabled, Policy::classifySlotActivation(0, nullptr, 3, 2));
|
||||
}
|
||||
|
||||
using Policy::StaleTokenAction;
|
||||
|
||||
TEST(StaleToken, ChoosesSafeLiveAndDisconnectedActions) {
|
||||
EXPECT_EQ(StaleTokenAction::Bounce,
|
||||
Policy::classifyStaleToken(true, true, true));
|
||||
EXPECT_EQ(StaleTokenAction::KeepAlive,
|
||||
Policy::classifyStaleToken(true, true, false));
|
||||
EXPECT_EQ(StaleTokenAction::Reconnect,
|
||||
Policy::classifyStaleToken(true, false, true));
|
||||
EXPECT_EQ(StaleTokenAction::Reconnect,
|
||||
Policy::classifyStaleToken(true, false, false));
|
||||
}
|
||||
|
||||
TEST(StaleToken, FailedMintAlwaysDefers) {
|
||||
EXPECT_EQ(StaleTokenAction::Defer,
|
||||
Policy::classifyStaleToken(false, false, true));
|
||||
EXPECT_EQ(StaleTokenAction::Defer,
|
||||
Policy::classifyStaleToken(false, false, false));
|
||||
EXPECT_EQ(StaleTokenAction::Defer,
|
||||
Policy::classifyStaleToken(false, true, true));
|
||||
EXPECT_EQ(StaleTokenAction::Defer,
|
||||
Policy::classifyStaleToken(false, true, false));
|
||||
}
|
||||
|
||||
using Policy::ClockSource;
|
||||
|
||||
TEST(FallbackClock, PrefersSystemThenRtcAndRejectsInvalidSources) {
|
||||
const uint32_t floor = 1767225600U;
|
||||
const uint32_t old_time = 1715770351U;
|
||||
const uint32_t plausible = 1786000000U;
|
||||
|
||||
EXPECT_EQ(ClockSource::System,
|
||||
Policy::chooseFallbackClock(false, plausible, plausible - 900U,
|
||||
floor));
|
||||
EXPECT_EQ(ClockSource::Rtc,
|
||||
Policy::chooseFallbackClock(false, old_time, plausible, floor));
|
||||
EXPECT_EQ(ClockSource::None,
|
||||
Policy::chooseFallbackClock(false, old_time, old_time, floor));
|
||||
EXPECT_EQ(ClockSource::None,
|
||||
Policy::chooseFallbackClock(true, plausible, plausible, floor));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
|
||||
@@ -183,6 +183,22 @@ TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownNanScore) {
|
||||
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
|
||||
}
|
||||
|
||||
TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownRadioMetrics) {
|
||||
JsonDocument scratch;
|
||||
char buffer[2048];
|
||||
int len = MQTTPayloadBuilder::buildPacketMessage(
|
||||
scratch, "node", "id", kTimestamp, "rx", "12:34:56", "18/07/2026",
|
||||
42, 4, "D", 20, "A0B1", -999.0f, -999, std::nanf(""), "hash",
|
||||
nullptr, 0, 0, 64, buffer, sizeof(buffer));
|
||||
|
||||
ASSERT_GT(len, 0);
|
||||
JsonDocument parsed;
|
||||
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
||||
EXPECT_FALSE(parsed["SNR"].is<JsonVariant>());
|
||||
EXPECT_FALSE(parsed["RSSI"].is<JsonVariant>());
|
||||
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
|
||||
}
|
||||
|
||||
TEST(MQTTPayloadBuilder, RawMessageHasExactContractAndEscapesData) {
|
||||
char buffer[512];
|
||||
JsonDocument doc;
|
||||
@@ -254,6 +270,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageRoundTripsSelfAndEntries) {
|
||||
{"0011223344556677", 9.75f, 42, "DEN,APRS", "active"},
|
||||
{"8899AABBCCDDEEFF", -3.5f, 3600, "", "stale"},
|
||||
};
|
||||
neighbors[0].rssi = -87;
|
||||
neighbors[1].rssi = -110;
|
||||
|
||||
JsonDocument scratch;
|
||||
char buffer[1024];
|
||||
@@ -278,10 +296,12 @@ TEST(MQTTPayloadBuilder, NeighborsMessageRoundTripsSelfAndEntries) {
|
||||
ASSERT_EQ(2U, arr.size());
|
||||
EXPECT_STREQ("0011223344556677", arr[0]["pubkey"].as<const char*>());
|
||||
EXPECT_FLOAT_EQ(9.75f, arr[0]["snr"].as<float>());
|
||||
EXPECT_EQ(-87, arr[0]["rssi"].as<int>());
|
||||
EXPECT_EQ(42U, arr[0]["heard_secs_ago"].as<uint32_t>());
|
||||
EXPECT_STREQ("DEN,APRS", arr[0]["scopes"].as<const char*>());
|
||||
EXPECT_STREQ("active", arr[0]["status"].as<const char*>());
|
||||
EXPECT_STREQ("8899AABBCCDDEEFF", arr[1]["pubkey"].as<const char*>());
|
||||
EXPECT_EQ(-110, arr[1]["rssi"].as<int>());
|
||||
EXPECT_STREQ("", arr[1]["scopes"].as<const char*>());
|
||||
EXPECT_STREQ("stale", arr[1]["status"].as<const char*>());
|
||||
}
|
||||
@@ -291,6 +311,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageMeasurementsMatchCompletePayload) {
|
||||
{"0011223344556677", 9.75f, UINT32_MAX, "DEN,APRS", "responded"},
|
||||
{"8899AABBCCDDEEFF", -3.5f, UINT32_MAX, "", "timeout"},
|
||||
};
|
||||
neighbors[0].rssi = -87;
|
||||
neighbors[1].rssi = -110;
|
||||
|
||||
size_t measured =
|
||||
MQTTPayloadBuilder::measureNeighborsMessageBase(
|
||||
@@ -320,6 +342,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageMeasuredPrefixStopsBeforeOverflow) {
|
||||
neighbors[i] = {
|
||||
keys[i], static_cast<float>(i), UINT32_MAX,
|
||||
long_scopes, "responded"};
|
||||
neighbors[i].rssi = -90 - i;
|
||||
}
|
||||
|
||||
constexpr size_t kBufferSize = 512;
|
||||
@@ -364,6 +387,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageFallbackMarksTruncated) {
|
||||
{"0011223344556677", 9.75f, 1, "DEN", "responded"},
|
||||
{"8899AABBCCDDEEFF", -3.5f, 2, "APRS", "responded"},
|
||||
};
|
||||
neighbors[0].rssi = -87;
|
||||
neighbors[1].rssi = -110;
|
||||
size_t first_only_size =
|
||||
MQTTPayloadBuilder::measureNeighborsMessageBase(
|
||||
"node", "id", kTimestamp, "DEN", "*", 2)
|
||||
@@ -386,6 +411,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageFallbackMarksTruncated) {
|
||||
TEST(MQTTPayloadBuilder, NeighborsMessageFailsCleanlyOnAllocationFailure) {
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry neighbor = {
|
||||
"0011223344556677", 9.75f, 1, "DEN", "responded"};
|
||||
neighbor.rssi = -87;
|
||||
RejectAllJsonAllocations allocator;
|
||||
JsonDocument scratch(&allocator);
|
||||
char buffer[512];
|
||||
@@ -422,6 +448,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageDropsTailWhenBufferFills) {
|
||||
snprintf(keys[i], sizeof(keys[i]), "%016X", i);
|
||||
neighbors[i].pubkey_hex = keys[i];
|
||||
neighbors[i].snr = static_cast<float>(i);
|
||||
neighbors[i].rssi = -90 - i;
|
||||
neighbors[i].heard_secs_ago = static_cast<uint32_t>(i) * 10U;
|
||||
neighbors[i].heard_unknown = false;
|
||||
neighbors[i].scopes = "DEN";
|
||||
@@ -453,6 +480,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageRendersUnknownHeardAgeAsNull) {
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry neighbors[2];
|
||||
neighbors[0] = {"0011223344556677", 9.75f, 42, "DEN", "responded"};
|
||||
neighbors[1] = {"8899AABBCCDDEEFF", -3.5f, 0, "DEN", "responded"};
|
||||
neighbors[0].rssi = -87;
|
||||
neighbors[1].rssi = -110;
|
||||
neighbors[1].heard_unknown = true;
|
||||
|
||||
JsonDocument scratch;
|
||||
@@ -480,6 +509,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageUnknownHeardAgeFitsMeasuredWidth) {
|
||||
// must never serialize wider than what that reserved.
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry measured = {
|
||||
"0011223344556677", 9.75f, UINT32_MAX, "DEN", "responded"};
|
||||
measured.rssi = -87;
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry unknown = measured;
|
||||
unknown.heard_unknown = true;
|
||||
unknown.heard_secs_ago = 0;
|
||||
|
||||
@@ -88,6 +88,13 @@ TEST(ReplyAppendf, NoWritePastBufferAcrossOverflowingChain) {
|
||||
EXPECT_EQ(c.buf()[c.logical - 1], '\0'); // still NUL-terminated
|
||||
}
|
||||
|
||||
TEST(MbedtlsErrorMagnitude, NormalizesEitherSignAndInt32Min) {
|
||||
EXPECT_EQ(0x7F00U, mbedtlsErrorMagnitude(0x7F00));
|
||||
EXPECT_EQ(0x7F00U, mbedtlsErrorMagnitude(-0x7F00));
|
||||
EXPECT_EQ(0U, mbedtlsErrorMagnitude(0));
|
||||
EXPECT_EQ(2147483648U, mbedtlsErrorMagnitude(INT32_MIN));
|
||||
}
|
||||
|
||||
TEST(ReplyAppendf, ExactFitBoundary) {
|
||||
char buf[11]; // room for exactly "0123456789" + NUL
|
||||
int pos = 0;
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "helpers/SerialPacketLog.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class FakePort : public Stream {
|
||||
public:
|
||||
explicit FakePort(size_t capacity = 64, size_t bytes_per_ms = 64)
|
||||
: _capacity(capacity),
|
||||
_free(capacity),
|
||||
_rate(bytes_per_ms),
|
||||
_last_ms(millis()) {}
|
||||
|
||||
int availableForWrite() override {
|
||||
uint32_t now = millis();
|
||||
_free += (size_t)(now - _last_ms) * _rate;
|
||||
_last_ms = now;
|
||||
if (_free > _capacity) _free = _capacity;
|
||||
return (int)_free;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
if (size > _free) size = _free;
|
||||
_written.append(reinterpret_cast<const char*>(buffer), size);
|
||||
_free -= size;
|
||||
if (_written.size() >= _wedge_after) wedge();
|
||||
return size;
|
||||
}
|
||||
|
||||
void wedge() {
|
||||
availableForWrite();
|
||||
_rate = 0;
|
||||
_free = 0;
|
||||
}
|
||||
void wedgeAfter(size_t bytes) { _wedge_after = bytes; }
|
||||
const std::string& written() const { return _written; }
|
||||
|
||||
private:
|
||||
size_t _capacity;
|
||||
size_t _free;
|
||||
size_t _rate;
|
||||
size_t _wedge_after = (size_t)-1;
|
||||
uint32_t _last_ms;
|
||||
std::string _written;
|
||||
};
|
||||
|
||||
class SerialPacketLogTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
g_mock_millis = 1000;
|
||||
mesh::serialLogDroppedCount() = 0;
|
||||
mesh::serialLogPortSeen() = false;
|
||||
}
|
||||
|
||||
static void primePort() {
|
||||
FakePort port(4096);
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("primed");
|
||||
ASSERT_TRUE(line.flush(port));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SerialPacketLogTest, WritesWholeLineThroughSmallFifo) {
|
||||
FakePort port;
|
||||
uint8_t raw[40];
|
||||
for (size_t i = 0; i < sizeof(raw); i++) raw[i] = (uint8_t)i;
|
||||
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("RAW: ");
|
||||
line.hex(raw, sizeof(raw));
|
||||
EXPECT_TRUE(line.flush(port));
|
||||
EXPECT_NE(std::string::npos, port.written().find("000102"));
|
||||
EXPECT_EQ(0U, mesh::serialLogDroppedCount());
|
||||
}
|
||||
|
||||
TEST_F(SerialPacketLogTest, DropsImmediatelyWhenHostStopsDraining) {
|
||||
primePort();
|
||||
FakePort port;
|
||||
port.wedge();
|
||||
|
||||
uint32_t before = millis();
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("RAW: 0102");
|
||||
EXPECT_FALSE(line.flush(port));
|
||||
EXPECT_EQ(before, millis());
|
||||
EXPECT_TRUE(port.written().empty());
|
||||
EXPECT_EQ(1U, mesh::serialLogDroppedCount());
|
||||
}
|
||||
|
||||
TEST_F(SerialPacketLogTest, GivesUpWithinBudgetWhenHostWedgesMidLine) {
|
||||
primePort();
|
||||
FakePort port(64, 64);
|
||||
port.wedgeAfter(64);
|
||||
uint8_t raw[200];
|
||||
memset(raw, 0xA5, sizeof(raw));
|
||||
|
||||
mesh::SerialLogLine<> line;
|
||||
line.hex(raw, sizeof(raw));
|
||||
uint32_t before = millis();
|
||||
EXPECT_FALSE(line.flush(port));
|
||||
uint32_t waited = millis() - before;
|
||||
EXPECT_GE(waited, (uint32_t)SERIAL_LOG_WRITE_BUDGET_MS);
|
||||
EXPECT_LE(waited, (uint32_t)SERIAL_LOG_WRITE_BUDGET_MS + 2);
|
||||
EXPECT_EQ(1U, mesh::serialLogDroppedCount());
|
||||
}
|
||||
|
||||
TEST_F(SerialPacketLogTest, ReportsDropsAfterHostRecovers) {
|
||||
primePort();
|
||||
FakePort wedged;
|
||||
wedged.wedge();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("RAW: 00");
|
||||
EXPECT_FALSE(line.flush(wedged));
|
||||
}
|
||||
|
||||
FakePort recovered(4096);
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("RAW: 01");
|
||||
EXPECT_TRUE(line.flush(recovered));
|
||||
EXPECT_EQ("DROP:3\r\nRAW: 01\r\n", recovered.written());
|
||||
}
|
||||
|
||||
TEST_F(SerialPacketLogTest, IgnoresDropsBeforeAnyHostConnects) {
|
||||
FakePort unattached;
|
||||
unattached.wedge();
|
||||
mesh::SerialLogLine<> line;
|
||||
line.printf("RAW: 00");
|
||||
EXPECT_FALSE(line.flush(unattached));
|
||||
EXPECT_EQ(0U, mesh::serialLogDroppedCount());
|
||||
}
|
||||
|
||||
TEST_F(SerialPacketLogTest, MarksTruncatedLines) {
|
||||
primePort();
|
||||
FakePort port(4096);
|
||||
uint8_t raw[64];
|
||||
memset(raw, 0x5A, sizeof(raw));
|
||||
|
||||
mesh::SerialLogLine<32> line;
|
||||
line.printf("RAW: ");
|
||||
line.hex(raw, sizeof(raw));
|
||||
EXPECT_FALSE(line.flush(port));
|
||||
EXPECT_LE(port.written().size(), (size_t)32);
|
||||
EXPECT_EQ(1U, mesh::serialLogDroppedCount());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
+174
-14
@@ -89,7 +89,83 @@ 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 "can add a dedicated logging USB port" in matrix
|
||||
assert "input-capable single-TTY terminal" in matrix
|
||||
|
||||
|
||||
def test_espnow_tx_power_matches_cli_callback_contract():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
header = (root / "src/helpers/esp32/ESPNOWRadio.h").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
implementation = (
|
||||
root / "src/helpers/esp32/ESPNOWRadio.cpp"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "bool setTxPower(int8_t dbm);" in header
|
||||
assert "bool ESPNOWRadio::setTxPower(int8_t dbm)" in implementation
|
||||
assert "esp_wifi_set_max_tx_power(dbm * 4) == ESP_OK" in implementation
|
||||
|
||||
|
||||
def test_flash_constrained_stm32_repeaters_pin_the_size_qualified_toolchain():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
toolchain = "platformio/toolchain-gccarmnoneeabi@^1.140201.0"
|
||||
profiles = (
|
||||
("variants/rak3x72/platformio.ini", "[env:RAK_3x72_repeater]"),
|
||||
("variants/tiny_relay/platformio.ini", "[env:Tiny_Relay_repeater]"),
|
||||
(
|
||||
"variants/wio-e5-mini/platformio.ini",
|
||||
"[env:wio-e5-mini_repeater]",
|
||||
),
|
||||
("variants/wio-e5-dev/platformio.ini", "[env:wio-e5_repeater]"),
|
||||
(
|
||||
"variants/wio-e5-dev/platformio.ini",
|
||||
"[env:wio-e5-repeater_bridge_rs232]",
|
||||
),
|
||||
)
|
||||
|
||||
for path, section_name in profiles:
|
||||
text = (root / path).read_text(encoding="utf-8")
|
||||
section = text.split(section_name, 1)[1].split("\n[", 1)[0]
|
||||
assert toolchain in section
|
||||
assert "-fno-schedule-insns2" in section
|
||||
assert "-Wl,--sort-section=alignment" in section
|
||||
assert "-D MESH_PACKET_LOGGING_COMPACT=1" in section
|
||||
|
||||
dispatcher = (root / "src/Dispatcher.cpp").read_text(encoding="utf-8")
|
||||
repeater = (
|
||||
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 "!MESH_PACKET_LOGGING_COMPACT" in dispatcher
|
||||
assert "#if MESH_PACKET_LOGGING_COMPACT" in repeater
|
||||
assert "mesh::Utils::printHex(logging_port, raw, len);" in repeater
|
||||
|
||||
|
||||
def test_usb_companion_profiles_enable_the_usb_transport():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
profiles = (
|
||||
(
|
||||
"variants/heltec_e290/platformio.ini",
|
||||
"[env:Heltec_E290_companion_usb]",
|
||||
"[env:Heltec_E290_repeater]",
|
||||
),
|
||||
(
|
||||
"variants/meshnology_w12/platformio.ini",
|
||||
"[env:meshnology_w12_companion_radio_usb]",
|
||||
"[env:meshnology_w12_companion_radio_ble]",
|
||||
),
|
||||
(
|
||||
"variants/xiao_nrf52/platformio.ini",
|
||||
"[env:solarxiao_30S_companion_radio_usb]",
|
||||
"[env:solarxiao_33S_companion_radio_usb]",
|
||||
),
|
||||
)
|
||||
|
||||
for relative, section, next_section in profiles:
|
||||
variant = (root / relative).read_text(encoding="utf-8")
|
||||
profile = variant.split(section, 1)[1].split(next_section, 1)[0]
|
||||
assert "-D ENABLE_USB_INTERFACE" in profile
|
||||
|
||||
|
||||
def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
|
||||
@@ -128,6 +204,21 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
|
||||
assert "-DMESH_DUAL_CDC_LOGGING=1" in full
|
||||
assert "-DMESH_DEBUG=1" in full
|
||||
assert "-DMESH_PACKET_LOGGING=1" in full
|
||||
assert 'disable_debug_flags "$env_name"' in build
|
||||
assert 'apply_debug_overrides "$env_name"' in build
|
||||
|
||||
debug_overrides = build.split("apply_debug_overrides()", 1)[1]
|
||||
debug_overrides = debug_overrides.split(
|
||||
"disable_usb_logging_for_mqtt()", 1
|
||||
)[0]
|
||||
assert "preserve_full_companion_logging" in debug_overrides
|
||||
assert 'is_companion_radio_full_target "$env_name"' in debug_overrides
|
||||
assert 'if [ "$preserve_full_companion_logging" -eq 0 ]' in debug_overrides
|
||||
|
||||
disable_debug = build.split("disable_debug_flags()", 1)[1]
|
||||
disable_debug = disable_debug.split("apply_mqtt_bridge_override()", 1)[0]
|
||||
assert 'is_companion_radio_full_target "$env_name"' in disable_debug
|
||||
assert 'usb_logging_undefs=""' in disable_debug
|
||||
|
||||
esp32_dual = build.split(
|
||||
"is_esp32_dual_cdc_companion_radio_full_target()", 1
|
||||
@@ -178,8 +269,7 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
|
||||
root / "examples/companion_radio/CompanionFeatures.h"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "COMPANION_FEATURE_DEDICATED_USB_LOGGING" in companion_features
|
||||
assert "defined(MESH_DUAL_CDC_LOGGING)" in companion
|
||||
assert "defined(COMPANION_RADIO_FULL)" not in companion
|
||||
assert "defined(COMPANION_RADIO_FULL)" in companion
|
||||
assert "_prefs.usb_logging_enabled = 0" in companion
|
||||
assert 'strcmp(value, "on reboot") == 0' in companion
|
||||
assert 'strcmp(value, "off reboot") == 0' in companion
|
||||
@@ -187,6 +277,67 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
|
||||
assert "rebooting to change USB interfaces" in companion
|
||||
assert "mesh::saveUsbLoggingBootPreference(enabled)" in companion
|
||||
|
||||
companion_main = (
|
||||
root / "examples/companion_radio/main.cpp"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "!mesh::hasDedicatedUsbLoggingPort()" in companion_main
|
||||
assert "mesh::isUsbLoggingEnabled()" in companion_main
|
||||
assert "enterUsbTerminalMode();" in companion_main
|
||||
|
||||
replacement = build.split(
|
||||
"get_esp32_full_companion_replacement()", 1
|
||||
)[1].split("get_full_companion_replacement()", 1)[0]
|
||||
assert 'is_esp32_companion_radio_full_target "$full_env"' in replacement
|
||||
assert "is_esp32_dual_cdc_companion_radio_full_target" not in replacement
|
||||
|
||||
|
||||
def test_measured_full_companion_promotions_are_exact_and_bounded():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
build = (root / "build.sh").read_text(encoding="utf-8")
|
||||
qualified = build.split("# Some qualified boards historically", 1)[1]
|
||||
qualified = qualified.split("\n fi\n}\n\nget_pio_envs", 1)[0]
|
||||
|
||||
expected = [
|
||||
"M5Stack_Unit_C6L_companion_radio_ble",
|
||||
"Heltec_Wireless_Tracker_companion_radio_ble",
|
||||
"LilyGo_T3S3_sx1276_companion_radio_ble",
|
||||
"Heltec_ct62_companion_radio_ble",
|
||||
"Meshadventurer_sx1262_companion_radio_ble",
|
||||
"Meshadventurer_sx1268_companion_radio_ble",
|
||||
"Heltec_Wireless_Paper_companion_radio_ble",
|
||||
"Heltec_E213_companion_radio_ble",
|
||||
"Xiao_S3_companion_radio_ble",
|
||||
"LilyGo_TETH_Elite_sx1262_companion_radio_ble",
|
||||
"LilyGo_T3S3_sx1262_companion_radio_ble",
|
||||
"LilyGo_TDeck_companion_radio_ble",
|
||||
"Ebyte_EoRa-S3_companion_radio_ble",
|
||||
"Tbeam_SX1262_companion_radio_ble",
|
||||
"Tbeam_SX1276_companion_radio_ble",
|
||||
"T_Beam_S3_Supreme_SX1262_companion_radio_ble",
|
||||
"GAT562_Mesh_Watch13_companion_radio_ble",
|
||||
"LilyGo_T-Echo-Lite_companion_radio_ble",
|
||||
"LilyGo_T_Impulse_Plus_companion_radio_ble",
|
||||
"WioTrackerL1Eink_companion_radio_ble",
|
||||
]
|
||||
listed = [
|
||||
line.strip()
|
||||
for line in qualified.splitlines()
|
||||
if line.strip().endswith("_companion_radio_ble")
|
||||
]
|
||||
assert listed == expected
|
||||
assert 'PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"' in qualified
|
||||
|
||||
profile = build.split("apply_companion_radio_full_profile()", 1)[1]
|
||||
profile = profile.split("apply_radio_overrides()", 1)[0]
|
||||
assert "-DWIFI_SSID=" in profile
|
||||
assert "+<helpers/esp32/SerialWifiInterface.cpp>" in profile
|
||||
assert "meshadventurer_sx1262_companion_radio_full" in profile
|
||||
assert "meshadventurer_sx1268_companion_radio_full" in profile
|
||||
assert "-DMAX_CONTACTS=160" in profile
|
||||
assert "-DMAX_GROUP_CHANNELS=30" in profile
|
||||
assert "-DOFFLINE_QUEUE_SIZE=64" in profile
|
||||
assert "160 contacts, 30 channels, and 64 queued frames" in profile
|
||||
|
||||
|
||||
def test_full_companion_wireless_startup_and_psram_contacts_are_resilient():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
@@ -361,9 +512,9 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
|
||||
"Station_G2_companion_radio_usb", release_files
|
||||
) == ("Station_G2_companion_radio_usb", False)
|
||||
|
||||
# When the canonical release contains only the qualified Full image, every
|
||||
# ordinary attached transport and the old USB-logging identity resolve to
|
||||
# it. An unqualified USB-UART bridge must never receive that substitution.
|
||||
# When the canonical release contains only a Full image, every ordinary
|
||||
# attached transport and the old USB-logging identity resolve to it. A
|
||||
# single-TTY USB-UART board uses terminal mode for its logging stream.
|
||||
g2_full_only = {
|
||||
"Station_G2_companion_radio_full": [Path("g2-full.bin")],
|
||||
"ThinkNode_M2_companion_radio_full": [Path("m2-full.bin")],
|
||||
@@ -377,14 +528,15 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
|
||||
assert provider.resolve_release_identity(
|
||||
old_identity, g2_full_only
|
||||
) == ("Station_G2_companion_radio_full", False)
|
||||
try:
|
||||
provider.resolve_release_identity(
|
||||
"ThinkNode_M2_companion_radio_usb", g2_full_only
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("USB-UART bridge was incorrectly mapped to Full")
|
||||
for old_identity in (
|
||||
"ThinkNode_M2_companion_radio_usb",
|
||||
"ThinkNode_M2_companion_radio_ble",
|
||||
"ThinkNode_M2_companion_radio_wifi",
|
||||
"ThinkNode_M2_companion_radio_usb-logging",
|
||||
):
|
||||
assert provider.resolve_release_identity(
|
||||
old_identity, g2_full_only
|
||||
) == ("ThinkNode_M2_companion_radio_full", False)
|
||||
|
||||
dual_notes = provider.normalize_nrf52_full_companion_metadata(
|
||||
{"title": "Companion USB", "subTitle": "USB logging"},
|
||||
@@ -403,6 +555,14 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
|
||||
assert "cannot reboot the board" in v4_notes
|
||||
assert "ordinary Wi-Fi" in v4_notes
|
||||
|
||||
single_tty_notes = provider.normalize_esp32_single_tty_full_companion_metadata(
|
||||
{"title": "Companion USB", "subTitle": "USB logging"},
|
||||
"PROFILE - old profile\n\nLOGGING USE - old use\n\nSELECTION - USB.",
|
||||
)
|
||||
assert "input-capable plaintext" in single_tty_notes
|
||||
assert "set usb.logging off" in single_tty_notes
|
||||
assert "do not share the single TTY" in single_tty_notes
|
||||
|
||||
legacy = {
|
||||
"role": "companionBle",
|
||||
"title": "Companion BLE",
|
||||
|
||||
@@ -66,6 +66,7 @@ build_flags =
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=E290Display
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
-D ENABLE_USB_INTERFACE
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=512
|
||||
build_src_filter = ${Heltec_E290_base.build_src_filter}
|
||||
|
||||
@@ -66,3 +66,19 @@ const char* HeltecV4R8Board::getManufacturerName() const {
|
||||
return "Heltec V4 R8 OLED";
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::setLoRaFemLnaEnabled(bool enable) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) return false;
|
||||
|
||||
loRaFEMControl.setLNAEnable(enable);
|
||||
loRaFEMControl.setRxModeEnable();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ public:
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
|
||||
@@ -21,6 +21,7 @@ void LoRaFEMControl::init(void) {
|
||||
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
|
||||
pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT);
|
||||
digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH);
|
||||
setLnaCanControl(true);
|
||||
}
|
||||
|
||||
void LoRaFEMControl::setSleepModeEnable(void) {
|
||||
|
||||
@@ -15,10 +15,12 @@ public:
|
||||
void setRxModeEnable(void);
|
||||
void setRxModeEnableWhenMCUSleep(void);
|
||||
void setLNAEnable(bool enabled);
|
||||
bool isLnaCanControl(void) { return true; }
|
||||
void setLnaCanControl(bool can_control) { }
|
||||
bool isLnaCanControl(void) const { return lna_can_control; }
|
||||
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
|
||||
bool isLNAEnabled(void) const { return lna_enabled; }
|
||||
LoRaFEMType getFEMType(void) const { return KCT8103L_PA; }
|
||||
|
||||
private:
|
||||
bool lna_enabled = false;
|
||||
bool lna_can_control = false;
|
||||
};
|
||||
|
||||
@@ -65,17 +65,16 @@ build_flags =
|
||||
-D PIN_BOARD_SCL=18
|
||||
-D DISPLAY_SCALE_X=2.5
|
||||
-D DISPLAY_SCALE_Y=3.75
|
||||
-D PIN_TFT_RST=-1
|
||||
-D PIN_TFT_RST=21
|
||||
-D PIN_TFT_VDD_CTL=-1
|
||||
-D PIN_TFT_LEDA_CTL=44
|
||||
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
|
||||
-D PIN_TFT_LEDA_CTL_ACTIVE=LOW
|
||||
-D PIN_TFT_CS=47
|
||||
-D PIN_TFT_DC=48
|
||||
-D PIN_TFT_SCL=16
|
||||
-D PIN_TFT_SDA=15
|
||||
-D PIN_TFT_MISO=45
|
||||
-D PIN_BUZZER=4
|
||||
-D PIN_TOUCH_RST=21
|
||||
build_src_filter = ${Heltec_v4_r8.build_src_filter}
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
lib_deps =
|
||||
|
||||
@@ -131,6 +131,7 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ENABLE_USB_INTERFACE
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
build_src_filter = ${meshnology_w12.build_src_filter}
|
||||
|
||||
@@ -92,7 +92,9 @@ build_src_filter = ${rak11310.build_src_filter}
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps = ${rak11310.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
lib_ignore = BLE
|
||||
lib_ignore =
|
||||
${rp2040_base.lib_ignore}
|
||||
BLE
|
||||
|
||||
; [env:RAK_11310_companion_radio_ble]
|
||||
; extends = rak11310
|
||||
|
||||
@@ -16,6 +16,7 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
|
||||
[env:RAK_3x72_repeater]
|
||||
extends = rak3x72
|
||||
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${rak3x72.build_flags}
|
||||
@@ -29,9 +30,12 @@ build_flags = ${rak3x72.build_flags}
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
-D LFS_FLASH_TOTAL_SIZE=16384
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
-D ADVERT_NAME='"RAK3x72 Repeater"'
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
|
||||
@@ -65,7 +65,9 @@ build_src_filter = ${rpi_picow.build_src_filter}
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps = ${rpi_picow.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
lib_ignore = BLE
|
||||
lib_ignore =
|
||||
${rp2040_base.lib_ignore}
|
||||
BLE
|
||||
|
||||
; [env:PicoW_companion_radio_ble]
|
||||
; extends = rpi_picow
|
||||
|
||||
@@ -17,6 +17,7 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
|
||||
[env:Tiny_Relay_repeater]
|
||||
extends = Tiny_Relay
|
||||
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${Tiny_Relay.build_flags}
|
||||
@@ -30,10 +31,13 @@ build_flags = ${Tiny_Relay.build_flags}
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
-D LFS_FLASH_TOTAL_SIZE=16384
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D ADVERT_NAME='"tiny_relay Repeater"'
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
-D ADVERT_NAME='"Tiny Relay"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
|
||||
@@ -91,7 +91,9 @@ build_src_filter = ${waveshare_rp2040_lora.build_src_filter}
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps = ${waveshare_rp2040_lora.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
lib_ignore = BLE
|
||||
lib_ignore =
|
||||
${rp2040_base.lib_ignore}
|
||||
BLE
|
||||
|
||||
; [env:waveshare_rp2040_lora_companion_radio_ble]
|
||||
; extends = waveshare_rp2040_lora
|
||||
|
||||
@@ -17,6 +17,7 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
|
||||
[env:wio-e5_repeater]
|
||||
extends = lora_e5
|
||||
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${lora_e5.build_flags}
|
||||
@@ -30,9 +31,12 @@ build_flags = ${lora_e5.build_flags}
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
-D LFS_FLASH_TOTAL_SIZE=16384
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
-D LORA_TX_POWER=22
|
||||
-D ADVERT_NAME='"WIO-E5 Repeater"'
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
@@ -42,6 +46,7 @@ build_src_filter = ${lora_e5.build_src_filter}
|
||||
|
||||
[env:wio-e5-repeater_bridge_rs232]
|
||||
extends = lora_e5
|
||||
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${lora_e5.build_flags}
|
||||
@@ -57,9 +62,12 @@ build_flags = ${lora_e5.build_flags}
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
-D LFS_FLASH_TOTAL_SIZE=16384
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
-D MESH_ENABLE_HOST_CLI=0 ; RS232 bridge image cannot also fit the USB/MQTT host service
|
||||
-D MESH_ENABLE_RECENT_REPEATERS=1
|
||||
-D LORA_TX_POWER=22
|
||||
|
||||
@@ -36,11 +36,14 @@ build_flags = ${lora_e5_mini.build_flags}
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
-D RADIO_LIVENESS_SOFT_ONLY=1 ; integrated STM32WL has no separate radio-reset pin
|
||||
-D WIO_E5_MINI_NO_EXTERNAL_SENSORS=1 ; the dedicated sensor env retains BME280 support
|
||||
-D LFS_FLASH_TOTAL_SIZE=16384
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
-D MESH_ENABLE_RECENT_REPEATERS=1
|
||||
-D LORA_TX_POWER=22
|
||||
-D ADVERT_NAME='"wio-e5-mini Repeater"'
|
||||
|
||||
@@ -181,6 +181,7 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D QSPIFLASH=1
|
||||
-D ENABLE_USB_INTERFACE
|
||||
build_src_filter = ${solarxiao.build_src_filter}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
|
||||
@@ -68,7 +68,9 @@ build_src_filter = ${Xiao_rp2040.build_src_filter}
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps = ${Xiao_rp2040.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
lib_ignore = BLE
|
||||
lib_ignore =
|
||||
${rp2040_base.lib_ignore}
|
||||
BLE
|
||||
|
||||
; [env:Xiao_rp2040_companion_radio_ble]
|
||||
; extends = Xiao_rp2040
|
||||
|
||||
Reference in New Issue
Block a user