Expand and harden full companion profiles

This commit is contained in:
mikecarper committed 2026-08-24 01:57:38 -07:00
1 parent 5362d36ac7
commit 1a43123d2c
44 files changed
+1522 -226

No files matched your search

+161 -26
View File
@@ -70,7 +70,7 @@ FALLBACK_VERSION_PREFIX="dev"
FALLBACK_VERSION_DATE_FORMAT='+%Y-%m-%d-%H-%M'
# External programs invoked by this script:
# bash, cat, cp, date, find, git, grep, head, mkdir, mv, pgrep, pio,
# bash, cat, cp, date, find, flock, git, grep, head, mkdir, mv, pgrep, pio,
# python3, rm, sed, sleep, sort, wc
# Keep this list in sync when adding or removing non-builtin command usage.
@@ -84,8 +84,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. nRF52 Full Companion provides binary Companion and logging on separate USB ports.
build-companion-firmwares-logging-matrix: Build canonical Companion targets in each applicable standard, MQTT, and expanded FULL profile. nRF52 Full Companion replaces separate USB, BLE, and USB-logging artifacts by exposing Companion and logging on separate USB ports.
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-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>.
@@ -95,7 +95,7 @@ Commands:
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
build-sensor-firmwares: Build all sensor firmwares for all build targets.
build-kiss-radio-firmwares: Build all KISS radio firmwares for all build targets.
get-companion-firmwares-to-build: List canonical attached companion targets for release automation; nRF52 uses Full Companion instead of separate USB/BLE artifacts.
get-companion-firmwares-to-build: List canonical attached companion targets for release automation; dual-CDC boards use Full Companion instead of separate transport artifacts.
get-repeater-firmwares-to-build: List standard and specialized external-storage repeater targets for release automation.
get-room-server-firmwares-to-build: List standard room-server targets for release automation.
@@ -505,7 +505,7 @@ prompt_for_build_mode() {
"Build all canonical firmwares (legacy setting aliases remain direct-build only)"
"Build the canonical release matrix with unified FULL ESP32 USB + WiFi output (plus logging fallbacks where WiFi is unavailable)"
"Build all repeater firmwares"
"Build canonical companion firmwares (nRF52 Full replaces USB/BLE; power saving and FEM/RX gain are runtime configurable)"
"Build canonical companion firmwares (dual-CDC Full replaces separate transports; power saving and FEM/RX gain are runtime configurable)"
"Build all chat room server firmwares"
"Build all sensor firmwares"
"Build FULL ESP32 firmwares (all features, USB logging, WiFi MQTT where available, and LoRa OTA)"
@@ -1479,13 +1479,14 @@ 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 generated nRF52 Full Companion supplies both attached transports and
# source-only serial mOTA in one image. It replaces the separate USB/BLE
# release artifacts without taking on a staging or self-install role.
# 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.
local env_name
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "NRF52_PLATFORM" ] \
&& is_companion_radio_full_target "$env_name" \
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
@@ -1972,11 +1973,12 @@ apply_debug_overrides() {
disable_usb_logging_for_mqtt() {
local env_name=$1
# ESP32 Full Companion has one serial stream, so plaintext diagnostics would
# corrupt its framed Companion traffic. nRF52 Full Companion has a dedicated
# second CDC interface and enables logging later in its profile overlay.
# Full Companion may enable diagnostics only when it has a dedicated second
# CDC interface. Plaintext on its primary framed stream corrupts Companion
# traffic.
if is_companion_radio_full_target "$env_name"; then
if is_nrf52_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"
@@ -2032,6 +2034,44 @@ is_nrf52_companion_radio_full_target() {
&& is_companion_radio_full_target "$1"
}
is_esp32_dual_cdc_companion_radio_full_target() {
is_esp32_companion_radio_full_target "$1" || return 1
case "${1,,}" in
lilygo_tbeam_1w_companion_radio_full|\
station_g2_companion_radio_full|\
station_g3_esp32_companion_radio_full|\
xiao_s3_wio_companion_radio_full|\
heltec_tracker_v2_companion_radio_full_femon|\
meshnology_w12_companion_radio_full|\
nibble_screen_connect_companion_radio_full_|\
nibble_zero_connect_companion_radio_full_|\
heltec_v4_2_v4_3_companion_radio_full_femon|\
heltec_v4_3_companion_radio_full_femoff|\
heltec_v4_tft_companion_radio_full_femon|\
heltec_v4_3_tft_companion_radio_full_femoff|\
heltec_v4_r8_companion_radio_full|\
heltec_v4_r8_tft_companion_radio_full)
return 0
;;
*) return 1 ;;
esac
}
prepare_esp32_dual_cdc_framework() {
local env_name=$1
local pio_env_name=$2
is_esp32_dual_cdc_companion_radio_full_target "$env_name" || return 0
# PlatformIO's standard ESP32 platform and pioarduino publish incompatible
# Arduino 2.x/3.x cores under the same global package name. `pio run` restores
# the standard core automatically, but pioarduino can otherwise proceed with
# no matching framework after a standard build. Enforce this environment's
# explicit platform_packages pins before SCons starts.
echo "Ensuring Arduino-ESP32 3.3.11 dependencies for ${env_name}..."
pio pkg install -e "$pio_env_name"
}
requires_esp32_companion_full_ota_fallback() {
# Some non-PSRAM ESP32 companions cannot hold their configured high-capacity contact,
# channel, and offline-queue tables together with BLE, WiFi, and LoRa OTA in
@@ -2485,9 +2525,10 @@ apply_companion_radio_full_profile() {
# Every full Companion is a LoRa mOTA source, never an update destination.
# Remove inherited staging/install stores before adding the platform's host
# folder transport.
append_platformio_build_unflags "-UENABLE_OTA -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1 -DDISABLE_LORA_OTA=1"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456"
# 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 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456"
if is_nrf52_companion_radio_full_target "$env_name"; then
# CDC 0 starts as Binary Companion. `motatool serve --serial` switches it
@@ -2507,6 +2548,16 @@ apply_companion_radio_full_profile() {
return 0
fi
if is_esp32_dual_cdc_companion_radio_full_target "$env_name"; then
# Qualified ESP32-S3 boards route their data connector to the native USB
# peripheral. Arduino-ESP32 3.x supplies two real TinyUSB CDC ACM
# instances: CDC 0 carries framed Companion traffic and accepts the
# flashing reboot gesture; CDC 1 is a write-only plaintext diagnostics
# stream and cannot reboot the board.
append_platformio_build_unflags "-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DARDUINO_USB_MODE=0 -DARDUINO_USB_CDC_ON_BOOT=1 -DMESH_DUAL_CDC_LOGGING=1 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1"
fi
# ESP32 keeps both source transports. TCP remains the normal unattended
# path; a host may explicitly hold the native USB port in Companion terminal
# mode when WiFi is unavailable and serve the same folder protocol there.
@@ -2788,6 +2839,7 @@ build_firmware() {
local had_platformio_build_src_filter=0
local had_platformio_extra_scripts=0
local build_status
local platformio_package_lock_fd=""
local -a pio_run_args=()
# Bash functions use dynamic scoping. These locals let one target be promoted
@@ -2928,6 +2980,27 @@ build_firmware() {
fi
print_build_flags "$env_name"
build_status=0
if [ "$env_platform" = "ESP32_PLATFORM" ]; then
# Official ESP32 and pioarduino builds share global package names even
# though they require incompatible Arduino cores. Hold the lock through
# dependency selection and compilation so parallel profile workers cannot
# replace a framework while another ESP32 target is using it.
mkdir -p -- ".pio"
build_status=$?
if [ "$build_status" -eq 0 ]; then
exec {platformio_package_lock_fd}>".pio/esp32-platformio-packages.lock"
build_status=$?
fi
if [ "$build_status" -eq 0 ]; then
flock "$platformio_package_lock_fd"
build_status=$?
fi
fi
if [ "$build_status" -eq 0 ]; then
prepare_esp32_dual_cdc_framework "$env_name" "$pio_env_name"
build_status=$?
fi
pio_run_args=(run -e "$pio_env_name")
if [[ "${PIO_BUILD_JOBS_OVERRIDE:-}" =~ ^[1-9][0-9]*$ ]]; then
pio_run_args+=(-j "$PIO_BUILD_JOBS_OVERRIDE")
@@ -2937,12 +3010,18 @@ build_firmware() {
# merges in one SCons invocation instead of paying startup twice.
pio_run_args+=(-t mergebin)
fi
pio "${pio_run_args[@]}"
build_status=$?
if [ "$build_status" -eq 0 ]; then
pio "${pio_run_args[@]}"
build_status=$?
fi
if [ "$build_status" -eq 0 ]; then
collect_build_artifacts "$env_name" "$env_platform" "$pio_env_name" "$firmware_filename"
build_status=$?
fi
if [ -n "$platformio_package_lock_fd" ]; then
flock -u "$platformio_package_lock_fd"
exec {platformio_package_lock_fd}>&-
fi
restore_platformio_build_flags "$had_platformio_build_flags" "$original_platformio_build_flags"
unset MESHCORE_ESP32_FULL_BUILD
@@ -3048,8 +3127,64 @@ get_nrf52_full_companion_replacement() {
printf '%s\n' "$full_env"
}
is_nrf52_companion_transport_replaced_by_full() {
get_nrf52_full_companion_replacement "$1" >/dev/null
get_esp32_dual_cdc_full_companion_replacement() {
local source_env=$1
local env_name=${source_env,,}
local full_env=""
[ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1
case "$env_name" in
*companion_radio_wifi_mqtt*) return 1 ;;
*companion_radio_usb*|*companion_radio_ble*|*companion_radio_wifi*) ;;
*) return 1 ;;
esac
# Full Companion includes USB, BLE, and ordinary WiFi Companion. Map legacy
# FEM-default aliases to the one runtime-configurable image for each physical
# V4 display/radio layout. Expansion-kit targets intentionally do not match:
# they have distinct sensor wiring and no corresponding Full recipe.
case "$env_name" in
heltec_v4_r8_tft_companion_radio_*)
full_env=heltec_v4_r8_tft_companion_radio_full
;;
heltec_v4_r8_companion_radio_*)
full_env=heltec_v4_r8_companion_radio_full
;;
heltec_v4_3_tft_companion_radio_*|heltec_v4_tft_companion_radio_*)
full_env=heltec_v4_tft_companion_radio_full_femon
;;
heltec_v4_3_companion_radio_*|heltec_v4_companion_radio_*)
full_env=heltec_v4_2_v4_3_companion_radio_full_femon
;;
*)
case "$env_name" in
*companion_radio_usb*)
full_env=${source_env/companion_radio_usb/companion_radio_full}
;;
*companion_radio_ble*)
full_env=${source_env/companion_radio_ble/companion_radio_full}
;;
*companion_radio_wifi*)
full_env=${source_env/companion_radio_wifi/companion_radio_full}
;;
*)
return 1
;;
esac
;;
esac
is_esp32_dual_cdc_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"
}
is_companion_transport_replaced_by_full() {
get_full_companion_replacement "$1" >/dev/null
}
is_runtime_setting_alias_target() {
@@ -3065,17 +3200,17 @@ is_runtime_setting_alias_target() {
is_redundant_bulk_build_target() {
# Keep every legacy name available to `build-firmware` and
# `build-matching-firmwares`, but do not republish binaries that differ only
# by a saved/default setting or by an attached nRF52 transport already
# supplied by Full Companion.
# by a saved/default setting or by an attached transport already supplied by
# a dual-CDC Full Companion.
if is_runtime_setting_alias_target "$1" \
|| is_nrf52_companion_transport_replaced_by_full "$1"; then
|| is_companion_transport_replaced_by_full "$1"; then
return 0
fi
return 1
}
resolve_logging_matrix_firmwares() {
# nRF52 Full Companion replaces normal USB, BLE, and USB-logging artifacts.
# 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.
resolve_all_firmwares
@@ -3797,7 +3932,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; nRF52 Full already provides logging on its second USB port, while ESP32 Full keeps its single USB stream protocol-safe."
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."
fi
for target in "${logging_targets[@]}"; do
+43 -18
View File
@@ -381,10 +381,31 @@
/(?:^|[_-])(?:ps|femoff)(?=$|[_-])/.test(lowerTarget));
}
function omitNrf52TransportsReplacedByFull(profiles) {
function isDualCdcFullCompanion(profile) {
if (!profile || profile.role !== "companion" || profile.mode !== "full") {
return false;
}
if (profile.installKinds.includes("zip")) return true;
const target = String(profile.target || "").toLowerCase();
if (/^heltec_v4(?:_2_v4_3|_3)?(?:_r8)?(?:_tft)?_companion_radio_full(?:_|$)/
.test(target)) {
return true;
}
return [
"lilygo_tbeam_1w_companion_radio_full",
"station_g2_companion_radio_full",
"station_g3_esp32_companion_radio_full",
"xiao_s3_wio_companion_radio_full",
"heltec_tracker_v2_companion_radio_full_femon",
"meshnology_w12_companion_radio_full",
"nibble_screen_connect_companion_radio_full_",
"nibble_zero_connect_companion_radio_full_",
].includes(target);
}
function omitTransportsReplacedByDualCdcFull(profiles) {
const fullKeys = new Set((profiles || []).filter(function (profile) {
return profile.role === "companion" && profile.mode === "full" &&
profile.installKinds.includes("zip");
return isDualCdcFullCompanion(profile);
}).map(function (profile) {
return profile.hardware + "\n" + profile.variant;
}));
@@ -392,20 +413,19 @@
return (profiles || []).filter(function (profile) {
const key = profile.hardware + "\n" + profile.variant;
const replacedAttachedTransport = profile.role === "companion" &&
(profile.mode === "usb" || profile.mode === "ble");
(profile.mode === "usb" || profile.mode === "ble" ||
profile.mode === "wifi");
return !(replacedAttachedTransport && fullKeys.has(key));
});
}
function applyNrf52FullCompanionCapabilities(profiles) {
function applyDualCdcFullCompanionCapabilities(profiles) {
return (profiles || []).map(function (profile) {
const nativeNrf52Full = profile.role === "companion" &&
profile.mode === "full" && profile.installKinds.includes("zip");
if (!nativeNrf52Full) return profile;
if (!isDualCdcFullCompanion(profile)) return profile;
// Current nRF52 Full Companion images expose two CDC ACM interfaces:
// framed Companion traffic on interface 00 and plaintext logging on 02.
// The saved USB logging gate covers only the second interface.
// 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.
profile.logging = "usb-runtime";
profile.loggingModes = ["none", "usb"];
profile.dedicatedUsbLogging = true;
@@ -512,8 +532,8 @@
}).filter(function (profile) {
return !isHiddenLegacyProfile(profile);
});
const profiles = omitNrf52TransportsReplacedByFull(
applyNrf52FullCompanionCapabilities(visibleProfiles)
const profiles = omitTransportsReplacedByDualCdcFull(
applyDualCdcFullCompanionCapabilities(visibleProfiles)
).sort(function (a, b) {
return a.target.localeCompare(b.target, undefined, {
numeric: true,
@@ -712,11 +732,11 @@
);
} else if (profile.logging === "usb-runtime") {
extra.push(
"Use get usb.logging and set usb.logging off|on to choose and save normal output-off or USB packet logging."
"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(
"This nRF52 Full Companion exposes two serial ports on one USB cable: interface 00 carries Companion/terminal/mOTA traffic and interface 02 carries plaintext logging. Match services by USB interface number instead of assuming tty or COM numbering."
"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."
);
}
}
@@ -757,7 +777,7 @@
if (profile.dedicatedUsbLogging) {
factRows.splice(3, 0, [
"USB port split",
"Interface 00 Companion; interface 02 logging",
"Interface 00 always; interface 02 after logging on + reboot",
]);
}
replaceFacts(facts, factRows);
@@ -1086,10 +1106,15 @@
flattenReleaseAssets: flattenReleaseAssets,
parseFirmwareAsset: parseFirmwareAsset,
parseTargetProfile: parseTargetProfile,
applyDualCdcFullCompanionCapabilities:
applyDualCdcFullCompanionCapabilities,
applyNrf52FullCompanionCapabilities:
applyNrf52FullCompanionCapabilities,
applyDualCdcFullCompanionCapabilities,
canonicalHardware: canonicalHardware,
omitNrf52TransportsReplacedByFull: omitNrf52TransportsReplacedByFull,
omitTransportsReplacedByDualCdcFull:
omitTransportsReplacedByDualCdcFull,
omitNrf52TransportsReplacedByFull:
omitTransportsReplacedByDualCdcFull,
hardwareFamilyFor: hardwareFamilyFor,
humanizeHardwareVariant: humanizeHardwareVariant,
buildCatalog: buildCatalog,
+14 -8
View File
@@ -41,7 +41,7 @@ retain 50 because their MQTT discovery tables are constrained by internal DRAM.
| 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. |
| nRF52 Full Companion | Uses one physical USB connection with interface `00` for framed Companion/terminal/mOTA traffic and interface `02` for plaintext logging. It also provides BLE and source-only LoRa OTA. `get/set usb.logging` persistently gates only the logging interface. |
| 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. |
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. GPS-preserving RAK nRF52 OTA profiles retain their GPS commands and provider. The RAK4631 Serial1 RS232 bridge remains GPS-off because both features require Serial1. |
`logging`, `OTA`, and `FULL` describe independent build features. Do not infer
@@ -61,19 +61,25 @@ 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 an nRF52 board has a generated Full Companion, that one artifact
replaces its separate USB, BLE, and USB packet-logging Companion artifacts.
It provides BLE plus two USB CDC interfaces: interface `00` carries framed
Companion/terminal/mOTA traffic and interface `02` carries plaintext logs.
- When a board has a dual-CDC 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.
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.
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 USB, BLE, WiFi, and Full Companion images also remain
separate because Full changes partitions, RAM use, active transports, and power
behavior.
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.
## Complete CLI policy
+2 -2
View File
@@ -71,7 +71,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS 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; nRF52 Full Companion gates its second USB port | 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; dual-CDC Full Companion adds/removes its second USB port after reboot | 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 |
@@ -233,7 +233,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS 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; nRF52 Full Companion gates its second USB port | 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; dual-CDC Full Companion adds/removes its second USB port after reboot | 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 |
+20 -3
View File
@@ -538,13 +538,30 @@ emitted only when that hardware/role has no matching WiFi MQTT environment.
get usb.logging
set usb.logging on
set usb.logging off
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; first boot defaults to on. nRF52 Full Companion applies the
gate only to USB interface `02`, its dedicated plaintext logging port. USB
interface `00` continues carrying Companion, terminal, and serial mOTA traffic.
it survives reboot; their first boot defaults to on.
On Full Companion these lines belong to its text terminal, not `meshcli`'s
Binary `get/set` parameter namespace. Open interface `00`, send
`+++MESHCORE-TERM-START`, and then issue the command. Running
`meshcli ... get usb.logging` directly can instead return
`Unknown var usb.logging` because that is a different protocol operation.
Dual-CDC Full Companion instead defaults logging to off and enumerates only
USB interface `00`, which carries Companion, terminal, and serial mOTA traffic.
Enabling logging adds interface `02`, its dedicated plaintext logging port, on
the next boot. Disabling it removes interface `02` on the next boot. A command
without the optional `reboot` argument saves the choice and reports that a
reboot is required when the USB interface count must change. The exact
`set usb.logging on reboot` and `set usb.logging off reboot` forms save the
choice, send their reply, and reboot one second later only when needed. This
behavior includes nRF52 and qualified 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`.
+52 -19
View File
@@ -11,6 +11,7 @@ firmware as an mOTA image.
| USB Binary Companion | Yes | Yes |
| BLE Binary Companion | Yes | Yes |
| USB ASCII terminal | Yes | Yes |
| Dedicated USB plaintext logging | Qualified native-USB ESP32-S3 profiles | Yes |
| Host-backed LoRa mOTA source | WiFi TCP 5001 | Exclusive USB mode |
| WiFi Companion/WebConfig | Yes | No - nRF52840 has no WiFi |
| LoRa self-update | No | No |
@@ -50,11 +51,12 @@ 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. On nRF52, Full Companion replaces separate USB and BLE
normal release artifacts whenever the exact board has both recipes. It also
replaces the USB-only packet-logging artifact: one physical USB connection
enumerates separate Companion and logging serial ports, so plaintext logs
cannot corrupt binary frames. In WebConfig, use the
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
**FEM RX boost** switch. From the text terminal (USB, or TCP 5002 on ESP32), use:
```text
@@ -248,30 +250,61 @@ The terminal supports Companion chat commands, including `channels`,
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. nRF52 Full Companion saves this setting and applies it only to its
dedicated logging port.
replies. Dual-CDC Full Companion saves this setting and applies it only to its
dedicated logging port. It starts off on a fresh installation.
### nRF52 dual USB serial ports
### Dual USB serial ports
Current nRF52 Full Companion firmware exposes two CDC ACM serial interfaces on
one physical USB cable:
Current nRF52 Full Companion and qualified native-USB ESP32-S3 Full Companion
firmware can expose two CDC ACM serial interfaces on one physical USB cable:
- USB interface `00` is the normal Binary Companion, text terminal, and serial
mOTA source port.
- USB interface `02` is a write-only plaintext packet/debug logging port. Host
input on this interface is ignored and cannot invoke firmware commands.
mOTA source port. It is always present.
- USB interface `02` is the optional write-only plaintext packet/debug logging
port. Host input on this interface is ignored and cannot invoke firmware
commands.
On Linux these normally appear as two `/dev/ttyACM*` devices. Match the stable
A fresh Full Companion starts with USB logging off and therefore enumerates
only interface `00`. Use `set usb.logging on` to save logging on; the reply says
that a reboot is required. Use `set usb.logging on reboot` to save it and have
the node reboot automatically after the reply. The second interface appears
after that reboot. Likewise, `set usb.logging off reboot` removes interface
`02`. The optional `reboot` word is accepted only in these exact command forms
and triggers a reboot only when the descriptor actually needs to change.
These are Full Companion **text-terminal** commands. The superficially similar
`meshcli ... get usb.logging` command uses the Binary Companion parameter
registry and can report `Unknown var usb.logging`; it does not forward that
line to the text terminal. Enter terminal mode on interface `00` with the
`+++MESHCORE-TERM-START` token as described above, then issue the command.
When logging is on, Linux normally shows two `/dev/ttyACM*` devices. Match the stable
`/dev/serial/by-id/*-if00` and `*-if02` links, or use a udev rule matching
`ID_USB_INTERFACE_NUM`, rather than assuming which tty number is assigned. On
Windows they appear as two COM ports; identify them by USB interface instead of
depending on a particular COM number. The bootloader may temporarily expose
only its normal DFU serial interface while an update is active.
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
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.
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
loading the software bootloader, and some flash configurations fail before the
application starts when a merged image inherits QIO. DIO trades some maximum
flash-read throughput for compatibility; it does not change a board's PSRAM
type or any ordinary non-Full firmware profile.
Point MeshCore Companion software, `meshcli`, and `motatool` at interface `00`.
Point a plaintext reader or USB-connected MQTT service at interface `02`. Use
`set usb.logging off|on` through the Companion terminal to persist whether the
second port emits output.
When enabled and rebooted, point a plaintext reader or USB-connected MQTT
service at interface `02`.
ESP32 Full Companion exposes this same text terminal on TCP port 5002. Connect
with `nc DEVICE_IP 5002`; no USB control token is needed. USB terminal mode and
+23 -11
View File
@@ -168,12 +168,22 @@ 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 nRF52 Full Companion profiles also use one binary for normal attached
Companion use and USB packet logging. One USB cable exposes interface `00` for
Binary Companion, terminal, and mOTA source traffic, plus interface `02` for
plaintext logging. The picker therefore omits the older separate nRF52 USB,
BLE, and USB-logging Companion choices when the matching Full artifact exists.
Use `set usb.logging off|on` to persist the second port's output state.
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.
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.
## Installation methods
@@ -210,8 +220,10 @@ saving, controllable FEM receive gain, and radio-chip receive gain are saved
settings rather than separate recommended firmware files. Do not substitute a
similarly named physical target.
For nRF52 hardware with a Full Companion image, the picker recommends that one
normal image instead of separate USB, BLE, and USB-logging images. Full
Companion provides both attached transports and a dedicated plaintext logging
port without mixing logs into framed Companion traffic. Exact filename search
still finds old aliases from earlier releases.
For hardware with a dual-CDC Full Companion image, the picker recommends that
one normal image instead of separate USB, BLE, ordinary WiFi, and USB-logging
images. Full Companion provides the attached transports and a dedicated
plaintext logging port when enabled without mixing logs into framed Companion
traffic. Logging is off by default, so only the framed port appears until it is
enabled and the node reboots.
Exact filename search still finds old aliases from earlier releases.
+6 -4
View File
@@ -53,12 +53,14 @@ reopening the controller while `motatool` owns the source port. The script
rejects an attempt to use the same port for both.
The USB ASCII switch (`+++MESHCORE-TERM-START`) is the local control path, not
the mOTA data framing. On an nRF52 full Companion, the script uses that mode
the mOTA data framing. On a dual-CDC full Companion, the script uses that mode
briefly for `ota status` and TempRadio commands. It then closes the CLI and
starts `motatool`, whose existing `ota folder on` preamble switches the same
USB interface `00` into exclusive mOTA mode. Its separate USB interface `02`
continues to provide plaintext logging and is not a controller or source port.
On an ESP32 Full Companion with serial folder support,
USB interface `00` into exclusive mOTA mode. If USB logging was enabled and the
Full Companion rebooted, its separate interface `02` continues to provide
plaintext logging and is not a controller or source port. With default logging
off, interface `02` is not enumerated.
On a native-USB ESP32-S3 Full Companion with serial folder support,
`motatool --companion-terminal` keeps the ASCII session open
while the same link carries framed folder requests. BLE remains available.
+5 -3
View File
@@ -94,11 +94,13 @@ GPS idle behavior; it does not change LoRa RXPS or WiFi modem sleep.
```
get usb.logging
set usb.logging {on|off}
set usb.logging {on|off} [reboot]
```
Shows or changes live USB debug and packet output in a Companion logging
artifact. The setting lasts until reboot; logging starts on again at the next
boot. Companion protocol frames and terminal replies remain enabled.
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`.
```
get radio.rxps
+67 -11
View File
@@ -1376,6 +1376,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
command_radio_cr = 0;
command_radio_repeat = 0;
command_radio_apply_deadline = 0;
#if MESH_USB_LOGGING_AVAILABLE
_usb_logging_reboot_at = 0;
#endif
#if defined(COMPANION_RADIO_FULL)
_temp_radio_set_at = 0;
_temp_radio_revert_at = 0;
@@ -1437,7 +1440,13 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.powersaving_enabled = 1;
_prefs.powersaving_policy_version = 0;
_prefs.wifi_enabled = 1;
#if defined(COMPANION_RADIO_FULL) && 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.
_prefs.usb_logging_enabled = 0;
#else
_prefs.usb_logging_enabled = 1;
#endif
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
@@ -1457,6 +1466,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
void MyMesh::begin(bool has_display, bool radio_available) {
_radio_available = radio_available;
setRadioAvailable(radio_available);
initializeContactStorage();
initializeOfflineQueue();
BaseChatMesh::begin();
@@ -1545,7 +1555,11 @@ void MyMesh::begin(bool has_display, bool radio_available) {
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
}
#if MESH_USB_LOGGING_AVAILABLE
mesh::setUsbLoggingEnabled(_prefs.usb_logging_enabled != 0);
const bool usb_logging_enabled = _prefs.usb_logging_enabled != 0;
mesh::setUsbLoggingEnabled(usb_logging_enabled);
if (!mesh::saveUsbLoggingBootPreference(usb_logging_enabled)) {
MESH_DEBUG_PRINTLN("Unable to save next-boot USB logging interface state");
}
#endif
#ifdef BLE_PIN_CODE // 123456 by default
@@ -4925,8 +4939,11 @@ void MyMesh::handleTerminalCommand(char* command) {
_prefs.powersaving_enabled ? "on" : "off");
#if MESH_USB_LOGGING_AVAILABLE
} else if (strcmp(command, "get usb.logging") == 0) {
terminalOutput().printf(" usb.logging %s\r\n",
mesh::isUsbLoggingEnabled() ? "on" : "off");
terminalOutput().printf(
" usb.logging %s%s\r\n",
mesh::isUsbLoggingEnabled() ? "on" : "off",
mesh::usbLoggingInterfaceRestartRequired()
? " (reboot required to change USB interfaces)" : "");
#endif
} else if (strncmp(command, "powersaving ", 12) == 0) {
char reply[160];
@@ -4978,18 +4995,49 @@ void MyMesh::handleTerminalCommand(char* command) {
|| config[11] == '\t')) {
const char* value = config + 11;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
terminalOutput().print(" ERROR: use set usb.logging <on|off>\r\n");
bool enabled = false;
bool reboot_if_needed = false;
bool valid = true;
if (strcmp(value, "on") == 0) {
enabled = true;
} else if (strcmp(value, "off") == 0) {
enabled = false;
} else if (strcmp(value, "on reboot") == 0) {
enabled = true;
reboot_if_needed = true;
} else if (strcmp(value, "off reboot") == 0) {
enabled = false;
reboot_if_needed = true;
} else {
valid = false;
}
if (!valid) {
terminalOutput().print(
" ERROR: use set usb.logging <on|off> [reboot]\r\n");
} else {
const bool enabled = strcmp(value, "on") == 0;
_prefs.usb_logging_enabled = enabled ? 1 : 0;
mesh::setUsbLoggingEnabled(enabled);
if (savePrefs()) {
terminalOutput().printf(" OK - USB logging %s (saved)\r\n",
enabled ? "on" : "off");
} else {
if (!savePrefs()) {
terminalOutput().print(
" ERROR: USB logging changed for this boot but save failed\r\n");
} else if (!mesh::saveUsbLoggingBootPreference(enabled)) {
terminalOutput().print(
" ERROR: setting saved, but next-boot USB interface state could not be saved\r\n");
} else if (mesh::usbLoggingInterfaceRestartRequired()) {
if (reboot_if_needed) {
terminalOutput().printf(
" OK - USB logging %s (saved); rebooting to change USB interfaces\r\n",
enabled ? "on" : "off");
_usb_logging_reboot_at = futureMillis(1000);
} else {
terminalOutput().printf(
" OK - USB logging %s (saved); reboot required to change USB interfaces\r\n",
enabled ? "on" : "off");
}
} else {
terminalOutput().printf(" OK - USB logging %s (saved)\r\n",
enabled ? "on" : "off");
}
}
} else
@@ -5090,7 +5138,7 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" powersaving [on|off]\r\n");
#if MESH_USB_LOGGING_AVAILABLE
terminalOutput().print(" get usb.logging\r\n");
terminalOutput().print(" set usb.logging <on|off>\r\n");
terminalOutput().print(" set usb.logging <on|off> [reboot]\r\n");
#endif
#if defined(ESP32) && defined(WIFI_SSID)
terminalOutput().print(" get wifi.powersave\r\n");
@@ -5361,6 +5409,14 @@ void MyMesh::checkSerialInterface() {
}
void MyMesh::loop() {
#if MESH_USB_LOGGING_AVAILABLE
if (_usb_logging_reboot_at != 0
&& millisHasNowPassed(_usb_logging_reboot_at)) {
_usb_logging_reboot_at = 0;
board.reboot();
return;
}
#endif
#if defined(COMPANION_RADIO_FULL)
serviceTempRadio();
#endif
+3
View File
@@ -415,6 +415,9 @@ private:
uint8_t command_radio_cr;
uint8_t command_radio_repeat;
unsigned long command_radio_apply_deadline;
#if MESH_USB_LOGGING_AVAILABLE
unsigned long _usb_logging_reboot_at;
#endif
#if defined(COMPANION_RADIO_FULL)
unsigned long _temp_radio_set_at;
unsigned long _temp_radio_revert_at;
+69 -24
View File
@@ -12,6 +12,13 @@
#else
#define COMPANION_IDF_PM_AVAILABLE 0
#endif
#if defined(BLE_PIN_CODE) && !CONFIG_IDF_TARGET_ESP32C6 \
&& (defined(CONFIG_BT_CTRL_MODEM_SLEEP) \
|| defined(CONFIG_BTDM_CTRL_MODEM_SLEEP))
#define COMPANION_BT_MODEM_SLEEP_AVAILABLE 1
#else
#define COMPANION_BT_MODEM_SLEEP_AVAILABLE 0
#endif
#if defined(COMPANION_RADIO_FULL)
#include <esp_heap_caps.h>
#endif
@@ -180,6 +187,22 @@ static uint32_t companionNominalCpuMhz() {
#endif
}
#if COMPANION_BT_MODEM_SLEEP_AVAILABLE
static void applyCompanionBluetoothSleep(bool enabled,
bool allow_uninitialized) {
esp_err_t result = enabled ? esp_bt_sleep_enable()
: esp_bt_sleep_disable();
if (result == ESP_OK
|| (allow_uninitialized && result == ESP_ERR_INVALID_STATE)) {
return;
}
mesh::usbLoggingPort().printf("Bluetooth sleep %s failed: %s\r\n",
enabled ? "enable" : "disable",
esp_err_to_name(result));
}
#endif
static bool applyCompanionPowerSaving(bool enabled) {
const uint32_t nominal_mhz = companionNominalCpuMhz();
const uint32_t max_mhz = enabled && nominal_mhz > 80 ? 80 : nominal_mhz;
@@ -224,14 +247,13 @@ static bool applyCompanionPowerSaving(bool enabled) {
}
#endif
#if defined(BLE_PIN_CODE) && !CONFIG_IDF_TARGET_ESP32C6
esp_err_t bt_result = enabled ? esp_bt_sleep_enable()
: esp_bt_sleep_disable();
if (bt_result != ESP_OK) {
mesh::usbLoggingPort().printf("Bluetooth sleep %s failed: %s\r\n",
enabled ? "enable" : "disable",
esp_err_to_name(bt_result));
}
#if COMPANION_BT_MODEM_SLEEP_AVAILABLE
// Power saving is applied before the wireless interfaces are constructed.
// An uninitialized controller is expected here; apply the saved setting
// again immediately after BLEDevice::init() below. Some prebuilt ESP-IDF
// targets omit Bluetooth modem sleep entirely; the compile-time guard avoids
// calling an API which can only return ESP_ERR_NOT_SUPPORTED in those builds.
applyCompanionBluetoothSleep(enabled, true);
#endif
#if COMPANION_IDF_PM_AVAILABLE
@@ -945,32 +967,47 @@ void halt() {
#if defined(BLE_PIN_CODE)
static bool companion_bluetooth_initialized = false;
#if defined(ESP32) && defined(COMPANION_RADIO_FULL) && defined(WIFI_SSID)
static uint32_t companion_bluetooth_start_at = 0;
#endif
static constexpr uint32_t COMPANION_BLUETOOTH_RETRY_MS = 5000UL;
static void scheduleCompanionBluetoothRetry() {
companion_bluetooth_start_at = millis() + COMPANION_BLUETOOTH_RETRY_MS;
if (companion_bluetooth_start_at == 0) companion_bluetooth_start_at = 1;
}
static void startCompanionBluetooth() {
if (companion_bluetooth_initialized) return;
companion_bluetooth_start_at = 0;
mesh::usbLoggingPort().println("Companion: starting Bluetooth");
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name,
the_mesh.getBLEPin());
if (!interface_manager.addInterface(InterfaceType::Bluetooth,
&bluetooth_interface)) {
mesh::usbLoggingPort().println(
"Companion: no interface slot available for Bluetooth");
"Companion: no interface slot available for Bluetooth; retrying");
scheduleCompanionBluetoothRetry();
return;
}
if (!bluetooth_interface.begin(BLE_NAME_PREFIX,
the_mesh.getNodePrefs()->node_name,
the_mesh.getBLEPin())) {
interface_manager.removeInterface(&bluetooth_interface);
mesh::usbLoggingPort().println(
"Companion: Bluetooth initialization failed; retrying in 5 seconds");
scheduleCompanionBluetoothRetry();
return;
}
#if defined(ESP32_PLATFORM) && COMPANION_BT_MODEM_SLEEP_AVAILABLE
applyCompanionBluetoothSleep(
the_mesh.getNodePrefs()->powersaving_enabled != 0, false);
#endif
companion_bluetooth_initialized = true;
if (interface_manager.isEnabled()) bluetooth_interface.enable();
}
static void serviceDeferredCompanionBluetooth() {
if (companion_bluetooth_initialized) return;
#if defined(ESP32) && defined(COMPANION_RADIO_FULL) && defined(WIFI_SSID)
if (companion_bluetooth_start_at == 0
|| (int32_t)(millis() - companion_bluetooth_start_at) < 0) return;
#endif
startCompanionBluetooth();
}
#endif
@@ -1109,6 +1146,11 @@ void setup() {
#error "need to define filesystem"
#endif
// nRF52 cannot decide whether to add its optional logging CDC interface
// until the saved Companion preferences above are available. ESP32 already
// fixed its descriptor from the early NVS mirror, so this is harmless there.
mesh::beginUsbLoggingPort();
// Load WiFi state before bringing up either wireless stack.
#ifdef WIFI_SSID
loadCompanionWiFiCredentials();
@@ -1118,6 +1160,14 @@ void setup() {
logFullCompanionMemory("before interfaces");
#endif
// NimBLE must reserve its internal controller/host memory before WiFi starts
// its AP and WebConfig services. Starting WiFi first can leave plenty of total
// heap but no internal block large enough for nimble_port_init().
#if defined(BLE_PIN_CODE) && defined(ESP32) \
&& defined(COMPANION_RADIO_FULL) && defined(WIFI_SSID)
startCompanionBluetooth();
#endif
// add wifi interface
#ifdef WIFI_SSID
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
@@ -1147,16 +1197,11 @@ void setup() {
logFullCompanionMemory("after WiFi start");
#endif
// The Full ESP32 profile starts WiFi/WebConfig first, gives its AP/server two
// seconds to settle and reserve heap, then starts BLE from loop(). Other BLE
// companions keep their existing immediate startup.
// Full ESP32 WiFi+BLE companions started BLE above so its controller memory
// could not be fragmented by WiFi. Other BLE companions start here.
#if defined(BLE_PIN_CODE)
#if defined(ESP32) && defined(COMPANION_RADIO_FULL) && defined(WIFI_SSID)
companion_bluetooth_start_at = millis() + 2000UL;
if (companion_bluetooth_start_at == 0) companion_bluetooth_start_at = 1;
mesh::usbLoggingPort().println(
"Companion: Bluetooth starts in 2 seconds");
#else
#if !(defined(ESP32) && defined(COMPANION_RADIO_FULL) \
&& defined(WIFI_SSID))
startCompanionBluetooth();
#endif
#endif
@@ -140,10 +140,12 @@ 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. nRF52 Full Companion uses "
"separate USB interfaces for Companion traffic and plaintext logging, "
"replacing its older separate USB-logging image. Open Release "
"notes for role, hardware, installation, and partition requirements."
"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. "
"Open Release notes for role, hardware, installation, and partition "
"requirements."
)
entry_count = 0
@@ -277,6 +279,14 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
notes = common.normalize_nrf52_full_companion_metadata(
firmware, notes
)
elif device_type == "esp32" and any(
common.ESP32_DUAL_CDC_FULL_RE.match(identity)
is not None
for identity in identities
):
notes = common.normalize_esp32_dual_cdc_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 "
+136 -46
View File
@@ -19,6 +19,15 @@ VERSION_LABEL_RE = re.compile(r"v\d+(?:\.\d+)+")
CATALOG_VERSION_TEXT_RE = re.compile(
r"Keymind Cascade MeshCore v[0-9][A-Za-z0-9_.-]*"
)
ESP32_DUAL_CDC_FULL_RE = re.compile(
r"^(?:"
r"heltec_v4(?:_2_v4_3|_3)?(?:_r8)?(?:_tft)?|"
r"LilyGo_TBeam_1W|Station_G2|Station_G3_ESP32|Xiao_S3_WIO|"
r"heltec_tracker_v2|meshnology_w12|"
r"nibble_(?:screen|zero)_connect"
r")_companion_radio_full(?:_|$)",
flags=re.IGNORECASE,
)
LEGACY_PORTABLE_CEILING_EXCEPTIONS = {
"heltec_ct62_repeater_lora_ota_no_external_sensors",
@@ -169,6 +178,12 @@ def canonical_runtime_identity(identity: str) -> str:
result,
flags=re.IGNORECASE,
)
result = re.sub(
r"^heltec_v4_companion_radio_full(?:_femon)?(?=-|$)",
"heltec_v4_2_v4_3_companion_radio_full_femon",
result,
flags=re.IGNORECASE,
)
return result
@@ -280,20 +295,23 @@ def normalize_nrf52_full_companion_metadata(
"""Describe the canonical dual-CDC nRF52 Full Companion accurately."""
firmware["title"] = "Full Companion"
firmware["subTitle"] = (
"USB Companion + USB logging + BLE + LoRa OTA source"
"USB Companion + optional USB logging + BLE + LoRa OTA source"
)
profile = (
"PROFILE - nRF52 Full Companion: one USB cable exposes interface 00 "
"PROFILE - nRF52 Full Companion: one USB cable always exposes interface 00 "
"for Binary Companion, the text terminal, and serial mOTA source "
"traffic, while interface 02 is a separate plaintext packet/debug "
"logging port. BLE remains available. Use set usb.logging off|on to "
"save whether interface 02 emits output."
"traffic. Fresh installs default USB logging off and do not enumerate "
"interface 02. Enabling logging and rebooting adds interface 02 as a "
"separate plaintext packet/debug port. BLE remains available. Use "
"set usb.logging on reboot or set usb.logging off reboot to save and "
"apply the interface count."
)
logging_use = (
"LOGGING USE - Point Companion software, meshcli, and motatool at USB "
"interface 00. Point a USB-connected MQTT/logging reader at interface "
"02. Match the USB interface number rather than assuming a tty or COM "
"port number. Input received on interface 02 is ignored."
"interface 00. After enabling logging and rebooting, point a "
"USB-connected MQTT/logging reader at interface 02. Match the USB "
"interface number rather than assuming a tty or COM port number. Input "
"received on interface 02 is ignored."
)
ota_use = (
"LORA OTA SOURCE - This Full Companion can serve a host-supplied "
@@ -313,8 +331,8 @@ def normalize_nrf52_full_companion_metadata(
continue
if paragraph.startswith("SELECTION "):
paragraphs.append(
"SELECTION - nRF52 Full Companion with dual USB serial ports, "
"BLE, and source-only LoRa OTA."
"SELECTION - nRF52 Full Companion with an optional second USB "
"logging port, BLE, and source-only LoRa OTA."
)
continue
paragraphs.append(paragraph)
@@ -326,12 +344,95 @@ def normalize_nrf52_full_companion_metadata(
return "\n\n".join(paragraphs)
def normalize_esp32_dual_cdc_full_companion_metadata(
firmware: dict, notes: str
) -> str:
"""Describe a native-USB ESP32-S3 Full Companion accurately."""
firmware["title"] = "Full Companion"
firmware["subTitle"] = (
"USB Companion + optional USB logging + BLE + Wi-Fi + LoRa OTA source"
)
profile = (
"PROFILE - Native-USB ESP32-S3 Full Companion: one USB cable always "
"exposes interface 00 for Binary Companion, the text terminal, flashing, and "
"serial mOTA source traffic. Fresh installs default USB logging off and "
"do not enumerate interface 02. Enabling logging and rebooting adds "
"interface 02 as a separate plaintext packet/debug port. BLE, Wi-Fi "
"Companion on TCP 5000, "
"WebConfig, TCP mOTA seeding on 5001, and the text terminal on 5002 "
"remain available. Use set usb.logging on reboot or set usb.logging "
"off reboot to save and apply the interface count."
)
logging_use = (
"LOGGING USE - Point Companion software, meshcli, motatool, and wired "
"flashing at USB interface 00. After enabling logging and rebooting, "
"point a USB-connected MQTT/logging reader at interface 02. Match the "
"USB interface number rather than assuming a tty or COM port number. "
"Input received on interface 02 is ignored and cannot reboot the board."
)
selection = (
"SELECTION - One Full image for this exact native-USB 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 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 canonical_full_identity_for_transport(identity: str) -> str | None:
match = re.search(
r"_companion_radio_(?:usb|ble|wifi(?!_mqtt))(?=-|_|$)",
identity,
flags=re.IGNORECASE,
)
if match is None:
return None
full_identity = (
identity[:match.start()] + "_companion_radio_full" +
identity[match.end():]
)
return canonical_runtime_identity(full_identity)
def release_identity_is_dual_cdc_full(
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
)
)
def resolve_release_identity(
old_identity: str, release_files: dict[str, list[Path]]
) -> tuple[str, bool]:
@@ -340,47 +441,27 @@ def resolve_release_identity(
if candidate in release_files:
return candidate, False
# Current nRF52 Full Companion also replaces the old USB-only logging
# artifact because its second CDC interface carries plaintext logs. Limit
# this fallback to identities with a native nRF52 DFU ZIP so an ESP32 Full
# Companion is never mistaken for a dual-port logging image.
# 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.
for candidate in candidates:
if not candidate.endswith("-logging"):
continue
logging_base = candidate.removesuffix("-logging")
match = re.search(
r"_companion_radio_(?:usb|ble)(?=-|_|$)",
logging_base,
flags=re.IGNORECASE,
)
if match is None:
continue
full_identity = (
logging_base[:match.start()] + "_companion_radio_full" +
logging_base[match.end():]
)
if full_identity in release_files and release_identity_has_nrf52_package(
full_identity = canonical_full_identity_for_transport(logging_base)
if full_identity is not None and release_identity_is_dual_cdc_full(
release_files, full_identity
):
return full_identity, False
# Canonical nRF52 Full Companion replaces separate USB and BLE artifacts.
# ESP32 transport-specific images still match exactly above and therefore
# remain separate. Full is source-only for LoRa OTA and needs no staging
# store or self-install slot.
# 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.
for candidate in candidates:
match = re.search(
r"_companion_radio_(?:usb|ble)(?=-|_|$)",
candidate,
flags=re.IGNORECASE,
)
if match is None:
continue
full_identity = (
candidate[:match.start()] + "_companion_radio_full" +
candidate[match.end():]
)
if full_identity in release_files:
full_identity = canonical_full_identity_for_transport(candidate)
if full_identity is not None and release_identity_is_dual_cdc_full(
release_files, full_identity
):
return full_identity, False
# Accept catalogs produced while portable MQTT observers were still
@@ -652,10 +733,12 @@ 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. On nRF52, Full Companion "
"replaces separate BLE, USB, and USB-logging choices with BLE plus "
"separate Companion and logging USB ports; on ESP32, Full Companion is "
"offered next to the BLE/USB variants. Open Release notes for role, "
"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, "
"hardware, installation, and partition requirements."
)
@@ -750,6 +833,13 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
notes = normalize_nrf52_full_companion_metadata(
firmware, notes
)
elif device_type == "esp32" and any(
ESP32_DUAL_CDC_FULL_RE.match(identity) is not None
for identity in resolved_identities
):
notes = normalize_esp32_dual_cdc_full_companion_metadata(
firmware, notes
)
firmware["version"] = {version_key: {"notes": notes, "files": files}}
updated_entries += 1
+29
View File
@@ -104,6 +104,35 @@ lib_deps =
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13-1/platform-espressif32.zip
; ESP32-S3 Full Companion images use the ROM-compatible dual-I/O flash mode.
; This is independent of the board's PSRAM type and of whether the USB
; connector exposes the S3 native USB peripheral. Ordinary non-Full profiles
; keep their existing board-specific flash mode.
[esp32_s3_full]
board_build.flash_mode = dio
; Full Companion profiles on boards whose connector is wired to the ESP32-S3
; native USB peripheral use Arduino-ESP32 3.x TinyUSB. This supplies two real
; CDC ACM interfaces: CDC 0 for framed Companion traffic and optional CDC 1
; for plaintext diagnostics. Do not apply this section to USB-UART-bridge
; boards; firmware cannot add an interface to an external bridge chip.
[esp32_s3_dual_cdc_full]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
; Pin the matching Arduino core as an explicit project dependency. Standard
; ESP32 profiles use Arduino 2.x under the same PlatformIO package name, so an
; alternating build otherwise can leave this Arduino 3.x profile resolving the
; wrong global framework directory (or no compatible directory at all).
platform_packages =
framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11.tar.xz
framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11-libs.tar.xz
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags =
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=0
build_flags =
-D ARDUINO_USB_MODE=0
-D ARDUINO_USB_CDC_ON_BOOT=1
; ----------------- NRF52 ---------------------
[nrf52_base]
+37
View File
@@ -3,6 +3,9 @@
#include <helpers/RemoteCliRequest.h>
#include <helpers/RemoteCliTimeout.h>
#include <Utils.h>
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
#include <esp_heap_caps.h>
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
@@ -19,6 +22,36 @@ bool BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Pack
return sendFlood(pkt, delay_millis);
}
bool BaseChatMesh::initializeContactStorage() {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
const int requested_capacity = MAX_CONTACTS + MAX_ANON_CONTACTS;
if (contact_capacity >= requested_capacity) return true;
ContactInfo* expanded_contacts = static_cast<ContactInfo*>(
heap_caps_calloc(requested_capacity, sizeof(ContactInfo),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
int* expanded_sort_array = static_cast<int*>(
heap_caps_calloc(requested_capacity, sizeof(int),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (expanded_contacts == NULL || expanded_sort_array == NULL) {
if (expanded_contacts != NULL) heap_caps_free(expanded_contacts);
if (expanded_sort_array != NULL) heap_caps_free(expanded_sort_array);
MESH_DEBUG_PRINTLN(
"BaseChatMesh: PSRAM contact allocation failed; using %d slots",
contact_capacity - MAX_ANON_CONTACTS);
return false;
}
memcpy(expanded_contacts, contacts, sizeof(ContactInfo) * num_contacts);
contacts = expanded_contacts;
sort_array = expanded_sort_array;
contact_capacity = requested_capacity;
MESH_DEBUG_PRINTLN("BaseChatMesh: allocated %d contact slots in PSRAM",
MAX_CONTACTS);
#endif
return true;
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@@ -86,7 +119,11 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
return &contacts[oldest_idx];
}
} else {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
if (num_contacts < contact_capacity) {
#else
if (num_contacts < MAX_ANON_CONTACTS+MAX_CONTACTS) {
#endif
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
+21 -1
View File
@@ -61,9 +61,23 @@ class BaseChatMesh : public mesh::Mesh {
friend class ContactsIterator;
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
enum {
CONTACT_PSRAM_FALLBACK_REGULAR_SLOTS =
MAX_CONTACTS < 16 ? MAX_CONTACTS : 16,
CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS =
MAX_ANON_CONTACTS + CONTACT_PSRAM_FALLBACK_REGULAR_SLOTS
};
ContactInfo* contacts;
ContactInfo contacts_fallback[CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS];
int* sort_array;
int sort_array_fallback[CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS];
int contact_capacity;
#else
ContactInfo contacts[MAX_CONTACTS+MAX_ANON_CONTACTS];
int num_contacts;
int sort_array[MAX_CONTACTS+MAX_ANON_CONTACTS];
#endif
int num_contacts;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
#ifdef MAX_GROUP_CHANNELS
@@ -81,6 +95,11 @@ protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
contacts = contacts_fallback;
sort_array = sort_array_fallback;
contact_capacity = CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS;
#endif
resetContacts();
#ifdef MAX_GROUP_CHANNELS
@@ -93,6 +112,7 @@ protected:
}
void bootstrapRTCfromContacts();
bool initializeContactStorage();
void resetContacts() {
memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0)
+2
View File
@@ -53,8 +53,10 @@ public:
#ifdef PIN_VBAT_READ
// battery read support
pinMode(PIN_VBAT_READ, INPUT);
#if ESP_ARDUINO_VERSION_MAJOR < 3
adcAttachPin(PIN_VBAT_READ);
#endif
#endif
#ifdef P_LORA_TX_LED
pinMode(P_LORA_TX_LED, OUTPUT);
+156 -8
View File
@@ -3,24 +3,116 @@
#if defined(ARDUINO)
#include <Arduino.h>
#include <atomic>
#include <new>
#if defined(MESH_DUAL_CDC_LOGGING)
#if !defined(NRF52_PLATFORM) || !defined(COMPANION_RADIO_FULL)
#error "MESH_DUAL_CDC_LOGGING is only supported by nRF52 Full Companion"
#if !defined(COMPANION_RADIO_FULL)
#error "MESH_DUAL_CDC_LOGGING is only supported by Full Companion"
#endif
#if !defined(CFG_TUD_CDC) || CFG_TUD_CDC < 2
#error "MESH_DUAL_CDC_LOGGING requires CFG_TUD_CDC >= 2"
#if defined(NRF52_PLATFORM)
#define MESH_NRF52_DUAL_CDC_LOGGING 1
#include <Adafruit_TinyUSB.h>
#if !defined(CFG_TUD_CDC) || CFG_TUD_CDC < 2
#error "MESH_DUAL_CDC_LOGGING requires CFG_TUD_CDC >= 2"
#endif
#elif defined(ESP32_PLATFORM) && defined(SOC_USB_OTG_SUPPORTED) && \
SOC_USB_OTG_SUPPORTED && !ARDUINO_USB_MODE
#define MESH_ESP32_DUAL_CDC_LOGGING 1
#include <USB.h>
#include <USBCDC.h>
#include <nvs.h>
#include <nvs_flash.h>
#if !defined(CONFIG_TINYUSB_CDC_MAX_PORTS) || \
CONFIG_TINYUSB_CDC_MAX_PORTS < 2
#error "MESH_DUAL_CDC_LOGGING requires two ESP32 TinyUSB CDC ports"
#endif
#else
#error "MESH_DUAL_CDC_LOGGING requires nRF52 or ESP32 native TinyUSB"
#endif
#include <Adafruit_TinyUSB.h>
#endif
namespace mesh {
static std::atomic<bool> usb_logging_enabled{true};
static std::atomic<bool> usb_logging_preference_known{false};
class NullUsbLoggingStream : public Stream {
public:
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t write(uint8_t) override { return 1; }
size_t write(const uint8_t*, size_t size) override { return size; }
};
static NullUsbLoggingStream null_usb_logging_stream;
#if defined(MESH_DUAL_CDC_LOGGING)
static Adafruit_USBD_CDC dedicated_usb_logging_port;
static bool dedicated_usb_logging_port_configured = false;
static bool dedicated_usb_logging_port_started = false;
#if defined(MESH_NRF52_DUAL_CDC_LOGGING)
static Adafruit_USBD_CDC dedicated_usb_logging_port;
#elif defined(MESH_ESP32_DUAL_CDC_LOGGING)
static constexpr char USB_LOGGING_NVS_NAMESPACE[] = "mesh_usb";
static constexpr char USB_LOGGING_NVS_KEY[] = "log_port";
alignas(USBCDC) static uint8_t dedicated_usb_logging_port_storage[
sizeof(USBCDC)];
static USBCDC* dedicated_usb_logging_port = nullptr;
static bool readEsp32UsbLoggingBootPreference() {
// Missing or unreadable state must stay protocol-safe: Full Companion then
// exposes only its primary framed CDC interface. The normal preference load
// below mirrors an explicitly enabled setting for the following reboot.
if (nvs_flash_init() != ESP_OK) return false;
nvs_handle_t handle;
if (nvs_open(USB_LOGGING_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) {
return false;
}
uint8_t value = 0;
const esp_err_t result = nvs_get_u8(handle, USB_LOGGING_NVS_KEY, &value);
nvs_close(handle);
return result == ESP_OK && value != 0;
}
static bool writeEsp32UsbLoggingBootPreference(bool enabled) {
if (nvs_flash_init() != ESP_OK) return false;
nvs_handle_t handle;
if (nvs_open(USB_LOGGING_NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
return false;
}
uint8_t current = 0;
esp_err_t result = nvs_get_u8(handle, USB_LOGGING_NVS_KEY, &current);
if (result == ESP_OK && current == (enabled ? 1 : 0)) {
nvs_close(handle);
return true;
}
result = nvs_set_u8(handle, USB_LOGGING_NVS_KEY, enabled ? 1 : 0);
if (result == ESP_OK) result = nvs_commit(handle);
nvs_close(handle);
return result == ESP_OK;
}
class Esp32UsbLoggingBootstrap {
public:
Esp32UsbLoggingBootstrap() {
const bool enabled = readEsp32UsbLoggingBootPreference();
usb_logging_enabled.store(enabled, std::memory_order_relaxed);
usb_logging_preference_known.store(true, std::memory_order_relaxed);
if (!enabled) return;
dedicated_usb_logging_port = new (dedicated_usb_logging_port_storage)
USBCDC(1);
dedicated_usb_logging_port_configured = true;
}
};
static Esp32UsbLoggingBootstrap esp32_usb_logging_bootstrap;
#endif
#endif
bool isUsbLoggingEnabled() {
@@ -29,15 +121,43 @@ bool isUsbLoggingEnabled() {
void setUsbLoggingEnabled(bool enabled) {
usb_logging_enabled.store(enabled, std::memory_order_relaxed);
usb_logging_preference_known.store(true, std::memory_order_relaxed);
}
bool saveUsbLoggingBootPreference(bool enabled) {
#if defined(MESH_ESP32_DUAL_CDC_LOGGING)
return writeEsp32UsbLoggingBootPreference(enabled);
#else
(void)enabled;
return true;
#endif
}
void beginUsbLoggingPort() {
#if defined(MESH_DUAL_CDC_LOGGING)
if (dedicated_usb_logging_port_started) return;
if (dedicated_usb_logging_port_started
|| !usb_logging_preference_known.load(std::memory_order_relaxed)
|| !isUsbLoggingEnabled()) {
return;
}
#if defined(MESH_ESP32_DUAL_CDC_LOGGING)
// ESP32 registers its descriptor before app_main starts native USB. If the
// saved boot mirror omitted CDC 1, enabling logging takes effect after the
// next reboot instead of trying to mutate a live USB descriptor.
if (dedicated_usb_logging_port == nullptr) return;
// Only the Companion interface may use the CDC control-line reboot gesture.
// 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->begin(115200);
#elif defined(MESH_NRF52_DUAL_CDC_LOGGING)
dedicated_usb_logging_port.begin(115200);
dedicated_usb_logging_port_configured = true;
#endif
dedicated_usb_logging_port_started = true;
#if defined(MESH_NRF52_DUAL_CDC_LOGGING)
// The nRF52 core starts TinyUSB before setup(). If the host completed
// enumeration in that small window, reconnect once so it reads the expanded
// two-CDC descriptor. Usually enumeration has not completed and no reconnect
@@ -48,12 +168,24 @@ void beginUsbLoggingPort() {
TinyUSBDevice.attach();
}
#endif
#endif
}
Stream& usbLoggingPort() {
#if defined(MESH_DUAL_CDC_LOGGING)
return dedicated_usb_logging_port;
#if defined(MESH_NRF52_DUAL_CDC_LOGGING)
if (isUsbLoggingEnabled() && dedicated_usb_logging_port_started) {
return dedicated_usb_logging_port;
}
#elif defined(MESH_ESP32_DUAL_CDC_LOGGING)
if (isUsbLoggingEnabled() && dedicated_usb_logging_port_started
&& dedicated_usb_logging_port != nullptr) {
return *dedicated_usb_logging_port;
}
#endif
return null_usb_logging_stream;
#else
if (!isUsbLoggingEnabled()) return null_usb_logging_stream;
return Serial;
#endif
}
@@ -66,5 +198,21 @@ bool hasDedicatedUsbLoggingPort() {
#endif
}
bool isDedicatedUsbLoggingPortConfigured() {
#if defined(MESH_DUAL_CDC_LOGGING)
return dedicated_usb_logging_port_configured;
#else
return false;
#endif
}
bool usbLoggingInterfaceRestartRequired() {
#if defined(MESH_DUAL_CDC_LOGGING)
return dedicated_usb_logging_port_configured != isUsbLoggingEnabled();
#else
return false;
#endif
}
} // namespace mesh
#endif
+11 -4
View File
@@ -16,17 +16,24 @@ class Stream;
namespace mesh {
// Logging starts enabled on first boot. Roles with saved preferences restore
// the persisted setting after their preferences are loaded.
// Ordinary logging images start enabled. Dual-CDC Full Companion starts with
// logging disabled and restores its saved choice after preferences load.
bool isUsbLoggingEnabled();
void setUsbLoggingEnabled(bool enabled);
// ESP32 native USB starts before setup(), so its next-boot interface count is
// mirrored outside the normal role preferences. Other platforms need no
// 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. nRF52 Full Companion builds expose a second CDC ACM interface
// so plaintext diagnostics never share the framed Companion stream.
// using Serial. Supported Full Companion builds expose a second CDC ACM
// interface so plaintext diagnostics never share the framed Companion stream.
void beginUsbLoggingPort();
Stream& usbLoggingPort();
bool hasDedicatedUsbLoggingPort();
bool isDedicatedUsbLoggingPortConfigured();
bool usbLoggingInterfaceRestartRequired();
} // namespace mesh
#endif
+6 -1
View File
@@ -16,7 +16,12 @@
namespace UserGpioPinPolicy {
inline bool isFirmwareReserved(uint8_t pin) {
static const int32_t reserved[] = {
// RadioLib 7 represents its disconnected-pin sentinel as unsigned
// 0xFFFFFFFF. Use a signed type wide enough for that value as well as the
// board definitions that use -1, then reject both forms with the 0..63
// range check below. A 32-bit signed initializer is a narrowing error under
// the GCC 14 toolchain used by Arduino-ESP32 3.x.
static const int64_t reserved[] = {
-1,
// LoRa radio, RF switches, FEMs, and activity indicators.
+6
View File
@@ -23,7 +23,13 @@ void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int len) {
}
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
void ESPNowBridge::send_cb(const esp_now_send_info_t *info,
esp_now_send_status_t status) {
const uint8_t *mac = info != nullptr ? info->des_addr : nullptr;
#else
void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
#endif
if (_instance) {
_instance->onDataSent(mac, status);
}
+5
View File
@@ -48,7 +48,12 @@ private:
#else
static void recv_cb(const uint8_t *mac, const uint8_t *data, int len);
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
static void send_cb(const esp_now_send_info_t *info,
esp_now_send_status_t status);
#else
static void send_cb(const uint8_t *mac, esp_now_send_status_t status);
#endif
/**
* ESP-NOW Protocol Structure:
+18
View File
@@ -1,7 +1,11 @@
#include "ESPNOWRadio.h"
#include <esp_now.h>
#include <esp_idf_version.h>
#include <WiFi.h>
#include <esp_wifi.h>
#if ESP_ARDUINO_VERSION_MAJOR >= 3
#include <esp_mac.h>
#endif
static uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static esp_now_peer_info_t peerInfo;
@@ -11,12 +15,26 @@ static uint8_t rx_buf[256];
static uint8_t last_rx_len = 0;
// callback when data is sent
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
static void OnDataSent(const esp_now_send_info_t *info,
esp_now_send_status_t status) {
(void)info;
#else
static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
(void)mac_addr;
#endif
is_send_complete = true;
ESPNOW_DEBUG_PRINTLN("Send Status: %d", (int)status);
}
#if ESP_ARDUINO_VERSION_MAJOR >= 3
static void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data,
int len) {
(void)info;
#else
static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
(void)mac;
#endif
ESPNOW_DEBUG_PRINTLN("Recv: len = %d", len);
memcpy(rx_buf, data, len);
last_rx_len = len;
+156 -5
View File
@@ -1,7 +1,21 @@
#include "SerialBLEInterface.h"
#include "../CompanionFrameQueue.h"
#include "esp_mac.h"
#if defined(CONFIG_BLUEDROID_ENABLED)
#include "esp_gap_ble_api.h"
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
#include <host/ble_store.h>
#endif
// Arduino 3.x relies on a library constructor to mark BLE as used before
// initArduino(), and that constructor ordering is not reliable with all link
// layouts/LTO combinations. A strong override keeps the controller memory
// available until SerialBLEInterface::begin(); Full Companion then reserves
// BLE controller/host memory before starting WiFi.
extern "C" bool bleInUse(void) {
return true;
}
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
@@ -12,7 +26,7 @@
#define ADVERT_RESTART_DELAY 1000 // millis
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
bool SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
_pin_code = pin_code;
if (strcmp(name, "@@MAC") == 0) {
@@ -26,29 +40,73 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
sprintf(dev_name, "%s%s", prefix, name);
// Create the BLE Device
#if defined(CONFIG_NIMBLE_ENABLED)
if (!BLEDevice::init(dev_name)) {
BLE_DEBUG_PRINTLN("BLEDevice::init failed");
return false;
}
#else
// Arduino-ESP32 2.x Bluedroid exposes a void init(). Its first observable
// failure is a null server below, which is handled without dereferencing it.
BLEDevice::init(dev_name);
#endif
BLEDevice::setSecurityCallbacks(this);
// ATT notifications consume three bytes of the negotiated MTU. Reserve
// that overhead so a MAX_FRAME_SIZE protocol frame fits without truncation.
BLEDevice::setMTU(MAX_FRAME_SIZE + 3);
BLESecurity sec;
#if defined(CONFIG_NIMBLE_ENABLED)
sec.setPassKey(true, pin_code);
sec.setAuthenticationMode(true, true, true);
#else
sec.setStaticPIN(pin_code);
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
#endif
//BLEDevice::setPower(ESP_PWR_LVL_N8);
// Create the BLE Server
pServer = BLEDevice::createServer();
if (pServer == NULL) {
BLE_DEBUG_PRINTLN("BLEDevice::createServer failed");
BLEDevice::deinit(false);
return false;
}
pServer->setCallbacks(this);
// Create the BLE Service
pService = pServer->createService(SERVICE_UUID);
if (pService == NULL) {
BLE_DEBUG_PRINTLN("BLEServer::createService failed");
BLEDevice::deinit(false);
pServer = NULL;
return false;
}
// Create a BLE Characteristic
pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
uint32_t tx_properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY;
uint32_t rx_properties = BLECharacteristic::PROPERTY_WRITE;
#if defined(CONFIG_NIMBLE_ENABLED)
// NimBLE ignores setAccessPermissions(). Authentication requirements must
// be part of the characteristic properties instead.
tx_properties |= BLECharacteristic::PROPERTY_READ_AUTHEN;
rx_properties |= BLECharacteristic::PROPERTY_WRITE_AUTHEN;
#endif
pTxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX, tx_properties);
if (pTxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(TX) failed");
BLEDevice::deinit(false);
pServer = NULL;
pService = NULL;
return false;
}
pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM);
pTxCharacteristic->setCallbacks(this);
#if !defined(CONFIG_NIMBLE_ENABLED)
// NimBLE creates and owns the 0x2902 descriptor automatically.
pTxDescriptor = new BLE2902();
// Make notification setup start/finish pairing before the client begins its
// short device-info request timeout. The RX and TX characteristics already
@@ -57,12 +115,24 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
pTxDescriptor->setAccessPermissions(
(esp_gatt_perm_t)(ESP_GATT_PERM_READ_ENC_MITM | ESP_GATT_PERM_WRITE_ENC_MITM));
pTxCharacteristic->addDescriptor(pTxDescriptor);
#endif
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE);
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX, rx_properties);
if (pRxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(RX) failed");
BLEDevice::deinit(false);
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
pTxDescriptor = NULL;
return false;
}
pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM);
pRxCharacteristic->setCallbacks(this);
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
return true;
}
// -------- BLESecurityCallbacks methods
@@ -89,6 +159,34 @@ bool SerialBLEInterface::onSecurityRequest() {
return true; // allow
}
#if defined(CONFIG_NIMBLE_ENABLED)
void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
const bool success = desc != NULL && desc->sec_state.encrypted &&
desc->sec_state.authenticated;
if (success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure");
if (desc != NULL) {
int remove_result = ble_store_util_delete_peer(&desc->peer_id_addr);
if (remove_result == 0) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Cleared failed peer bond");
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - No peer bond cleared, err=%d",
remove_result);
}
}
deviceConnected = false;
if (desc != NULL) {
pServer->disconnect(desc->conn_handle);
}
scheduleAdvertisingRestart((uint32_t)millis());
}
}
#else
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
if (cmpl.success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
@@ -112,12 +210,35 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
scheduleAdvertisingRestart((uint32_t)millis());
}
}
#endif
// -------- BLEServerCallbacks methods
void SerialBLEInterface::onConnect(BLEServer* pServer) {
}
#if defined(CONFIG_NIMBLE_ENABLED)
void SerialBLEInterface::onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", desc->conn_handle,
pServer->getPeerMTU(desc->conn_handle));
last_conn_id = desc->conn_handle;
deviceConnected = false; // becomes usable only after authentication completes
oldDeviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer,
ble_gap_conn_desc* desc,
uint16_t mtu) {
(void)pServer;
(void)desc;
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", mtu);
}
#else
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
@@ -136,11 +257,13 @@ void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
}
#endif
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
BLE_DEBUG_PRINTLN("onDisconnect()");
deviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
@@ -151,7 +274,15 @@ void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
// -------- BLECharacteristicCallbacks methods
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) {
#if defined(CONFIG_NIMBLE_ENABLED)
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc) {
(void)desc;
#else
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
esp_ble_gatts_cb_param_t* param) {
(void)param;
#endif
if (_tx_disconnect_recovery.pending()) {
BLE_DEBUG_PRINTLN("onWrite(): dropping frame while BLE reconnect is pending");
return;
@@ -173,6 +304,19 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat
}
}
#if defined(CONFIG_NIMBLE_ENABLED)
void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc,
uint16_t subValue) {
(void)desc;
if (pCharacteristic == pTxCharacteristic) {
// NimBLE uses bit 0 for notification subscriptions.
notificationsEnabled.store((subValue & 0x0001u) != 0,
std::memory_order_release);
}
}
#endif
void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) {
(void)pCharacteristic;
notifySucceeded = status == SUCCESS_NOTIFY;
@@ -188,6 +332,7 @@ void SerialBLEInterface::clearBuffers() {
xQueueReset(recv_queue);
send_queue_len = 0;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.complete();
_tx_reset_pending.store(false, std::memory_order_release);
@@ -314,7 +459,13 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
BLE_WRITE_MIN_INTERVAL) // space the writes apart
) {
const uint16_t peer_mtu = pServer->getPeerMTU(last_conn_id);
const bool notifications_ready = pTxDescriptor != NULL && pTxDescriptor->getNotifications();
#if defined(CONFIG_NIMBLE_ENABLED)
const bool notifications_ready =
notificationsEnabled.load(std::memory_order_acquire);
#else
const bool notifications_ready =
pTxDescriptor != NULL && pTxDescriptor->getNotifications();
#endif
const bool frame_fits = peer_mtu > 3 && send_queue[0].len <= peer_mtu - 3;
const bool delivery_required = mesh::companionFrameRequiresDelivery(
send_queue[0].buf, send_queue[0].len);
+18 -1
View File
@@ -19,6 +19,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
bool deviceConnected;
bool oldDeviceConnected;
bool notifySucceeded;
std::atomic<bool> notificationsEnabled{false};
bool _isEnabled;
uint16_t last_conn_id;
uint32_t _pin_code;
@@ -54,16 +55,31 @@ protected:
void onPassKeyNotify(uint32_t pass_key) override;
bool onConfirmPIN(uint32_t pass_key) override;
bool onSecurityRequest() override;
#if defined(CONFIG_NIMBLE_ENABLED)
void onAuthenticationComplete(ble_gap_conn_desc* desc) override;
#else
void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) override;
#endif
// BLEServerCallbacks methods
void onConnect(BLEServer* pServer) override;
#if defined(CONFIG_NIMBLE_ENABLED)
void onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) override;
void onMtuChanged(BLEServer* pServer, ble_gap_conn_desc* desc, uint16_t mtu) override;
#else
void onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) override;
void onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) override;
#endif
void onDisconnect(BLEServer* pServer) override;
// BLECharacteristicCallbacks methods
#if defined(CONFIG_NIMBLE_ENABLED)
void onWrite(BLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override;
void onSubscribe(BLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc,
uint16_t subValue) override;
#else
void onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) override;
#endif
void onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) override;
public:
@@ -75,6 +91,7 @@ public:
deviceConnected = false;
oldDeviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_relaxed);
_adv_restart_started = 0;
_adv_restart_pending = false;
_isEnabled = false;
@@ -92,7 +109,7 @@ public:
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
bool begin(const char* prefix, char* name, uint32_t pin_code);
// BaseSerialInterface methods
void enable() override;
+7 -2
View File
@@ -199,7 +199,7 @@ bool SerialBLEInterface::removeStoredBondForPeer(const char* cause) {
return true;
}
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
bool SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
@@ -208,7 +208,11 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
if (!Bluefruit.begin()) {
instance = nullptr;
BLE_DEBUG_PRINTLN("Bluefruit.begin failed");
return false;
}
char dev_name[32+16];
if (strcmp(name, "@@MAC") == 0) {
@@ -275,6 +279,7 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
Bluefruit.Advertising.restartOnDisconnect(true);
return true;
}
void SerialBLEInterface::clearBuffers() {
+1 -1
View File
@@ -76,7 +76,7 @@ public:
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
bool begin(const char* prefix, char* name, uint32_t pin_code);
void disconnect();
void enable() override;
+1
View File
@@ -1,4 +1,5 @@
#include "ST7735Display.h"
#include "../UsbLogging.h"
#include <TFT_eSPI.h>
//#include <Fonts/GFXFF/FreeSans9pt7b.h>
+52 -3
View File
@@ -42,6 +42,21 @@ const releases = [
asset("RAK_4631_companion_radio_full-" + family + ".zip"),
asset("RAK_4631_companion_radio_usb-logging-" + family + ".uf2"),
asset("RAK_4631_companion_radio_usb-logging-" + family + ".zip"),
asset(
"heltec_v4_2_v4_3_companion_radio_full_femon-" + family +
"-merged.bin"
),
asset(
"heltec_v4_2_v4_3_companion_radio_full_femon-" + family + ".bin"
),
asset("heltec_v4_companion_radio_usb-" + family + "-merged.bin"),
asset("heltec_v4_companion_radio_usb-" + family + ".bin"),
asset("heltec_v4_companion_radio_ble-" + family + "-merged.bin"),
asset("heltec_v4_companion_radio_ble-" + family + ".bin"),
asset(
"heltec_v4_companion_radio_wifi_femon-" + family + "-merged.bin"
),
asset("heltec_v4_companion_radio_wifi_femon-" + family + ".bin"),
]),
release("repeater-room-" + family, "2026-08-23T12:00:05Z", [
asset("Station_G2_repeater-" + family + "-deadbee-merged.bin"),
@@ -110,8 +125,8 @@ assert(!releaseSet.releases.some(function (item) {
const catalog = picker.buildCatalog(releases);
assert.strictEqual(catalog.releaseSet.familyTag, family);
assert.strictEqual(catalog.profiles.length, 11);
assert.strictEqual(catalog.rows.length, 33);
assert.strictEqual(catalog.profiles.length, 12);
assert.strictEqual(catalog.rows.length, 41);
function profile(target) {
const found = catalog.profiles.find(function (item) {
@@ -125,7 +140,9 @@ const companionFull = profile("Station_G2_companion_radio_full");
assert.strictEqual(companionFull.hardware, "Station_G2");
assert.strictEqual(companionFull.role, "companion");
assert.strictEqual(companionFull.mode, "full");
assert.strictEqual(companionFull.logging, "none");
assert.strictEqual(companionFull.logging, "usb-runtime");
assert.deepStrictEqual(companionFull.loggingModes, ["none", "usb"]);
assert.strictEqual(companionFull.dedicatedUsbLogging, true);
assert.strictEqual(companionFull.ota, "lora-source");
assert.strictEqual(companionFull.feature, "full");
assert.strictEqual(companionFull.variant, "default");
@@ -141,6 +158,37 @@ assert(catalog.rows.some(function (item) {
return item.target === "RAK_4631_companion_radio_usb-logging";
}));
const v4Full = profile("heltec_v4_2_v4_3_companion_radio_full_femon");
assert.strictEqual(v4Full.logging, "usb-runtime");
assert.deepStrictEqual(v4Full.loggingModes, ["none", "usb"]);
assert.strictEqual(v4Full.dedicatedUsbLogging, true);
["usb", "ble", "wifi"].forEach(function (mode) {
assert(!catalog.profiles.some(function (item) {
return item.hardware === "heltec_v4" && item.role === "companion" &&
item.mode === mode;
}));
});
["RAK_3112", "heltec_rc32", "heltec_rc32_without_display"].forEach(
function (hardware) {
const candidates = ["full", "usb", "ble", "wifi"].map(function (mode) {
return Object.assign(
picker.parseTargetProfile(
hardware + "_companion_radio_" + mode
),
{ installKinds: ["bin"] }
);
});
const classified = picker.applyDualCdcFullCompanionCapabilities(candidates);
assert.strictEqual(classified[0].logging, "none");
assert.strictEqual(classified[0].dedicatedUsbLogging, undefined);
assert.strictEqual(
picker.omitTransportsReplacedByDualCdcFull(classified).length,
candidates.length
);
}
);
const companionBle = picker.parseTargetProfile(
"Heltec_t096_companion_radio_ble_ps_femon"
);
@@ -472,6 +520,7 @@ assert.deepStrictEqual(
"RAK_4631",
"Station_G2",
"Station_G3_ESP32",
"heltec_v4",
"solarxiao_33S",
"wio-e5",
].sort()
+10 -7
View File
@@ -11,11 +11,14 @@ no longer a user-facing CLI.
## Setup
Uses the repo's Python venv (`meshcore/`). Dependencies: `detools` (delta), `cryptography` (Ed25519),
`intelhex` (nRF52 `.hex` handling).
Use one pipx-managed `detools` environment. `cryptography` (Ed25519) and
`intelhex` (nRF52 `.hex` handling) are injected into that same environment;
they are not separate pipx applications.
```bash
./meshcore/bin/pip install detools cryptography intelhex
pipx install detools
pipx inject detools cryptography intelhex
DETOOLS_PYTHON="$(pipx environment --value PIPX_LOCAL_VENVS)/detools/bin/python"
```
## Files
@@ -31,10 +34,10 @@ Uses the repo's Python venv (`meshcore/`). Dependencies: `detools` (delta), `cry
## Tests
```bash
./meshcore/bin/python tools/mota/test_mota.py # EndF, merkle+proofs, v2 apps/v3 nRF52 bootloader,
# signing, tamper detection, approval enforcement
./meshcore/bin/python tools/mota/gen_vectors.py # regenerate the native-test cross-check vectors
./meshcore/bin/python tools/mota/gen_targets.py # regenerate src/helpers/ota/OtaTargets.h (needs `pio`)
"$DETOOLS_PYTHON" tools/mota/test_mota.py # EndF, merkle+proofs, v2 apps/v3 nRF52 bootloader,
# signing, tamper detection, approval enforcement
"$DETOOLS_PYTHON" tools/mota/gen_vectors.py # regenerate the native-test cross-check vectors
"$DETOOLS_PYTHON" tools/mota/gen_targets.py # regenerate src/helpers/ota/OtaTargets.h (needs `pio`)
```
## `EndF` build integration
+217 -6
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
Tests for motalib - run with the meshcore venv:
Tests for motalib - run with the pipx-managed detools environment:
./meshcore/bin/python tools/mota/test_mota.py
"$(pipx environment --value PIPX_LOCAL_VENVS)/detools/bin/python" tools/mota/test_mota.py
(Also pytest-compatible: functions are named test_*.)
"""
@@ -89,7 +89,7 @@ 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 "nRF52 Full already provides logging on its second USB port" in matrix
assert "can add a dedicated logging USB port" in matrix
def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
@@ -110,7 +110,7 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
redundant = build.split("is_redundant_bulk_build_target()", 1)[1]
redundant = redundant.split("resolve_logging_matrix_firmwares()", 1)[0]
assert "is_runtime_setting_alias_target" in redundant
assert "is_nrf52_companion_transport_replaced_by_full" in redundant
assert "is_companion_transport_replaced_by_full" in redundant
logging_matrix = build.split("resolve_logging_matrix_firmwares()", 1)[1]
logging_matrix = logging_matrix.split("resolve_companion_firmwares()", 1)[0]
@@ -123,17 +123,175 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases():
assert "-DMOTA_TARGET_ID=0" in full
assert "-UOTA_FLASH_STORE" in full
assert "-UOTA_SD_STORE" in full
assert "-UWEBCONFIG_DISABLED" in full
assert "-DCFG_TUD_CDC=2" in full
assert "-DMESH_DUAL_CDC_LOGGING=1" in full
assert "-DMESH_DEBUG=1" in full
assert "-DMESH_PACKET_LOGGING=1" in full
esp32_dual = build.split(
"is_esp32_dual_cdc_companion_radio_full_target()", 1
)[1].split("requires_esp32_companion_full_ota_fallback()", 1)[0]
assert "heltec_v4_2_v4_3_companion_radio_full_femon" in esp32_dual
assert "rak_3112_companion_radio_full" not in esp32_dual
assert "heltec_rc32_companion_radio_full" not in esp32_dual
assert "heltec_rc32_without_display_companion_radio_full" not in esp32_dual
for relative, section, next_section in (
(
"variants/rak3112/platformio.ini",
"[env:RAK_3112_companion_radio_full]",
"[env:RAK_3112_sensor]",
),
(
"variants/heltec_rc32/platformio.ini",
"[env:heltec_rc32_without_display_companion_radio_full]",
"[env:heltec_rc32_without_display_sensor]",
),
(
"variants/heltec_rc32/platformio.ini",
"[env:heltec_rc32_companion_radio_full]",
"[env:heltec_rc32_sensor]",
),
):
variant = (root / relative).read_text(encoding="utf-8")
profile = variant.split(section, 1)[1].split(next_section, 1)[0]
assert "esp32_s3_dual_cdc_full" not in profile
usb_logging = (root / "src/helpers/UsbLogging.cpp").read_text(
encoding="utf-8"
)
assert "Adafruit_USBD_CDC dedicated_usb_logging_port" in usb_logging
assert "new (dedicated_usb_logging_port_storage)" in usb_logging
assert "USBCDC(1)" in usb_logging
assert "dedicated_usb_logging_port->enableReboot(false)" in usb_logging
assert "return result == ESP_OK && value != 0" in usb_logging
assert "TinyUSBDevice.detach()" in usb_logging
assert "return dedicated_usb_logging_port" in usb_logging
assert "return *dedicated_usb_logging_port" in usb_logging
assert "return null_usb_logging_stream" in usb_logging
companion = (root / "examples/companion_radio/MyMesh.cpp").read_text(
encoding="utf-8"
)
assert "defined(COMPANION_RADIO_FULL) && defined(MESH_DUAL_CDC_LOGGING)" 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
assert "reboot required to change USB interfaces" in companion
assert "rebooting to change USB interfaces" in companion
assert "mesh::saveUsbLoggingBootPreference(enabled)" in companion
def test_full_companion_wireless_startup_and_psram_contacts_are_resilient():
root = Path(__file__).resolve().parents[2]
main = (root / "examples/companion_radio/main.cpp").read_text(
encoding="utf-8"
)
setup = main.split("void setup()", 1)[1]
assert setup.index("startCompanionBluetooth();") < setup.index("WiFi.onEvent")
assert "if (!bluetooth_interface.begin(" in main
assert "interface_manager.removeInterface(&bluetooth_interface)" in main
assert "Bluetooth initialization failed; retrying in 5 seconds" in main
esp_ble = (root / "src/helpers/esp32/SerialBLEInterface.cpp").read_text(
encoding="utf-8"
)
nrf_ble = (root / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text(
encoding="utf-8"
)
assert 'extern "C" bool bleInUse(void)' in esp_ble
assert "bool SerialBLEInterface::begin(" in esp_ble
assert "Arduino-ESP32 2.x Bluedroid exposes a void init()" in esp_ble
assert "if (pServer == NULL)" in esp_ble
assert "if (pService == NULL)" in esp_ble
assert "if (pTxCharacteristic == NULL)" in esp_ble
assert "if (pRxCharacteristic == NULL)" in esp_ble
assert "bool SerialBLEInterface::begin(" in nrf_ble
assert "if (!Bluefruit.begin())" in nrf_ble
contacts_header = (root / "src/helpers/BaseChatMesh.h").read_text(
encoding="utf-8"
)
contacts_source = (root / "src/helpers/BaseChatMesh.cpp").read_text(
encoding="utf-8"
)
assert "CONTACT_PSRAM_FALLBACK_REGULAR_SLOTS" in contacts_header
assert "ContactInfo* contacts" in contacts_header
assert "int* sort_array" in contacts_header
assert "heap_caps_calloc" in contacts_source
assert "MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT" in contacts_source
assert "contact_capacity = requested_capacity" in contacts_source
assert setup.index("the_mesh.begin(") < setup.index("startCompanionBluetooth();")
companion = (root / "examples/companion_radio/MyMesh.cpp").read_text(
encoding="utf-8"
)
assert companion.index("initializeContactStorage();") < companion.index(
"initializeOfflineQueue();"
)
def test_esp32_s3_full_profiles_inherit_dio_boot_mode():
root = Path(__file__).resolve().parents[2]
project = (root / "platformio.ini").read_text(encoding="utf-8")
shared = project.split("[esp32_s3_full]", 1)[1].split(
"\n[", 1
)[0]
assert "board_build.flash_mode = dio" in shared
dual_cdc = project.split("[esp32_s3_dual_cdc_full]", 1)[1].split(
"\n[", 1
)[0]
assert (
"board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}"
in dual_cdc
)
assert "esp32-core-3.3.11.tar.xz" in dual_cdc
assert "esp32-core-3.3.11-libs.tar.xz" in dual_cdc
build = (root / "build.sh").read_text(encoding="utf-8")
framework_preflight = build.split(
"prepare_esp32_dual_cdc_framework()", 1
)[1].split("requires_esp32_companion_full_ota_fallback()", 1)[0]
assert "is_esp32_dual_cdc_companion_radio_full_target" in framework_preflight
assert 'pio pkg install -e "$pio_env_name"' in framework_preflight
build_call = build.split('print_build_flags "$env_name"', 1)[1].split(
"restore_platformio_build_flags", 1
)[0]
assert 'prepare_esp32_dual_cdc_framework "$env_name" "$pio_env_name"' in build_call
assert 'if [ "$build_status" -eq 0 ]; then' in build_call
assert 'flock "$platformio_package_lock_fd"' in build_call
assert 'flock -u "$platformio_package_lock_fd"' in build_call
s3_full_variant_dirs = {
"heltec_rc32",
"heltec_tracker_v2",
"heltec_v4",
"heltec_v4_r8",
"lilygo_tbeam_1w",
"meshnology_w12",
"nibble_screen_connect",
"nibble_zero_connect",
"rak3112",
"station_g2",
"station_g3_esp32",
"xiao_s3_wio",
}
profile_count = 0
for variant_dir in sorted(s3_full_variant_dirs):
path = root / "variants" / variant_dir / "platformio.ini"
content = path.read_text(encoding="utf-8")
for section in content.split("\n["):
header = section.split("]", 1)[0].lower()
if "companion_radio_full" not in header:
continue
profile_count += 1
assert (
"board_build.flash_mode = "
"${esp32_s3_full.board_build.flash_mode}"
) in section, f"ESP32-S3 Full profile lacks shared DIO mode: {path}"
assert profile_count == 17
def test_release_catalog_resolves_canonical_runtime_aliases():
@@ -146,10 +304,23 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
provider = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider)
assert provider.ESP32_DUAL_CDC_FULL_RE.match(
"heltec_v4_2_v4_3_companion_radio_full_femon"
)
assert not provider.ESP32_DUAL_CDC_FULL_RE.match(
"RAK_3112_companion_radio_full"
)
assert not provider.ESP32_DUAL_CDC_FULL_RE.match(
"heltec_rc32_companion_radio_full"
)
assert not provider.ESP32_DUAL_CDC_FULL_RE.match(
"heltec_rc32_without_display_companion_radio_full"
)
release_files = {
"RAK_4631_companion_radio_full": [Path("rak-full.zip")],
"Heltec_t096_companion_radio_ble_femon": [Path("t096.zip")],
"heltec_v4_companion_radio_ble": [Path("v4.bin")],
"heltec_v4_2_v4_3_companion_radio_full_femon": [Path("v4.bin")],
"Station_G2_repeater_observer_mqtt-full-usb-wifi-ota": [
Path("g2.bin")
],
@@ -165,7 +336,13 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
) == ("Heltec_t096_companion_radio_ble_femon", False)
assert provider.resolve_release_identity(
"heltec_v4_3_companion_radio_ble_femoff", release_files
) == ("heltec_v4_companion_radio_ble", False)
) == ("heltec_v4_2_v4_3_companion_radio_full_femon", False)
assert provider.resolve_release_identity(
"heltec_v4_companion_radio_usb-logging", release_files
) == ("heltec_v4_2_v4_3_companion_radio_full_femon", False)
assert provider.resolve_release_identity(
"heltec_v4_companion_radio_wifi_femon", release_files
) == ("heltec_v4_2_v4_3_companion_radio_full_femon", False)
assert provider.resolve_release_identity(
"Station_G2_logging_repeater-logging", release_files
) == ("Station_G2_repeater_observer_mqtt-full-usb-wifi-ota", True)
@@ -177,6 +354,31 @@ 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.
g2_full_only = {
"Station_G2_companion_radio_full": [Path("g2-full.bin")],
"ThinkNode_M2_companion_radio_full": [Path("m2-full.bin")],
}
for old_identity in (
"Station_G2_companion_radio_usb",
"Station_G2_companion_radio_ble",
"Station_G2_companion_radio_wifi",
"Station_G2_companion_radio_usb-logging",
):
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")
dual_notes = provider.normalize_nrf52_full_companion_metadata(
{"title": "Companion USB", "subTitle": "USB logging"},
"PROFILE - old profile\n\nLOGGING USE - old use\n\nSELECTION - USB.",
@@ -185,6 +387,15 @@ def test_release_catalog_resolves_canonical_runtime_aliases():
assert "interface 02" in dual_notes
assert "Input received on interface 02 is ignored" in dual_notes
v4_notes = provider.normalize_esp32_dual_cdc_full_companion_metadata(
{"title": "Companion USB", "subTitle": "USB logging"},
"PROFILE - old profile\n\nLOGGING USE - old use\n\nSELECTION - USB.",
)
assert "interface 00" in v4_notes
assert "interface 02" in v4_notes
assert "cannot reboot the board" in v4_notes
assert "ordinary Wi-Fi" in v4_notes
legacy = {
"role": "companionBle",
"title": "Companion BLE",
+8
View File
@@ -192,6 +192,10 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_without_display_companion_radio_full]
extends = env:heltec_rc32_without_display_companion_radio_wifi
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
[env:heltec_rc32_without_display_sensor]
extends = Heltec_RC32
build_flags =
@@ -331,6 +335,10 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_companion_radio_full]
extends = env:heltec_rc32_companion_radio_wifi
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
[env:heltec_rc32_sensor]
extends = Heltec_RC32_with_display
build_flags =
@@ -414,6 +414,15 @@ lib_deps =
${Heltec_tracker_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_tracker_v2_companion_radio_full_femon]
extends = env:heltec_tracker_v2_companion_radio_wifi_femon
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_tracker_v2_companion_radio_wifi_femon.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_tracker_v2_companion_radio_wifi_femoff]
extends = env:heltec_tracker_v2_companion_radio_wifi_femon
build_flags =
+41
View File
@@ -520,6 +520,29 @@ build_flags =
${env:heltec_v4_companion_radio_wifi_femon.build_flags}
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
; Full Companion needs two independent native USB CDC interfaces. The pinned
; Arduino-ESP32 2.0.x platform supports only one; the 3.x platform already used
; by this tree's C6 targets supplies CDC 0 + CDC 1 on ESP32-S3.
[env:heltec_v4_2_v4_3_companion_radio_full_femon]
extends = env:heltec_v4_companion_radio_wifi_femon
platform = ${esp32_s3_dual_cdc_full.platform}
; Marking the merged image QIO makes this V4 reset in the ROM before the
; software bootloader can start. Keep the Full image on the shared DIO policy.
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_companion_radio_wifi_femon.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_3_companion_radio_full_femoff]
extends = env:heltec_v4_3_companion_radio_wifi_femoff
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_3_companion_radio_wifi_femoff.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_companion_radio_wifi_mqtt_femon]
extends = env:heltec_v4_companion_radio_wifi_femon
extra_scripts =
@@ -752,6 +775,24 @@ build_flags =
${env:heltec_v4_tft_companion_radio_wifi_femon.build_flags}
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
[env:heltec_v4_tft_companion_radio_full_femon]
extends = env:heltec_v4_tft_companion_radio_wifi_femon
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_tft_companion_radio_wifi_femon.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_3_tft_companion_radio_full_femoff]
extends = env:heltec_v4_3_tft_companion_radio_wifi_femoff
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_3_tft_companion_radio_wifi_femoff.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_tft_sensor]
extends = heltec_v4_tft
build_flags =
+18
View File
@@ -195,6 +195,15 @@ lib_deps =
${heltec_v4_r8_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_companion_radio_full]
extends = env:heltec_v4_r8_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_r8_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_r8_sensor]
extends = heltec_v4_r8_oled
build_flags =
@@ -320,6 +329,15 @@ lib_deps =
${heltec_v4_r8_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_tft_companion_radio_full]
extends = env:heltec_v4_r8_tft_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:heltec_v4_r8_tft_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:heltec_v4_r8_tft_sensor]
extends = heltec_v4_r8_tft
build_flags =
+9
View File
@@ -176,6 +176,15 @@ lib_deps =
${LilyGo_TBeam_1W.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:LilyGo_TBeam_1W_companion_radio_full]
extends = env:LilyGo_TBeam_1W_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:LilyGo_TBeam_1W_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
; === LILYGO T-Beam 1W Repeater with ESPNow Bridge ===
[env:LilyGo_TBeam_1W_repeater_bridge_espnow]
extends = LilyGo_TBeam_1W
+9
View File
@@ -190,6 +190,15 @@ lib_deps =
${meshnology_w12.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:meshnology_w12_companion_radio_full]
extends = env:meshnology_w12_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:meshnology_w12_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:meshnology_w12_sensor]
extends = meshnology_w12
build_flags =
+17 -8
View File
@@ -139,8 +139,8 @@ lib_deps =
${nibble_screen_connect_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:nibble_screen_connect_companion_radio_wifi_]
extends = nibble_screen_connect_base
[env:nibble_screen_connect_companion_radio_wifi_]
extends = nibble_screen_connect_base
build_flags =
${nibble_screen_connect_base.build_flags}
-I examples/companion_radio/ui-new
@@ -157,12 +157,21 @@ build_src_filter = ${nibble_screen_connect_base.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${nibble_screen_connect_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:nibble_screen_connect_kiss_modem_]
lib_deps =
${nibble_screen_connect_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:nibble_screen_connect_companion_radio_full_]
extends = env:nibble_screen_connect_companion_radio_wifi_
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:nibble_screen_connect_companion_radio_wifi_.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:nibble_screen_connect_kiss_modem_]
extends = nibble_screen_connect_base
build_src_filter = ${nibble_screen_connect_base.build_src_filter}
+<../examples/kiss_modem/>
@@ -157,3 +157,11 @@ lib_deps =
${nibble_zero_connect_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:nibble_zero_connect_companion_radio_full_]
extends = env:nibble_zero_connect_companion_radio_wifi_
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:nibble_zero_connect_companion_radio_wifi_.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
+4
View File
@@ -283,6 +283,10 @@ lib_deps =
${rak3112.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_3112_companion_radio_full]
extends = env:RAK_3112_companion_radio_wifi
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
[env:RAK_3112_sensor]
extends = rak3112
build_flags =
+13
View File
@@ -4,6 +4,10 @@ board = station-g2
; Preserve the original 0x10000/0x150000 dual-OTA layout so app-only updates do
; not require replacing the partition table or erasing an installed G2.
board_build.partitions = default.csv
; The Arduino-ESP32 3.x board manifest defaults to QIO, but the Station G2
; bootloader watchdog-resets before starting the app in that mode. The deployed
; hardware and the working 2.x bootloader both require DIO.
board_build.flash_mode = dio
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
@@ -318,6 +322,15 @@ lib_deps =
${Station_G2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Station_G2_companion_radio_full]
extends = env:Station_G2_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:Station_G2_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:Station_G2_kiss_modem]
extends = Station_G2
build_flags =
+9
View File
@@ -156,6 +156,15 @@ lib_deps =
${Station_G3_ESP32.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Station_G3_ESP32_companion_radio_full]
extends = env:Station_G3_ESP32_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:Station_G3_ESP32_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:Station_G3_ESP32_kiss_modem]
extends = Station_G3_ESP32
build_src_filter = ${Station_G3_ESP32.build_src_filter}
+9
View File
@@ -302,6 +302,15 @@ lib_deps =
${Xiao_S3_WIO.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Xiao_S3_WIO_companion_radio_full]
extends = env:Xiao_S3_WIO_companion_radio_wifi
platform = ${esp32_s3_dual_cdc_full.platform}
board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode}
build_unflags = ${esp32_s3_dual_cdc_full.build_unflags}
build_flags =
${env:Xiao_S3_WIO_companion_radio_wifi.build_flags}
${esp32_s3_dual_cdc_full.build_flags}
[env:Xiao_S3_WIO_sensor]
extends = Xiao_S3_WIO
build_src_filter = ${Xiao_S3_WIO.build_src_filter}