diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index c74746cea..76ee06c11 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -96,6 +96,9 @@ jobs: - name: Verify bridge filter wiring and shorthand permissions run: python3 -B test/test_transport_filter_wiring.py -v + - name: Verify MQTT start results and connection-attempt accounting + run: python3 -B test/test_mqtt_transport_results.py -v + - name: Verify Companion contact and channel TX routing run: | sudo apt-get update @@ -176,7 +179,10 @@ jobs: run: python3 -B test/test_esp32_usb_sleep.py - name: Verify GPS status distinguishes user settings from receiver sleep - run: python3 -B test/test_gps_status.py + run: | + python3 -B test/test_gps_status.py + python3 -B test/test_gps_upstream_adaptations.py -v + python3 -B test/test_t1000e_sensor_power.py -v - name: Verify Companion ASCII defaults and USB session cleanup run: | @@ -224,15 +230,19 @@ jobs: run: | python3 -B test/test_logging_sleep_contract.py python3 -B test/test_picker_controls_generator.py + python3 -B test/test_firmware_picker_package.py node test/test_firmware_picker.js - name: Verify Full logging compiler flags and packaged capabilities run: | + python3 -B test/test_upstream_observer_recipes.py -v python3 -B test/test_full_build_flag_dedup.py python3 -B test/test_full_logging_compile.py python3 -B test/test_full_logging_resume.py python3 -B test/test_firmware_capabilities.py bash test/test_build_profiles.sh + bash test/test_full_infrastructure_selection.sh + bash test/test_nrf52_ota_profiles.sh bash test/test_xiao_qspi_combined.sh - name: Verify nRF52 UF2-reset CLI coverage @@ -253,6 +263,7 @@ jobs: python3 -B test/test_neighbor_scope_storage.py python3 -B test/test_ed25519_compact.py python3 -B test/test_cascade_release_package.py + python3 -B test/test_local_release_full_selection.py python3 -B test/test_t096_full_memory.py python3 -B test/test_nrf52_ble_startup.py python3 -B test/test_nrf52_ble_trace.py diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 124d0e388..7e0a8fe68 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # ./build-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving17.1.2" +export FIRMWARE_VERSION="PowerSaving17.1.5" ############# Repeaters ############# # Commonly-used boards -## ESP32 - 21 boards +## ESP32 - 22 boards ./build.sh build-firmware \ Heltec_ct62_repeater \ Heltec_E290_repeater \ @@ -22,6 +22,7 @@ LilyGo_TDeck_repeater \ Station_G2_repeater \ T_Beam_S3_Supreme_SX1262_repeater \ Tbeam_SX1262_repeater \ +ThinkNode_M2_Repeater \ ThinkNode_M5_Repeater \ Xiao_C3_repeater \ Xiao_C6_repeater_ \ @@ -91,6 +92,7 @@ Heltec_t096_companion_radio_ble_femoff \ Heltec_t1_companion_radio_ble \ Heltec_t114_companion_radio_ble \ MKE_s3_companion_radio_ble \ +Mesh_pocket_companion_radio_ble \ LilyGo_T-Echo_Card_companion_radio_ble \ LilyGo_T-Echo_companion_radio_ble \ LilyGo_T-Echo-Lite_companion_radio_ble \ @@ -146,6 +148,9 @@ heltec_v4_companion_radio_usb_ps_femoff \ heltec_v4_companion_radio_usb_ps_femon \ heltec_tracker_v2_companion_radio_usb_femoff \ heltec_tracker_v2_companion_radio_usb_femon \ +Heltec_v3_companion_radio_usb \ +Mesh_pocket_companion_radio_usb \ +ThinkNode_M2_companion_radio_usb \ MKE_s3_companion_radio_usb \ LilyGo_TBeam_1W_companion_radio_usb \ LilyGo_TDeck_companion_radio_usb \ @@ -156,11 +161,16 @@ Xiao_S3_WIO_companion_radio_usb ############# Sensors ############# ./build.sh build-firmware \ +Heltec_t096_sensor \ Heltec_t114_sensor \ t1000e_sensor +############# Bridge ############# +./build.sh build-firmware \ +Xiao_nrf52_repeater_bridge_rs232 + ############# Sample builds ############# -# 27 boards +# 28 boards ./build.sh build-firmware \ Heltec_t096_companion_radio_ble_femon \ Heltec_t096_companion_radio_ble_femoff \ @@ -175,6 +185,7 @@ heltec_v4_3_companion_radio_ble_ps_femoff \ heltec_v4_companion_radio_ble_ps_femon \ heltec_v4_repeater \ heltec_v4_r8_companion_radio_ble_ps \ +ProMicro_repeater \ RAK_3401_companion_radio_ble \ RAK_3401_repeater \ RAK_4631_companion_radio_ble \ @@ -190,6 +201,17 @@ Xiao_C6_repeater_ \ Xiao_nrf52_companion_radio_ble \ Xiao_nrf52_repeater +# Upstream application targets pending feedback. These are not evidence of +# factory bootloader compatibility; OTAFIX qualification remains independent. +./build.sh build-firmware \ +wio_wm1110_repeater \ +wio_wm1110_room_server \ +wio_wm1110_companion_radio_ble + +./build.sh build-firmware \ +meshnology_w12_repeater \ +meshnology_w12_companion_radio_ble + # Heltec v4 USB power-saving variants ./build.sh build-firmware \ heltec_v4_companion_radio_ble_ps_femon \ diff --git a/build_legacy.sh b/build_legacy.sh index 21d4770bd..622183162 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -30,6 +30,7 @@ AUTO_REDUCED_FALLBACK_TARGET="" AUTO_PUBLISH_REDUCED_SECOND_PASS=0 SKIP_DECLARED_REDUCTIONS=0 COMPLETE_OTA_FIRST_PASS=0 +NRF52_OTA_SENSOR_PROFILE="" FIRMWARE_OUTPUT_ENV_NAME="" BUILD_PROFILE_OVERRIDE="${BUILD_PROFILE_OVERRIDE:-auto}" BUILD_PROFILE_EXPLICIT="${BUILD_PROFILE_EXPLICIT:-0}" @@ -139,7 +140,7 @@ Options: --firmware-version : Firmware version to embed. --radio-preset : Override the USA Cascadia radio default. Stable names are usa-cascadia and target; legacy menu numbers remain accepted. --profile : Override runtime settings embedded in the firmware (not its feature set). - --build-profile : Select feature/partition policy. Auto uses the combined Full MQTT/USB/WiFi recipe when it covers the plain infrastructure target; otherwise it first builds complete LoRa-OTA-capable firmware with a measured-size fallback. Internal-flash nRF52 repeaters and room servers also publish the reduced image for delta-staging headroom; XIAO nRF52 repeaters with external-QSPI staging publish one combined image. Standard preserves portable images, including the 1.25 MiB slot; full requires the expanded ESP32 recipe. + --build-profile : Select feature/partition policy. Every non-Companion nRF52 repeater, room server and sensor builds both Full sensors + LoRa OTA and Reduced sensors + LoRa OTA, in its exact existing storage/layout contract. Both must pass qualification; an oversized full image is an error, not a successful reduced-only fallback. Other platforms retain their existing auto/standard/full policy. --auto|--standard|--full: Short forms of --build-profile. --full-exact: Build one expanded ESP32 release image under the requested target's own LoRa OTA identity. --skip-kiss|--include-kiss: Exclude (default) or include KISS modem targets in bulk builds. @@ -152,10 +153,9 @@ Options: and status live outside OUTPUT_DIR so --clean cannot erase them. Examples: -Build firmware for the "RAK_4631_repeater" device target. Auto first builds -the complete LoRa OTA recipe, then also publishes the reduced -no-external-sensors repeater because this nRF52 repeater stages deltas in -internal flash. +Build both qualified sensor/LoRa OTA profiles for "RAK_4631_repeater". +This policy also applies to release matrices, sensors and external-storage +nRF52 recipes; it does not change their target IDs or flash layouts. $ bash build.sh build-firmware RAK_4631_repeater --build-profile auto Run without arguments to choose an interactive build action/target, an optional @@ -2124,8 +2124,10 @@ is_xiao_qspi_combined_repeater_target() { is_xiao_qspi_canonical_repeater_build() { is_xiao_qspi_combined_repeater_target "$1" \ - && [ "$BUILD_PROFILE_FOR_TARGET" = "standard" ] \ - && [ -z "$FIRMWARE_FILENAME_INFIX" ] + && { [ "$BUILD_PROFILE_FOR_TARGET" = "standard" ] \ + || [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; } \ + && { [ -z "$FIRMWARE_FILENAME_INFIX" ] \ + || [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; } } is_lora_ota_no_external_sensors_target() { @@ -2136,6 +2138,9 @@ is_lora_ota_no_external_sensors_target() { is_rak_i2c_voltage_monitor_ota_target() { local target_lc=${1,,} + if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; then + case "$target_lc" in rak_3401_*|rak_4631_*) return 0 ;; esac + fi case "$target_lc" in rak_3401_*lora_ota_no_external_sensors|rak_4631_*lora_ota_no_external_sensors) return 0 @@ -2149,6 +2154,12 @@ is_rak_i2c_voltage_monitor_ota_target() { is_rak_gps_retaining_ota_target() { local target_lc=${1,,} + if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; then + case "$target_lc" in + rak_4631_*bridge_rs232_serial1*) return 1 ;; + rak_3401_*|rak_4631_*) return 0 ;; + esac + fi # Serial1 is the RAK12501 UART on RAK4631. The Serial1 RS-232 bridge owns # that port instead, so its reduced profile intentionally uses the INA-only # recipe and must not promise or require the WisBlock GPS provider. @@ -2429,6 +2440,12 @@ get_unified_full_infrastructure_target() { case "${mqtt_base,,}" in *_repeater_bridge_espnow) mqtt_base=${mqtt_base%_bridge_espnow} ;; esac + # EoRa's deployed normal identity capitalizes Repeater, while upstream's + # observer recipe does not. Normalize only the feature-base lookup, never + # the logical target name whose spelling determines the on-air mOTA ID. + case "$mqtt_base" in + Ebyte_EoRa-S3_Repeater) mqtt_base=Ebyte_EoRa-S3_repeater ;; + esac case "${mqtt_base,,}" in *_repeater|*_room_server|*_repeater_observer_mqtt|*_room_server_observer_mqtt) ;; *) return 1 ;; @@ -2971,7 +2988,8 @@ get_reduced_lora_ota_target() { return 0 fi if ! is_repeater_role_target "$target" \ - && ! is_room_server_role_target "$target"; then + && ! is_room_server_role_target "$target" \ + && ! is_sensor_role_target "$target"; then return 1 fi @@ -2983,6 +3001,47 @@ get_reduced_lora_ota_target() { return 1 } +is_nrf52_sensor_ota_pair_target() { + [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "NRF52_PLATFORM" ] \ + && ! is_companion_build "$1" \ + && ! is_kiss_modem_target "$1" \ + && { is_repeater_role_target "$1" \ + || is_room_server_role_target "$1" \ + || is_sensor_role_target "$1"; } +} + +get_nrf52_sensor_ota_pair_target() { + local target=$1 + local candidate + is_nrf52_sensor_ota_pair_target "$target" || return 1 + # Preserve an explicit/deployed OTA identity and every external-storage + # contract. Ordinary internal targets keep the historical auto-build ID of + # their reduced alias, so the same pair addresses already installed nodes. + if is_lora_ota_only_target "$target" \ + || [ "${PIO_ENV_QSPI_OTA_BY_NAME[$target]:-0}" = "1" ] \ + || [ "${PIO_ENV_SD_OTA_BY_NAME[$target]:-0}" = "1" ]; then + printf '%s\n' "$target" + elif candidate=$(get_reduced_lora_ota_target "$target"); then + printf '%s\n' "$candidate" + else + printf '%s\n' "$target" + fi +} + +normalize_nrf52_sensor_ota_pair_targets() { + local target candidate + local -a normalized=() + local -A seen=() + for target in "${RESOLVED_BUILD_TARGETS[@]}"; do + candidate=$(get_nrf52_sensor_ota_pair_target "$target") || candidate=$target + if [ -z "${seen[$candidate]+x}" ]; then + normalized+=("$candidate") + seen["$candidate"]=1 + fi + done + RESOLVED_BUILD_TARGETS=("${normalized[@]}") +} + is_lora_ota_build() { local env_name=$1 local env_name_lc=${env_name,,} @@ -2993,6 +3052,13 @@ is_lora_ota_build() { return 1 fi + # Both required nRF52 sensor profiles include the real self-update path, + # regardless of ordinary standard-profile opt-ins or logging overrides. + if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ] \ + && is_nrf52_sensor_ota_pair_target "$env_name"; then + return 0 + fi + # ESP32 USB and WiFi companions keep OTA so they can seed a host folder over # serial or TCP and can participate in LoRa OTA without using the FULL profile. if is_esp32_usb_wifi_companion_ota_build "$env_name"; then @@ -3107,29 +3173,29 @@ is_esp32_full_only_bulk_target() { [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1 case "$env_name" in - heltec_e290_repeater|heltec_e290_repeater_bridge_espnow|heltec_e290_room_server|\ + heltec_e290_repeater|heltec_e290_repeater_bridge_espnow|heltec_e290_room_server|heltec_e290_repeater_observer_mqtt|heltec_e290_room_server_observer_mqtt|\ heltec_v3_repeater|heltec_v3_repeater_bridge_rs232|heltec_v3_repeater_bridge_espnow|heltec_v3_repeater_observer_mqtt|heltec_v3_repeater_observer_mqtt_sim|heltec_v3_room_server|heltec_v3_room_server_observer_mqtt|heltec_v3_sensor|\ heltec_wsl3_repeater|heltec_wsl3_repeater_bridge_rs232|heltec_wsl3_repeater_bridge_espnow|heltec_wsl3_repeater_observer_mqtt|heltec_wsl3_room_server|heltec_wsl3_room_server_observer_mqtt|heltec_wsl3_sensor|\ lilygo_tbeam_1w_repeater|lilygo_tbeam_1w_repeater_bridge_espnow|lilygo_tbeam_1w_repeater_observer_mqtt|lilygo_tbeam_1w_room_server|lilygo_tbeam_1w_room_server_observer_mqtt|\ - heltec_rc32_repeater|heltec_rc32_repeater_bridge_espnow|heltec_rc32_room_server|heltec_rc32_sensor|\ - heltec_rc32_without_display_repeater|heltec_rc32_without_display_repeater_bridge_espnow|heltec_rc32_without_display_room_server|heltec_rc32_without_display_sensor|\ - heltec_wireless_tracker_repeater|heltec_wireless_tracker_repeater_bridge_espnow|heltec_wireless_tracker_room_server|\ + heltec_rc32_repeater|heltec_rc32_repeater_bridge_espnow|heltec_rc32_room_server|heltec_rc32_sensor|heltec_rc32_repeater_observer_mqtt|heltec_rc32_room_server_observer_mqtt|\ + heltec_rc32_without_display_repeater|heltec_rc32_without_display_repeater_bridge_espnow|heltec_rc32_without_display_room_server|heltec_rc32_without_display_sensor|heltec_rc32_without_display_repeater_observer_mqtt|heltec_rc32_without_display_room_server_observer_mqtt|\ + heltec_wireless_tracker_repeater|heltec_wireless_tracker_repeater_bridge_espnow|heltec_wireless_tracker_room_server|heltec_wireless_tracker_repeater_observer_mqtt|heltec_wireless_tracker_room_server_observer_mqtt|\ thinknode_m9_repeater_|thinknode_m9_room_server_|\ heltec_t190_repeater_|heltec_t190_repeater_observer_mqtt|heltec_t190_repeater_bridge_espnow_|heltec_t190_room_server_|heltec_t190_room_server_observer_mqtt|\ - meshnology_w12_repeater|meshnology_w12_repeater_bridge_espnow|meshnology_w12_room_server|meshnology_w12_sensor|\ + meshnology_w12_repeater|meshnology_w12_repeater_bridge_espnow|meshnology_w12_room_server|meshnology_w12_sensor|meshnology_w12_repeater_observer_mqtt|meshnology_w12_room_server_observer_mqtt|\ mke_s3_repeater|mke_s3_repeater_bridge_rs232|mke_s3_repeater_bridge_espnow|mke_s3_room_server|mke_s3_sensor|\ heltec_v2_repeater|heltec_v2_repeater_bridge_espnow|heltec_v2_room_server|heltec_v2_companion_radio_wifi|\ - heltec_wireless_paper_repeater|heltec_wireless_paper_repeater_bridge_espnow|heltec_wireless_paper_room_server|\ + heltec_wireless_paper_repeater|heltec_wireless_paper_repeater_bridge_espnow|heltec_wireless_paper_room_server|heltec_wireless_paper_repeater_observer_mqtt|heltec_wireless_paper_room_server_observer_mqtt|\ heltec_tracker_v1_1_repeater_observer_mqtt|heltec_tracker_v1_1_room_server_observer_mqtt|\ heltec_tracker_v2_repeater|heltec_tracker_v2_repeater_bridge_espnow|heltec_tracker_v2_repeater_observer_mqtt|heltec_tracker_v2_room_server|heltec_tracker_v2_room_server_observer_mqtt|heltec_tracker_v2_sensor|\ t_beam_s3_supreme_sx1262_repeater|t_beam_s3_supreme_sx1262_repeater_bridge_espnow|t_beam_s3_supreme_sx1262_repeater_observer_mqtt|t_beam_s3_supreme_sx1262_room_server|t_beam_s3_supreme_sx1262_room_server_observer_mqtt|\ - heltec_e213_repeater|heltec_e213_repeater_bridge_espnow|heltec_e213_room_server|\ + heltec_e213_repeater|heltec_e213_repeater_bridge_espnow|heltec_e213_room_server|heltec_e213_repeater_observer_mqtt|heltec_e213_room_server_observer_mqtt|\ station_g2_repeater_observer_mqtt|station_g2_room_server_observer_mqtt|\ - xiao_s3_repeater|xiao_s3_repeater_bridge_espnow|xiao_s3_room_server|xiao_s3_sensor|\ + xiao_s3_repeater|xiao_s3_repeater_bridge_espnow|xiao_s3_room_server|xiao_s3_sensor|xiao_s3_repeater_observer_mqtt|xiao_s3_room_server_observer_mqtt|\ heltec_v4_repeater|heltec_v4_repeater_bridge_espnow|heltec_v4_repeater_observer_mqtt|heltec_v4_room_server|heltec_v4_room_server_observer_mqtt|heltec_v4_sensor|\ heltec_v4_expansionkit_repeater|heltec_v4_expansionkit_repeater_observer_mqtt|heltec_v4_expansionkit_room_server_observer_mqtt|\ heltec_v4_tft_repeater|heltec_v4_tft_repeater_bridge_espnow|heltec_v4_tft_room_server|heltec_v4_tft_sensor|\ - lilygo_teth_elite_sx1262_repeater|lilygo_teth_elite_sx1262_room_server|\ + lilygo_teth_elite_sx1262_repeater|lilygo_teth_elite_sx1262_room_server|lilygo_teth_elite_sx1262_repeater_observer_mqtt|lilygo_teth_elite_sx1262_room_server_observer_mqtt|\ station_g3_esp32_repeater|station_g3_esp32_logging_repeater|station_g3_esp32_room_server|station_g3_esp32_repeater_observer_mqtt|station_g3_esp32_room_server_observer_mqtt|\ rak_3112_repeater|rak_3112_repeater_bridge_rs232|rak_3112_repeater_bridge_espnow|rak_3112_repeater_observer_mqtt|rak_3112_room_server|rak_3112_room_server_observer_mqtt|rak_3112_sensor|\ xiao_s3_wio_repeater|xiao_s3_wio_repeater_bridge_espnow|xiao_s3_wio_repeater_observer_mqtt|xiao_s3_wio_room_server|xiao_s3_wio_room_server_observer_mqtt|xiao_s3_wio_sensor|\ @@ -3239,6 +3305,10 @@ declare_build_capability_contract() { declare_full_logging_application_contract "$env_name" record_build_capability "profile.${BUILD_PROFILE_FOR_TARGET}" + if is_nrf52_sensor_ota_pair_target "$env_name" \ + && [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; then + record_build_capability "sensor.profile.${NRF52_OTA_SENSOR_PROFILE}" + fi if [ "$env_platform" = "ESP32_PLATFORM" ] \ && [ "$BUILD_PROFILE_FOR_TARGET" = "full" ]; then @@ -3584,6 +3654,12 @@ apply_repeater_neighbor_capacity() { if ! is_repeater_role_target "$env_name"; then return 0 fi + # Paired nRF52 OTA profiles retain their exact board recipe's capacity. + # Enlarging a table here can invalidate the full image's reserved OTA RAM. + if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ] \ + && is_nrf52_sensor_ota_pair_target "$env_name"; then + return 0 + fi # Repeater discovery uses one-byte indexes, so 254 is the largest usable # table. Keep explicitly constrained targets at their measured safe capacity. @@ -3667,7 +3743,8 @@ apply_lora_ota_no_external_sensors_profile() { return 0 fi if ! is_lora_ota_build "$env_name" \ - || ! is_lora_ota_no_external_sensors_target "$env_name"; then + || { ! is_lora_ota_no_external_sensors_target "$env_name" \ + && [ "${NRF52_OTA_SENSOR_PROFILE:-}" != "reduced" ]; }; then return 0 fi @@ -4210,12 +4287,17 @@ write_build_capability_manifest() { local env_platform=$2 local pio_env_name=$3 local firmware_filename=$4 + local artifact_target=${FIRMWARE_OUTPUT_ENV_NAME:-$env_name} + if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ] \ + && is_nrf52_sensor_ota_pair_target "$env_name"; then + artifact_target+="-${NRF52_OTA_SENSOR_PROFILE}-ota" + fi local build_output_dir="${PIO_BUILD_DIR_OVERRIDE:-${PLATFORMIO_BUILD_DIR:-.pio/build}}/${pio_env_name}" local -a checker_args=( --image "${build_output_dir}/firmware.elf" --output "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" --target "$env_name" - --artifact-target "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}" + --artifact-target "$artifact_target" --platformio-env "$pio_env_name" --platform "$env_platform" --build-profile "$BUILD_PROFILE_FOR_TARGET" @@ -4332,6 +4414,32 @@ output_artifact_exists() { [ -s "${OUTPUT_DIR}/$1" ] } +nrf52_sensor_profile_manifest_matches() { + python3 - "$1" "$2" "$3" "$4" <<'PY' +import json, sys +try: + manifest = json.load(open(sys.argv[1])) + target, profile, artifact_target = sys.argv[2:] + capabilities = set(manifest.get("capabilities", [])) + sensor = {value for value in capabilities if value.startswith("sensor.profile.")} + proven = {item.get("capability") for item in manifest.get("verification", []) + if item.get("present") is True} + matched = (profile in {"full", "reduced"} and bool(target) + and manifest.get("platform") == "NRF52_PLATFORM" + and manifest.get("target") == target + and manifest.get("artifact_target") == artifact_target + and sensor == {"sensor.profile." + profile} + and manifest.get("verified") is True + and manifest.get("ota_update_verified") is True + and "lora" in manifest.get("ota_update_methods", []) + and "ota.update.lora" in capabilities + and "ota.update.lora" in proven) +except (OSError, ValueError, TypeError, AttributeError): + matched = False +sys.exit(0 if matched else 1) +PY +} + build_artifacts_exist() { # Keep the two-argument helper form usable by focused resume tests and # external callers. Production builds pass the logical environment as the @@ -4353,6 +4461,15 @@ build_artifacts_exist() { "${OUTPUT_DIR}/${firmware_filename}" >/dev/null 2>&1 || return 1 grep -q '"verified": true' \ "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" || return 1 + if [ "$env_platform" = "NRF52_PLATFORM" ] \ + && [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; then + # A cached pre-pair image may be verified for Bluetooth only, or may carry + # no sensor policy. Rebuild it instead of claiming a current OTA option. + nrf52_sensor_profile_manifest_matches \ + "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" "$env_name" \ + "$NRF52_OTA_SENSOR_PROFILE" \ + "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}-${NRF52_OTA_SENSOR_PROFILE}-ota" || return 1 + fi # Old manifests can be marked verified while lacking a newly required # capability. Never let --resume bypass the current packaged-logging gate. python3 - "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" \ @@ -4503,7 +4620,58 @@ run_pio_with_size_detection() { return "$build_status" } +run_nrf52_sensor_ota_pair() { + local target + target=$(get_nrf52_sensor_ota_pair_target "$1") || return 1 + # Dynamic locals keep the pair isolated from subsequent ESP32/Companion + # jobs, even when the matrix caller has a different profile or logging mode. + local ESP32_FULL_BUILD=0 + local BUILD_PROFILE_EFFECTIVE="auto" + local REQUIRE_OTA_UPDATES=1 + local FIRMWARE_FILENAME_INFIX="" + local FIRMWARE_OUTPUT_ENV_NAME="" + local SKIP_DECLARED_REDUCTIONS=0 + local COMPLETE_OTA_FIRST_PASS=0 + local NRF52_OTA_SENSOR_PROFILE="" + local full_status=0 reduced_status=0 + + echo "Required nRF52 OTA pair: ${target}; exact target identity and storage/layout retained." + NRF52_OTA_SENSOR_PROFILE="full" + FIRMWARE_FILENAME_INFIX="full-ota" + SKIP_DECLARED_REDUCTIONS=1 + COMPLETE_OTA_FIRST_PASS=1 + if build_firmware_one_profile "$target"; then + echo "Qualified Full sensors + LoRa OTA: ${target}." + else + full_status=$? + echo "QUALIFICATION FAILED: ${target}, Full sensors + LoRa OTA (status ${full_status})." >&2 + fi + + NRF52_OTA_SENSOR_PROFILE="reduced" + FIRMWARE_FILENAME_INFIX="reduced-ota" + SKIP_DECLARED_REDUCTIONS=0 + COMPLETE_OTA_FIRST_PASS=0 + if build_firmware_one_profile "$target"; then + echo "Qualified Reduced sensors + LoRa OTA: ${target}." + else + reduced_status=$? + echo "QUALIFICATION FAILED: ${target}, Reduced sensors + LoRa OTA (status ${reduced_status})." >&2 + fi + if [ "$full_status" -ne 0 ] || [ "$reduced_status" -ne 0 ]; then + echo "INCOMPLETE nRF52 OTA PAIR: ${target}; both sensor options must qualify. A reduced-only result is not a successful release." >&2 + return 1 + fi +} + build_firmware() { + if is_nrf52_sensor_ota_pair_target "$1"; then + run_nrf52_sensor_ota_pair "$1" + else + build_firmware_one_profile "$1" + fi +} + +build_firmware_one_profile() { local env_name=$1 local pio_env_name local env_platform @@ -4552,10 +4720,11 @@ build_firmware() { return 1 fi pio_env_name=$(get_pio_build_env "$env_name") - if [ "$COMPLETE_OTA_FIRST_PASS" = "1" ] \ + if { [ "$COMPLETE_OTA_FIRST_PASS" = "1" ] \ + || [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; } \ && [ -n "${PIO_ENV_COMPLETE_OTA_BASE_BY_NAME[$env_name]+x}" ]; then pio_env_name=${PIO_ENV_COMPLETE_OTA_BASE_BY_NAME[$env_name]} - echo "Complete OTA pass uses feature-rich base environment ${pio_env_name} with stable target identity ${env_name}." + echo "Sensor OTA profile uses feature-rich base environment ${pio_env_name} with stable target identity ${env_name}." fi # Canonical bulk releases omit the redundant portable image for targets @@ -4626,7 +4795,11 @@ build_firmware() { configure_unified_full_infrastructure_output "$pio_env_name" echo "Effective feature profile for ${env_name}: ${BUILD_PROFILE_FOR_TARGET}" - commit_hash=$(git rev-parse --short HEAD) + # Publication tools bind artifacts to an explicit eight-character prefix + # of the full source commit. Git's adaptive --short length can grow (or be + # configured differently), which otherwise makes valid releases look stale. + commit_hash=$(git rev-parse HEAD) + commit_hash=${commit_hash:0:8} firmware_build_date=$(date -u '+%d-%b-%Y') firmware_build_epoch=$(date -u '+%s') firmware_version=${FIRMWARE_VERSION:-} @@ -5445,6 +5618,10 @@ resolve_command_targets() { echo "OTA required: infrastructure must prove a WiFi, Bluetooth, or LoRa self-update path; Companions may update over USB." fi + # Base and reduced aliases resolve to one exact-identity pair; external + # storage and deployed compatibility identities deliberately stay separate. + normalize_nrf52_sensor_ota_pair_targets + # Keep one queue so parallel workers stay saturated. The scheduler may pull # a later target forward when a generated alias shares an active PlatformIO # base environment, so this is a best-effort start order rather than a phase @@ -5490,6 +5667,10 @@ configure_effective_build_profile() { return 1 fi BUILD_PROFILE_EFFECTIVE="full" + elif [ -n "$target" ] && is_nrf52_sensor_ota_pair_target "$target"; then + BUILD_PROFILE_EFFECTIVE="auto" + SINGLE_TARGET_FULL_BUILD=0 + echo "nRF52 sensor OTA policy: both full and reduced profiles are required in the exact existing layout." elif [ "$SINGLE_TARGET_FULL_BUILD" = "1" ]; then BUILD_PROFILE_EFFECTIVE="full" elif is_automatic_profile_command "$command_name"; then @@ -6402,6 +6583,12 @@ validate_command() { run_auto_two_pass_build() { local target=$1 + # nRF52 no longer treats a reduced success as a fallback for a failed full + # option. The same strict pairing policy covers explicit and matrix builds. + if is_nrf52_sensor_ota_pair_target "$target"; then + run_nrf52_sensor_ota_pair "$target" + return $? + fi local fallback_target=$AUTO_REDUCED_FALLBACK_TARGET local original_esp32_full_build=$ESP32_FULL_BUILD local original_build_profile_effective=$BUILD_PROFILE_EFFECTIVE diff --git a/docs/_data/bootloader_manifest.json b/docs/_data/bootloader_manifest.json index 061435082..80eeadfd2 100644 --- a/docs/_data/bootloader_manifest.json +++ b/docs/_data/bootloader_manifest.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, "repository": "mikecarper/Adafruit_nRF52_Bootloader_OTAFIX", - "tag": "v0.11.0-OTAFIX2.4.10", - "version": "2.4.10", - "releaseUrl": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.10", + "tag": "v0.11.0-OTAFIX2.4.11", + "version": "2.4.11", + "releaseUrl": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11", "prerelease": false, "profiles": [ { @@ -18,22 +18,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-gat562_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-gat562_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-gat562_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-gat562_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84992, - "sha256": "4bbe3a8fa3951a67fa8c1c6a910744af6370d9a666eb7b88b6b760c5139f6c32" + "sha256": "2e3c5292abd9f87380e63926a01dc0cb437002d33acff8de2919d9cd79273927" }, "zip": { - "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/gat562_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "fe8346cd8d066caf4b4ee6a4c8605677e6457f022670d0fe270e021c98c5a1d4" + "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "35bbb66831af4c1aaba1ae139f2c7d149a1b281c28aff3dac31a78f67ac026b0" }, "hex": { - "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/gat562_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530672, - "sha256": "fa22eefe6c5a5eecf8c89abc2a195a3ccbe3bf28b7fb32d8b3c966489b92588b" + "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530716, + "sha256": "8df22a6951916f0eee1910cf8d449eb32fa712799ad12713045e06f93e355f82" } } }, @@ -47,57 +47,28 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "594506767173bb83ef773b69464fe2fd94ff71d7b4c1318c2fee2bdb04cd1dd2" + "sha256": "3087bdd540db6a6399120a23012dc9d1e45ca9b72938abd49c389319f1a027db" }, "zip": { - "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "47bf7082e0a021c68c9c479d0effdc0f38fa150d9c49012922cad54c143240d1" + "sha256": "7251ed15ce18dba03151b676d7401f50a043a61370e6eb7230daddfa9248dda4" }, "hex": { - "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530068, - "sha256": "5bb3041b17d25bf45fffc51316fac350497102aaa6363a4f04dcde31d5ee8614" - } - } - }, - { - "id": "heltec_mesh_tower_v2", - "label": "Heltec MeshTower V2 - internal storage", - "meshcoreHardware": [ - "Heltec_tower_v2" - ], - "deviceName": "TOWER_V2_OTA", - "storage": "internal", - "files": { - "uf2": { - "name": "update-heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "size": 84480, - "sha256": "543766e38f347ce8c7bdeff6050fcd8d2c93e62c2a0510b318ac4e3c31f38552" - }, - "zip": { - "name": "heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192834, - "sha256": "aa4558d5ad651bf820f73e8256d0130aa6d41bdb3a54b73297253c0a986573f0" - }, - "hex": { - "name": "heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_tower_v2_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530462, - "sha256": "a1371c6dc1c22648dd8625c93f9789f65e5b0123b0cb8d4ca6bfd9c51bc51a4a" + "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530112, + "sha256": "f0b836e53af97d5445236ef89adb8443e5c8a4691e71488ff930cca2b3a57d90" } } }, { "id": "heltec_mesh_tower_v2_sdcard", - "label": "Heltec MeshTower V2 - microSD", + "label": "Heltec MeshTower V2 - microSD + internal (SD primary)", "meshcoreHardware": [ "Heltec_tower_v2_sdcard" ], @@ -105,22 +76,22 @@ "storage": "sd", "files": { "uf2": { - "name": "update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "size": 86528, - "sha256": "69ca460d976b778f3465be10ed11ed1abcd1fbe0bca8ce85b5fc2a5620f3e7a8" + "name": "update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "size": 88064, + "sha256": "26e8cc90bdf3a138bd8f5471f17c04cbeaddf5dfb01bf62601917f7d0c8fa825" }, "zip": { - "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192834, - "sha256": "d967469e8c10885b00b235d3d2b451029699b2b41b6882d0011120667036ed8b" + "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192833, + "sha256": "ac5fdfa777ba583cc9e62e7cd1fef3cf0a3b97da5b221a332380b2209d31b79c" }, "hex": { - "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 532874, - "sha256": "5c491b436f34cb164c17195083309f375f84628ccb0c1f8ef166ce6d9ad5d2e1" + "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 535746, + "sha256": "5980d8f96a3b9dbb55bbf488cbeacdcae1c6f59ff78c2766254c8b95c5e6beab" } } }, @@ -134,22 +105,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 87552, - "sha256": "d3f01cb890f21ca9f93fd2ce5d7dfc722dd0eb15069ba05b06bf1440b876f7b4" + "sha256": "a7db3f390ab8934a33ccc1f60ac0acf1b5200f58fc6c037e233a178d5d416da8" }, "zip": { - "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "88780871f385463258e3b3447b41baf8158f651ae5554e952f958a9bb488f3b5" + "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "4c2f1adfa28ad8a4a856c7c720ad1dbcb8cf13fcdc381152761bd72fadba227c" }, "hex": { - "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 534426, - "sha256": "7a793922c1ab66b6b93b308a57f400aa2820adfd9daf8b379093ba6f39dbd2ec" + "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 534470, + "sha256": "878071c4e7bc16a9ec664773d34eadb7be6846e8791bcc87270ba5e41b463e2b" } } }, @@ -163,22 +134,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 87552, - "sha256": "c39f8878bbbdb8df323bb77526088a7ff711b5b532a8e965b22fa9cd502a97a1" + "sha256": "3a053de7ba28e63756cd52d0e5632ce07bd244eeb271720f1178e71178680d77" }, "zip": { - "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "f3b876332448098cf68cb9c475729bd2f87e3cdfa71676bc362d7fd900abebbb" + "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "b7144a4493a23902b07e31435f8c3758afe0b5d87f777236705865f51bb00ecb" }, "hex": { - "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 534464, - "sha256": "22c8701030be8ea3fa68612a99fe40b08e2ddae085768c87f82ad562dc2777ed" + "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 534508, + "sha256": "8ba4ddc312f2246e5ace749e5734934ab5537c4ef60354a34658eebfa4d50449" } } }, @@ -193,22 +164,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 88064, - "sha256": "daf96a592012b2579876777265a891407631ef8a9cba059ebca1e78a7fe307d0" + "sha256": "d8307134c4f63a9a91d86937abe25863722ff61f822524fc027847c6137282cc" }, "zip": { - "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "cf6b2e0770f9bba9fbaa8a5c967af325dc695479e2d3f6b79349b4dcf13545dc" + "sha256": "a473faf32cf3e4dc13c5d8340e7ffebd04741be3e37f8c2cb98b2c176e01a777" }, "hex": { - "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 535336, - "sha256": "f2912d3a4cf6bd81226d17fc75324cd83d7b97a8e4808e7d5c191634865c553d" + "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 535380, + "sha256": "5afa51d860b54dc820d94f67efb8f7f449b0a4f461bbc176ccbd27a9730bc4e9" } } }, @@ -222,22 +193,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "78460767341d867cd9a77845cad9de7d00e396ca5ab8340927a605c4fa899f2d" + "sha256": "c2ad478f12cd3176f5ea4442726fdaebb6071218564a97b7c90cf52ed214f369" }, "zip": { - "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "425c26680603a2c9b6b7becb95412611a8449ea51cf427f0997335c233a4275f" + "sha256": "f7a21a21afad7ecbf0e29f65e243b103f4b423fe575734fa4f00db56947f7c64" }, "hex": { - "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 529990, - "sha256": "9b1b4eed8033085e07bfb6c162fb0a46ee3aca157ec6d3fae162b548c9bc9764" + "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530034, + "sha256": "dbad71f1975f26c4f42e3057d2e6de82201fa9488e39556070ad68fa650204fe" } } }, @@ -251,22 +222,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86528, - "sha256": "506719ba59e5172beab8904044b6f8700442467d5a2df6889710de8800e77a30" + "sha256": "9a3cd2a73f91f9f1ca01f44ba56c4b1ad2317511e766533ed61c361aca6e37fa" }, "zip": { - "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "cbcb872bb3525596e699d37a39bff85f2d02b178174c9812c461b21ba0753599" + "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "a7cbdf2c911542222c0d12eb5fb2ab9bdf2263495b591d1a946860b324bf4731" }, "hex": { - "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 532886, - "sha256": "b7112639c09eae5dd2a015c6a5454d6d71bf0b677e9752ed23cc4dfc0e454cb7" + "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 532930, + "sha256": "5c8f10b9b1217f36b8ff6f0dac957f108ca4125bc5e737dd827c08c01a5e556f" } } }, @@ -281,22 +252,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 87040, - "sha256": "a3db93e42b1d59d5b89bab58ebee1207cc5634dd7d6543a885484fb46e509073" + "sha256": "5ee193ed3ac342502efb03adc3aa214eb56198f22975fe5dfcf4d5743d4788d0" }, "zip": { - "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "adf1991f5377ed69905e16df138911096c31ecb3b4f4a6e9da417e5c91d3fb2d" + "sha256": "2ed15158b2b015a445c3d91ad616c8550e3b6ed59de2ca54832de4cb8d5ce10d" }, "hex": { - "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 533482, - "sha256": "b750f8ad93f346a9d5a4ca701bbaa5697e63b08b028f43321a8c0ddd4530346b" + "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 533526, + "sha256": "d3531513bc550431caf9893dd88a8324f83f401b7b1700bbee6cd6c8a2724983" } } }, @@ -310,22 +281,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "73784ca136fe9c0ebe22d4a0222dd18572bbd5740322ef133f6edbdd6b51353e" + "sha256": "c5858e6ed516af22202a2c9b1c155764ae25adaf55a42941d9665df77d44fad7" }, "zip": { - "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194546, - "sha256": "d1bc22c9822447f18b1590662ca95b612e3ccbf1353892a67441c87315ab457c" + "sha256": "322b8c8e9caaca660bd2ed68f1d19f5cd38575ea7def6f262e652e2a872f7f2d" }, "hex": { - "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 535040, - "sha256": "c6229d65c7f7543bc48cb0a362a4799c7b0781684f9a63156686cf66aa085d79" + "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 535084, + "sha256": "9de94798b5c25018ee9a5494b776bd5469f5c743d0fa9cbb82dfc7dda115fbbf" } } }, @@ -339,22 +310,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-pca10056_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-pca10056_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-pca10056_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-pca10056_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86528, - "sha256": "8f6b2f37b33e2b4c7a5b038e9deb04e088e1da4b716d7f6787f201acd9af8315" + "sha256": "78ab8f3f8ea44d60c13d6fd34088451b1bd26745757deed44826c15c7ee70ea8" }, "zip": { - "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/pca10056_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "76db09c07e9175204368a939eb71e920aae7168633dec4f122851c62027d83bc" + "sha256": "d5d01a01f98850b3282cf7f443fa13e642a42617d7267cee22739880fbcf2ff9" }, "hex": { - "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/pca10056_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 532976, - "sha256": "6e77e4fc3adf07e3ca455ce02ca021d8097261b43834844f6225d8f1c80616c8" + "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 533020, + "sha256": "51ceaa88711ca24a8430bb6f35766df729245cbe2858336dad3082751c692fc2" } } }, @@ -368,22 +339,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "a445375e828a3724f3e9d7854175d5a1c120eef0613cc6fd7c0d4cfa03a5d04a" + "sha256": "b849203d30a56b5243f8d92e308c6cd9a1eb3b35459072204f9f044d74dbad09" }, "zip": { - "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "10b32c1133be24a532f9678d7517c926ed474b751f99bc33d35a3a01c54d3aba" + "sha256": "3b2ef682cc2641626b4db3d8bfe821cd86dd701f38c7c0c055864d78da0fbb88" }, "hex": { - "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530382, - "sha256": "650ac8431867a21b31087ace9ef585e5d946952775dca63d256093a7804e4a0e" + "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530426, + "sha256": "bcfda096684f5da9f00c36a0baef81a0b4655424f594cc29953c27ca9051263f" } } }, @@ -397,22 +368,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "35fea3ca9a5f440a99279861b17174f54cf14ab406c0b2eb6bbddc76af6f65e2" + "sha256": "ca98f27f0289189d0b9b600c5a5a82acad14a56ed4ee4922c6c17fe1d669aadf" }, "zip": { - "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194546, - "sha256": "60f74057e9ccd33cb90d59027d6e10231ea1b1c2f9244b1f319d10bb8c4a4a81" + "sha256": "2ed90c5624a3cdb9852fb22da42f02c2d55d93e74e19978068570c1edce90ee8" }, "hex": { - "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 537336, - "sha256": "f8907bcfc2658e59d580dd1618d11d1f27b1f1887ba8617504d7d0d25347ba0f" + "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 537380, + "sha256": "6a0ada53219f67dd1a99da32d1257acff5fa617db26a9c3931dfa69e64bfca96" } } }, @@ -426,22 +397,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "ea2fc2e1982a2e9365e42fea8471b2770f805d9d1fb37f4e2e49e43c3a83dce3" + "sha256": "56a66098af73f8ef71d7709fb2e4803f0822b146310882cfccd9cd67dc7e6f8a" }, "zip": { - "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194546, - "sha256": "b1743ea1e54a7c69fc04fe6dd1467b221bd82a3658fd3170a8dc9d6ffb0a7322" + "sha256": "25e4f5af918ee1db913eb66f867f601578267ca5ebd8d4ebe7f6a43e193992e0" }, "hex": { - "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/t1000_e_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 534892, - "sha256": "863c2bdf6c01a709cb64808a51b1ff6710c35f40f10b86280ce07b763410a709" + "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 534936, + "sha256": "9d75645dcf05a3f026ae2e798294dfbdb256288853fa18a8fc3d81a36df2079f" } } }, @@ -455,22 +426,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "42e64392dacda730d83335335bb143af64fdb53abef7228c0fedbb4a12850a61" + "sha256": "e9285e60f5789380f5aa7c61773e3d4d5dc61054688ec4e83830d3ea118f89d3" }, "zip": { - "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192833, - "sha256": "0bf8e7acecf1019b90562c69e9a6e68ba4787652e3f3dd6060441f8a64c36c6c" + "sha256": "3cac3e8f3b73a4e46e99fd11502954dd82e416274f413019d79265d8bca4e4b0" }, "hex": { - "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 532614, - "sha256": "74007bc4c78bc5a32246af1ace7083fc01c2a43dabd27906e8d8d3fd3fbe5226" + "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 532658, + "sha256": "2c42f4334ac94e05cce770fe9570b9442f5d37c54fb3c42d3e9f3caf2d5f36d1" } } }, @@ -484,22 +455,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84480, - "sha256": "c38ead189e8baf624ff9fddf31d2f16cd24f056a8ad2d66247e59828cefa1e8a" + "sha256": "babdc56164774911c53c4c0384ff12a110b07a5afc4998ee2ada021808a15a28" }, "zip": { - "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "3a70cba6350a97d74d537799325a40612c17cc9901a2835ef80e2ccfb534bcfe" + "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "d5997d7c3cd0917b5fc0302a50eb0543897b5009371e5400c728901002d8ef04" }, "hex": { - "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530096, - "sha256": "77c76e4a61a06a547b0d49b40e29ea56263bb45d10091fc70d9c9472af48e3ad" + "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530140, + "sha256": "25fe01c8b9e2b73d9b8723e538dbeda48e136142884bb1769a527b4c183359db" } } }, @@ -513,22 +484,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "a68f9dec813758bfcdb6584aed61919de77e6de4ed7ff7bb054afe5579d70891" + "sha256": "c177e30bebd37a5a495780ebe472eaa49e50294f96cfb836568bce03dfc69d32" }, "zip": { - "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "a306f36d968b7cf92d3f849135049fb9dfbcd8bb650e5fe79e59b51c90e431b4" + "sha256": "4615d333486d6638fe7b10f0ed82b2bb4ba25424bf11b1662a35f117d6f4060a" }, "hex": { - "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 532702, - "sha256": "a9d52a8215645d8431d9665a5abb419dc895157c3afc65564bc2aeaa22cb21f1" + "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 532746, + "sha256": "5a6c937a222d2434a59b63eb7369a392f018404dbfc71913a27c48c64042fbec" } } }, @@ -544,22 +515,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "84bc013be4242f77369ac9e87e8465716a9fc535f29b33a7b9e5fc0e25a62793" + "sha256": "94270e321b602086b9b00a3a80d5f9cec07d16bd095efc252f2d6ddf4086adb9" }, "zip": { - "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194545, - "sha256": "b40783b87f87de5b206c98fe970f0a94f8d55ba728c888333a323a6641aab63d" + "sha256": "1d6436b2c4c434f5f5a0cb8646c97a3087afaf43cc79403da78ed85b36ea0313" }, "hex": { - "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 537246, - "sha256": "309a3f79d43bd638a31ea5bb54b8d4a9b3fb402e5c06ea017a02d69154cab39c" + "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 537290, + "sha256": "4513b435762d69db75fc233a099d21a02efe6481d75f38dd37a64aa5da5116a6" } } }, @@ -573,22 +544,22 @@ "storage": "adaptive", "files": { "uf2": { - "name": "update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 88064, - "sha256": "196c4720a4a4c04dcbce984b4fe9c995cdfac2551af9e5a2bb01b88a287a1bcc" + "sha256": "e26087a540b235b5968d7e5fee7e9c8f6dcff8480ad7ea922792e88307335b67" }, "zip": { - "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192833, - "sha256": "6fb37f8212d84263c1cdd8869cb844d8fbc5cbc470c864bf34221790ffc1b164" + "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192834, + "sha256": "4971a54970011c2df80ece58d917817de0d157e3fcb1a8490d1fa1f65e279db7" }, "hex": { - "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 535390, - "sha256": "f875900258e584dd9c577c05a21a2d65e1a511aa5e1b8e5680ad99d98fa910d7" + "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 535384, + "sha256": "2d8a773b4f94a9e2e6fc13923101ca6e66384330489014cb5e4c3968dc941a21" } } }, @@ -603,22 +574,22 @@ "storage": "adaptive", "files": { "uf2": { - "name": "update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 88064, - "sha256": "a628260984daaa7d29413bb59d23eccd104ad685e8fa6401d481dfac9fa2b00d" + "sha256": "40bb22fcfb90aae335a8bb6bb6e876f3407d87407ead8ee3908bb0ba0b2541a7" }, "zip": { - "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "size": 192834, - "sha256": "303d5d98d1e585798d11c6f37d74a8b33e9c99dd71a41b69d90b6efcd0d22565" + "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "size": 192833, + "sha256": "d3c12363a620083f18e828e724e0af67a2b141b21dc4768d0707cfaadc32b98d" }, "hex": { - "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 535786, - "sha256": "1d4bc0f07065976b6ddd01bf2eb27bb9804dac69cdaf418afc7445cf9507f33b" + "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 535780, + "sha256": "963cb6e47179551ce975b54502fb8dbe62a9601ba7432cbf8b23cf4c86f2a6ef" } } }, @@ -632,22 +603,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 84992, - "sha256": "71116deddf4aa41d96408fbc4f3702a07fcabd913a6c0c61c624f26145185541" + "sha256": "0d45f1d73fdcea41bf647347096b33919ae69ce957a06d6ff0e48be8767f1081" }, "zip": { - "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.zip", + "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", "size": 192834, - "sha256": "23088b4263d1ea1e2e594094ee2345c26b3d37e89a302862bbb4b1848baa99c4" + "sha256": "716520cce6e46b7b2d84272a8a5a29d1c38344dfef7455d974969b700527fda3" }, "hex": { - "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.10_s140_6.1.1.hex", - "size": 530818, - "sha256": "e59dbc29d7ab3aab00f0838195f521b6f48339e12e4f64c78c44d6ee9d6a0340" + "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", + "size": 530862, + "sha256": "d7503e65e4f37d028c58650bd31236ca493cff5dae1700e00baa8bbe2dbdca2a" } } }, @@ -671,22 +642,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "e78ac1a5f71ef5c3625c9742fd88ac2b1d807301c0adcca3db3b6afbafc92cb8" + "sha256": "1c6f18a21e0a6992f0ec2cb8f48e79ada5ccd7111b5b53f73b679c4dad10a9f5" }, "zip": { - "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194546, - "sha256": "1cc905af2ac0ed474df0ccde310faec7a10adc4c1b78c84c255de1bcee05bb78" + "sha256": "f0bb4832a96a625d18edb272fdc6ae2bc82ca291731f3edcd178c1c22bbe322d" }, "hex": { - "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 537284, - "sha256": "39a857cd4a2cdd5adfc10d805a9a5b2aea5c30ec474a2b1ddeb8408e3dfb1ad0" + "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 537328, + "sha256": "dbef733404fe15e06e0d6ffa006f34094a53a19fc62314cc28764afe3deb5154" } } }, @@ -700,27 +671,35 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_mbr.uf2", + "name": "update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", "size": 86016, - "sha256": "78fffe89ca5d9591f5851cbf610ea246789ec3ccf736bd8d799774bc4a82f4d3" + "sha256": "3a412b6eb12199b68cc3651082f0fb9f16ea1398c75537138d96068776e7084a" }, "zip": { - "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.zip", + "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", "size": 194546, - "sha256": "d878c8b815bd8519e840b3221219c4bef8af8c964b039f89c79d71a6641e759b" + "sha256": "954a5ee6b0d008e6918d91f0de5af7c80cf170c42ea9f8dc0957e9698a90f5c5" }, "hex": { - "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.10/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.10_s140_7.3.0.hex", - "size": 537318, - "sha256": "cf43bffaa2ba751b0bd773bd50d2e9c8f9c99741de950e6166ea416f1585f5fb" + "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", + "size": 537362, + "sha256": "07ccd6e07d9eebb6ce077302a393df2edb59c3106b93202a627f755c3980d53d" } } } ], "unavailableProfiles": [ + { + "id": "heltec_mesh_tower_v2", + "label": "Heltec MeshTower V2 - historical internal-only profile", + "meshcoreHardware": [ + "Heltec_tower_v2" + ], + "reason": "Retired from normal releases in favor of the combined SD/internal profile. Existing internal-only bootloaders require a one-time local installation of the normal combined image via USB/BLE DFU or SWD; do not use a cross-profile LoRa update." + }, { "id": "gat562_mesh_watch13", "label": "GAT562 Mesh Watch13", @@ -802,7 +781,7 @@ "meshcoreHardware": [ "ThinkNode_M8" ], - "reason": "ThinkNode M8 shares some flash wiring with M1 but has different buttons and peripheral controls. It needs an M8-specific profile." + "reason": "A dedicated M8 bootloader is awaiting factory USB identification and hardware testing. No verified download is available." }, { "id": "wio_wm1110", @@ -810,7 +789,14 @@ "meshcoreHardware": [ "wio_wm1110" ], - "reason": "Wio WM1110 is a separate tracker board with different LED and peripheral pins. Its XIAO build setting does not make it XIAO hardware." + "reason": "This firmware targets the Wio WM1110 Dev Kit (SDK carrier), whose LEDs and controls differ from Wio Tracker 1110 and XIAO. Its bootloader identity and installation route still need qualification." } - ] + ], + "profileSourceOverrides": { + "heltec_mesh_tower_v2_sdcard": { + "source_tag": "0.11.0-OTAFIX2.4.11", + "source_commit": "39177b9bbaa894352141734350d6ef2fab18dc4f", + "workflow_run": 37072081200 + } + } } diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 7de97a624..04263bfa7 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -317,9 +317,9 @@ // Corrected assets can have a new source hash on the existing release // page. Keep the complete version/flavor boundary, allowing its commit // suffix to differ from the original tag's commit. - const familyRoot = familyTag.replace(/-[0-9a-f]{7,8}$/i, ""); + const familyRoot = familyTag.replace(/-[0-9a-f]{7,40}$/i, ""); const sourceSuffix = familyRoot === familyTag - ? "(?:-[0-9a-f]{7,8})?" : "-[0-9a-f]{7,8}(?:-[0-9a-f]{7,8})?"; + ? "(?:-[0-9a-f]{7,40})?" : "-[0-9a-f]{7,40}(?:-[0-9a-f]{7,40})?"; const versionPattern = new RegExp( "^(.*?)-(ota-)?" + escapeRegExp(familyRoot) + sourceSuffix + "$", "i" @@ -407,6 +407,12 @@ function variantForProfile(role, tail) { let value = String(tail); + if (["repeater", "room", "sensor"].includes(role)) { + // Publication suffixes distinguish downloads, not physical wiring. + // Sensor support itself is read from qualified release controls below. + value = value.replace(/-(?:full|reduced)-ota$/i, "") + .replace(/-reduced$/i, ""); + } if (role === "companion") { value = value.replace( /^(?:full|wifi_mqtt|usb_ble|usb_wifi|ble|usb|wifi|serial|ethernet)(?=$|[_-])/i, @@ -468,7 +474,7 @@ ? "lora-source" : ""; let variant = variantForProfile(parts.role, parts.tail); - if (/^RAK_(?:3401|4631)_repeater_unified_lora_ota$/i.test(target)) { + if (/^RAK_(?:3401|4631)_repeater_unified_lora_ota(?:-(?:full|reduced)(?:-ota)?)?$/i.test(target)) { variant = "default"; } if (sourceHardware === "Station_G2_logging") { @@ -718,6 +724,29 @@ if (typeof controls.dedicatedUsbLogging === "boolean") { profile.dedicatedUsbLogging = controls.dedicatedUsbLogging; } + const sensorSource = controls.sensorProfileSource; + const sensorSourceMatches = typeof sensorSource === "string" && /^[0-9a-f]{40}$/i.test(sensorSource) && + profile.files.length > 0 && profile.files.every(function (file) { + const suffix = file.name.replace(/\.(bin|hex|uf2|zip)$/i, "").match(/-([0-9a-f]{7,40})$/i); + return suffix && sensorSource.toLowerCase().startsWith(suffix[1].toLowerCase()); + }); + if (controls.platform === "NRF52_PLATFORM" && ["repeater", "room", "sensor"].includes(profile.role) && + controls.otaRole === "lora-receiver" && Array.isArray(controls.updateMethods) && + controls.updateMethods.includes("lora") && sensorSourceMatches && + ["full", "reduced"].includes(controls.sensorProfile)) { + profile.sensorProfile = controls.sensorProfile; + profile.feature = controls.sensorProfile === "full" ? "full" : "standard"; + if (controls.sensorProfile === "full") { + // A full publication may retain an old reduced logical OTA identity. + // That identity must not mislabel the actual qualified sensor policy. + profile.variant = variantWithoutSensorMarker(profile.variant); + } + } + } + + function variantWithoutSensorMarker(value) { + return String(value || "default").replace(/(?:^|-)no-external-sensors(?=-|$)/g, "") + .replace(/^-+|-+$/g, "").replace(/--+/g, "-") || "default"; } function hardwareFamilyFor(hardware, hardwareNames) { @@ -998,6 +1027,11 @@ } function firmwareProfileLabel(profile) { + if (profile.sensorProfile) { + const label = (profile.sensorProfile === "full" ? "Full supported sensors" : "Reduced sensors") + " + LoRa OTA"; + const variant = variantWithoutSensorMarker(profile.variant); + return label + (variant !== "default" ? " - " + humanizeVariant(variant, profile.hardware) : ""); + } const parts = []; if (profile.feature === "full") parts.push("Full"); else if (profile.feature === "standard" && profile.ota !== "lora-receiver") { @@ -1342,7 +1376,12 @@ "Requires the external W25Q16 storage board, its exact documented wiring, and the matching storage-aware OTAFIX bootloader." ); } - if (profile.variant.includes("no-external-sensors")) { + if (profile.sensorProfile === "full") { + extra.push("Includes the complete sensor drivers supported by this board's full recipe, with LoRa OTA retained. Attached sensor hardware and compatible wiring are still required."); + } else if (profile.sensorProfile === "reduced") { + extra.push("Omits selected optional environmental/ranging sensor drivers to reduce flash use, with LoRa OTA retained. Generic I2C and supported board peripherals remain available."); + } + if (!profile.sensorProfile && profile.variant.includes("no-external-sensors")) { if (/^RAK_(3401|4631)$/i.test(profile.hardware)) { extra.push("This profile uses internal storage for LoRa OTA; no external storage board is required."); } diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index 5afa476f8..dd8841e9b 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -28,8 +28,8 @@ and partition policy: | Selection | Behavior | | --- | --- | -| `auto` | For one explicit target, pass 1 builds the complete supported LoRa-OTA-capable recipe. ESP32 boards with a qualified expanded profile use it; other standalone repeaters and room servers attempt the complete recipe in their current application region. Other roles keep every capability declared by their resolved PlatformIO recipe, including LoRa OTA, and fail instead of silently removing one that does not fit. A measured repeater or room-server flash/partition overflow starts the standard `no_external_sensors` LoRa OTA pass. Internal-flash nRF52 repeaters and room servers publish that reduced pass even when the complete image fits, because the smaller running image leaves more room to stage a delta; matched QSPI/SD roles do not need the redundant artifact. Compiler errors and missing-capability checks never trigger or conceal a reduced build. Canonical bulk commands keep their established standard partition contract. | -| `standard` | Immediately uses the deployed/portable partition contract and its documented reductions. This is useful when the operator already knows the expanded or complete image is unsuitable. | +| `auto` | nRF52 repeater, room-server, and sensor builds publish the required pair: Full supported sensors + LoRa OTA and Reduced sensors + LoRa OTA. Both must pass flash, RAM, packaged-application OTA, and layout checks; a reduced image cannot substitute for a failing full image. The same policy applies to canonical bulk/release builds and external-QSPI/SD recipes. ESP32 boards with a qualified expanded profile use it; other standalone ESP32 repeaters and room servers attempt the complete recipe in their current application region, with a measured flash/partition-overflow fallback to their reduced LoRa OTA recipe. Other roles keep every capability declared by their resolved PlatformIO recipe and fail instead of silently removing one that does not fit. Compiler errors and missing-capability checks never trigger or conceal a reduced build. | +| `standard` | Preserves the deployed/portable partition contract and documented reductions. nRF52 infrastructure still publishes both OTA sensor profiles in that same exact layout; this is not an override to remove OTA or skip the full profile. | | `full` | Requires a qualified ESP32 expanded-partition target (or an explicitly named Full Companion). Install a matching merged image when this changes the partition table. | Successful builds also emit @@ -107,7 +107,7 @@ getters; relaying a command over LoRa keeps its remote restrictions. | Standard non-MQTT repeater or room server | Keeps the normal role CLI and, where USB is a safe plaintext console, embeds debug/packet logging behind persistent `get/set usb.logging`. The explicitly selected portable policy can omit WebConfig and browser WiFi OTA, so those commands are unavailable and the omission is recorded in the capability manifest. | | Legacy standard logging | No longer emitted separately. Its behavior is compiled into the ordinary artifact. Size-constrained STM32 targets embed packet logging without verbose `MESH_DEBUG`. | | LoRa-OTA (`-ota-`) | LoRa OTA adds the `ota ...` commands; it does not otherwise reduce the role CLI. ESP32 `no_external_sensors` artifacts retain the compact browser WiFi uploader, the complete CLI, and up to 254 neighbors, subject to recorded internal-DRAM reductions. | -| Internal-flash nRF52 repeater/room-server auto pair | `full-ota` retains the board's external-sensor drivers; `reduced-ota` omits the declared optional sensors to leave additional internal-flash staging room. RAK3401 and RAK4631 reduced builds retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors at a measured cost below 5 KiB. Both artifacts carry the same stable OTA target identity and are checked for `ota ...` and `retry.preset`; RAK artifacts also verify the retained monitor drivers. | +| nRF52 repeater/room-server/sensor release pair | `full-ota` retains the complete supported sensor recipe; `reduced-ota` omits declared optional sensor drivers. Both include verified LoRa OTA, use the same logical target identity and exact storage/layout contract within the pair, and retain the role's normal commands. Repeaters/room servers also verify `retry.preset`. RAK3401 and RAK4631 reduced builds retain GPS where compatible and INA219/INA226/INA260/INA3221 I2C voltage/current monitors. External QSPI/SD targets also publish both choices. Companion/Terminal Chat and KISS roles do not enter this policy. | | ESP32 MQTT observer or ESP-NOW bridge | Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. | | FULL ESP32 USB + WiFi | Uses the matching MQTT target with packet logging 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. | @@ -135,6 +135,15 @@ USB logging is runtime controlled. Bulk and release-matrix commands omit legacy names whose behavior is already available from a canonical image: +- Every canonical nRF52 repeater, room-server, and sensor recipe emits two + separately named, qualified OTA artifacts: `full-ota` and `reduced-ota`. + The capability sidecar records `sensor.profile.full` or + `sensor.profile.reduced` so the picker can identify the actual profile. + Boards without optional sensor drivers still receive the two named choices; + the size saving may be small or zero. A missing or unqualified member blocks + publication of an incomplete pair. This does not make an unsupported + bootloader or a different physical storage layout compatible. + - Companion `_ps` names are replaced by the ordinary Companion image plus the persisted `set powersaving on|off` setting. - Companion `_femoff` names are replaced by the matching controllable-FEM @@ -181,6 +190,15 @@ only when no exact Full recipe has passed the combined flash/RAM qualification. ESP32 deliberately uses one TTY: Binary Companion and plaintext USB logging are mutually exclusive there. nRF52 retains its optional second CDC port. +The [OTAFIX 2.4.11 release](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11) +is the current bootloader reference, not an all-board qualification claim. +Consult its exact-board manifest and unavailable-profile reasons. Internal +application storage accepts exact-base in-place deltas that fit the workspace; +full application packages need the matching external NOR or microSD profile. +The retired internal-only MeshTower V2 bootloader requires one local +installation of the combined SD/internal profile. Its changed identity/layout +must not be crossed by an ordinary LoRa application or bootloader update. + ## Complete CLI policy The compact ESP32 CLI has been removed. `retry.preset` is a checked invariant diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 665db59a1..471680dae 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -20,7 +20,7 @@ The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, target names, and download links come directly from the published firmware assets. -
+
Current release set

Loading release information...

@@ -171,6 +171,8 @@ from the published firmware assets. | No logging | Normal standalone operation without the dedicated logging/MQTT profile | | Receives LoRa OTA | Repeater, room server, or sensor image that can stage an exact matching update received over LoRa; any sensor/storage tradeoff appears on the same Firmware profile button | | Receives LoRa OTA - Reduced optional sensors | Compact OTA image that omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals | +| Reduced sensors + LoRa OTA | Qualified nRF52 repeater, room server, or sensor build with the reduced optional sensor recipe and LoRa OTA retained | +| Full supported sensors + LoRa OTA | Qualified nRF52 build with all sensor drivers supported by that board's full recipe and LoRa OTA retained; does not add support for every possible attached sensor | | LoRa OTA source only | Full Companion serving a host-supplied update to another node without self-installing it | Connection and bridge choices depend on the selected role. Companion firmware @@ -396,8 +398,13 @@ is available. ESP32 and other chip families do not show this section. The web picker checks the latest stable OTAFIX release; the downloadable picker includes the bootloader release named in its saved catalog. If a board has no verified mapping or a file is missing, the picker explains that instead of -offering another board's bootloader. MeshTower internal and microSD remain -separate; RAK3401 and RAK4631 each use their adaptive storage bootloader. +offering another board's bootloader. OTAFIX 2.4.11 publishes a combined +MeshTower V2 SD/internal image, mapped only to `Heltec_tower_v2_sdcard`. +The historical `Heltec_tower_v2` internal-only profile is explicitly retired: +move to the normal combined image with a one-time local USB/BLE DFU or SWD +installation, not a cross-profile LoRa bootloader update. The picker does not +silently substitute its bootloader. RAK3401 and RAK4631 each use their +board-specific adaptive storage bootloader. Ikoka Stick, Nano and Handheld plus SolarXiao use the XIAO bootloader; Wio Tracker L1 1W and E-Ink use the L1 bootloader. See the [board coverage audit](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/blob/feature/ota-delta-apply/docs/meshcore-hardware-coverage.md) @@ -420,6 +427,18 @@ buttons. They separate revisions, display type, expansion kit, radio/PA layout, pin map, and other physical differences without crowding the Hardware dropdown. **Firmware profile** buttons combine OTA support, Full/standard, and choices that need a different image or wiring, such as serial port or external storage. +Future nRF52 repeater, room-server and sensor releases publish two separately +qualified choices: **Reduced sensors + LoRa OTA** and +**Full supported sensors + LoRa OTA**, with `-reduced-ota` and `-full-ota` artifact suffixes. Each keeps +its exact board/role OTA identity and storage layout; the suffix is not a new +radio or partition identity. Labels come from the release's qualified +`sensor.profile.reduced` / `sensor.profile.full` metadata and verified LoRa +receiver, not from the filename alone. Both profiles remain separate in local +and published releases, including boards whose optional sensor set is small +enough that the images have little or no size difference. Packaging requires +both profiles with matching packaged OTA identity and flash geometry, even +when a matrix is resumed or partial publication is requested. A failed build +is reported, never presented as a supported download. Companion is unchanged. The legacy `no_external_sensors` profile is labeled **Receives LoRa OTA - Reduced optional sensors** on the same button. On RAK3401 and RAK4631 this compact profile uses internal storage without an external storage board. The diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 42610f392..a5859948e 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -63,6 +63,12 @@ static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) { #define SENSOR_READ_INTERVAL_SECS 60 #endif +// Minimum interval between telemetry-triggered time acquisitions, not the +// persisted gps_interval which controls position update cadence. +#ifndef GPS_READ_INTERVAL_SECS + #define GPS_READ_INTERVAL_SECS 60 +#endif + /* ------------------------------ Code -------------------------------- */ #define FIRMWARE_VER_LEVEL 1 @@ -212,10 +218,8 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, } } // query other sensors -- target specific - if ((telemetry_permissions & TELEM_PERM_LOCATION) - && sensors.getLocationProvider() != nullptr - && sensors.getLocationProvider()->getGPSPowerSaving()) { - sensors.getLocationProvider()->syncTime(); + if (telemetry_permissions & TELEM_PERM_LOCATION) { + sensors.requestGpsTelemetryTimeSync(GPS_READ_INTERVAL_SECS); } sensors.querySensors(telemetry_permissions, telemetry); // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); diff --git a/lib/PsychicMqttClient/src/PsychicMqttClient.cpp b/lib/PsychicMqttClient/src/PsychicMqttClient.cpp index fe4119c8d..4b0161478 100644 --- a/lib/PsychicMqttClient/src/PsychicMqttClient.cpp +++ b/lib/PsychicMqttClient/src/PsychicMqttClient.cpp @@ -373,21 +373,52 @@ bool PsychicMqttClient::connected() return _connected; } -void PsychicMqttClient::connect() +esp_err_t PsychicMqttClient::applyConfig() { #if ESP_IDF_VERSION_MAJOR == 5 if (_mqtt_cfg.broker.address.uri == nullptr) - { - ESP_LOGE(TAG, "MQTT URI not set."); - return; - } - int desired_buffer = _mqtt_cfg.buffer.size > 0 ? _mqtt_cfg.buffer.size : 1024; #else if (_mqtt_cfg.uri == nullptr) +#endif { ESP_LOGE(TAG, "MQTT URI not set."); - return; + return ESP_ERR_INVALID_STATE; } + + if (_client == nullptr) + { + _client = esp_mqtt_client_init(&_mqtt_cfg); + if (_client == nullptr) return ESP_ERR_NO_MEM; + // Register only once. Retain a dirty config until the whole update succeeds. + const esp_err_t registered = + esp_mqtt_client_register_event(_client, MQTT_EVENT_ANY, _onMqttEventStatic, this); + if (registered != ESP_OK) + { + // No task has started yet, so this newly-created handle is safe to + // discard. Retry init+registration on the next start attempt. + esp_mqtt_client_destroy(_client); + _client = nullptr; + return registered; + } + _config_dirty = false; + return ESP_OK; + } + if (!_config_dirty) return ESP_OK; + const esp_err_t result = esp_mqtt_set_config(_client, &_mqtt_cfg); + if (result != ESP_OK) + { + ESP_LOGE(TAG, "Failed to apply MQTT config: %s", esp_err_to_name(result)); + return result; + } + _config_dirty = false; + return ESP_OK; +} + +esp_err_t PsychicMqttClient::connect() +{ +#if ESP_IDF_VERSION_MAJOR == 5 + int desired_buffer = _mqtt_cfg.buffer.size > 0 ? _mqtt_cfg.buffer.size : 1024; +#else int desired_buffer = _mqtt_cfg.buffer_size > 0 ? _mqtt_cfg.buffer_size : 1024; #endif @@ -402,38 +433,12 @@ void PsychicMqttClient::connect() { ESP_LOGE(TAG, "Failed to allocate reassembly buffer (%u bytes)", (unsigned)_buffer_capacity); _buffer_capacity = 0; + return ESP_ERR_NO_MEM; } } - if (_client == nullptr) - { - _client = esp_mqtt_client_init(&_mqtt_cfg); - // Register event handler only once when client is first created - // to avoid memory leak from repeated registrations - esp_mqtt_client_register_event(_client, MQTT_EVENT_ANY, _onMqttEventStatic, this); - _config_dirty = false; - } - else - { - if (_config_dirty) - { - esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg); - ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result); - if (cfg_result == ESP_OK) - { - _config_dirty = false; - ESP_LOGD(TAG, "connect(): applied mqtt config update"); - } - else - { - ESP_LOGW(TAG, "connect(): failed to apply mqtt config, will retry"); - } - } - else - { - ESP_LOGD(TAG, "connect(): mqtt config unchanged, skipping set_config"); - } - } + const esp_err_t cfg_result = applyConfig(); + if (cfg_result != ESP_OK) return cfg_result; esp_err_t start_result = esp_mqtt_client_start(_client); ESP_ERROR_CHECK_WITHOUT_ABORT(start_result); @@ -446,36 +451,22 @@ void PsychicMqttClient::connect() { ESP_LOGE(TAG, "MQTT client failed to start: %s", esp_err_to_name(start_result)); } + return start_result; } -void PsychicMqttClient::reconnect() +esp_err_t PsychicMqttClient::reconnect() { if (_client == nullptr) { ESP_LOGW(TAG, "MQTT client not initialized, cannot reconnect."); - return; + return ESP_ERR_INVALID_STATE; } - if (_config_dirty) - { - // Apply config only when mutating setters changed _mqtt_cfg. - esp_err_t cfg_result = esp_mqtt_set_config(_client, &_mqtt_cfg); - ESP_ERROR_CHECK_WITHOUT_ABORT(cfg_result); - if (cfg_result == ESP_OK) - { - _config_dirty = false; - ESP_LOGD(TAG, "reconnect(): applied mqtt config update"); - } - else - { - ESP_LOGW(TAG, "reconnect(): failed to apply mqtt config, reconnecting with previous config"); - } - } - else - { - ESP_LOGD(TAG, "reconnect(): mqtt config unchanged, skipping set_config"); - } - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_reconnect(_client)); + const esp_err_t cfg_result = applyConfig(); + if (cfg_result != ESP_OK) return cfg_result; + const esp_err_t result = esp_mqtt_client_reconnect(_client); + if (result != ESP_OK) return result; ESP_LOGI(TAG, "MQTT client reconnect requested."); + return ESP_OK; } void PsychicMqttClient::disconnect() diff --git a/lib/PsychicMqttClient/src/PsychicMqttClient.h b/lib/PsychicMqttClient/src/PsychicMqttClient.h index a2415560f..c00573da0 100644 --- a/lib/PsychicMqttClient/src/PsychicMqttClient.h +++ b/lib/PsychicMqttClient/src/PsychicMqttClient.h @@ -350,7 +350,7 @@ public: * * @note All parameters must be set before calling this method. */ - void connect(); + esp_err_t connect(); /** * @brief Reconnects a previously started MQTT client. @@ -360,7 +360,7 @@ public: * when auto-reconnect is disabled. Updates config before reconnecting * so credential changes (e.g., refreshed JWT tokens) take effect. */ - void reconnect(); + esp_err_t reconnect(); /** * @brief Disconnects the MQTT client from the server. @@ -504,6 +504,9 @@ private: static void _onMqttEventStatic(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); void _onMqttEvent(esp_event_base_t base, int32_t event_id, void *event_data); + // Configuration is transactional: never start/reconnect on stale credentials + // after a failed esp_mqtt_set_config(). + esp_err_t applyConfig(); bool _isTopicMatch(const char *topic, const char *subscription); // Fixed-size callback storage. Each slot is "used" once registered; we never diff --git a/scripts/build_local_release.py b/scripts/build_local_release.py index 5204e2c00..c2f4fb8f4 100644 --- a/scripts/build_local_release.py +++ b/scripts/build_local_release.py @@ -17,7 +17,7 @@ import tempfile from build_esp32_partition_migration import bundle_release, verify_archive from package_esp32_partition_migration import BOARDS -from package_cascade_release import category, collect_artifacts +from package_cascade_release import category, collect_artifacts, nrf52_sensor_profile ROOT = Path(__file__).resolve().parents[1] @@ -103,6 +103,8 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]: Explicit alternate images and old partition identities still belong in their separately built migration packages or direct --full-exact builds. This also removes stale sibling artifacts when a matrix build is resumed. + nRF52 Full/reduced sensor OTA records always pass through independently, + even when their logical target ID and storage layout are the same. """ chosen: dict[str, tuple[int, dict]] = {} passthrough: list[dict] = [] @@ -169,6 +171,8 @@ def stage_release(work: Path, migration_work: Path, destination: Path, version: files.append(str(target.relative_to(staging))) entries.append({"target": manifest["target"], "artifact_target": manifest["artifact_target"], "platform": manifest["platform"], "profile": manifest["build_profile"], + "sensor_profile": nrf52_sensor_profile(manifest), + "ota_update_methods": manifest.get("ota_update_methods", []), "files": files}) migrations = staging / "esp32-partition-migration" diff --git a/scripts/check_firmware_capabilities.py b/scripts/check_firmware_capabilities.py index 9813c6a49..e6a2470d2 100644 --- a/scripts/check_firmware_capabilities.py +++ b/scripts/check_firmware_capabilities.py @@ -15,6 +15,8 @@ import zlib sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools" / "mota")) from motalib import parse_nrf52_layout +OTAFIX_RELEASE = "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11" + def nrf52_lora_details(application): """Inspect application bytes, not just an ELF symbol or an OTA CLI stub.""" @@ -31,13 +33,14 @@ def nrf52_lora_details(application): notes = ["Use a destination-specific .mota package matching the board, target, and storage layout.", "An in-place delta requires the exact firmware currently running as its base.", "The package must fit the receiver's staging and apply workspace.", + "Check the release's exact-board bootloader manifest; hardware-pending boards are not covered by the normal release.", "Artifact inspection verifies compiled support; it does not verify the bootloader installed on a physical device."] if layout.auto_store: notes.append("One application detects RAK external NOR and chooses QSPI only with the exact matching OTAFIX bootloader; otherwise it uses internal flash where safe.") notes.append("External QSPI accepts full images and in-place deltas; internal flash accepts in-place deltas only and needs the retained-RAM OTAFIX profile.") - notes.append("The merged RAK OTAFIX bootloader is a local 2.4.8 build; install the exact RAK core package by serial/BLE DFU or SWD. Bootloader self-update is unavailable in this merged profile.") + notes.append("OTAFIX 2.4.11 provides normal RAK3401/RAK4631 *_auto bootloaders with standard board identities and internal signed bootloader-update staging. Historical *_AUTO_DFU or dedicated external identities need the documented local identity-matching migration, not a cross-profile LoRa update.") elif layout.hybrid_ram: - notes.append("Requires OTAFIX 2.4.6 retained-RAM handoff support; a transfer cannot resume after the receiver restarts.") + notes.append("Requires the exact-board retained-RAM handoff bootloader profile; a transfer cannot resume after the receiver restarts.") elif layout.external_backed: notes.append("Requires the matching external-storage hardware, wiring, and storage-aware OTAFIX bootloader.") else: @@ -45,10 +48,9 @@ def nrf52_lora_details(application): return { "storage": storage, "package_types": ["full", "in_place_delta"] if layout.auto_store or layout.external_backed else ["in_place_delta"], - "bootloader": ("Merged RAK3401 or RAK4631 OTAFIX bootloader, or the exact legacy storage profile" + "bootloader": ("Matching normal RAK3401 or RAK4631 adaptive OTAFIX bootloader, or the exact legacy storage profile" if layout.auto_store else "Matching board/storage OTAFIX bootloader"), - "bootloader_release": (None if layout.auto_store else - "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.6"), + "bootloader_release": OTAFIX_RELEASE, "notes": notes, } diff --git a/scripts/generate_picker_controls.py b/scripts/generate_picker_controls.py index 9a96f8b16..0a2940ca7 100644 --- a/scripts/generate_picker_controls.py +++ b/scripts/generate_picker_controls.py @@ -10,6 +10,7 @@ import re from pathlib import Path from package_esp32_partition_migration import BOARDS +from package_cascade_release import nrf52_sensor_profile ROOT = Path(__file__).resolve().parents[1] @@ -81,6 +82,12 @@ def runtime_metadata(manifest, mqtt, release_source=None): else: ota_role = 'none' result = {'otaRole': ota_role} + sensor_profile = nrf52_sensor_profile(manifest) + if sensor_profile: + source = manifest.get('source_commit', release_source) + if not isinstance(source, str) or not re.fullmatch(r'[0-9a-f]{40}', source): + raise ValueError('Sensor profiles require an exact source commit: ' + manifest['target']) + result.update(sensorProfile=sensor_profile, sensorProfileSource=source) if {'logging.usb.packets', 'logging.usb.control'} <= packaged: result['loggingModes'] = ['none', 'usb', 'wifi', 'both'] if mqtt else ['none', 'usb'] result['loggingControl'] = 'logging.output' if mqtt else 'usb.logging' diff --git a/scripts/package_cascade_release.py b/scripts/package_cascade_release.py index 4fc716c1a..7bfe5692f 100644 --- a/scripts/package_cascade_release.py +++ b/scripts/package_cascade_release.py @@ -12,6 +12,7 @@ import re import shutil import sys from urllib.parse import quote, urljoin +import zipfile sys.path.insert(0, str(Path(__file__).resolve().parent)) from firmware_memory_manifest import validate_package @@ -20,6 +21,9 @@ from package_nrf52_font_license import validate_artifact_notice ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools" / "mota")) +from motalib import parse_endf_ident, parse_nrf52_layout, target_id_for_env + FIRMWARE_SUFFIXES = {".bin", ".uf2", ".hex", ".zip"} @@ -27,12 +31,96 @@ def is_companion(target): return "companion" in target.lower() or "comp_radio" in target.lower() +def nrf52_sensor_profile(manifest): + """Read the qualified sensor policy, never infer it from an artifact name.""" + choices = {value.removeprefix("sensor.profile.") + for value in manifest.get("capabilities", []) if value.startswith("sensor.profile.")} + if not choices: + return "" + target = manifest["target"] + if len(choices) != 1 or not choices <= {"full", "reduced"}: + raise ValueError(f"{target}: ambiguous or unknown sensor profile") + verified = {item["capability"] for item in manifest.get("verification", []) + if item.get("present") is True and item.get("source") in + ("linked image", "packaged application")} + if (manifest.get("platform") != "NRF52_PLATFORM" or is_companion(target) + or not re.search(r"_(?:repeater|room_server|room_svr|sensor)(?:[_-]|$)", target, re.I) + or manifest.get("verified") is not True + or manifest.get("ota_update_verified") is not True + or "lora" not in manifest.get("ota_update_methods", []) + or "ota.update.lora" not in manifest.get("capabilities", []) + or "ota.update.lora" not in verified): + raise ValueError(f"{target}: sensor profile requires qualified nRF52 infrastructure LoRa OTA") + return next(iter(choices)) + + +def release_profile_label(manifest): + sensor = nrf52_sensor_profile(manifest) + if sensor: + return ("Full supported sensors" if sensor == "full" else "Reduced sensors") + " + LoRa OTA" + return manifest["build_profile"] + + +def validate_nrf52_sensor_pairs(records): + """Publish the new sensor policies only as an identity/layout-matched pair. + + The DFU application is the authority for identity and flash geometry. Old + releases without the new capability or publication suffix are unchanged. + Partial/resumed matrices must not publish just one half of a new pair. + """ + groups = {} + for record in records: + manifest = record["manifest"] + sensor = nrf52_sensor_profile(manifest) + artifact_target = manifest.get("artifact_target", manifest["target"]) + hint = (manifest.get("platform") == "NRF52_PLATFORM" + and (re.search(r"-(?:full|reduced)-ota$", artifact_target) + or any(re.search(r"-(?:full|reduced)-ota-v\d+\.", path.name) + for path in record["files"]))) + if not sensor: + if hint: + raise ValueError(f"{artifact_target}: sensor pair publication suffix lacks qualified metadata") + continue + if not artifact_target.endswith("-" + sensor + "-ota"): + raise ValueError(f"{artifact_target}: sensor pair publication suffix disagrees with metadata") + if any(not path.name.startswith(artifact_target + "-v") for path in record["files"] + if path.suffix in FIRMWARE_SUFFIXES): + raise ValueError(f"{artifact_target}: sensor pair artifact filename disagrees with metadata") + packages = [path for path in record["files"] if path.suffix == ".zip"] + if len(packages) != 1: + raise ValueError(f"{artifact_target}: sensor pair requires exactly one DFU application package") + try: + with zipfile.ZipFile(packages[0]) as package: + app = json.loads(package.read("manifest.json"))["manifest"]["application"] + application = package.read(app["bin_file"]) + except (OSError, ValueError, KeyError, zipfile.BadZipFile) as exc: + raise ValueError(f"{artifact_target}: invalid sensor pair DFU application: {exc}") from exc + identity = parse_endf_ident(application) + layout = parse_nrf52_layout(application) + if identity is None or layout is None or not identity.target_id or not identity.hw_id or not identity.fw_version: + raise ValueError(f"{artifact_target}: sensor pair needs valid packaged EndF identity and flash layout") + if identity.target_id != target_id_for_env(manifest["target"]): + raise ValueError(f"{artifact_target}: sensor pair packaged target ID disagrees with logical target") + if len(application) > layout.linked_app_end - layout.app_base: + raise ValueError(f"{artifact_target}: sensor pair application exceeds its linked flash region") + proof = (identity.target_id, identity.hw_id, identity.fw_version, + layout.app_base, layout.linked_app_end, layout.stage_ceiling, layout.flags, + manifest.get("ota_update_requirements", {}), manifest.get("source_commit")) + groups.setdefault(manifest["target"], []).append((sensor, proof)) + for target, pair in groups.items(): + if sorted(item[0] for item in pair) != ["full", "reduced"]: + raise ValueError(f"{target}: sensor pair requires exactly one full and one reduced profile") + if pair[0][1] != pair[1][1]: + raise ValueError(f"{target}: sensor pair disagrees on packaged target, firmware or storage layout") + + def validate_manifest(manifest): target = manifest["target"] if manifest.get("schema_version", 0) < 2 or not manifest.get("verified"): raise ValueError(f"{target}: firmware capability qualification failed") if not is_companion(target) and not manifest.get("ota_update_verified"): raise ValueError(f"{target}: infrastructure has no verified wireless updater") + nrf52_sensor_profile(manifest) if "companion_radio_full" in target.lower(): required = {"companion.usb_mota_source", "companion.mota_sender", "companion.temp_radio", "companion.ota_cli"} @@ -82,6 +170,7 @@ def collect_artifacts(directory, version): raise ValueError("firmware without qualification: " + ", ".join(sorted(p.name for p in unaccounted))) if not records: raise ValueError("no qualified firmware artifacts") + validate_nrf52_sensor_pairs(records) return records @@ -89,6 +178,8 @@ def category(record): manifest = record["manifest"] if is_companion(manifest["target"]): return "companion" + if nrf52_sensor_profile(manifest): + return "lora-ota" if manifest["build_profile"] == "full": return "full-profiles" if "lora_ota" in manifest["target"].lower(): @@ -227,10 +318,11 @@ def main(): label = "merged.bin (USB)" if path.name.endswith("-merged.bin") else path.suffix[1:] file_links.append(f'{html.escape(label)}') methods = ", ".join(manifest.get("ota_update_methods", [])) or "USB" - summaries.append({**manifest, "files": [path.name for path in record["files"]]}) - rows.append(f"{html.escape(manifest['artifact_target'])}{html.escape(manifest['build_profile'])}{html.escape(methods)}{' · '.join(file_links)}") + summaries.append({**manifest, "sensor_profile": nrf52_sensor_profile(manifest), + "files": [path.name for path in record["files"]]}) + rows.append(f"{html.escape(manifest['artifact_target'])}{html.escape(release_profile_label(manifest))}{html.escape(methods)}{' · '.join(file_links)}") (destination / "TARGET-MANIFEST.json").write_text(json.dumps(summaries, indent=2) + "\n") - columns = ("artifact_target", "target", "platform", "build_profile", + columns = ("artifact_target", "target", "platform", "build_profile", "sensor_profile", "ota_update_methods", "files") (destination / "TARGET-MANIFEST.tsv").write_text( "\t".join(columns) + "\n" + "".join( diff --git a/src/helpers/MQTTConnectionHealth.h b/src/helpers/MQTTConnectionHealth.h new file mode 100644 index 000000000..645a8f0c0 --- /dev/null +++ b/src/helpers/MQTTConnectionHealth.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +// Pure attempt-accounting rules shared by the bridge and host tests. Publishing +// failures are deliberately separate: a broker that never connects cannot publish. +namespace MQTTConnectionHealth { + +inline bool isOutageSlot(bool enabled, bool ready, bool connected) { + return enabled && ready && !connected; +} + +inline bool disconnectFailedAttempt(bool pending, bool was_connected) { + return pending && !was_connected; +} + +inline bool clearPendingAfterRejectedRequest(bool starting, bool was_pending) { + // A failed start left nothing running; reconnect can reject a duplicate while + // the previous attempt is still in flight. + // Its callback may complete during/after the SDK call. A rejected duplicate + // must leave the CURRENT flag untouched, never store true from a stale copy. + return starting || !was_pending; +} + +inline uint32_t incrementFailures(uint32_t count) { + return count == UINT32_MAX ? count : count + 1; +} + +inline int clampFailureTotal(uint64_t count) { + return count > static_cast(INT32_MAX) ? INT32_MAX : static_cast(count); +} + +struct Totals { + int up = 0; + int total = 0; + int breakers = 0; + uint64_t failures = 0; + uint32_t worst_outage_ms = 0; + bool outage_timed = false; +}; + +inline void addSlot(Totals& totals, bool enabled, bool ready, bool connected, + bool breaker, uint32_t outage_start, uint32_t now, + uint32_t connect_failures, uint32_t start_failures) { + // A disabled/unready slot is not an outage, but its history remains counted. + totals.failures += static_cast(connect_failures) + start_failures; + if (!enabled || !ready) return; + totals.total++; + if (breaker) totals.breakers++; + if (connected) { + totals.up++; + } else if (outage_start != 0) { + const uint32_t elapsed = now - outage_start; // wrap-safe under 49.7 days + if (!totals.outage_timed || elapsed > totals.worst_outage_ms) { + totals.worst_outage_ms = elapsed; + } + totals.outage_timed = true; + } +} + +} // namespace MQTTConnectionHealth diff --git a/src/helpers/MQTTMessageBuilder.cpp b/src/helpers/MQTTMessageBuilder.cpp index 34b206779..5b836f159 100644 --- a/src/helpers/MQTTMessageBuilder.cpp +++ b/src/helpers/MQTTMessageBuilder.cpp @@ -61,13 +61,14 @@ int MQTTMessageBuilder::buildStatusMessage( int internal_heap, int packets_sent, int packets_received, - const char* repeat + const char* repeat, + const MQTTConnHealth& conn_health ) { return MQTTPayloadBuilder::buildStatusMessage( doc, origin, origin_id, model, firmware_version, radio, client_version, status, timestamp, buffer, buffer_size, battery_mv, uptime_secs, errors, queue_len, noise_floor, tx_air_secs, rx_air_secs, recv_errors, internal_heap, - packets_sent, packets_received, repeat); + packets_sent, packets_received, repeat, conn_health); } int MQTTMessageBuilder::buildPacketMessage( diff --git a/src/helpers/MQTTMessageBuilder.h b/src/helpers/MQTTMessageBuilder.h index 496cebbc2..10a43d087 100644 --- a/src/helpers/MQTTMessageBuilder.h +++ b/src/helpers/MQTTMessageBuilder.h @@ -70,6 +70,7 @@ public: * @param recv_errors Radio receive/CRC errors (optional, -1 to omit) * @param internal_heap Internal heap free bytes (optional, -1 to omit) * @param repeat Repeat/forwarding status ("on" or "off"); nullptr omits the field + * @param conn_health Slot connection health; defaults omit every field * @return Length of JSON string, or 0 on error */ static int buildStatusMessage( @@ -95,7 +96,8 @@ public: int internal_heap = -1, int packets_sent = -1, int packets_received = -1, - const char* repeat = nullptr + const char* repeat = nullptr, + const MQTTConnHealth& conn_health = MQTTConnHealth() ); /** diff --git a/src/helpers/MQTTPayloadBuilder.cpp b/src/helpers/MQTTPayloadBuilder.cpp index 2209403dc..b102498aa 100644 --- a/src/helpers/MQTTPayloadBuilder.cpp +++ b/src/helpers/MQTTPayloadBuilder.cpp @@ -46,7 +46,8 @@ int MQTTPayloadBuilder::buildStatusMessage( int internal_heap, int packets_sent, int packets_received, - const char* repeat + const char* repeat, + const MQTTConnHealth& conn_health ) { doc.clear(); JsonObject root = doc.to(); @@ -63,9 +64,12 @@ int MQTTPayloadBuilder::buildStatusMessage( root["repeat"] = repeat; } + const bool has_conn_health = conn_health.slots_up >= 0 || conn_health.slots_total >= 0 || + conn_health.worst_outage_secs >= 0 || conn_health.heap_largest >= 0 || + conn_health.connect_failures > 0 || conn_health.slots_breaker > 0; if (battery_mv >= 0 || uptime_secs >= 0 || errors >= 0 || queue_len >= 0 || noise_floor > -999 || tx_air_secs >= 0 || rx_air_secs >= 0 || recv_errors >= 0 || - internal_heap >= 0 || packets_sent >= 0 || packets_received >= 0) { + internal_heap >= 0 || packets_sent >= 0 || packets_received >= 0 || has_conn_health) { JsonObject stats = root["stats"].to(); if (battery_mv >= 0) stats["battery_mv"] = battery_mv; @@ -79,6 +83,18 @@ int MQTTPayloadBuilder::buildStatusMessage( if (rx_air_secs >= 0) stats["rx_air_secs"] = rx_air_secs; if (recv_errors >= 0) stats["recv_errors"] = recv_errors; if (internal_heap >= 0) stats["internal_heap"] = internal_heap; + if (conn_health.heap_largest >= 0) stats["heap_largest"] = conn_health.heap_largest; + if (conn_health.slots_up >= 0) stats["mqtt_slots_up"] = conn_health.slots_up; + if (conn_health.slots_total >= 0) stats["mqtt_slots_total"] = conn_health.slots_total; + // Absence is not proof of health: an initial attempt has no timed outage yet. + // mqtt_slots_up < mqtt_slots_total is the alert condition. + if (conn_health.worst_outage_secs >= 0) { + stats["mqtt_outage_secs"] = conn_health.worst_outage_secs; + } + if (conn_health.connect_failures > 0) { + stats["mqtt_connect_failures"] = conn_health.connect_failures; + } + if (conn_health.slots_breaker > 0) stats["mqtt_slots_breaker"] = conn_health.slots_breaker; } return serializeComplete(root, buffer, buffer_size); diff --git a/src/helpers/MQTTPayloadBuilder.h b/src/helpers/MQTTPayloadBuilder.h index 6cc7c7a2b..b76e58363 100644 --- a/src/helpers/MQTTPayloadBuilder.h +++ b/src/helpers/MQTTPayloadBuilder.h @@ -4,6 +4,17 @@ #include #include +// A healthy slot reports sibling outages because a disconnected slot cannot +// report itself. Defaults omit the fields for existing callers. +struct MQTTConnHealth { + int slots_up = -1; + int slots_total = -1; // enabled and fully configured, connected or not + int worst_outage_secs = -1; // no timed outage until the first DISCONNECTED + int heap_largest = -1; // trend sampled at publish time, not per attempt + int connect_failures = -1; // failed attempts/starts across all slots since boot + int slots_breaker = -1; +}; + // Mesh-independent JSON serialization core for MQTT publication payloads. // MQTTMessageBuilder keeps the firmware-facing API and delegates these three // deterministic contracts here so they can be exercised by native tests. @@ -32,7 +43,8 @@ public: int internal_heap = -1, int packets_sent = -1, int packets_received = -1, - const char* repeat = nullptr + const char* repeat = nullptr, + const MQTTConnHealth& conn_health = MQTTConnHealth() ); static int buildPacketMessage( diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 361c45638..7971f29da 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -101,6 +101,20 @@ public: } virtual void loop() { } virtual void setTelemetryLocationAccessAvailable(bool available); + bool requestGpsTelemetryTimeSync(uint64_t min_interval_secs) { +#if ENV_INCLUDE_GPS + // A denied request must not consume the provider's throttle or reclaim a + // bridge-owned UART. Shared GPS rails make isEnabled() an unsafe substitute + // for the manager's established transport ownership. + if (!gps_transport_available || !telemetryGpsDetected()) return false; + LocationProvider* location = getLocationProvider(); + return location != nullptr && location->getGPSPowerSaving() + && location->requestTimeSync(min_interval_secs); +#else + (void)min_interval_secs; + return false; +#endif + } void setGpsTelemetryStopAfterFix(bool enabled) { #if ENV_INCLUDE_GPS gps_stop_after_fix = enabled; diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index c892be89f..290da840c 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -423,12 +423,21 @@ void MQTTBridge::formatMqttStatsReply(char* buf, size_t bufsize) { if (b->_filtered_packets > 0) { replyAppendf(buf, bufsize, &pos, " filt=%lu", b->_filtered_packets); } + int down = 0; + for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { + if (MQTTConnectionHealth::isOutageSlot( + b->_slots[i].enabled, b->isSlotReady(i), b->_slots[i].connected)) down++; + } + if (down > 0) replyAppendf(buf, bufsize, &pos, " down=%d", down); replyAppendf(buf, bufsize, &pos, " |"); for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { if (!b->_slots[i].enabled || !b->_slots[i].client) continue; - replyAppendf(buf, bufsize, &pos, " s%d=%lu/%lu", i + 1, + // Preserve existing sN=ok/err parsers; the suffix marks a down slot. + replyAppendf(buf, bufsize, &pos, " s%d=%lu/%lu%s", i + 1, b->_slots[i].client->getPublishOk(), - b->_slots[i].client->getPublishErr()); + b->_slots[i].client->getPublishErr(), + MQTTConnectionHealth::isOutageSlot( + b->_slots[i].enabled, b->isSlotReady(i), b->_slots[i].connected) ? "!" : ""); } } @@ -632,6 +641,13 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index) replyAppendf(buf, bufsize, &pos, ", no error info"); } + // Error detail has priority when a remote reply is clamped. + const uint64_t cf = static_cast(slot.connect_failures) + slot.start_failures; + if (cf > 0) { + replyAppendf(buf, bufsize, &pos, ", cf:%lu", + static_cast(MQTTConnectionHealth::clampFailureTotal(cf))); + } + // Appended last so it never displaces connection diagnostics. replyAppendf // clamps rather than overflows, but a clipped type list is worse than no // list: "...,13,14," parses as a real, different allowlist, and this is the @@ -774,6 +790,7 @@ MQTTBridge::MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs, _slots[i].port = 1883; _slot_reconfigure_pending[i] = false; _slot_force_jwt_mint[i] = false; + _slot_attempt_pending[i] = false; _status_publish_pending[i] = false; } @@ -1764,6 +1781,7 @@ bool MQTTBridge::ensureSlotClient(int index) { slot.client->onConnect([this, index](bool sessionPresent) { MQTT_DEBUG_PRINTLN("MQTT%d connected", index + 1); + _slot_attempt_pending[index] = false; _slots[index].connected = true; _slot_force_jwt_mint[index] = false; // NOTE: reconnect_backoff / max_backoff_failures are NOT reset here. @@ -1795,6 +1813,14 @@ bool MQTTBridge::ensureSlotClient(int index) { }); slot.client->onDisconnect([this, index](bool sessionPresent) { MQTT_DEBUG_PRINTLN("MQTT%d disconnected", index + 1); + // A deliberate stop clears the flag first. A late disconnect from a + // bounced live session must not be mistaken for a failed new attempt. + if (MQTTConnectionHealth::disconnectFailedAttempt( + _slot_attempt_pending[index], _slots[index].connected)) { + _slot_attempt_pending[index] = false; + _slots[index].connect_failures = + MQTTConnectionHealth::incrementFailures(_slots[index].connect_failures); + } _slots[index].disconnect_count++; if (_slots[index].first_disconnect_time == 0) { _slots[index].first_disconnect_time = millis(); @@ -1872,6 +1898,7 @@ void MQTTBridge::releaseSlotAuthToken(int index) { void MQTTBridge::destroySlotClients(bool force) { for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { MQTTSlot& slot = _slots[i]; + _slot_attempt_pending[i] = false; if (slot.client != nullptr) { if (force) { slot.client->forceStop(); @@ -1941,6 +1968,7 @@ bool MQTTBridge::setupSlot(int index) { // fields in place before connect() restarts the ESP-IDF client. if (slot.initial_connect_done) { if (slot.client->connected()) { + _slot_attempt_pending[index] = false; slot.client->disconnect(); } // Clear TLS verification fields so a stale CA-bundle attach or cert @@ -2127,7 +2155,15 @@ bool MQTTBridge::setupSlot(int index) { } } - slot.client->connect(); + // A reconfigure can retain a started client. Use reconnect in that case, + // and do not claim activation after a rejected start/config transaction. + const esp_err_t result = reconnectSlotClient(index); + if (result != ESP_OK) { + MQTT_DEBUG_PRINTLN("MQTT%d start failed (%s) - will retry", index + 1, + esp_err_to_name(result)); + slot.last_reconnect_attempt = millis(); + return false; + } slot.initial_connect_done = true; return true; } @@ -2140,6 +2176,7 @@ void MQTTBridge::teardownSlot(int index, bool force) { if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return; MQTTSlot& slot = _slots[index]; + _slot_attempt_pending[index] = false; if (slot.client && (force || slot.client->connected())) { if (force) { slot.client->forceStop(); @@ -2170,17 +2207,30 @@ void MQTTBridge::teardownSlot(int index, bool force) { _slot_force_jwt_mint[index] = false; } -void MQTTBridge::reconnectSlotClient(int index) { - if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return; +esp_err_t MQTTBridge::reconnectSlotClient(int index) { + if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return ESP_ERR_INVALID_ARG; MQTTSlot& slot = _slots[index]; - if (slot.client == nullptr) return; + if (slot.client == nullptr) return ESP_ERR_INVALID_STATE; - if (!slot.client->isStarted()) { + const bool was_pending = _slot_attempt_pending[index]; + const bool starting = !slot.client->isStarted(); + _slot_attempt_pending[index] = true; // callbacks may run before the call returns + esp_err_t result; + if (starting) { MQTT_DEBUG_PRINTLN("MQTT%d start (client was stopped)", index + 1); - slot.client->connect(); - return; + result = slot.client->connect(); + } else { + result = slot.client->reconnect(); } - slot.client->reconnect(); + if (result != ESP_OK) { + if (MQTTConnectionHealth::clearPendingAfterRejectedRequest(starting, was_pending)) { + _slot_attempt_pending[index] = false; + } + if (starting) { + slot.start_failures = MQTTConnectionHealth::incrementFailures(slot.start_failures); + } + } + return result; } void MQTTBridge::maintainSlotConnections() { @@ -2336,13 +2386,11 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns MQTT_DEBUG_PRINTLN("MQTT%d token renewal: reconnecting with fresh credentials", index + 1); if (slot.client->isStarted()) { // Retain the esp-mqtt task and its stack across the TLS handshake. + _slot_attempt_pending[index] = false; slot.client->softDisconnect(); - slot.client->setCredentials(_jwt_username, slot.auth_token); - slot.client->reconnect(); - } else { - slot.client->setCredentials(_jwt_username, slot.auth_token); - slot.client->connect(); } + slot.client->setCredentials(_jwt_username, slot.auth_token); + reconnectSlotClient(index); reconnect_attempted = true; _last_slot_reconnect_ms = now_millis; MQTT_DEBUG_PRINTLN("MQTT%d int_heap=%d at token renewal reconnect", index + 1, @@ -2748,7 +2796,7 @@ void MQTTBridge::publishStatusToSlot(int index) { battery_mv, uptime_secs, errors, _queue_count, noise_floor, tx_air_secs, rx_air_secs, recv_errors, internal_heap_free, packets_sent, packets_received, - repeatStatus() + repeatStatus(), collectConnHealth() ); if (len > 0) { @@ -2764,6 +2812,27 @@ void MQTTBridge::publishStatusToSlot(int index) { } } +MQTTConnHealth MQTTBridge::collectConnHealth() const { + MQTTConnHealth health; + MQTTConnectionHealth::Totals totals; + const uint32_t now = static_cast(millis()); + for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { + const MQTTSlot& slot = _slots[i]; + // Counters survive disabling/reconfiguring a slot and bridge restarts. + // Use a wide sum so the exposed counter saturates instead of wrapping. + MQTTConnectionHealth::addSlot(totals, slot.enabled, isSlotReady(i), slot.connected, + slot.circuit_breaker_tripped, static_cast(slot.current_outage_started_ms), + now, slot.connect_failures, slot.start_failures); + } + health.slots_up = totals.up; + health.slots_total = totals.total; + health.slots_breaker = totals.breakers; + if (totals.outage_timed) health.worst_outage_secs = static_cast(totals.worst_outage_ms / 1000UL); + health.connect_failures = MQTTConnectionHealth::clampFailureTotal(totals.failures); + health.heap_largest = static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); + return health; +} + void MQTTBridge::updateCachedConnectionStatus() { bool any_connected = false; for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { @@ -2986,6 +3055,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) { // Disconnect all slot clients when WiFi drops for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { if (_slots[i].client && _slots[i].connected) { + _slot_attempt_pending[i] = false; _slots[i].client->disconnect(); } } @@ -3684,7 +3754,7 @@ bool MQTTBridge::publishStatus() { battery_mv, uptime_secs, errors, _queue_count, noise_floor, tx_air_secs, rx_air_secs, recv_errors, internal_heap_free, packets_sent, packets_received, - repeatStatus() + repeatStatus(), collectConnHealth() ); if (len > 0) { @@ -4442,8 +4512,9 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) { } else if (action == MQTTConnectionPolicy::StaleTokenAction::Bounce) { MQTT_DEBUG_PRINTLN( "MQTT%d bouncing for the corrected-clock token", i + 1); + _slot_attempt_pending[i] = false; _slots[i].client->softDisconnect(); - _slots[i].client->reconnect(); + reconnectSlotClient(i); } else { MQTT_DEBUG_PRINTLN( "MQTT%d token re-created without a live-session bounce", i + 1); diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 2401691e2..9592628bb 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -11,6 +11,8 @@ #include #include "helpers/JWTHelper.h" #include "helpers/MQTTConnectionPolicy.h" +#include "helpers/MQTTConnectionHealth.h" +#include "helpers/MQTTPayloadBuilder.h" #include "helpers/MQTTPacketFilter.h" #include "helpers/MQTTPresets.h" #include "helpers/MQTTLifecycle.h" @@ -144,11 +146,13 @@ private: int last_sock_errno; // socket errno unsigned long last_error_time; // millis() of last error uint32_t disconnect_count; // Number of disconnect callbacks since boot + uint32_t connect_failures; // attempts ending before onConnect + uint32_t start_failures; // rejected starts (not rejected reconnects) unsigned long first_disconnect_time; // millis() of first disconnect after boot // Current-outage timer (used by AlertReporter to fire faults after a sustained // outage). Reset to 0 on each successful connect, set to millis() on first - // disconnect-after-connect. first_disconnect_time is intentionally separate + // DISCONNECTED (including a failed initial attempt). first_disconnect_time is intentionally separate // so the existing 'mqttN.diag' "first_disc" semantics don't change. unsigned long current_outage_started_ms; }; @@ -250,6 +254,10 @@ private: // callback sets this byte and the bridge task consumes it. volatile bool _slot_force_jwt_mint[RUNTIME_MQTT_SLOTS]; + // Set before starting/reconnecting, cleared by connect/failed-attempt/stop. + // Must be set before the SDK call because its event task can callback at once. + volatile bool _slot_attempt_pending[RUNTIME_MQTT_SLOTS]; + // Pending on-connect status publish: set from the onConnect callback (which // runs on the esp-mqtt event task, NOT this bridge task), consumed by the MQTT // task (Core 0). publishStatusToSlot() touches the shared status doc/buffer/ @@ -337,9 +345,10 @@ private: // internal heap. On non-PSRAM: inline in the class object so the allocation doesn't // interleave with large TLS buffers at startup. static const size_t PUBLISH_JSON_BUFFER_SIZE = 2048; - // Status keeps its own smaller ceiling: raising it would change which oversized - // status documents get published instead of dropped. - static const size_t STATUS_JSON_BUFFER_SIZE = 768; + // Actual field maxima (including JSON-escaped node names) plus slot health + // exceed 768. Reuse the existing 2048-byte scratch; only the PSRAM-failure + // stack fallback grows by 256 bytes. + static const size_t STATUS_JSON_BUFFER_SIZE = 1024; static_assert(STATUS_JSON_BUFFER_SIZE <= PUBLISH_JSON_BUFFER_SIZE, "status payloads serialize into the shared publish buffer"); #if defined(BOARD_HAS_PSRAM) @@ -484,7 +493,7 @@ private: int activatedSlotCount() const; bool canActivateSlot(int index) const; void teardownSlot(int index, bool force = false); - void reconnectSlotClient(int index); + esp_err_t reconnectSlotClient(int index); void maintainSlotConnections(); // Maintain all slot connections (token renewal, reconnect) void maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced, bool& reconnect_attempted, bool& teardown_attempted); bool createSlotAuthToken(int index); // Create/renew JWT token for a slot @@ -495,6 +504,7 @@ private: bool publishToSlot(int index, const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0); bool publishToAllSlots(const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0); void publishStatusToSlot(int index); + MQTTConnHealth collectConnHealth() const; void updateCachedConnectionStatus(); void processPacketQueue(); diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 2d5191500..4e0f62e8f 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -691,8 +691,11 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, char charpin[20]; snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code); - // If we want to control BLE LED ourselves, uncomment this: - // Bluefruit.autoConnLed(false); +#ifdef DISABLE_BLE_LED + // Opt out of Bluefruit's automatic LED control without changing the normal + // board/library default or the application's own LED handling. + Bluefruit.autoConnLed(false); +#endif // Configure individual connection values rather than the all-or-nothing // Bluefruit preset. Defaults are identical to BANDWIDTH_MAX; constrained // board profiles can lower event/buffer reservations without shrinking the diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h index e4a4bd767..c1f4603bc 100644 --- a/src/helpers/ota/OtaTargets.h +++ b/src/helpers/ota/OtaTargets.h @@ -3,7 +3,7 @@ #include // AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand. -// 634 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) +// 659 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) // to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT // transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes. // Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID; @@ -32,373 +32,375 @@ inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t if (!buffer || !capacity) return nullptr; buffer[0] = 0; static const uint32_t IDS[] = { - 0x149bcb23, 0x30d4e442, 0x263a8d80, 0x9070ae2c, 0xf190d834, 0x32890982, 0xacb599d5, 0x82d3b273, - 0xe96a6af8, 0x180bbe1f, 0xf9986af5, 0x98519a1d, 0x06b71719, 0x6384719d, 0x4c0461c8, 0x4fa88992, - 0xcd6604d0, 0xc54ea258, 0x217958b4, 0x03d9704a, 0x8456f753, 0xc8104197, 0xf5eb61ec, 0x70d2d590, - 0xd5e04824, 0xa416bb20, 0x875f3744, 0xdaf349be, 0x93b93199, 0x710abb59, 0x5293da13, 0xc6dd7723, - 0xf2eec98c, 0x48a986f1, 0xd2cf978c, 0x53626334, 0xefe4f19f, 0xfcaf3730, 0xd90deef7, 0x2d2a4089, - 0x89ea5c4e, 0xc0eb712a, 0x67293f43, 0x3f364910, 0xac4b4eb5, 0x822e96ca, 0xe077fb8d, 0x2bbe7fd8, - 0x9e40157c, 0xb12e6e01, 0xf6f835ec, 0x7fab017f, 0x9e23928a, 0xf310a4d1, 0xdd5953ec, 0x97574bc2, - 0x2d918a3c, 0xd1755faa, 0x8a6ceaf0, 0x57f69746, 0xbc05d6a7, 0x5bad80f1, 0xb4f727a8, 0x112d34c4, - 0xf10a26df, 0xc5409ced, 0xf9022545, 0x2023036d, 0x4abd24af, 0x35d42b35, 0x9f736ca7, 0x3fbb1498, - 0x42b0818c, 0x80dc72a9, 0xafeed10c, 0x98411ac6, 0xa14d9f4a, 0x6494fda0, 0x32bcdd14, 0xfe937f14, - 0x5b9b1933, 0x1b4399de, 0xf7e47568, 0xb1153422, 0x388f2804, 0x075ca3f9, 0x2f9f8e7d, 0xc9dc5d8c, - 0x34b0e5f3, 0xb4d0ad97, 0xcfa0e233, 0x17d5e1ff, 0xc99313c6, 0xffb3854f, 0x21a73973, 0x3067f656, - 0xfdb25057, 0x836e5931, 0x0f1767cd, 0x57172fcc, 0xa529b4f5, 0x85a8c944, 0x3c60586d, 0x0a9dbbf0, - 0xb519a89f, 0xa3e8d8ce, 0xb16009a0, 0xdc780e80, 0x28b8007c, 0x4aa63180, 0x6fddcbf1, 0xf4fb5484, - 0x2e5384e2, 0xb62a1e75, 0x9e64845d, 0x19232e8b, 0xfaf164f4, 0x38b948b4, 0x462ddb9b, 0xed21ca4f, - 0x9f5f39a7, 0x081d2219, 0x7c498bc4, 0xc283ad60, 0x4fbdc1e1, 0xc797fd73, 0xab9871c8, 0x2075687c, - 0x5d726067, 0x9969bbc9, 0x804f7a3a, 0x0823a21a, 0x22f900de, 0xaca0e153, 0x516c9c13, 0xcb1c2e65, - 0xd1b29b18, 0x644bb68d, 0xd19a9759, 0xde40f56f, 0x6dc15ab3, 0xc59d294e, 0xceeddfaf, 0x21519537, - 0x382bb181, 0xad1bcd63, 0x5a2980e6, 0x90581c83, 0x90a6a854, 0x356df17c, 0x99a084fc, 0x64c9dfc9, - 0x2a2ffc92, 0x2c13b8e9, 0x1ee7d867, 0xb016979a, 0xb49b6b55, 0x01d8f124, 0x86fb85f0, 0xd33b0986, - 0x1a4d0096, 0xa8f5940f, 0x1cf5e14c, 0x2707d3d1, 0x86ac3b1b, 0x7af848da, 0xd522a14f, 0x76657d76, - 0x21ee03bd, 0x5977d941, 0xe2a1907b, 0xbb0ca8db, 0xf75feb3e, 0xb0a1e71f, 0xbc9cdc70, 0x19e31fc6, - 0xa01b330e, 0x36c78d86, 0xdf722326, 0xfa901c87, 0xeebe3469, 0xb0f5aff6, 0x5729ad07, 0xaa2aef42, - 0xb4cb035a, 0xbb429dde, 0x883e3edc, 0x4a9d1c6c, 0xa0965a6b, 0x59ea25fa, 0x1758bdb9, 0x1313f16d, - 0x0372b0f1, 0xa2fb4192, 0xdd39457b, 0x7964461c, 0x1d1c0d92, 0x5fc52bcb, 0x0dacb683, 0xe792a051, - 0x2d5ea842, 0x30c59b1d, 0xab74db9e, 0xeed78ce4, 0x7778b7e1, 0x0b6847ed, 0xab3e1ad3, 0x04a8b763, - 0xc456437e, 0xb3ecafca, 0xf3afcde6, 0x87513d56, 0xca812b41, 0x18594231, 0x188f3ac1, 0x402055ec, - 0x12393f1f, 0x16fbab07, 0xed3ef5b8, 0x8ea86381, 0x09f57289, 0x148aeaab, 0x6e9f7fec, 0x6a25d024, - 0xc5a2cb41, 0xc4a11710, 0x753947a4, 0xe6fa98b4, 0xf8c27cf4, 0x90e227d9, 0x812f5e52, 0x61f961c5, - 0x1ddd15c8, 0x49d67d0d, 0xbc003322, 0x835b70c0, 0xd7460575, 0x36acced5, 0x33e2c171, 0x5be91148, - 0xa165ae99, 0x04c5c2b7, 0x6c1ca659, 0xa295fb04, 0x61c405ca, 0xe893fe91, 0x2b599d31, 0xf1442d25, - 0x15082ede, 0x6ff350d1, 0xe03d0cd9, 0x82e1eb2c, 0x0ee19b12, 0x6c67ed89, 0xd1c05d0d, 0xb90150b0, - 0x7285bdb3, 0x77cadc99, 0xc177e6cd, 0xf7a38993, 0x83762f33, 0x86cadda9, 0x7e49da49, 0x305d6f92, - 0xbc6b870b, 0x78845fdd, 0x6d35f434, 0xa52d76a8, 0xd8de9639, 0x8ab5c51b, 0x80097676, 0xffe70df5, - 0x3da80417, 0x47ca3c13, 0x28b499c7, 0xf1ab4c28, 0xe5331f02, 0xfba7177b, 0xde7dc472, 0x3625bb80, - 0x69df7765, 0xe9fbc9be, 0x40818863, 0x65deac33, 0xd86cf67a, 0xed73ac03, 0xab0d550a, 0x1bd2a266, - 0x3655c748, 0x9a5ae8fc, 0x1e9eb97a, 0x139536f4, 0x0716c724, 0x8de23fa7, 0xb92ec96b, 0x420ebc63, - 0x967ccaee, 0x8021afa2, 0x5f7c7688, 0xbd04e6e7, 0x1aeac884, 0x07849209, 0x0c79b100, 0xbfb60e7c, - 0x8a63de3a, 0x96036a68, 0xb76a745c, 0x43023193, 0xa7d97db7, 0x468a133e, 0xb59ea2ed, 0xbe372c6d, - 0x6215e9eb, 0x07907102, 0x3898ea56, 0x2fb7e218, 0x8c61d4c8, 0x4f722e70, 0x694eed25, 0x85b8b9d7, - 0xe8421e36, 0x82eaaf9e, 0x9f9caba4, 0xe4a2de74, 0x582c7826, 0xaeb637f1, 0x86813271, 0x67b2ff84, - 0xd0e2e616, 0x3f9edfe3, 0xc23f8082, 0x0e74ae16, 0x8f256e56, 0xf1407f39, 0xe90a3181, 0x3b1b4237, - 0x21c6d08d, 0x34302c14, 0x268d6fc1, 0xeabe3b40, 0x3c172d27, 0xf7c5d584, 0x81960cc0, 0x504020ea, - 0x9d8b60ae, 0xa2ce002f, 0xba4f47f3, 0x6f16479e, 0x1018e5d1, 0xc0ab5040, 0xa2bf90bd, 0xe6bff8f9, - 0xb9cd26cb, 0x9bc34bd5, 0x144504b0, 0xdbfe2675, 0xb00241d0, 0x88e77fee, 0xae2d9caa, 0xa0043e00, - 0x26bbf4ed, 0xdcc0f158, 0xe3320463, 0x852b928a, 0x0ec870e9, 0x8f00c1f2, 0xcea95e20, 0xd45277ff, - 0x2cad9a9d, 0x02a4a8c1, 0x4a709348, 0x35d960bd, 0x62977023, 0x17a7ed8c, 0x07f5f5b5, 0xfd0d4b8f, - 0xf5afb7dd, 0x6ebf28cb, 0x2d589527, 0x77d0810c, 0x11b131e7, 0x54850763, 0x0fa61805, 0x8039574f, - 0xa65555d6, 0x22b1804c, 0xa9f631b1, 0x49d11eca, 0xd41f0638, 0xe3105eb8, 0xf8259880, 0x063f1f00, - 0x70d71f92, 0xa8a79da5, 0xc0d3815b, 0x966ac88e, 0x104b3f8b, 0x23b49348, 0xc42cc90d, 0xc24791c3, - 0x15e11067, 0x2dbb2669, 0x90799c32, 0x881a6a48, 0xf26af51c, 0x8174cbdc, 0x0bf0f061, 0x2509dd80, - 0x56293b24, 0x8c81078c, 0xbd6a1c97, 0xf4d98c12, 0xccac4283, 0xa68c09e9, 0x5f606274, 0x84923735, - 0x2c0b8b05, 0xdba69401, 0xcb6555ee, 0xd60daf25, 0x410bd800, 0x9aca380e, 0xd8641710, 0x4d0b1601, - 0xab2ac09e, 0xa17fc4f7, 0xe4b2bd22, 0x90c4eb3f, 0x19d9ce91, 0x121cc72f, 0xfa8510fa, 0xd34d8461, - 0x98077550, 0x11628e29, 0x2ed959aa, 0xf1e7cadb, 0xced7e127, 0x6925aaf0, 0x2fa509c1, 0xa52ba3c8, - 0xaf0b81dd, 0x9f4d58ac, 0x7a55dec3, 0x5e69738a, 0x48d3b5d4, 0x14e6965d, 0x76f3c984, 0x41e33a97, - 0x29a0da19, 0x29bc38dc, 0x05f5ffae, 0xc62da371, 0xf1d3c5a8, 0xc6d55752, 0x60683191, 0x8a7cb79b, - 0x4fa4ae55, 0xd1d61a8b, 0x9cb0b232, 0x51fa1a49, 0xdb73638b, 0xd904c2d5, 0x3a14aac7, 0x620569f4, - 0x08483f6b, 0x0db48f35, 0x20532f6c, 0x293ced32, 0xb38ebf22, 0x7c89c73d, 0x2d7c11e8, 0x61b32037, - 0xad1cd53d, 0x0e18b360, 0x3caf966e, 0xcb59ea4e, 0xe3a28284, 0x05747eb6, 0x75587f24, 0xf2128c81, - 0x1b34e7b4, 0x8e549407, 0x6ca142f4, 0x1deac038, 0x2a2f303b, 0x0ba4453d, 0xf8d1958f, 0x75e85141, - 0xd62ed92e, 0x7abaf0d5, 0x87edc08b, 0x3c49caf8, 0x58689ef9, 0x91878dd8, 0x72a93c45, 0x4dbf035e, - 0xff4102d6, 0x2f9a9185, 0x000fc519, 0x7b071fa0, 0x3e55e34d, 0x54606a4f, 0x16482630, 0xce0f07ad, - 0x01c43d49, 0x51fe3801, 0x857149e5, 0xd9d64938, 0xcb7da3f5, 0x9952fbe3, 0x3729cd74, 0xdc27c37c, - 0x9b9f8775, 0xb27514a3, 0xe9ce038c, 0xe295a7a4, 0x189660b3, 0x99907475, 0x2082078d, 0x24a94c81, - 0x3750e80d, 0xe4b49973, 0xe1372b52, 0x5a79bac1, 0x6f339514, 0x8eaa289e, 0x70e91bd9, 0x5a7e9c2d, - 0x1211b151, 0x0a32044b, 0xa13fe853, 0x79b9ff01, 0xc3decea2, 0x3c012018, 0x6afd6f75, 0x7517d400, - 0xd7598668, 0x61bdb52a, 0x413287d8, 0xe8cd2377, 0x3309bdaf, 0x7330afbb, 0x9df48ce6, 0xa5cb783a, - 0x4b9f8283, 0x311846c9, 0x8b6b29b7, 0x37928233, 0x88ac45da, 0x087f99c0, 0xdf41affe, 0x2afe6c63, - 0xf2d61a1c, 0x14b1b097, 0xec334f3f, 0x1da30414, 0xae8fe1c9, 0x67acd73f, 0xfb118ed8, 0xe4213729, - 0x1298038d, 0x2c47acff, 0x7d641646, 0x51efdbfe, 0xf2aada5b, 0xe59c9373, 0x236f605c, 0x93994fed, - 0x66c47cfd, 0x48061a01, 0x7ed7137d, 0xa01688a7, 0xa5f4a562, 0xef83222b, 0xc36ec867, 0xb932238e, - 0x92f5bbea, 0x26d91ab2, 0x708ac4dc, 0xdc51bf13, 0xc27f8304, 0x7190df40, 0x609d50b3, 0x5718c7ea, - 0x37443a5b, 0x603384a7, 0xce3ef40f, 0x475a2a6a, 0xf4175d2c, 0x3d831006, 0x556eb1bc, 0xb67370b3, - 0x0c689b90, 0x91eac424, 0x7a84a8c4, 0x67843a5f, 0x0e83fd51, 0xfad357ab, 0x16af5c67, 0x8c105104, - 0xa80042a0, 0x4bff0748, 0x77c8f2b5, 0x71ee0e15, 0x8824cd0c, 0xe5439198, 0xe90a6e74, 0x833e5cdc, - 0x93145fbb, 0x8d3dd6d5, 0x33a154ff, 0x6658b418, 0x0cfb9c00, 0x964f4f5c, 0x0a5dc760, 0xd08496ce, - 0x293ac2c8, 0x60ebfa87, 0x60f5fdc2, 0xe8f79d22, 0xb97bbb15, 0xd466590c, 0x9f27be15, 0x396fecc7, - 0xf94e3407, 0xe1930103, 0xa4740508, 0x9f19bcd1, 0xfcebff61, 0xac5c1405, 0xccb734e2, 0x634ff632, - 0xf967a300, 0xa0cc1c36, + 0x149bcb23, 0x30d4e442, 0x263a8d80, 0x9070ae2c, 0xe0aea3d5, 0xf190d834, 0x0773c386, 0x32890982, + 0xacb599d5, 0x82d3b273, 0xe96a6af8, 0x180bbe1f, 0xf9986af5, 0x98519a1d, 0x06b71719, 0x6384719d, + 0x4c0461c8, 0x4fa88992, 0xcd6604d0, 0xc54ea258, 0x217958b4, 0x03d9704a, 0x8456f753, 0xc8104197, + 0xf5eb61ec, 0x70d2d590, 0xd5e04824, 0xa416bb20, 0x875f3744, 0xdaf349be, 0x93b93199, 0x710abb59, + 0x5293da13, 0xc6dd7723, 0xf2eec98c, 0x48a986f1, 0xd2cf978c, 0x53626334, 0xefe4f19f, 0xfcaf3730, + 0xd90deef7, 0x2d2a4089, 0x89ea5c4e, 0xc0eb712a, 0x23b8a187, 0x67293f43, 0x4cc23248, 0x3f364910, + 0xac4b4eb5, 0x822e96ca, 0xe077fb8d, 0x2bbe7fd8, 0x9e40157c, 0x8d8bde08, 0xb12e6e01, 0x930c9fa0, + 0xf6f835ec, 0x7fab017f, 0x9e23928a, 0xf310a4d1, 0xdd5953ec, 0x97574bc2, 0x2d918a3c, 0xd1755faa, + 0x89c28271, 0x8a6ceaf0, 0xba2943f4, 0x57f69746, 0xbc05d6a7, 0x5bad80f1, 0xb4f727a8, 0x112d34c4, + 0xf10a26df, 0xc5409ced, 0xf9022545, 0x75f74679, 0x2023036d, 0x6600f959, 0x4abd24af, 0x35d42b35, + 0x9f736ca7, 0x3fbb1498, 0x42b0818c, 0x80dc72a9, 0xafeed10c, 0x98411ac6, 0xa14d9f4a, 0x6494fda0, + 0x32bcdd14, 0xfe937f14, 0x5b9b1933, 0x1b4399de, 0xf7e47568, 0xb1153422, 0x388f2804, 0x075ca3f9, + 0x2f9f8e7d, 0xc9dc5d8c, 0x34b0e5f3, 0xb4d0ad97, 0xcfa0e233, 0x17d5e1ff, 0xc99313c6, 0xffb3854f, + 0x21a73973, 0x3067f656, 0xfdb25057, 0x836e5931, 0x0f1767cd, 0x57172fcc, 0xa529b4f5, 0x85a8c944, + 0x3c60586d, 0x0a9dbbf0, 0xb519a89f, 0xa3e8d8ce, 0xb16009a0, 0xdc780e80, 0x28b8007c, 0x4aa63180, + 0x6fddcbf1, 0xf4fb5484, 0x2e5384e2, 0xb62a1e75, 0x9e64845d, 0x19232e8b, 0xfaf164f4, 0x38b948b4, + 0x462ddb9b, 0xed21ca4f, 0x9f5f39a7, 0x081d2219, 0x7c498bc4, 0xc283ad60, 0x4fbdc1e1, 0xc797fd73, + 0xab9871c8, 0x2075687c, 0x5d726067, 0x9969bbc9, 0x804f7a3a, 0x0823a21a, 0x22f900de, 0xaca0e153, + 0x516c9c13, 0xcb1c2e65, 0xd1b29b18, 0x644bb68d, 0xd19a9759, 0xde40f56f, 0x6dc15ab3, 0xc59d294e, + 0xceeddfaf, 0x21519537, 0x382bb181, 0xad1bcd63, 0x5a2980e6, 0x90581c83, 0x90a6a854, 0x356df17c, + 0x99a084fc, 0x64c9dfc9, 0x2a2ffc92, 0x2c13b8e9, 0x1ee7d867, 0xb016979a, 0xb49b6b55, 0x01d8f124, + 0x86fb85f0, 0xd33b0986, 0x1a4d0096, 0xa8f5940f, 0x1cf5e14c, 0x2707d3d1, 0x86ac3b1b, 0x7af848da, + 0xd522a14f, 0x76657d76, 0x21ee03bd, 0x5977d941, 0xe2a1907b, 0xbb0ca8db, 0xf75feb3e, 0xb0a1e71f, + 0xbc9cdc70, 0x19e31fc6, 0xa01b330e, 0x36c78d86, 0xdf722326, 0xfa901c87, 0xeebe3469, 0xb0f5aff6, + 0x5729ad07, 0xaa2aef42, 0xb4cb035a, 0xbb429dde, 0x883e3edc, 0x4a9d1c6c, 0xa0965a6b, 0x59ea25fa, + 0x1758bdb9, 0x1313f16d, 0x0372b0f1, 0xa2fb4192, 0xdd39457b, 0x7964461c, 0x1d1c0d92, 0x5fc52bcb, + 0x0dacb683, 0xe792a051, 0x2d5ea842, 0x30c59b1d, 0xab74db9e, 0xeed78ce4, 0x7778b7e1, 0x0b6847ed, + 0xab3e1ad3, 0x04a8b763, 0xc456437e, 0xb3ecafca, 0xf3afcde6, 0x87513d56, 0xca812b41, 0x18594231, + 0x188f3ac1, 0x402055ec, 0x12393f1f, 0x16fbab07, 0xed3ef5b8, 0x8ea86381, 0x09f57289, 0x148aeaab, + 0xcbb2bf38, 0x6e9f7fec, 0x9a340cd3, 0x6a25d024, 0xc5a2cb41, 0xc4a11710, 0x753947a4, 0xe6fa98b4, + 0xf8c27cf4, 0xa139c6ae, 0x90e227d9, 0x89e8d957, 0x812f5e52, 0x61f961c5, 0x1ddd15c8, 0x49d67d0d, + 0xbc003322, 0x835b70c0, 0xd7460575, 0x36acced5, 0x33e2c171, 0x5be91148, 0xa165ae99, 0x04c5c2b7, + 0x6c1ca659, 0xa295fb04, 0x61c405ca, 0xe893fe91, 0x2b599d31, 0xf1442d25, 0x15082ede, 0x6ff350d1, + 0xe03d0cd9, 0x82e1eb2c, 0x0ee19b12, 0x6c67ed89, 0xd1c05d0d, 0xb90150b0, 0x7285bdb3, 0x77cadc99, + 0xc177e6cd, 0xf7a38993, 0x83762f33, 0x86cadda9, 0x7e49da49, 0x305d6f92, 0xbc6b870b, 0x78845fdd, + 0x6d35f434, 0xa52d76a8, 0xd8de9639, 0x8ab5c51b, 0x80097676, 0xffe70df5, 0x3da80417, 0x47ca3c13, + 0x28b499c7, 0xf1ab4c28, 0xe5331f02, 0xfba7177b, 0xde7dc472, 0x3625bb80, 0x69df7765, 0x988cc4e1, + 0xe9fbc9be, 0x40818863, 0x65deac33, 0xd86cf67a, 0xed73ac03, 0xab0d550a, 0x1bd2a266, 0x3655c748, + 0x9a5ae8fc, 0x1e9eb97a, 0x139536f4, 0x0716c724, 0x8de23fa7, 0xb92ec96b, 0x420ebc63, 0x967ccaee, + 0x8021afa2, 0x5f7c7688, 0xbd04e6e7, 0x1aeac884, 0x07849209, 0x0c79b100, 0xbfb60e7c, 0x8a63de3a, + 0x96036a68, 0xb76a745c, 0x43023193, 0xa7d97db7, 0x468a133e, 0xb59ea2ed, 0xcb75495b, 0xbe372c6d, + 0xb762985a, 0x6215e9eb, 0x07907102, 0x3898ea56, 0x2fb7e218, 0x8c61d4c8, 0x4f722e70, 0x694eed25, + 0x85b8b9d7, 0xe8421e36, 0x82eaaf9e, 0x9f9caba4, 0xe4a2de74, 0x582c7826, 0xaeb637f1, 0x86813271, + 0x67b2ff84, 0xd0e2e616, 0x3f9edfe3, 0xc23f8082, 0x0e74ae16, 0x8f256e56, 0x6915c6e1, 0xf1407f39, + 0x1b4dc5ab, 0xe90a3181, 0x3b1b4237, 0x21c6d08d, 0x34302c14, 0x268d6fc1, 0xeabe3b40, 0x3c172d27, + 0xf7c5d584, 0x81960cc0, 0x504020ea, 0x9d8b60ae, 0xa2ce002f, 0xba4f47f3, 0x6f16479e, 0x1018e5d1, + 0xc0ab5040, 0xa2bf90bd, 0xe6bff8f9, 0xb9cd26cb, 0x9bc34bd5, 0x144504b0, 0xdbfe2675, 0xb00241d0, + 0x88e77fee, 0xae2d9caa, 0xa0043e00, 0x26bbf4ed, 0xdcc0f158, 0xe3320463, 0x852b928a, 0x0ec870e9, + 0x8f00c1f2, 0xcea95e20, 0xd45277ff, 0x2cad9a9d, 0x02a4a8c1, 0x4a709348, 0x35d960bd, 0x62977023, + 0x17a7ed8c, 0x07f5f5b5, 0xfd0d4b8f, 0x8d88f33c, 0xf5afb7dd, 0x3a0ea830, 0x6ebf28cb, 0x2d589527, + 0x77d0810c, 0x11b131e7, 0x54850763, 0x0fa61805, 0x8039574f, 0xa65555d6, 0x22b1804c, 0xa9f631b1, + 0x49d11eca, 0xd41f0638, 0xe3105eb8, 0xf8259880, 0x063f1f00, 0x70d71f92, 0xa8a79da5, 0xc0d3815b, + 0x966ac88e, 0x104b3f8b, 0x23b49348, 0xc42cc90d, 0xc24791c3, 0x15e11067, 0x2dbb2669, 0x90799c32, + 0x881a6a48, 0xf26af51c, 0x8174cbdc, 0x0bf0f061, 0x2509dd80, 0x56293b24, 0x8c81078c, 0xbd6a1c97, + 0xf4d98c12, 0xccac4283, 0xa68c09e9, 0x5f606274, 0x84923735, 0x2c0b8b05, 0xdba69401, 0xcb6555ee, + 0xd60daf25, 0x410bd800, 0x9aca380e, 0xd8641710, 0x4d0b1601, 0xab2ac09e, 0xa17fc4f7, 0xe4b2bd22, + 0x90c4eb3f, 0x19d9ce91, 0x121cc72f, 0xfa8510fa, 0xd34d8461, 0x98077550, 0x11628e29, 0x2ed959aa, + 0xf1e7cadb, 0xced7e127, 0x6925aaf0, 0x2fa509c1, 0xa52ba3c8, 0xaf0b81dd, 0x9f4d58ac, 0x7a55dec3, + 0x5e69738a, 0x48d3b5d4, 0x14e6965d, 0x76f3c984, 0x41e33a97, 0x29a0da19, 0x29bc38dc, 0x05f5ffae, + 0xc62da371, 0xf1d3c5a8, 0xc6d55752, 0x60683191, 0x8a7cb79b, 0x4fa4ae55, 0xd1d61a8b, 0x9cb0b232, + 0x51fa1a49, 0xdb73638b, 0xd904c2d5, 0x3a14aac7, 0x620569f4, 0x08483f6b, 0x0db48f35, 0x20532f6c, + 0x293ced32, 0xb38ebf22, 0x7c89c73d, 0x2d7c11e8, 0x5bb567cd, 0x61b32037, 0xad1cd53d, 0x0e18b360, + 0x3caf966e, 0xcb59ea4e, 0xe3a28284, 0xa8cb8e8b, 0x05747eb6, 0x75587f24, 0xf2128c81, 0x1b34e7b4, + 0x8e549407, 0x6ca142f4, 0x1deac038, 0x2a2f303b, 0x0ba4453d, 0xf8d1958f, 0x75e85141, 0xd62ed92e, + 0x7abaf0d5, 0x87edc08b, 0x3c49caf8, 0x58689ef9, 0x91878dd8, 0x72a93c45, 0x4dbf035e, 0xff4102d6, + 0x2f9a9185, 0x000fc519, 0x7b071fa0, 0x3e55e34d, 0x54606a4f, 0x16482630, 0xce0f07ad, 0x01c43d49, + 0x51fe3801, 0x857149e5, 0xd9d64938, 0xcb7da3f5, 0x9952fbe3, 0x3729cd74, 0xdc27c37c, 0x9b9f8775, + 0xb27514a3, 0xe9ce038c, 0xe295a7a4, 0x189660b3, 0x99907475, 0x2082078d, 0x24a94c81, 0x3750e80d, + 0xe4b49973, 0xe1372b52, 0x5a79bac1, 0x6f339514, 0x8eaa289e, 0x70e91bd9, 0x5a7e9c2d, 0x1211b151, + 0x0a32044b, 0xa13fe853, 0x79b9ff01, 0xc3decea2, 0x3c012018, 0x6afd6f75, 0x7517d400, 0xd7598668, + 0x61bdb52a, 0x413287d8, 0xe8cd2377, 0x3309bdaf, 0x7330afbb, 0x9df48ce6, 0xa5cb783a, 0x4b9f8283, + 0x311846c9, 0x8b6b29b7, 0x37928233, 0x88ac45da, 0x087f99c0, 0xdf41affe, 0x2afe6c63, 0xf2d61a1c, + 0x14b1b097, 0xec334f3f, 0x1da30414, 0xae8fe1c9, 0x67acd73f, 0xfb118ed8, 0xe4213729, 0x1298038d, + 0x2c47acff, 0x7d641646, 0x51efdbfe, 0xf2aada5b, 0xe59c9373, 0x236f605c, 0x93994fed, 0x66c47cfd, + 0x48061a01, 0x7ed7137d, 0xa01688a7, 0xa5f4a562, 0xef83222b, 0xc36ec867, 0xb932238e, 0x92f5bbea, + 0x26d91ab2, 0x708ac4dc, 0xdc51bf13, 0xc27f8304, 0x7190df40, 0x609d50b3, 0x5718c7ea, 0x37443a5b, + 0x603384a7, 0xce3ef40f, 0x475a2a6a, 0xf4175d2c, 0x3d831006, 0x556eb1bc, 0xb67370b3, 0x0c689b90, + 0x91eac424, 0x7a84a8c4, 0x67843a5f, 0x0e83fd51, 0xfad357ab, 0x16af5c67, 0x8c105104, 0xa80042a0, + 0x4bff0748, 0x77c8f2b5, 0x71ee0e15, 0x8824cd0c, 0xe5439198, 0xe90a6e74, 0x833e5cdc, 0x93145fbb, + 0x8d3dd6d5, 0x33a154ff, 0x6658b418, 0x0cfb9c00, 0x964f4f5c, 0x0a5dc760, 0xd08496ce, 0x293ac2c8, + 0x23b70842, 0x60ebfa87, 0x0ea78cab, 0x60f5fdc2, 0xe8f79d22, 0xb97bbb15, 0xd466590c, 0x9f27be15, + 0x396fecc7, 0xf94e3407, 0xe1930103, 0xa4740508, 0x9f19bcd1, 0xfcebff61, 0xac5c1405, 0xccb734e2, + 0x634ff632, 0xf967a300, 0xa0cc1c36, }; static const uint16_t OFFSETS[] = { - 0, 232, 452, 649, 874, 1085, 1312, 1549, 1773, 1968, 2187, 2428, - 2687, 2908, 3130, 3377, 3609, 3823, 4039, 4264, 4347, + 0, 227, 448, 665, 896, 1112, 1319, 1536, 1768, 1937, 2118, 2349, + 2591, 2847, 3082, 3330, 3535, 3739, 3912, 4128, 4330, }; static const uint16_t RAW_LENGTHS[] = { - 511, 505, 509, 512, 502, 500, 512, 510, 505, 489, 507, 507, - 490, 502, 507, 511, 509, 510, 498, 102, + 510, 512, 505, 509, 505, 512, 494, 506, 512, 499, 511, 507, + 507, 509, 481, 501, 498, 511, 488, 388, }; static const uint16_t FIRSTS[] = { - 0, 28, 65, 94, 122, 156, 192, 226, 261, 293, 329, 361, - 394, 425, 453, 484, 517, 552, 591, 628, 634, + 0, 27, 64, 95, 121, 158, 192, 226, 262, 295, 330, 366, + 398, 431, 463, 486, 522, 554, 596, 628, 659, }; - // 4347 encoded bytes; 20 independent chunks, at most 512 decoded bytes. + // 4330 encoded bytes; 20 independent chunks, at most 512 decoded bytes. static const uint8_t NAMES[] = { - 0x8d, 0x90, 0x03, 0x6f, 0x40, 0x41, 0x0c, 0xc7, 0xbb, 0x60, 0xb6, 0x1d, 0xcc, 0x36, 0xe2, 0xd9, - 0x66, 0xdc, 0x1c, 0x9a, 0x3c, 0xf7, 0xa5, 0x77, 0xd3, 0xa7, 0x9f, 0xed, 0xe8, 0xd8, 0x3f, 0x7e, - 0xb0, 0xaa, 0x6f, 0x3c, 0xe1, 0x2a, 0x1f, 0xa9, 0x91, 0xe3, 0x29, 0x34, 0x9c, 0xe6, 0x2a, 0x0b, - 0x39, 0x43, 0x51, 0x36, 0x64, 0xd4, 0x09, 0x41, 0xe7, 0x85, 0xd3, 0x50, 0x19, 0x87, 0xce, 0x61, - 0xca, 0x96, 0x52, 0xa8, 0x3c, 0xa2, 0x9c, 0x94, 0x27, 0x81, 0x4a, 0x61, 0x4e, 0xd1, 0x91, 0x5c, - 0x3e, 0x1c, 0x3c, 0x49, 0x1a, 0x66, 0x2a, 0x41, 0x13, 0x28, 0x0f, 0xb0, 0xbe, 0x78, 0x32, 0x33, - 0x3b, 0x89, 0x53, 0xe3, 0xc7, 0xb8, 0x4b, 0x2e, 0xc0, 0xed, 0xd0, 0x7f, 0xab, 0xdf, 0xfd, 0xa0, - 0x5f, 0xff, 0x4e, 0xbf, 0x5e, 0x9e, 0xf5, 0x31, 0x61, 0x51, 0xc8, 0x5e, 0x61, 0xc6, 0x48, 0xd7, - 0x9e, 0xe4, 0x41, 0xdd, 0x51, 0xe6, 0x58, 0x1c, 0x34, 0xbc, 0xf3, 0x2e, 0x7a, 0x74, 0x58, 0x3d, - 0x5b, 0xc2, 0x03, 0x61, 0xfc, 0x4e, 0x0b, 0xda, 0xff, 0xaf, 0x56, 0x7e, 0x22, 0xca, 0xc4, 0x24, - 0x8f, 0x62, 0xdf, 0x45, 0xee, 0x7f, 0x88, 0xdc, 0xfc, 0xce, 0xa6, 0xf9, 0x7f, 0x91, 0x5b, 0xde, - 0x9b, 0x9c, 0x2b, 0x6f, 0x82, 0x89, 0x6f, 0x99, 0x7f, 0xc0, 0x51, 0x40, 0x19, 0x49, 0x68, 0x70, - 0x75, 0x72, 0xf2, 0x7d, 0xb3, 0x72, 0x77, 0x3d, 0x31, 0x79, 0xcf, 0xf7, 0xb5, 0x60, 0xcd, 0xfc, - 0xdb, 0xbe, 0xe4, 0xf8, 0x60, 0x6f, 0xff, 0xfc, 0x51, 0xfa, 0x59, 0xf5, 0x21, 0x71, 0xd5, 0xd3, - 0xbb, 0x40, 0xf5, 0x63, 0x8e, 0x4b, 0x81, 0x3b, 0x94, 0x51, 0xb5, 0x56, 0x05, 0x41, 0x0c, 0x1d, - 0x1a, 0x74, 0xa8, 0x70, 0xff, 0x04, 0xdc, 0x5a, 0xdc, 0x5d, 0xca, 0xac, 0xe5, 0xb1, 0x39, 0x6f, - 0xec, 0x24, 0xb3, 0xec, 0xe7, 0xe3, 0xee, 0x54, 0xa3, 0xb9, 0xaa, 0x36, 0xd1, 0x21, 0x53, 0x0e, - 0xeb, 0xa7, 0x47, 0x07, 0x87, 0x97, 0x10, 0x91, 0x2d, 0xb9, 0xd4, 0x40, 0x5e, 0xa6, 0x51, 0xb5, - 0xac, 0x1f, 0x9d, 0xac, 0xc2, 0x1e, 0xb9, 0x66, 0xc3, 0xf8, 0x1a, 0x96, 0xa7, 0xa6, 0x80, 0x31, - 0x60, 0x1a, 0x91, 0x95, 0xda, 0x42, 0x13, 0x31, 0x87, 0x3c, 0x2e, 0xcc, 0x40, 0xee, 0x6d, 0x48, - 0x1d, 0x79, 0x07, 0x9c, 0x16, 0xe4, 0x21, 0x33, 0xa8, 0x26, 0x20, 0x88, 0x1a, 0xad, 0x24, 0x53, - 0xba, 0x49, 0x22, 0x60, 0x7d, 0x81, 0x56, 0xe9, 0x17, 0x80, 0x5e, 0xc8, 0x98, 0x8a, 0x2b, 0x04, - 0x94, 0xe0, 0x7c, 0xad, 0xb4, 0xa0, 0x13, 0xcf, 0xaa, 0x6d, 0x7d, 0x66, 0x7a, 0xf6, 0x2b, 0xc4, - 0xff, 0x80, 0x75, 0x7b, 0x6f, 0x41, 0x90, 0xaf, 0xef, 0xde, 0x3a, 0x96, 0xa7, 0x5e, 0xe1, 0x1e, - 0x80, 0xfa, 0xef, 0x81, 0x86, 0x1e, 0xb6, 0xff, 0x87, 0x6b, 0xb3, 0x28, 0x25, 0x88, 0x37, 0x29, - 0xbf, 0x49, 0xa3, 0x7c, 0x4c, 0x83, 0xf3, 0xd9, 0x99, 0xaf, 0xb5, 0x37, 0x2a, 0x63, 0x1e, 0x1d, - 0x8c, 0xd6, 0xd4, 0xa0, 0xff, 0x13, 0xbf, 0x04, 0xa4, 0xdf, 0x97, 0xa4, 0x6b, 0x8a, 0xa5, 0xaf, - 0x22, 0x14, 0x24, 0xc1, 0xa4, 0xb7, 0xab, 0xc4, 0x6a, 0xbd, 0x0e, 0xd8, 0x7a, 0x1d, 0x90, 0xf5, - 0x3a, 0x60, 0xeb, 0x01, 0x4c, 0x4e, 0xc5, 0x95, 0xc3, 0x40, 0x0c, 0x55, 0x01, 0x2e, 0xc1, 0x7b, - 0xf3, 0x7d, 0x99, 0x3a, 0x48, 0x17, 0xff, 0x0d, 0xc8, 0x31, 0x8e, 0xe6, 0x49, 0x13, 0xaa, 0x3e, - 0x4c, 0x37, 0xe9, 0x33, 0x75, 0x3c, 0x15, 0x0e, 0xd0, 0xf0, 0xf9, 0x81, 0x4d, 0x5f, 0x3a, 0x59, - 0x15, 0xc4, 0xde, 0xf2, 0xe4, 0x76, 0x50, 0xce, 0xec, 0x0a, 0x2b, 0x35, 0xf0, 0xda, 0xc7, 0x25, - 0x83, 0x2d, 0x27, 0xd9, 0xd0, 0x8b, 0xc8, 0x0c, 0x63, 0x5d, 0xb3, 0x52, 0x6d, 0x9c, 0x4c, 0x94, - 0x68, 0x71, 0x49, 0x2a, 0x6f, 0xff, 0x3f, 0x08, 0x32, 0x67, 0x97, 0x7a, 0x49, 0x50, 0x17, 0x7b, - 0x81, 0x9f, 0x18, 0x2d, 0xcf, 0xd2, 0xb6, 0xd4, 0x24, 0xaa, 0x57, 0xe6, 0x9f, 0xde, 0x6a, 0xec, - 0xcd, 0x30, 0x4b, 0xe4, 0x99, 0xaa, 0x5b, 0xe7, 0xad, 0x51, 0xed, 0xe3, 0xb8, 0x6c, 0x12, 0x75, - 0x90, 0xe2, 0x90, 0x04, 0xbc, 0x2d, 0xac, 0xc9, 0x4d, 0xb8, 0x14, 0x1b, 0x1d, 0x26, 0x8a, 0x57, - 0x96, 0x17, 0xc9, 0xad, 0x3c, 0x50, 0xb7, 0x72, 0x18, 0x1a, 0x9a, 0x60, 0x73, 0x23, 0xd8, 0x65, - 0x44, 0xb8, 0x87, 0x0c, 0x2b, 0x79, 0xd0, 0x02, 0x17, 0xab, 0xf5, 0x3a, 0x20, 0xeb, 0x65, 0x10, - 0xc1, 0x8e, 0x6a, 0xa9, 0x2a, 0x3e, 0x4b, 0x51, 0xe2, 0x84, 0x3d, 0xc4, 0xd0, 0xd2, 0x00, 0x6b, - 0x24, 0x40, 0xc2, 0x9e, 0x41, 0x01, 0xcc, 0x06, 0x00, 0x62, 0xf0, 0x00, 0x34, 0x46, 0xd5, 0xe8, - 0x0d, 0xc5, 0x30, 0x58, 0x53, 0x99, 0x99, 0x69, 0x2b, 0xc3, 0xd6, 0x7b, 0x74, 0xd7, 0x67, 0xd0, - 0x6b, 0xfc, 0xc0, 0x8a, 0x21, 0x70, 0xfb, 0x30, 0xf3, 0x64, 0x8b, 0x7f, 0xa0, 0x3c, 0x92, 0xc2, - 0xbf, 0xef, 0xdf, 0x2f, 0xcc, 0x4c, 0x08, 0x58, 0xb0, 0xa6, 0x02, 0x76, 0x3c, 0x95, 0x49, 0x44, - 0xf2, 0x08, 0xa7, 0xd2, 0x1b, 0xfd, 0x4f, 0x48, 0xa1, 0x6c, 0xb9, 0xd6, 0x8a, 0x59, 0x06, 0xf2, - 0xd5, 0x56, 0xa9, 0x70, 0x31, 0xc2, 0x2e, 0x73, 0x81, 0xbd, 0x04, 0x5c, 0x8c, 0xd7, 0xd6, 0xe3, - 0x37, 0x0e, 0x16, 0xe5, 0xec, 0x05, 0x72, 0x14, 0x68, 0x19, 0xa9, 0xde, 0xca, 0x58, 0x91, 0x63, - 0x20, 0x1b, 0xd8, 0x07, 0xd8, 0xe0, 0x1a, 0x79, 0xac, 0xfe, 0x0c, 0xda, 0xe1, 0x62, 0xf1, 0xc0, - 0x41, 0xd0, 0x4a, 0x78, 0xbd, 0xe2, 0x7a, 0x28, 0x75, 0x69, 0x46, 0x2f, 0x54, 0xd6, 0x3e, 0xf4, - 0x8d, 0x23, 0xc8, 0xc6, 0x41, 0x9f, 0x0e, 0x08, 0x4d, 0x56, 0x0e, 0x7e, 0x50, 0x71, 0x51, 0x16, - 0xd6, 0xb0, 0x45, 0x2f, 0xb4, 0x61, 0x94, 0x39, 0x61, 0x42, 0x05, 0x27, 0x09, 0xbc, 0x58, 0x78, - 0x4a, 0x2a, 0x79, 0xde, 0x89, 0x5b, 0xff, 0x4a, 0x90, 0xa3, 0xa5, 0x9a, 0x49, 0x4c, 0x3f, 0x7e, - 0xb5, 0x70, 0x34, 0x22, 0xf6, 0xd1, 0x80, 0xf4, 0x15, 0x8e, 0x89, 0x0d, 0xd7, 0xe3, 0x00, 0x8e, - 0x87, 0xd0, 0xe0, 0x76, 0x02, 0xdc, 0x51, 0x97, 0x2b, 0x1c, 0x45, 0xf2, 0x85, 0x69, 0x93, 0x57, - 0x25, 0x11, 0xa1, 0x6f, 0x70, 0x75, 0x26, 0x76, 0x68, 0x02, 0x62, 0xf0, 0x00, 0x14, 0x42, 0x0d, - 0xc8, 0x0e, 0x06, 0x31, 0x38, 0xc3, 0x72, 0xf0, 0x6c, 0x1d, 0xa1, 0xb8, 0x4f, 0x7e, 0x65, 0x5f, - 0x33, 0xfd, 0x17, 0xdd, 0xa4, 0xb8, 0x7d, 0x6d, 0xae, 0xf9, 0x89, 0x4a, 0xa5, 0x1c, 0x47, 0x6d, - 0xcc, 0xbd, 0x0d, 0xa9, 0x63, 0xef, 0x30, 0xa6, 0x05, 0x7b, 0xcc, 0x4a, 0xc2, 0x20, 0xf0, 0x3e, - 0x94, 0x0c, 0xde, 0xc7, 0x6c, 0x18, 0xea, 0x7d, 0x16, 0x41, 0xeb, 0x0b, 0xb2, 0x50, 0x8f, 0x14, - 0x28, 0x55, 0x8a, 0xd0, 0xf0, 0xde, 0xa2, 0x50, 0x1c, 0x51, 0x84, 0xba, 0x52, 0xb4, 0xec, 0xd2, - 0x12, 0xf3, 0x5e, 0xaa, 0x50, 0xed, 0x9c, 0x43, 0x85, 0xaf, 0x25, 0xae, 0x19, 0x96, 0xe5, 0x3e, - 0xfa, 0x37, 0xda, 0x81, 0xea, 0x05, 0x92, 0x7b, 0xcc, 0x22, 0x17, 0xff, 0x84, 0x24, 0xc1, 0xf9, - 0x31, 0xbc, 0x47, 0x69, 0x77, 0xda, 0xf0, 0xe0, 0xb3, 0x15, 0xf3, 0xea, 0xef, 0x2f, 0x0a, 0xdb, - 0x03, 0x41, 0x4f, 0x78, 0xf8, 0xa2, 0x2e, 0xe4, 0xc4, 0x9f, 0x08, 0x85, 0xde, 0x3a, 0x86, 0x2e, - 0xb6, 0x17, 0xc3, 0xa9, 0xee, 0x85, 0x5a, 0x34, 0x64, 0xbd, 0x83, 0xfa, 0x59, 0x57, 0xcb, 0x4b, - 0x63, 0xe0, 0x37, 0xc8, 0x66, 0xf9, 0xb9, 0xfd, 0xb4, 0xd8, 0xcc, 0x9d, 0x6e, 0xd7, 0x0b, 0xc3, - 0x9b, 0xcd, 0xdf, 0x42, 0xd8, 0x66, 0xd3, 0xa4, 0xc9, 0x1c, 0x58, 0xd8, 0xbb, 0x3e, 0x2b, 0xaa, - 0xd1, 0x6b, 0x54, 0xcd, 0x1b, 0xf7, 0xbf, 0xfb, 0xfc, 0xbf, 0x7b, 0xfc, 0x33, 0x6d, 0x90, 0x85, - 0x8e, 0x04, 0x21, 0x0c, 0x86, 0x1b, 0xcf, 0xb9, 0xbb, 0x5b, 0xe4, 0xdc, 0xef, 0xde, 0xa6, 0x19, - 0x29, 0x59, 0xc6, 0xe0, 0x4a, 0x57, 0x1e, 0xff, 0x06, 0xd6, 0xc9, 0xc6, 0x90, 0x9f, 0x7e, 0xfd, - 0x0a, 0x74, 0xa8, 0x12, 0xca, 0xb0, 0xf7, 0x89, 0x1f, 0x28, 0x4a, 0x30, 0x33, 0xb5, 0x4d, 0x1a, - 0x6d, 0x1a, 0xe4, 0x24, 0xd7, 0x06, 0xfb, 0x5a, 0x69, 0x54, 0x54, 0x1b, 0xa5, 0x60, 0x39, 0x0e, - 0xd3, 0x8a, 0xe0, 0x2c, 0xa4, 0x0d, 0x9c, 0x5b, 0x37, 0xda, 0x1d, 0x77, 0x5d, 0x1a, 0x5f, 0xfb, - 0xf2, 0xbb, 0x36, 0xf2, 0xb8, 0x51, 0x72, 0x51, 0xff, 0x89, 0x8c, 0xf6, 0xcb, 0x34, 0x68, 0xc1, - 0xae, 0x25, 0x97, 0x5a, 0x90, 0xc9, 0x52, 0x22, 0xc4, 0x70, 0x81, 0x26, 0x75, 0xc4, 0x3d, 0x62, - 0xf4, 0x8f, 0xe1, 0xd0, 0x98, 0x1a, 0x87, 0x17, 0x51, 0x72, 0x10, 0xbb, 0x8f, 0xf4, 0x46, 0xfc, - 0x17, 0xeb, 0xe0, 0x41, 0x75, 0xab, 0x6a, 0xdc, 0xaf, 0xd4, 0xce, 0x61, 0x6d, 0x72, 0xaa, 0x61, - 0xd9, 0x26, 0x2c, 0x5a, 0x7c, 0x59, 0xb0, 0xc8, 0x89, 0x61, 0x99, 0x7f, 0x17, 0xd1, 0xe0, 0x12, - 0x5b, 0xd0, 0x99, 0x07, 0xc1, 0x59, 0x98, 0xd2, 0xcf, 0x03, 0x6b, 0x33, 0xb4, 0xb5, 0x89, 0xfc, - 0x5e, 0xa4, 0xb8, 0x3e, 0x95, 0x87, 0xc3, 0x28, 0x5b, 0x73, 0xd4, 0x38, 0xc3, 0xb0, 0x26, 0xc4, - 0xb5, 0x6e, 0x92, 0x0a, 0xb3, 0x4e, 0xd2, 0x96, 0x2c, 0x1e, 0x0a, 0xae, 0x82, 0xc1, 0x95, 0x37, - 0xb8, 0x0a, 0x06, 0x5b, 0x33, 0x06, 0x5b, 0xd6, 0xb0, 0x70, 0x32, 0xf3, 0x8f, 0x51, 0xb3, 0x7f, - 0x6c, 0x50, 0x35, 0x76, 0x04, 0x31, 0x0c, 0x55, 0x1b, 0x66, 0x86, 0x2e, 0x54, 0x07, 0x8f, 0x90, - 0x2e, 0x9c, 0x94, 0x7a, 0x06, 0xcd, 0xae, 0x87, 0xec, 0xc8, 0x0a, 0x9d, 0x7e, 0x99, 0xc6, 0x2f, - 0x95, 0x58, 0x1f, 0xa0, 0x4d, 0xa5, 0x90, 0xc1, 0xef, 0x2b, 0xe4, 0x3b, 0x94, 0x4c, 0x30, 0x78, - 0x16, 0x56, 0x4e, 0x90, 0xbd, 0xaf, 0x30, 0x12, 0x7f, 0x13, 0xa3, 0xd7, 0xa3, 0xa4, 0xfa, 0x14, - 0x81, 0x35, 0xa6, 0x40, 0x4a, 0x88, 0x61, 0x2f, 0x99, 0xac, 0x4f, 0x6f, 0xe0, 0x30, 0xbd, 0x8a, - 0x54, 0x47, 0xcf, 0xb0, 0xd6, 0x3b, 0xac, 0x5c, 0xad, 0x4a, 0x34, 0x6d, 0x25, 0xb0, 0x30, 0xfe, - 0xb5, 0x8e, 0x9a, 0x9d, 0x6d, 0x11, 0x52, 0x0c, 0xb5, 0xff, 0x81, 0x8d, 0x92, 0x5a, 0xca, 0xfc, - 0x61, 0x50, 0x2c, 0x4e, 0x9c, 0xaf, 0x51, 0x28, 0x0a, 0x6c, 0x34, 0xbf, 0x2e, 0xcc, 0x20, 0x6e, - 0x25, 0x88, 0xf3, 0x23, 0xc4, 0xf9, 0x04, 0xb1, 0xa7, 0xd2, 0xf8, 0x2a, 0xa8, 0xba, 0xff, 0x94, - 0x95, 0x75, 0x1e, 0x75, 0x49, 0x98, 0x51, 0xe5, 0x6b, 0x38, 0xce, 0xbe, 0xca, 0x72, 0x9c, 0x7f, - 0x45, 0x3d, 0x4e, 0x7f, 0x5c, 0xe6, 0x46, 0xf9, 0xf2, 0x44, 0xff, 0x76, 0xc2, 0x79, 0x65, 0x86, - 0xcd, 0xf2, 0x08, 0x7d, 0xb9, 0x89, 0x0e, 0xf7, 0x43, 0xc7, 0x3f, 0x1c, 0x53, 0x49, 0x31, 0xe2, - 0xa3, 0x0a, 0xc4, 0xff, 0x11, 0x82, 0x0b, 0x0c, 0x11, 0x4e, 0x7b, 0x1c, 0x60, 0xbb, 0x70, 0xbd, - 0xcd, 0xca, 0x5b, 0xaa, 0x60, 0x7b, 0x82, 0x7e, 0x9c, 0xa0, 0xef, 0xcc, 0xa0, 0xaf, 0xbe, 0xb2, - 0x32, 0x45, 0x77, 0x38, 0x0c, 0xd6, 0x06, 0x19, 0xac, 0x01, 0x32, 0x18, 0x00, 0x5c, 0x4d, 0x35, - 0x42, 0xc4, 0x00, 0x10, 0xdc, 0x0e, 0xaf, 0x70, 0xe7, 0x0d, 0xc9, 0x23, 0xd0, 0x0a, 0x2d, 0x27, - 0x36, 0xc7, 0x6d, 0x6c, 0xc3, 0x6e, 0xe0, 0x9e, 0x8f, 0x7b, 0x37, 0x3e, 0x72, 0xc2, 0x76, 0x64, - 0x89, 0x3b, 0x75, 0xb6, 0x8c, 0xc0, 0xb5, 0xe7, 0x65, 0x43, 0x47, 0xa3, 0xaf, 0xa4, 0xb3, 0x8a, - 0x9d, 0x6c, 0x39, 0x07, 0xe6, 0x23, 0x5d, 0x8e, 0x50, 0xb8, 0x56, 0xf7, 0x04, 0x63, 0xe8, 0x6d, - 0x26, 0xdb, 0x66, 0x1d, 0x82, 0xfe, 0x44, 0x97, 0xf9, 0xab, 0x8b, 0x14, 0xa5, 0x75, 0x43, 0xde, - 0xab, 0xf5, 0xf0, 0xbc, 0x52, 0x43, 0xd1, 0x52, 0x0e, 0x31, 0x84, 0xec, 0x3d, 0x46, 0x21, 0x7b, - 0x33, 0x9d, 0xa8, 0x2c, 0x7f, 0xcf, 0xad, 0xfd, 0x9b, 0xdb, 0xf3, 0x48, 0xd2, 0x44, 0xd6, 0xad, - 0xf8, 0xd8, 0x44, 0xf7, 0x30, 0x8e, 0xb2, 0xf2, 0xeb, 0x64, 0x13, 0x7f, 0xbd, 0xe5, 0x60, 0x1f, - 0xe6, 0x22, 0xda, 0x58, 0x93, 0x63, 0x9a, 0xf7, 0xd5, 0x94, 0x6d, 0x85, 0xde, 0x27, 0x60, 0x92, - 0x20, 0x7d, 0x99, 0x41, 0x4a, 0x52, 0x6e, 0xbc, 0x81, 0xa5, 0x19, 0x56, 0x97, 0x19, 0x82, 0x1c, - 0xa5, 0x58, 0x94, 0x5f, 0x02, 0x74, 0x4d, 0x0a, 0x56, 0x15, 0x96, 0x20, 0x15, 0xb2, 0x70, 0x07, - 0xcb, 0x21, 0x39, 0x45, 0x18, 0x29, 0x84, 0xd8, 0xf2, 0x12, 0xf3, 0xf2, 0xc1, 0x96, 0x1a, 0x19, - 0x01, 0x2d, 0xc4, 0x6a, 0x94, 0x2a, 0xc8, 0x28, 0x51, 0x24, 0x4d, 0xa2, 0x70, 0x63, 0xc5, 0x90, - 0x8c, 0xe5, 0x37, 0x36, 0xa0, 0xd0, 0x04, 0x01, 0x63, 0xa2, 0x0d, 0x00, 0x00, 0xa5, 0x4f, 0xb5, - 0x56, 0x44, 0x41, 0x0c, 0x4d, 0x45, 0x85, 0x6b, 0x85, 0x4b, 0x85, 0xf3, 0x07, 0xb8, 0x95, 0xdb, - 0xe7, 0x8c, 0x84, 0xb3, 0xf3, 0x24, 0x79, 0x27, 0x19, 0xec, 0xeb, 0x71, 0x67, 0xba, 0x6d, 0xaf, - 0x5f, 0x48, 0xb5, 0xd4, 0x0e, 0xd9, 0xb1, 0x20, 0xeb, 0x0d, 0x1e, 0x1c, 0x44, 0xdf, 0xa2, 0x52, - 0x47, 0x2e, 0x93, 0xc2, 0xac, 0x48, 0x8b, 0x46, 0x7a, 0x47, 0x0a, 0x43, 0x96, 0x53, 0xa8, 0xdf, - 0x54, 0xfb, 0xfb, 0xaf, 0xaa, 0x20, 0x6d, 0xe7, 0x38, 0x09, 0xa3, 0xba, 0x98, 0x04, 0x7d, 0x43, - 0xb0, 0x71, 0x6b, 0x1e, 0x66, 0xeb, 0x64, 0x86, 0xad, 0x44, 0x6a, 0x61, 0xf6, 0x2b, 0x6b, 0xee, - 0x47, 0xd6, 0xf8, 0xc1, 0xee, 0x80, 0x09, 0x13, 0x07, 0x03, 0x06, 0xc0, 0x15, 0x51, 0x97, 0x89, - 0xf8, 0xba, 0xb7, 0xf7, 0x7d, 0x78, 0x1a, 0x1b, 0x51, 0x87, 0x92, 0x1d, 0xb2, 0x20, 0x3d, 0x64, - 0x52, 0x76, 0x0d, 0x1a, 0xb1, 0x89, 0x1a, 0xc0, 0x75, 0x6a, 0x1e, 0xcf, 0x04, 0x7b, 0x5b, 0x27, - 0xa1, 0x2f, 0x5b, 0xd7, 0x29, 0x53, 0x71, 0xc3, 0xd4, 0x8f, 0xfa, 0x29, 0x7e, 0xa1, 0xac, 0x4f, - 0x4d, 0x53, 0x94, 0x6e, 0xbd, 0xce, 0x9d, 0xfa, 0x1e, 0xf0, 0x63, 0xe2, 0x08, 0x1e, 0x39, 0x8d, - 0x45, 0xd7, 0xea, 0x9b, 0xeb, 0x67, 0x4b, 0x31, 0x61, 0xb4, 0xe4, 0x5d, 0x78, 0xf5, 0x3e, 0x03, - 0x8d, 0x8f, 0xb5, 0x7a, 0x03, 0x31, 0x10, 0x06, 0xb7, 0x0b, 0x33, 0x33, 0xa7, 0x0a, 0x63, 0x1d, - 0x66, 0xa6, 0x72, 0x7d, 0xb0, 0x8e, 0xf5, 0x59, 0x74, 0x82, 0xc0, 0xd3, 0x87, 0xc1, 0x6c, 0xb7, - 0x82, 0x7f, 0x66, 0xe0, 0x94, 0xf1, 0xd7, 0x03, 0x85, 0xb7, 0xf3, 0x7b, 0x51, 0x46, 0x61, 0x96, - 0x59, 0x8b, 0x42, 0xc5, 0x24, 0xa0, 0xd5, 0x90, 0xa6, 0xc0, 0x91, 0x81, 0x36, 0xa5, 0x04, 0x5a, - 0x32, 0x4f, 0x64, 0xa0, 0x7e, 0xf5, 0x66, 0x15, 0xed, 0xcb, 0xf2, 0xca, 0xc6, 0x0a, 0x46, 0x4a, - 0xe8, 0x40, 0x32, 0x25, 0xd1, 0x04, 0x31, 0x53, 0x18, 0x72, 0x82, 0x59, 0xd4, 0x16, 0xa6, 0xbc, - 0x0d, 0xa1, 0x2b, 0x67, 0xab, 0xeb, 0x6f, 0x6b, 0x08, 0x43, 0xc3, 0xe2, 0x47, 0x42, 0xb2, 0x5a, - 0xaa, 0x67, 0x18, 0x56, 0xe1, 0xf7, 0x32, 0x8a, 0xc4, 0x39, 0xe8, 0xce, 0x41, 0x8d, 0x62, 0xfe, - 0x5d, 0x97, 0x23, 0x23, 0x98, 0x0c, 0x38, 0x46, 0x99, 0xc0, 0x41, 0xfb, 0xe6, 0x46, 0x49, 0x83, - 0xda, 0xe1, 0xb9, 0xb4, 0xc2, 0xf5, 0x7a, 0x3c, 0x12, 0xda, 0x73, 0x4b, 0x78, 0xc9, 0xbd, 0xc5, - 0xbf, 0x89, 0xfa, 0x6d, 0x0a, 0x04, 0x2e, 0x3f, 0x94, 0x46, 0x7f, 0xc6, 0x8f, 0xa7, 0x3d, 0xe7, - 0x30, 0xfe, 0x69, 0x31, 0xfe, 0xcc, 0xd2, 0x0c, 0xda, 0x73, 0x5c, 0xda, 0xff, 0x86, 0xfa, 0x0b, - 0x5c, 0x06, 0xf2, 0x63, 0x3b, 0x72, 0xd4, 0x86, 0x30, 0xff, 0xae, 0x7e, 0x97, 0xa2, 0x6c, 0x49, - 0x81, 0x91, 0x4f, 0x6a, 0x4b, 0x0e, 0xaf, 0xe5, 0x8f, 0xf7, 0x0e, 0x6c, 0x50, 0x83, 0x6e, 0x04, - 0x41, 0x18, 0xfe, 0xeb, 0x86, 0xb5, 0xed, 0x36, 0xac, 0x1f, 0xa0, 0x66, 0x50, 0xc5, 0xf9, 0xb2, - 0xf8, 0x9b, 0x9b, 0xd5, 0xec, 0xe0, 0xf4, 0xf4, 0x67, 0xdf, 0x8e, 0xf9, 0x89, 0x3e, 0x44, 0x54, - 0x7e, 0x96, 0xf8, 0x79, 0xfc, 0x79, 0xc1, 0x63, 0x24, 0x2c, 0xc3, 0x94, 0x2e, 0x2e, 0x6f, 0x2f, - 0xe1, 0xc9, 0x38, 0x75, 0x12, 0x21, 0x13, 0x68, 0xc7, 0x17, 0x12, 0x6e, 0xc4, 0x74, 0x96, 0x37, - 0x2e, 0xad, 0x85, 0xc2, 0x18, 0xc4, 0xd2, 0xe7, 0x98, 0xd6, 0x34, 0xa7, 0xec, 0x58, 0xd6, 0xb4, - 0x2e, 0x65, 0x0c, 0xc3, 0xba, 0x50, 0x5b, 0x4f, 0x47, 0x52, 0x3b, 0xb8, 0xbf, 0xcd, 0xc2, 0x00, - 0xcd, 0xf6, 0xfc, 0x9f, 0x6d, 0xff, 0x07, 0xcd, 0x75, 0x01, 0x40, 0xd3, 0x1f, 0x75, 0x84, 0xbf, - 0x4b, 0x5c, 0xd4, 0xea, 0x6d, 0xb6, 0x16, 0xa4, 0x86, 0x8e, 0xeb, 0x82, 0x8e, 0x8b, 0xe2, 0x5f, - 0xd0, 0x72, 0x0f, 0xec, 0x72, 0x47, 0xd6, 0x36, 0xa4, 0xdb, 0x02, 0x8d, 0x95, 0xb5, 0xa0, 0x95, - 0x1e, 0x9d, 0x7b, 0x83, 0x97, 0xcb, 0x96, 0x75, 0x2c, 0x12, 0x27, 0x82, 0x97, 0x73, 0x2c, 0xd1, - 0xe7, 0xcd, 0xb7, 0x75, 0xbc, 0x10, 0xbf, 0x89, 0xb0, 0x35, 0x3f, 0x1f, 0x99, 0x42, 0xf6, 0xea, - 0x1a, 0xe6, 0x7a, 0xd8, 0xe7, 0x3a, 0xec, 0xf3, 0x3d, 0x64, 0x23, 0x6c, 0x72, 0x48, 0xa5, 0x17, - 0xb2, 0x45, 0xe7, 0xc1, 0x22, 0x1a, 0x51, 0x49, 0xeb, 0x20, 0x91, 0xe0, 0x52, 0xed, 0xac, 0xce, - 0x6e, 0x38, 0x31, 0x52, 0x1b, 0x1a, 0x77, 0xfc, 0x02, 0x27, 0x36, 0xaf, 0x59, 0xa3, 0x3a, 0xdc, - 0xd1, 0x02, 0x0e, 0x05, 0xec, 0xfe, 0x47, 0xf6, 0x2e, 0xba, 0xef, 0x00, 0x6d, 0xd0, 0xb5, 0x72, - 0x03, 0x41, 0x0c, 0x06, 0x60, 0x85, 0xd3, 0x5d, 0x9a, 0x30, 0x75, 0x6e, 0xcd, 0xf0, 0x00, 0x61, - 0xea, 0x52, 0x6b, 0x0e, 0x64, 0x7b, 0x27, 0x0b, 0x9e, 0xd5, 0x9a, 0xde, 0x3e, 0x3a, 0x33, 0x35, - 0xcb, 0xfb, 0xeb, 0x1b, 0xc1, 0x37, 0x71, 0x37, 0xce, 0x06, 0x64, 0x43, 0xdf, 0x93, 0x47, 0x1e, - 0x95, 0x2b, 0x8d, 0x16, 0xa6, 0xce, 0xf4, 0x62, 0xab, 0x9c, 0x45, 0x1f, 0x67, 0xca, 0x61, 0xa2, - 0x09, 0x0a, 0x7d, 0x4e, 0xe0, 0xe2, 0x4f, 0x31, 0xa3, 0x71, 0x19, 0x19, 0xb8, 0xf0, 0xd4, 0xa3, - 0x38, 0x90, 0x87, 0x4b, 0xe7, 0x0c, 0x32, 0xf9, 0x81, 0xac, 0x2f, 0xe4, 0xc0, 0x28, 0x1b, 0x6b, - 0x4c, 0xbb, 0x71, 0x80, 0x43, 0x65, 0xd4, 0xae, 0x38, 0x84, 0xd3, 0xf9, 0x77, 0x04, 0x30, 0xa2, - 0xb0, 0x4e, 0xbb, 0xce, 0x18, 0x87, 0xe5, 0xca, 0xce, 0xf2, 0x8f, 0xed, 0xbe, 0xd6, 0xf0, 0x98, - 0x23, 0x1e, 0x87, 0xaa, 0xad, 0x20, 0x5a, 0xa1, 0x44, 0x4b, 0x0a, 0x26, 0x5e, 0x65, 0x1d, 0x42, - 0xe2, 0x9e, 0x75, 0x43, 0x38, 0x5b, 0xa1, 0x45, 0x4c, 0x96, 0x9d, 0xcc, 0xeb, 0x44, 0xf8, 0x56, - 0x96, 0x86, 0x4c, 0x02, 0x35, 0x54, 0xa9, 0x6b, 0x2e, 0x95, 0x71, 0x91, 0x77, 0x8b, 0xda, 0xf9, - 0x18, 0x5d, 0x88, 0xd1, 0x3a, 0xa4, 0x91, 0x9c, 0xe5, 0x1f, 0xa7, 0x49, 0x0c, 0x7b, 0x9f, 0xcf, - 0xc8, 0xd5, 0x9d, 0xe0, 0xcb, 0x9c, 0x7a, 0x39, 0xa1, 0x9e, 0x2c, 0xe2, 0xa2, 0x0d, 0xde, 0xa5, - 0xe7, 0x4a, 0xb5, 0x02, 0xa7, 0x2b, 0xca, 0x93, 0x99, 0xf2, 0x64, 0x43, 0xf9, 0x13, 0x0b, 0xe0, - 0xb5, 0x82, 0xbf, 0x3a, 0x08, 0x68, 0x91, 0x08, 0x56, 0x25, 0x52, 0x0e, 0x39, 0xf5, 0x44, 0x56, - 0x24, 0xd6, 0x52, 0x1a, 0x76, 0xb7, 0xbc, 0x90, 0xf7, 0x50, 0x26, 0x91, 0x21, 0xfc, 0x03, 0x7d, - 0xcf, 0x85, 0x71, 0xc5, 0x30, 0x0c, 0x06, 0x60, 0x1d, 0x94, 0x99, 0x99, 0x3b, 0x40, 0x61, 0x8d, - 0xf2, 0x02, 0xba, 0x80, 0x5e, 0xeb, 0x80, 0x94, 0x93, 0x52, 0x9c, 0xbe, 0xf6, 0x63, 0x0e, 0x98, - 0xed, 0xef, 0x37, 0xb0, 0x8b, 0xe3, 0x82, 0xd0, 0x12, 0x25, 0x62, 0x4c, 0x84, 0x99, 0x92, 0xda, - 0xd7, 0x65, 0x15, 0xb1, 0x13, 0x46, 0x8d, 0x52, 0x27, 0xf8, 0xed, 0x1a, 0x0e, 0x61, 0x37, 0x77, - 0x66, 0x58, 0x4a, 0x4a, 0xa5, 0xef, 0x28, 0x55, 0x14, 0xd5, 0xa4, 0x08, 0x67, 0xb1, 0xba, 0xf4, - 0x9d, 0x90, 0xac, 0x62, 0xf9, 0x46, 0xd8, 0x13, 0x29, 0xd1, 0x48, 0xbf, 0xc2, 0xe4, 0xae, 0x5f, - 0x52, 0x3a, 0x8e, 0x0a, 0x4c, 0x3e, 0xa2, 0x1a, 0x61, 0xee, 0x8f, 0x54, 0x26, 0x42, 0x21, 0x0b, - 0x5c, 0x37, 0x3e, 0x8b, 0xc2, 0x57, 0x9f, 0x16, 0xfb, 0xb2, 0x65, 0x6f, 0xf7, 0xb8, 0x93, 0x21, - 0x6e, 0xa7, 0x9f, 0xdb, 0x1e, 0xe2, 0xe0, 0x45, 0xe5, 0xd1, 0x25, 0x2a, 0xe3, 0x28, 0x38, 0xf0, - 0x04, 0x2c, 0xf4, 0xae, 0x05, 0x0b, 0x1d, 0x06, 0x36, 0xb0, 0xcd, 0xa8, 0xdd, 0xdd, 0xdf, 0x85, - 0xf3, 0x5d, 0x54, 0xdc, 0xc2, 0x66, 0x21, 0x1a, 0xa1, 0xd4, 0x11, 0xb2, 0x20, 0xfd, 0xf8, 0x95, - 0xc1, 0x32, 0x62, 0x13, 0x35, 0x58, 0xec, 0x45, 0x81, 0x85, 0xd6, 0x20, 0x2c, 0x0c, 0x24, 0x02, - 0x78, 0xbb, 0x7d, 0xa2, 0xf1, 0x69, 0x76, 0x43, 0x9a, 0xd9, 0xbe, 0x34, 0xb3, 0xdd, 0x34, 0xab, - 0x38, 0xd5, 0x9d, 0xeb, 0x73, 0x67, 0xdb, 0xee, 0xec, 0xa0, 0xfb, 0x0f, 0x62, 0x08, 0x72, 0xf4, - 0x06, 0x14, 0x42, 0x17, 0xe9, 0x0d, 0x03, 0x31, 0x14, 0x80, 0x55, 0xae, 0xbb, 0x2a, 0xc3, 0x01, - 0xba, 0x36, 0xfb, 0x3a, 0xfa, 0x0c, 0x72, 0x33, 0x86, 0x91, 0x23, 0x29, 0x70, 0xfb, 0x32, 0x53, - 0x96, 0x03, 0x82, 0xff, 0x61, 0x1a, 0xc7, 0x09, 0xd6, 0x3c, 0x4e, 0xa5, 0x77, 0xec, 0x51, 0xca, - 0xc6, 0x31, 0x56, 0x03, 0xc1, 0x4d, 0xbb, 0x18, 0x06, 0xb8, 0x59, 0x68, 0x05, 0x37, 0x2b, 0xd7, - 0x3a, 0x08, 0x7a, 0xa7, 0x8a, 0x23, 0x37, 0x34, 0x42, 0x20, 0x34, 0x51, 0x69, 0x24, 0x70, 0x82, - 0x95, 0xb8, 0xe6, 0x8e, 0x90, 0x74, 0xf2, 0xbc, 0x82, 0x1b, 0xd1, 0x24, 0x4d, 0xe0, 0x94, 0x2b, - 0x25, 0x59, 0x92, 0xe0, 0x38, 0x37, 0x83, 0x23, 0xe6, 0x11, 0x5f, 0x2e, 0xe0, 0x1c, 0xbf, 0xbe, - 0x05, 0x4a, 0x5e, 0x59, 0x20, 0x30, 0x92, 0xd1, 0xf9, 0x72, 0xc0, 0x7a, 0x56, 0x1a, 0xec, 0x65, - 0x51, 0x8c, 0x6f, 0x63, 0x70, 0x60, 0x29, 0x91, 0xad, 0x44, 0xcf, 0x48, 0x6b, 0x23, 0x79, 0xfa, - 0xf7, 0x52, 0xa8, 0x70, 0x2a, 0x65, 0x1f, 0xa7, 0x69, 0x94, 0xe0, 0x2a, 0xc9, 0xe7, 0x71, 0xf1, - 0xfe, 0x1b, 0x4e, 0x17, 0xde, 0xb5, 0x8e, 0x9a, 0x8f, 0x9b, 0xdd, 0xac, 0x48, 0xe3, 0xdf, 0xc1, - 0x64, 0x33, 0x12, 0x4f, 0xf6, 0x89, 0xfe, 0x3b, 0xfa, 0x8d, 0xfc, 0x4c, 0x7d, 0x62, 0xb9, 0x72, - 0x88, 0xff, 0x5f, 0xf1, 0x36, 0xd9, 0x40, 0xd8, 0x0c, 0xcc, 0xa3, 0x28, 0x46, 0x1d, 0xd8, 0xb0, - 0xfe, 0x0f, 0x78, 0xfa, 0x2d, 0x83, 0xef, 0xb9, 0x3e, 0x00, 0xad, 0x90, 0x35, 0x77, 0xc3, 0x40, - 0x0c, 0x80, 0xb5, 0x95, 0x99, 0x99, 0xb7, 0x62, 0x68, 0x2e, 0x33, 0xe3, 0xa8, 0x27, 0xc7, 0x17, - 0xf7, 0x0c, 0x27, 0xbf, 0x3b, 0xa7, 0xce, 0xbf, 0x0f, 0x73, 0x32, 0x76, 0x11, 0xc3, 0x27, 0xc1, - 0xfb, 0xd9, 0x03, 0xfe, 0x48, 0xf3, 0x24, 0xcc, 0x2f, 0x7e, 0x92, 0x83, 0x59, 0x0e, 0x42, 0x52, - 0x92, 0x15, 0x6a, 0xb2, 0x25, 0xa3, 0xe5, 0x0b, 0xd8, 0xcc, 0x1b, 0x0b, 0x26, 0x3d, 0x69, 0x0c, - 0x06, 0x6c, 0x8b, 0x00, 0x26, 0xb5, 0x08, 0x05, 0x45, 0x42, 0xc3, 0x22, 0xfa, 0xac, 0x09, 0x39, - 0x22, 0x54, 0x8c, 0xa2, 0x50, 0x8e, 0x29, 0xf2, 0xd1, 0x08, 0x65, 0x58, 0x1b, 0x98, 0x62, 0x0e, - 0xca, 0x8e, 0xfe, 0x2b, 0x97, 0x4e, 0xd6, 0x82, 0x00, 0x1f, 0x65, 0x2d, 0x2e, 0x28, 0xc4, 0x0f, - 0xf6, 0x49, 0x63, 0x73, 0xd6, 0xe0, 0x9d, 0xb2, 0x65, 0x96, 0x22, 0xd6, 0x07, 0x57, 0x1f, 0xaf, - 0xcf, 0x1c, 0x57, 0x61, 0xea, 0x1c, 0x15, 0x84, 0xe9, 0x47, 0x7e, 0xa7, 0x83, 0xe7, 0x93, 0xcc, - 0x7b, 0xa2, 0x2b, 0x85, 0xb1, 0xcc, 0x49, 0x98, 0xef, 0x6e, 0xd8, 0xab, 0xc5, 0xc1, 0x54, 0x16, - 0x15, 0x24, 0x31, 0x26, 0x8f, 0x3f, 0xfa, 0x9e, 0xb8, 0x5e, 0x29, 0x1f, 0x6f, 0x3b, 0x71, 0xbc, - 0x89, 0x35, 0x8f, 0x96, 0x96, 0xb6, 0x23, 0x50, 0x9b, 0x44, 0x32, 0x01, 0x13, 0x6d, 0x27, 0x8d, - 0x24, 0xff, 0x73, 0x1a, 0x7c, 0x44, 0x14, 0x55, 0x06, 0xdd, 0x24, 0xca, 0x5f, 0x75, 0x1c, 0xa9, - 0x9c, 0xd6, 0x73, 0x56, 0x1a, 0x7d, 0xc2, 0x84, 0x8a, 0x63, 0x18, 0x69, 0x66, 0x66, 0xba, 0x32, - 0x43, 0x49, 0x2c, 0xbf, 0x2f, 0x99, 0xe8, 0x4b, 0xb6, 0x95, 0xcb, 0xfb, 0x3e, 0x6c, 0x55, 0xf8, - 0x4a, 0x7d, 0x50, 0x03, 0x6f, 0x05, 0x41, 0x10, 0x9e, 0x06, 0xb5, 0x6d, 0xdb, 0x8e, 0x51, 0xc7, - 0x35, 0xc2, 0xc9, 0x61, 0x7a, 0x39, 0xec, 0xce, 0xea, 0xe1, 0xdf, 0xd7, 0x78, 0x0e, 0x87, 0x9f, - 0xe0, 0xde, 0x79, 0x2e, 0x66, 0x89, 0x57, 0x07, 0x78, 0x71, 0x7f, 0x73, 0xb0, 0x8f, 0x01, 0x0b, - 0xe5, 0xc9, 0xcf, 0x91, 0xf1, 0xc2, 0x98, 0xb1, 0x10, 0xbf, 0xc6, 0xd0, 0x97, 0xc6, 0xd6, 0xa2, - 0xe0, 0x90, 0x04, 0xf4, 0x65, 0x1c, 0x45, 0xb1, 0x8c, 0xd0, 0x90, 0x22, 0xcf, 0x91, 0x81, 0x3e, - 0xb3, 0xff, 0xdf, 0x4c, 0x22, 0xfb, 0x96, 0x4c, 0x9e, 0x0c, 0x0a, 0xed, 0x1c, 0x0c, 0x31, 0x0b, - 0xfc, 0x1e, 0xc0, 0x4c, 0xc5, 0xae, 0xff, 0xf7, 0x69, 0xac, 0x72, 0xf1, 0xf7, 0x54, 0x0d, 0x08, - 0x6e, 0x6f, 0x77, 0x77, 0x97, 0xaa, 0x84, 0xfa, 0x19, 0xc1, 0x48, 0xce, 0xfa, 0xd0, 0x52, 0x22, - 0xb6, 0xe5, 0x4f, 0x57, 0x0f, 0x66, 0x6c, 0x3c, 0x64, 0xe7, 0xa1, 0x64, 0xa4, 0xe2, 0xc7, 0x4c, - 0x7a, 0x19, 0x5a, 0x92, 0x96, 0x8d, 0x85, 0xd6, 0x12, 0xc6, 0x96, 0xef, 0x21, 0xc0, 0x03, 0x9e, - 0x92, 0x27, 0xf0, 0xfe, 0x00, 0xef, 0x73, 0xca, 0x90, 0x20, 0xbc, 0x7f, 0xd9, 0xdb, 0x3f, 0xde, - 0xaf, 0xc9, 0xbd, 0x89, 0xca, 0xc2, 0xda, 0x57, 0x5c, 0x63, 0x25, 0x0a, 0xc6, 0xfe, 0x14, 0xcc, - 0xa1, 0x6f, 0xe2, 0x30, 0x22, 0x24, 0xab, 0x24, 0x17, 0x60, 0xbe, 0xdc, 0xd7, 0x78, 0x89, 0x82, - 0xc5, 0x0a, 0xcf, 0x4d, 0xfe, 0xa7, 0x8e, 0x06, 0xe4, 0xb3, 0x9f, 0xe4, 0x5d, 0x25, 0xb4, 0x5d, - 0x7f, 0xb4, 0x43, 0x15, 0x58, 0xdd, 0x25, 0x3c, 0xef, 0x95, 0x50, 0x35, 0x7a, 0x43, 0x31, 0x0c, - 0x56, 0xc6, 0x32, 0x33, 0x1e, 0xa1, 0xdc, 0xee, 0x5d, 0xcb, 0xbc, 0xe9, 0x33, 0x28, 0x8d, 0x1f, - 0x58, 0xaf, 0x92, 0x03, 0xb7, 0x2f, 0x33, 0x67, 0xb2, 0x45, 0x3f, 0xc1, 0x99, 0x25, 0x53, 0xe2, - 0xe9, 0xd5, 0xea, 0xda, 0xd6, 0x1a, 0x0a, 0x73, 0x89, 0x4a, 0xd2, 0x22, 0x41, 0xb6, 0xcf, 0x9f, - 0xf2, 0x26, 0x25, 0xe8, 0xdd, 0xde, 0x42, 0xc7, 0x65, 0x65, 0x62, 0xe0, 0x88, 0x62, 0x7c, 0x60, - 0xb4, 0x05, 0xc1, 0x32, 0x56, 0x0a, 0x03, 0x79, 0x50, 0xc5, 0x92, 0x3d, 0x95, 0x30, 0x20, 0x54, - 0x91, 0x49, 0x24, 0x30, 0xf1, 0x09, 0x62, 0xf0, 0x0d, 0x1c, 0xa6, 0x3f, 0xcd, 0x6a, 0x14, 0x35, - 0x19, 0xc1, 0xdd, 0x75, 0xd4, 0xce, 0xa3, 0x94, 0x77, 0x90, 0xa3, 0xaf, 0x90, 0xb3, 0x68, 0x25, - 0xf8, 0x6b, 0x42, 0xd2, 0x2a, 0x72, 0x1b, 0x86, 0x77, 0xfe, 0xb7, 0x57, 0x6b, 0x84, 0x98, 0x1f, - 0xb0, 0x27, 0xdc, 0x5f, 0xfd, 0xd6, 0xc6, 0x42, 0x53, 0xed, 0xf7, 0x36, 0x3e, 0xc8, 0xee, 0x5b, - 0xfb, 0xfe, 0xba, 0xde, 0x2c, 0x0a, 0x58, 0x50, 0x92, 0x60, 0x8a, 0x27, 0xac, 0x85, 0x76, 0xa8, - 0x87, 0x8f, 0x88, 0x27, 0x6f, 0xc1, 0x7c, 0x54, 0x37, 0x20, 0xef, 0x28, 0x06, 0x12, 0x49, 0x19, - 0xa2, 0x29, 0xd0, 0x35, 0x4c, 0x82, 0xbe, 0xf5, 0x6e, 0xf5, 0x4e, 0x60, 0xc1, 0x62, 0x90, 0x93, - 0xc1, 0xc8, 0x48, 0x9d, 0x44, 0xf2, 0x80, 0xa6, 0x14, 0x95, 0x45, 0x3f, 0xd8, 0xb9, 0x03, 0x74, - 0x8e, 0x45, 0x76, 0xc4, 0x30, 0x0c, 0x40, 0xb5, 0x1e, 0x5a, 0x95, 0xf9, 0x06, 0x65, 0x3a, 0x41, - 0x99, 0x79, 0xa5, 0x17, 0xdb, 0x9a, 0x89, 0x42, 0x4a, 0x65, 0x4f, 0x73, 0xfc, 0x32, 0xd7, 0xdd, - 0x98, 0xfd, 0xff, 0x87, 0xcb, 0x94, 0xab, 0xfc, 0x58, 0x1c, 0xe1, 0xd1, 0x3a, 0x5a, 0x29, 0xeb, - 0xa4, 0x62, 0xa9, 0x50, 0x13, 0xc7, 0x82, 0xa6, 0x20, 0x58, 0xc0, 0xda, 0xc3, 0x6c, 0x7f, 0x58, - 0x14, 0x30, 0xeb, 0x49, 0x39, 0x79, 0x9e, 0x87, 0xde, 0xc0, 0x6c, 0xc3, 0x7d, 0x86, 0x6e, 0xce, - 0xde, 0x63, 0x29, 0x8e, 0x4a, 0xe8, 0x9e, 0x53, 0x4d, 0x49, 0x20, 0x85, 0x51, 0x34, 0xca, 0x6e, - 0x40, 0x48, 0xbe, 0xae, 0xa4, 0x81, 0xae, 0x8a, 0x94, 0xe8, 0x49, 0x1f, 0x48, 0xa1, 0x1b, 0x48, - 0x4b, 0xae, 0x92, 0x02, 0x6d, 0x9a, 0x04, 0x68, 0x6f, 0x44, 0xb5, 0x2f, 0x8e, 0x9f, 0x74, 0xfd, - 0xa0, 0xf7, 0xbe, 0xc1, 0xda, 0x9b, 0xf1, 0xdf, 0x14, 0x52, 0xd2, 0x8a, 0xc2, 0x6b, 0xf9, 0xff, - 0xc5, 0xfa, 0x55, 0x2c, 0xe6, 0x0d, 0x89, 0xe5, 0x7d, 0x08, 0x3f, 0x1c, 0x13, 0xbf, 0xee, 0xda, - 0xdb, 0x31, 0x27, 0xbe, 0x4a, 0xf0, 0xcd, 0x82, 0x71, 0x0d, 0x7e, 0xe7, 0x22, 0xc0, 0xcd, 0xee, - 0xdd, 0xca, 0xd2, 0xca, 0x3a, 0x9a, 0xc4, 0x0d, 0x28, 0x0e, 0xed, 0x7d, 0xfd, 0x85, 0x86, 0x05, - 0x9b, 0x72, 0x79, 0x79, 0x79, 0x09, 0x3f, 0xc3, 0xe1, 0x86, 0xe5, 0x52, 0x13, 0x9b, 0x93, 0x3e, - 0xcd, 0xc7, 0x90, 0xd4, 0x90, 0x04, 0x00, 0x85, 0x51, 0x03, 0x92, 0x44, 0x31, 0x10, 0xed, 0xb5, - 0x6d, 0x5f, 0x61, 0x7d, 0x80, 0xb5, 0xad, 0x62, 0xd7, 0x47, 0xcf, 0x56, 0x3e, 0xd2, 0xa9, 0xce, - 0x1f, 0xdc, 0x7e, 0x6c, 0x16, 0xe2, 0xc7, 0xc0, 0xaf, 0xe2, 0x2f, 0x71, 0xbc, 0x90, 0xe4, 0xe9, - 0x08, 0x85, 0x0c, 0x39, 0x09, 0x09, 0x46, 0x2c, 0x0e, 0x72, 0xe2, 0xa0, 0x66, 0xa4, 0x5c, 0x42, - 0xa2, 0x9d, 0x08, 0x2d, 0x69, 0xcb, 0x62, 0x61, 0x9e, 0x39, 0x2e, 0x6e, 0x24, 0x43, 0x02, 0xb3, - 0xd7, 0x4a, 0x87, 0xe8, 0x71, 0x6c, 0x1c, 0xad, 0x58, 0xa3, 0x38, 0xbe, 0x62, 0x74, 0x23, 0x82, - 0xa5, 0x50, 0x59, 0x8b, 0x31, 0xfb, 0x14, 0x03, 0xfc, 0x29, 0x87, 0xf1, 0xf2, 0xa4, 0xeb, 0xc3, - 0x1d, 0x34, 0x16, 0x36, 0xd3, 0xd6, 0x85, 0xcd, 0xac, 0x4a, 0x29, 0x98, 0x6c, 0x02, 0x4e, 0xd6, - 0x1c, 0xc1, 0x54, 0x93, 0xe8, 0xf8, 0x79, 0x37, 0x22, 0xec, 0x8e, 0x44, 0x18, 0xd3, 0x92, 0x3a, - 0x3b, 0xee, 0xae, 0x5d, 0x92, 0x9d, 0x69, 0x82, 0xcd, 0xd4, 0x05, 0x57, 0xd0, 0x15, 0xe5, 0xff, - 0x13, 0x8a, 0x3d, 0x3e, 0x39, 0x86, 0xd9, 0x26, 0xfd, 0xb1, 0xcf, 0x41, 0x49, 0xba, 0x67, 0x58, - 0xac, 0x51, 0x92, 0x35, 0x9a, 0xb3, 0x2d, 0x99, 0x26, 0x2b, 0xed, 0xc2, 0xe4, 0xef, 0xfd, 0x6b, - 0x57, 0xf2, 0x83, 0x12, 0xf9, 0x6e, 0x2a, 0x1d, 0x45, 0xb0, 0x5b, 0xc4, 0x28, 0xa7, 0x38, 0x97, - 0xa4, 0x76, 0xcb, 0xa5, 0xcd, 0x36, 0x09, 0xce, 0x1a, 0x47, 0x12, 0x95, 0x94, 0xe0, 0x94, 0x33, - 0x8e, 0xf6, 0x4b, 0xff, 0x54, 0x37, 0x51, 0x00, 0x62, 0x88, 0xc8, 0x4c, 0xcc, 0x8f, 0x0f, 0x36, - 0x8e, 0x0f, 0xf7, 0xf4, 0x8f, 0x07, 0x54, 0x25, 0xd7, 0x06, 0x08, 0xc0, 0x40, 0x00, 0x00, 0x7f, - 0x80, 0xd0, 0x21, 0xdb, 0xc5, 0xdd, 0x3d, 0xe3, 0xa3, 0x15, 0xed, 0x5d, 0x57, 0x55, 0xb1, 0xa9, - 0x3a, 0xe1, 0xdd, 0x49, 0xa3, 0x88, 0x1a, 0x35, 0x97, 0x03, 0xb7, 0xc2, 0x87, 0xea, 0xfb, 0xed, - 0xa9, 0xcd, 0x09, 0x97, 0x52, 0x12, 0xf9, 0x01, 0x3c, 0xc8, 0xff, 0xa1, 0xa1, 0xf2, 0x28, 0x1d, - 0xd0, 0x54, 0x3d, 0xb9, 0xcc, 0x22, 0x11, 0x96, 0x4d, 0x78, 0x01, + 0x8c, 0x51, 0x03, 0x6f, 0x40, 0x41, 0x0c, 0xee, 0x82, 0xd9, 0x76, 0x30, 0xdb, 0x88, 0x67, 0x9b, + 0x71, 0x73, 0x68, 0xf2, 0xdc, 0x77, 0x98, 0x7e, 0xfd, 0x6c, 0x47, 0x77, 0xf5, 0x07, 0x58, 0x95, + 0x37, 0x9e, 0x70, 0x95, 0x8f, 0xc4, 0xc8, 0xf1, 0x14, 0x2a, 0x4e, 0x73, 0x91, 0x85, 0x9c, 0xa1, + 0x15, 0x3a, 0x64, 0x94, 0x09, 0x41, 0xe7, 0x85, 0x93, 0x50, 0x19, 0x87, 0xce, 0x61, 0xca, 0x9a, + 0x52, 0xa8, 0x3c, 0xa2, 0x9c, 0x84, 0x27, 0x0b, 0x95, 0xf6, 0xf9, 0x87, 0x2c, 0x1d, 0xd9, 0xcb, + 0xfb, 0x4f, 0x6a, 0xbc, 0x87, 0x2a, 0xe6, 0x14, 0x9f, 0x12, 0xd0, 0xf2, 0xa9, 0x56, 0xe9, 0xc9, + 0xa6, 0x61, 0x26, 0x12, 0x54, 0x81, 0xf0, 0x00, 0xeb, 0x8b, 0x27, 0x33, 0xb3, 0x93, 0x38, 0x35, + 0x7e, 0x8c, 0xbb, 0xe4, 0x02, 0xdc, 0x0e, 0xfd, 0xb7, 0x30, 0xba, 0x1f, 0x60, 0xd4, 0xbf, 0x83, + 0x51, 0xff, 0x7a, 0x3c, 0x61, 0x2b, 0x90, 0xbd, 0xc0, 0x8c, 0x91, 0xae, 0xef, 0x33, 0x0f, 0xdb, + 0x1d, 0x65, 0x8e, 0xad, 0x83, 0x86, 0x77, 0x58, 0x8a, 0x1e, 0x2f, 0xac, 0x9e, 0x2d, 0xe1, 0x81, + 0x65, 0xfc, 0x6e, 0x17, 0xb4, 0xff, 0x7f, 0x5b, 0xf9, 0x89, 0x15, 0x2a, 0x26, 0xfb, 0xb0, 0xec, + 0x5b, 0xc8, 0xfd, 0x0f, 0x90, 0x9b, 0xdf, 0x9d, 0x69, 0xfe, 0x1f, 0xe4, 0x96, 0xf7, 0x47, 0xce, + 0x85, 0x57, 0xc1, 0xc4, 0xf7, 0xd6, 0xbc, 0x97, 0xa3, 0x80, 0x32, 0xb2, 0xa1, 0xc2, 0xd5, 0xc9, + 0xc9, 0xf7, 0xcc, 0xca, 0xdd, 0xf5, 0xc4, 0xe4, 0xbd, 0xbe, 0xaf, 0x04, 0x6b, 0xe6, 0xef, 0x86, + 0x60, 0x03, 0x00, 0xad, 0x50, 0xb5, 0x62, 0xed, 0x30, 0x0c, 0xd5, 0x5b, 0x5e, 0x99, 0x99, 0x3f, + 0xa1, 0x4c, 0x6b, 0x99, 0x19, 0x46, 0x85, 0x94, 0xc6, 0x21, 0xa5, 0x92, 0xef, 0xcd, 0xe7, 0x97, + 0x99, 0x69, 0xb2, 0x45, 0x87, 0x60, 0x95, 0x72, 0x12, 0xe3, 0xe3, 0xf2, 0xe1, 0xde, 0xce, 0xee, + 0x29, 0xfa, 0x9c, 0x15, 0x28, 0x6e, 0x60, 0x18, 0x2b, 0xea, 0x41, 0x8b, 0x50, 0x41, 0xae, 0x15, + 0x68, 0x65, 0xce, 0x50, 0xab, 0x02, 0x2d, 0x96, 0x24, 0x33, 0xb9, 0x9b, 0xa2, 0x1f, 0xb9, 0x16, + 0xfe, 0x2d, 0xef, 0x1d, 0x2c, 0xe2, 0x96, 0xc9, 0x93, 0x30, 0xe5, 0x12, 0xe7, 0xc7, 0xc6, 0xf0, + 0xf6, 0x84, 0x04, 0x60, 0x8d, 0x52, 0x4b, 0x3e, 0xfa, 0x76, 0x66, 0xe2, 0x06, 0xd8, 0xcd, 0x0d, + 0xe7, 0x77, 0xe8, 0x5e, 0x4a, 0x30, 0x82, 0x85, 0xc2, 0xe0, 0x35, 0x4f, 0x63, 0x62, 0x54, 0x31, + 0xe3, 0x80, 0x32, 0x68, 0x7c, 0x00, 0xe8, 0x44, 0x4f, 0x4c, 0x70, 0x46, 0x48, 0x5a, 0xe4, 0x5c, + 0x42, 0xa3, 0x52, 0xae, 0x2c, 0x50, 0xb3, 0x3c, 0x31, 0x3e, 0xf9, 0x16, 0xe2, 0x77, 0xc0, 0xba, + 0xd8, 0x53, 0x92, 0x2a, 0x09, 0x66, 0xe7, 0xd6, 0x42, 0xd3, 0x8d, 0xc1, 0x9b, 0x06, 0xf4, 0xe0, + 0xf3, 0x59, 0xdd, 0xfc, 0xd8, 0x23, 0xdb, 0x0d, 0x4f, 0xf7, 0x35, 0x4f, 0xdf, 0xcd, 0xf7, 0xcf, + 0xd9, 0x6a, 0x32, 0xd2, 0x08, 0x95, 0x53, 0x57, 0x9e, 0x64, 0x19, 0xdd, 0x66, 0x29, 0xfe, 0xe4, + 0xc4, 0xdb, 0xce, 0xc3, 0x4a, 0x9a, 0xde, 0xfa, 0x1f, 0x2c, 0x4d, 0x68, 0xbe, 0xa8, 0xeb, 0x12, + 0x6c, 0x90, 0x45, 0x7a, 0x04, 0x41, 0x08, 0x85, 0x39, 0x40, 0x67, 0x1b, 0xcf, 0x2a, 0xee, 0x6e, + 0x37, 0xc8, 0x2d, 0xf8, 0x4a, 0xa8, 0x34, 0x6d, 0x74, 0xc9, 0xd8, 0xed, 0xc7, 0x7d, 0x56, 0x38, + 0xfc, 0x0f, 0xc8, 0xa9, 0x4a, 0x64, 0x30, 0x98, 0xf7, 0x37, 0x0c, 0xd4, 0x92, 0x4a, 0x14, 0x50, + 0x74, 0xa4, 0xd0, 0x1d, 0x39, 0xb5, 0x4f, 0x09, 0xf6, 0x44, 0x6a, 0x9c, 0x26, 0xe0, 0x60, 0xa3, + 0x96, 0x45, 0x6a, 0xa2, 0x04, 0xc8, 0x46, 0x63, 0x35, 0x37, 0xaa, 0x42, 0x93, 0xab, 0x51, 0xba, + 0xc7, 0x29, 0x97, 0x4e, 0x42, 0xcb, 0xb1, 0xad, 0xd4, 0x00, 0x8d, 0xd4, 0xad, 0x6a, 0x58, 0x1a, + 0x0c, 0xca, 0xb2, 0xa0, 0xae, 0x08, 0x1e, 0x5c, 0xa7, 0xaa, 0xe0, 0xa1, 0x13, 0x35, 0x3c, 0xf4, + 0xd8, 0x31, 0x9c, 0xce, 0x01, 0xe0, 0x02, 0x75, 0x60, 0xfb, 0x4f, 0x48, 0xb1, 0x6d, 0xa4, 0x07, + 0x97, 0xeb, 0x57, 0xcf, 0x56, 0x88, 0xae, 0x37, 0x88, 0x4e, 0x67, 0x44, 0xf0, 0x37, 0x55, 0x96, + 0x5e, 0x7e, 0xbf, 0x76, 0x9d, 0x47, 0x47, 0xb5, 0x38, 0x07, 0x17, 0x0d, 0x9c, 0x8e, 0x10, 0x56, + 0xc2, 0xac, 0xe4, 0x18, 0xb1, 0x16, 0x4b, 0x35, 0x64, 0x73, 0xa4, 0x39, 0x50, 0x88, 0x6f, 0xa3, + 0x4f, 0x55, 0x12, 0x14, 0x4a, 0x52, 0xd8, 0x08, 0x52, 0x7f, 0x54, 0x1e, 0x2b, 0x9f, 0x1e, 0x8e, + 0x30, 0x9c, 0x28, 0x3e, 0x59, 0xe4, 0xd0, 0x84, 0x87, 0x1e, 0x87, 0xa1, 0xa1, 0x09, 0x36, 0x37, + 0x82, 0x5d, 0x46, 0x84, 0x7b, 0xc8, 0xb0, 0x12, 0x00, 0x62, 0xf0, 0x48, 0xcd, 0x29, 0x49, 0x05, + 0x34, 0x42, 0x0d, 0x3a, 0x16, 0x44, 0x31, 0xb4, 0xd1, 0xda, 0xb6, 0x6d, 0xe3, 0x3f, 0x36, 0x46, + 0x73, 0x31, 0x46, 0xe7, 0xe2, 0xe9, 0xef, 0xf7, 0xd9, 0x98, 0xa8, 0xee, 0x81, 0x40, 0xfb, 0xfa, + 0xfa, 0x81, 0xa5, 0xc0, 0xfa, 0x54, 0xb0, 0x28, 0x03, 0x93, 0xc5, 0xac, 0x82, 0x82, 0x92, 0x8c, + 0xa5, 0x01, 0xa5, 0xa8, 0x99, 0x0c, 0x08, 0x79, 0xec, 0xc0, 0x43, 0xc1, 0x70, 0x38, 0xd0, 0x4e, + 0xe6, 0x30, 0xeb, 0x68, 0xb8, 0x40, 0xae, 0x03, 0xe9, 0x39, 0xa8, 0xcd, 0xdb, 0xfb, 0x1b, 0x5c, + 0xc6, 0xa4, 0x19, 0x92, 0x65, 0x98, 0x12, 0x3a, 0xe5, 0xea, 0x46, 0xca, 0x62, 0x34, 0x4e, 0x6a, + 0x48, 0x1b, 0x38, 0x24, 0x4a, 0xaa, 0x85, 0x2e, 0x56, 0x0f, 0xa7, 0xff, 0x5e, 0x7f, 0x5f, 0x86, + 0x21, 0x20, 0x1c, 0x54, 0x21, 0x10, 0x4e, 0xea, 0xf9, 0x42, 0x14, 0x18, 0x83, 0x09, 0x49, 0x27, + 0x81, 0x85, 0x16, 0x2a, 0xc2, 0x66, 0x13, 0xd5, 0x31, 0x59, 0x4a, 0xa5, 0x6a, 0x4d, 0xbc, 0xf1, + 0x17, 0x13, 0x65, 0x2d, 0x2c, 0x76, 0x80, 0x10, 0x76, 0x7a, 0x67, 0xd3, 0xf6, 0x15, 0xdb, 0x8f, + 0xc6, 0xd2, 0x9d, 0xa1, 0x52, 0x75, 0xa5, 0xf8, 0xd6, 0x5e, 0x87, 0x9d, 0xf1, 0x07, 0x2b, 0x46, + 0x0a, 0xa6, 0x65, 0xce, 0xf7, 0xe0, 0x37, 0xad, 0xd7, 0x4c, 0x44, 0x35, 0xa0, 0x57, 0xec, 0x62, + 0xd6, 0x4b, 0x7a, 0xb3, 0x23, 0xa8, 0x6f, 0xb2, 0xf2, 0x36, 0xd4, 0x44, 0xd7, 0x49, 0xc8, 0x75, + 0xe1, 0x2e, 0x85, 0x33, 0xb7, 0x10, 0xc7, 0xf5, 0xba, 0x9a, 0xd7, 0x9c, 0xed, 0x8c, 0xfe, 0x01, + 0x7d, 0x51, 0x35, 0x76, 0x03, 0x31, 0x10, 0xfd, 0x5d, 0xa8, 0x0a, 0x33, 0xf4, 0xa9, 0x0c, 0x87, + 0xc8, 0x2d, 0x66, 0x69, 0x14, 0xeb, 0x59, 0x2c, 0x79, 0x7d, 0x7c, 0x33, 0x7b, 0x59, 0xac, 0x3f, + 0x1f, 0x30, 0x62, 0x95, 0xb8, 0xa4, 0x14, 0xf2, 0x72, 0xcc, 0x81, 0xea, 0x3e, 0x95, 0x56, 0xbb, + 0xdc, 0x48, 0x6b, 0x28, 0xe4, 0x95, 0xb4, 0x34, 0x95, 0x42, 0x92, 0x60, 0x6d, 0x85, 0xc0, 0xaf, + 0xc1, 0xdd, 0x58, 0xc6, 0x48, 0xda, 0x56, 0xac, 0x71, 0x17, 0xd8, 0x71, 0x9e, 0x38, 0xe0, 0x8d, + 0x8a, 0x20, 0xab, 0x7f, 0x26, 0x8e, 0xce, 0xd8, 0x29, 0xde, 0x6d, 0x11, 0x39, 0xd4, 0x1c, 0x48, + 0xfb, 0x94, 0x70, 0x6f, 0xad, 0xa6, 0xf5, 0x02, 0x3e, 0xe9, 0x78, 0xef, 0x2e, 0xb2, 0x89, 0x36, + 0xe0, 0x2e, 0x71, 0xd0, 0xd2, 0xe4, 0x8a, 0xca, 0x51, 0x9e, 0x80, 0xbf, 0x35, 0xb3, 0x06, 0x46, + 0x85, 0xe2, 0x05, 0x8a, 0x8b, 0x78, 0x9b, 0xc4, 0x02, 0x6f, 0x4b, 0x82, 0xb8, 0x3e, 0xa0, 0x75, + 0xbd, 0xa3, 0x75, 0x73, 0x50, 0xf7, 0xfa, 0x18, 0xff, 0x72, 0xd0, 0x81, 0x2b, 0x26, 0x4a, 0x1d, + 0xa2, 0x7f, 0xad, 0xa9, 0x36, 0x17, 0xb9, 0x3b, 0xd1, 0xfe, 0x16, 0x62, 0x7f, 0xd0, 0xc7, 0xfd, + 0xb1, 0xcc, 0x1f, 0x8a, 0x52, 0x1f, 0x11, 0x7a, 0x3c, 0x31, 0xe2, 0x7a, 0x6d, 0xc4, 0x29, 0xd1, + 0x6d, 0x44, 0xf5, 0x90, 0xfa, 0xcb, 0xe6, 0x9c, 0xf7, 0x92, 0xed, 0x2a, 0x21, 0x83, 0xeb, 0x46, + 0x55, 0xdb, 0xf8, 0x7e, 0x5c, 0xdc, 0x0e, 0xe7, 0x85, 0x4f, 0x83, 0x6e, 0x44, 0x51, 0x10, 0x3d, + 0x71, 0x56, 0x61, 0x6d, 0x06, 0xb5, 0xdb, 0xbf, 0x99, 0xa7, 0xb9, 0xe9, 0xd3, 0xf5, 0xe2, 0xef, + 0xd7, 0x66, 0x34, 0x3c, 0xc2, 0x3f, 0x17, 0x9e, 0x63, 0x6a, 0x7d, 0xd1, 0x27, 0xc5, 0xaa, 0xd4, + 0xa1, 0x4c, 0x95, 0x24, 0x1b, 0x26, 0xa9, 0x22, 0xd1, 0x2c, 0x0a, 0x12, 0x5c, 0x2a, 0x21, 0x70, + 0xd6, 0x74, 0xd1, 0xb4, 0x6f, 0xa7, 0x22, 0x9d, 0x0c, 0xd7, 0xa5, 0xf1, 0x7e, 0x32, 0xd4, 0xb9, + 0xa3, 0x43, 0xe9, 0xfa, 0x14, 0x79, 0xea, 0xc9, 0x0b, 0xbf, 0xc2, 0x19, 0x15, 0x3c, 0x7d, 0xde, + 0x71, 0xbf, 0x9a, 0xd7, 0xbf, 0x9a, 0xd3, 0xbf, 0x9a, 0xd7, 0xaf, 0xae, 0x61, 0xc0, 0xf9, 0xf0, + 0x2a, 0x71, 0xa1, 0xdd, 0xb8, 0x3b, 0xe9, 0xe3, 0x97, 0xd7, 0x03, 0xf8, 0x7d, 0xff, 0x34, 0xa5, + 0x93, 0xb8, 0x9c, 0xa6, 0x91, 0xa8, 0x2e, 0x84, 0xb1, 0xac, 0x39, 0xf4, 0x6c, 0x71, 0x49, 0x2a, + 0x72, 0x6c, 0x5b, 0x6c, 0x69, 0xf0, 0x8c, 0x23, 0xa5, 0x4a, 0x1a, 0x2f, 0x16, 0x2f, 0x87, 0xdb, + 0x02, 0x4a, 0xbc, 0x6a, 0x87, 0xc7, 0x69, 0x46, 0x89, 0x6a, 0x9e, 0x3a, 0x47, 0xa5, 0x4a, 0xb8, + 0x44, 0x55, 0x87, 0xd6, 0xa7, 0x7e, 0x00, 0x1b, 0xba, 0x48, 0xd8, 0xa2, 0x6a, 0xff, 0xd6, 0xb1, + 0xe1, 0x8a, 0xb4, 0xc3, 0xf9, 0x80, 0x08, 0xe7, 0xc3, 0x94, 0x83, 0x3c, 0x68, 0xcc, 0xb1, 0x35, + 0xa6, 0xe6, 0xf7, 0x97, 0x2c, 0xf6, 0x00, 0x6c, 0x50, 0x45, 0x72, 0x05, 0x41, 0x08, 0x65, 0x1b, + 0x77, 0xf7, 0x33, 0x44, 0x6f, 0x43, 0xb5, 0x30, 0x7f, 0x18, 0xa3, 0x03, 0xc4, 0x4e, 0x1f, 0xd7, + 0xae, 0xac, 0x70, 0x9e, 0x40, 0x4b, 0x83, 0x53, 0xc2, 0xfb, 0x0b, 0xd4, 0x1b, 0x54, 0x91, 0x11, + 0x8d, 0xf4, 0x9e, 0x14, 0x76, 0x51, 0xe2, 0x47, 0x8a, 0xe3, 0xad, 0x3b, 0x2c, 0x19, 0x4d, 0x26, + 0x0a, 0x4b, 0x4e, 0x3a, 0xf2, 0x14, 0x06, 0x4c, 0x6d, 0x70, 0x58, 0x6e, 0x1c, 0x93, 0x8c, 0x25, + 0x4c, 0x2c, 0x13, 0x6a, 0xc8, 0x2c, 0x18, 0x07, 0x82, 0xb3, 0xe6, 0x6e, 0x18, 0xe0, 0xec, 0xce, + 0x22, 0x9c, 0x3d, 0x70, 0xc3, 0xb0, 0xd6, 0xb3, 0x19, 0x8e, 0x92, 0x69, 0x84, 0xb5, 0x22, 0xea, + 0x1a, 0xd8, 0x51, 0xa9, 0x50, 0x78, 0xfd, 0x58, 0x81, 0x1d, 0xfc, 0x10, 0xa9, 0x26, 0x6b, 0x5f, + 0x17, 0xb0, 0x57, 0x4d, 0xd6, 0x7f, 0x91, 0x3f, 0xac, 0xaf, 0x3e, 0xc9, 0xaf, 0xfd, 0x25, 0xbf, + 0xf0, 0xf5, 0x6b, 0x1d, 0xa3, 0x72, 0x9e, 0x11, 0x92, 0x95, 0x49, 0x1e, 0x60, 0x63, 0xa0, 0x59, + 0x48, 0x4f, 0x58, 0x82, 0x3a, 0xfb, 0x9b, 0x32, 0x27, 0x73, 0xd8, 0xf8, 0xfb, 0x75, 0xe1, 0x17, + 0xe2, 0x56, 0x85, 0x38, 0xff, 0x89, 0x38, 0x5f, 0x21, 0xfe, 0x6f, 0x17, 0x36, 0x34, 0xca, 0x04, + 0xc7, 0x6f, 0xa6, 0x7d, 0xe5, 0xaf, 0xd6, 0xbd, 0x0c, 0xc6, 0x04, 0x05, 0x20, 0x94, 0xcd, 0x07, + 0xf7, 0xbf, 0x18, 0x9a, 0x9b, 0xf9, 0x91, 0x5c, 0xc3, 0x07, 0xb5, 0x9d, 0x0f, 0xd5, 0x76, 0x00, + 0x74, 0xce, 0x83, 0x72, 0x6c, 0x41, 0x10, 0x06, 0xe0, 0x2e, 0x5e, 0xc4, 0xb6, 0x6d, 0xeb, 0x09, + 0x62, 0xab, 0xdc, 0x35, 0xe8, 0xcd, 0xce, 0xd1, 0x9c, 0xf4, 0x4c, 0xb2, 0xaf, 0x9f, 0xac, 0x5d, + 0x6e, 0x7e, 0x3f, 0x5c, 0x50, 0xe4, 0x49, 0xe1, 0xbb, 0x61, 0x8a, 0xc8, 0x39, 0x7c, 0x10, 0x29, + 0x31, 0x2a, 0x1b, 0xa7, 0x22, 0x31, 0x36, 0x41, 0x16, 0xda, 0x58, 0x94, 0x11, 0xc1, 0x1a, 0xa6, + 0x0e, 0x96, 0xbf, 0x9c, 0x84, 0xe1, 0xd0, 0xfc, 0x6e, 0xc6, 0x56, 0x53, 0x0c, 0xc3, 0x4c, 0x29, + 0x09, 0x4f, 0x0c, 0xd3, 0x28, 0xd9, 0xe8, 0x0f, 0x42, 0x72, 0x69, 0x62, 0x73, 0x30, 0x63, 0xa5, + 0x23, 0xfe, 0x26, 0xc6, 0xf8, 0xd3, 0x7b, 0x18, 0xb1, 0x36, 0xc6, 0x62, 0x03, 0xe6, 0xb0, 0x7e, + 0xd6, 0xfb, 0xc2, 0x42, 0x85, 0x6d, 0xdc, 0x8d, 0xbc, 0xbb, 0x9a, 0x77, 0x47, 0x6b, 0xdc, 0xd1, + 0x8a, 0x3b, 0xdb, 0xe0, 0xce, 0xd5, 0xff, 0x1e, 0xab, 0x71, 0x17, 0x1a, 0xdc, 0xbf, 0xcf, 0x37, + 0x07, 0x2d, 0xcd, 0x99, 0xbc, 0x39, 0x99, 0x37, 0x27, 0x73, 0x26, 0x63, 0xa0, 0xb3, 0xa2, 0x0d, + 0x36, 0x68, 0x93, 0xec, 0xf6, 0x0f, 0xf6, 0x61, 0xa8, 0xfe, 0x71, 0x57, 0x0d, 0x3a, 0xd2, 0x80, + 0x76, 0x3a, 0x4a, 0x9c, 0x65, 0x00, 0x13, 0xda, 0x50, 0x60, 0x56, 0x24, 0x3a, 0x4b, 0x91, 0xc6, + 0x84, 0x33, 0x48, 0xfb, 0xfb, 0x78, 0xb0, 0xab, 0x65, 0x8c, 0xbb, 0x67, 0xc7, 0x2d, 0x93, 0xed, + 0xe5, 0x43, 0xcd, 0xb1, 0xf5, 0xc2, 0x93, 0x6e, 0xb9, 0x71, 0x96, 0xdf, 0xf8, 0x99, 0x2c, 0x22, + 0x5a, 0x90, 0x9c, 0x22, 0x8c, 0x14, 0x80, 0x00, 0xa5, 0xd1, 0xb1, 0x4a, 0xc0, 0x40, 0x0c, 0x80, + 0xe1, 0x4c, 0xae, 0x2a, 0xd8, 0xfa, 0x04, 0xba, 0x15, 0x6a, 0xfb, 0x0c, 0xe2, 0x60, 0xc7, 0xee, + 0x21, 0x6d, 0x23, 0x1e, 0x77, 0x97, 0x94, 0xe4, 0x14, 0x7d, 0x7b, 0xeb, 0x52, 0x14, 0x04, 0x87, + 0x5b, 0x43, 0xf8, 0xf8, 0x43, 0x20, 0x44, 0x8d, 0x84, 0x42, 0xa2, 0x28, 0xf6, 0x82, 0xc3, 0xb0, + 0x2d, 0x19, 0x57, 0xcd, 0x3b, 0x49, 0x50, 0x41, 0xa3, 0x2d, 0x28, 0x2e, 0x89, 0xe1, 0xee, 0xcd, + 0x17, 0x68, 0x62, 0x70, 0xc7, 0xac, 0x1b, 0x67, 0x68, 0x8c, 0x77, 0xa6, 0xc2, 0x06, 0xad, 0x6a, + 0x46, 0x67, 0x7b, 0x67, 0x83, 0xcb, 0xb1, 0xaf, 0x14, 0xae, 0xc6, 0x4a, 0xe0, 0xc2, 0x4b, 0x58, + 0xe3, 0x3f, 0xe7, 0xdc, 0x7f, 0x5b, 0xed, 0x0f, 0xab, 0x3d, 0xad, 0xdb, 0x5f, 0x31, 0x7d, 0xa5, + 0x70, 0x3d, 0x56, 0x02, 0xf0, 0xcc, 0xbc, 0x17, 0x66, 0x99, 0xe6, 0x07, 0x3c, 0x37, 0x6e, 0x30, + 0xa9, 0x11, 0x6a, 0x21, 0x3c, 0x5e, 0xc7, 0x1f, 0xc7, 0x4c, 0x28, 0xa1, 0xb3, 0xb8, 0x9a, 0x03, + 0x4c, 0x21, 0x7d, 0x3e, 0x29, 0xce, 0xdd, 0xe3, 0xfa, 0xaa, 0xdd, 0x14, 0x0a, 0xff, 0xd9, 0xf0, + 0x05, 0x7c, 0x50, 0xb5, 0x5a, 0x06, 0x41, 0x0c, 0x4c, 0x87, 0x3b, 0x15, 0xee, 0x15, 0xae, 0x3d, + 0x6e, 0x1d, 0xf4, 0x39, 0xcb, 0xc7, 0xad, 0x66, 0x0d, 0x7b, 0x7a, 0x5c, 0x7f, 0xab, 0xc7, 0x07, + 0xae, 0x85, 0x7e, 0x3e, 0x63, 0xbc, 0x5d, 0x3b, 0x29, 0x6b, 0x5e, 0xbb, 0x16, 0x89, 0xb0, 0x64, + 0xe3, 0x72, 0x2b, 0xd8, 0x62, 0xc8, 0x2b, 0xc1, 0x78, 0x1f, 0x0b, 0x18, 0x53, 0x22, 0x46, 0x34, + 0x5c, 0x91, 0x81, 0x31, 0xfb, 0x06, 0xc5, 0x9a, 0xb4, 0x6e, 0xa2, 0x16, 0x9a, 0x60, 0xed, 0x83, + 0x1f, 0xc8, 0x51, 0x9e, 0x28, 0xc0, 0x38, 0xb3, 0xc1, 0x48, 0xe1, 0x81, 0x02, 0x0c, 0xe0, 0x51, + 0x1e, 0xaa, 0x96, 0xaa, 0xc5, 0xc6, 0x94, 0xd6, 0x0e, 0x83, 0xad, 0xb4, 0x33, 0xef, 0xda, 0xc1, + 0x3f, 0xda, 0xc1, 0x1f, 0xed, 0xd0, 0x1f, 0x6d, 0xf7, 0xce, 0xcd, 0x0e, 0xc6, 0xa7, 0xad, 0xed, + 0xfd, 0xed, 0x96, 0x15, 0x56, 0xd0, 0xc5, 0x4e, 0x3d, 0x26, 0xb1, 0x08, 0xa2, 0xba, 0x23, 0xa4, + 0xe8, 0x2c, 0x3f, 0xc2, 0x14, 0x17, 0x9f, 0xce, 0x68, 0x7c, 0x4a, 0xff, 0x6a, 0xce, 0xe0, 0x7f, + 0x6c, 0xec, 0x4d, 0x6f, 0x84, 0xcd, 0xdf, 0x1e, 0xab, 0xf3, 0x04, 0xc3, 0x07, 0xaf, 0x33, 0xc3, + 0xe6, 0x02, 0xfa, 0x58, 0x0e, 0x00, 0x6d, 0x90, 0xb5, 0x72, 0x04, 0x31, 0x10, 0x44, 0x27, 0x33, + 0x33, 0x33, 0x53, 0x66, 0xce, 0xcd, 0xcc, 0x14, 0xf6, 0xd2, 0x9c, 0x4b, 0x75, 0xd2, 0xce, 0x5a, + 0xda, 0x35, 0xfc, 0xbd, 0xeb, 0x18, 0x33, 0xe1, 0xeb, 0x37, 0x4d, 0xb7, 0x4a, 0xff, 0x5d, 0x08, + 0x5e, 0x71, 0x65, 0x92, 0x4c, 0x3b, 0xc6, 0xa3, 0xce, 0x1c, 0x2c, 0x27, 0xec, 0xa7, 0x6c, 0xa9, + 0xf3, 0x98, 0x7d, 0x83, 0x9d, 0x0f, 0x84, 0x62, 0x12, 0x3f, 0x56, 0x12, 0xc3, 0xfa, 0x91, 0x12, + 0x04, 0x9a, 0x69, 0x05, 0x89, 0xa3, 0xc5, 0x5c, 0xa6, 0x35, 0x2d, 0x66, 0x2e, 0xa0, 0xc5, 0x1f, + 0x95, 0x53, 0x34, 0x98, 0x57, 0xce, 0xc1, 0x48, 0xc4, 0x86, 0x06, 0xab, 0xa0, 0x49, 0x04, 0x56, + 0x45, 0x9f, 0x0c, 0x76, 0x49, 0x2c, 0x3f, 0x34, 0x25, 0x81, 0x63, 0xfb, 0xcd, 0x16, 0xe6, 0x2b, + 0x4d, 0x69, 0x48, 0xc4, 0xa0, 0x74, 0x40, 0x33, 0x68, 0xbc, 0xeb, 0x3c, 0xe5, 0x30, 0xdf, 0x56, + 0x60, 0xae, 0x90, 0xda, 0x57, 0x97, 0xd7, 0x57, 0x13, 0x3f, 0x7b, 0xbd, 0xc4, 0x99, 0x56, 0x29, + 0xc3, 0xfd, 0xee, 0xec, 0x1e, 0xee, 0xb6, 0x05, 0x6c, 0x15, 0x00, 0x53, 0x75, 0x80, 0xa9, 0x2a, + 0x60, 0xa9, 0xc9, 0x62, 0xba, 0xce, 0x70, 0xb5, 0xd9, 0x50, 0x8b, 0xf5, 0x71, 0x72, 0xd8, 0x2e, + 0x03, 0xed, 0x0b, 0x41, 0x6d, 0xe4, 0xc2, 0xa6, 0xf3, 0xb6, 0x40, 0x78, 0xdf, 0xc5, 0x0e, 0x76, + 0x70, 0xd8, 0xde, 0xb5, 0xd0, 0xf6, 0x7a, 0x41, 0x78, 0xbd, 0xd8, 0xf3, 0x78, 0x1d, 0x76, 0xbc, + 0xaa, 0x3d, 0xdf, 0xa8, 0x06, 0x9a, 0xa8, 0xf3, 0x5e, 0x6a, 0xbe, 0xfc, 0x07, 0xad, 0xcf, 0xb5, + 0x5a, 0x2c, 0x41, 0x10, 0x05, 0xe0, 0xba, 0x17, 0x8d, 0x70, 0x77, 0x8f, 0x71, 0x88, 0x71, 0x4d, + 0xb0, 0xf4, 0x7c, 0x23, 0xb5, 0x3b, 0x3d, 0xd2, 0x35, 0x2d, 0x6b, 0x4f, 0x8f, 0x3b, 0x84, 0xa4, + 0xa5, 0xff, 0xa1, 0x0b, 0x95, 0xb7, 0x8e, 0x05, 0x37, 0x17, 0x62, 0x03, 0xdc, 0xad, 0x63, 0x0d, + 0x6b, 0xd8, 0x86, 0x67, 0x5b, 0x28, 0x1d, 0xe4, 0x88, 0x92, 0xc0, 0x13, 0x5d, 0x6e, 0x5d, 0xfb, + 0x20, 0xca, 0x70, 0xab, 0x95, 0xc7, 0xfe, 0xf6, 0x05, 0x22, 0x29, 0xca, 0x40, 0x2b, 0xd1, 0xb0, + 0x41, 0xac, 0x04, 0x61, 0xce, 0x34, 0x5f, 0x73, 0x21, 0xf5, 0x67, 0xca, 0x39, 0x14, 0x12, 0x73, + 0x41, 0xfd, 0x96, 0x4b, 0x0e, 0x3c, 0x5b, 0x1a, 0x10, 0x29, 0xe0, 0xd8, 0xd6, 0xd9, 0xd2, 0x3f, + 0x76, 0x09, 0x4a, 0x89, 0x32, 0xf6, 0x78, 0x1f, 0x18, 0x42, 0xfe, 0xf4, 0x5f, 0x7c, 0x00, 0x2d, + 0xe0, 0xa6, 0x67, 0xfb, 0xf4, 0xdd, 0xb1, 0x76, 0x62, 0x1d, 0xb5, 0x07, 0x71, 0x9d, 0xb5, 0xaf, + 0x59, 0xb6, 0x70, 0xcd, 0xb5, 0xf5, 0xed, 0xf5, 0x5f, 0x05, 0x2b, 0x4f, 0x82, 0x91, 0x4f, 0x82, + 0x91, 0xf7, 0x07, 0xa3, 0x9f, 0x04, 0x23, 0x5f, 0xd3, 0x0d, 0xed, 0xfe, 0xe1, 0xb1, 0x76, 0x55, + 0xa8, 0xdf, 0xce, 0x81, 0xba, 0xdf, 0xd6, 0x41, 0x54, 0xb0, 0x4b, 0xb4, 0xe4, 0x52, 0x6d, 0xa1, + 0xb1, 0xf6, 0x7b, 0x96, 0xd9, 0x4a, 0x2d, 0xcf, 0x69, 0xf6, 0x09, 0x31, 0xdb, 0x50, 0x15, 0x45, + 0xbd, 0x9f, 0x28, 0xbd, 0x1f, 0x14, 0x84, 0x56, 0xc5, 0x55, 0x06, 0xbb, 0x52, 0x4b, 0x83, 0xc6, + 0x24, 0x7c, 0x81, 0xa1, 0x30, 0xde, 0x53, 0xdf, 0x27, 0xe9, 0x04, 0xbe, 0xf6, 0x1e, 0x00, 0x7c, + 0x8e, 0xc5, 0x72, 0x03, 0x31, 0x0c, 0x86, 0xd5, 0xd3, 0xf6, 0x54, 0x66, 0xe6, 0x6b, 0xf8, 0x0d, + 0xca, 0x8c, 0x67, 0xcd, 0x82, 0x92, 0x78, 0x6a, 0x5b, 0x19, 0x6b, 0x83, 0x4f, 0x5f, 0xa5, 0xcc, + 0x66, 0xfc, 0xbf, 0x0f, 0x1c, 0x49, 0xd3, 0xb3, 0xe5, 0x46, 0x1f, 0xbb, 0xa5, 0x32, 0x0a, 0x79, + 0xe1, 0x00, 0xe3, 0x39, 0x05, 0x67, 0x7c, 0x6c, 0x31, 0x6d, 0xc6, 0x39, 0xc0, 0x85, 0xf1, 0xd4, + 0x15, 0x72, 0x06, 0x1d, 0x95, 0x6b, 0x56, 0x8a, 0x25, 0x0c, 0xd4, 0xa2, 0x58, 0x9f, 0xc1, 0x32, + 0x5a, 0x0e, 0x31, 0x72, 0x1e, 0xa3, 0x67, 0xa4, 0x9e, 0x9e, 0xe9, 0xc7, 0xd7, 0x24, 0x81, 0x91, + 0xb3, 0x03, 0x94, 0x0a, 0xa6, 0xec, 0x5a, 0xb1, 0x37, 0xec, 0x31, 0xc4, 0x99, 0x61, 0x4c, 0x2c, + 0xc1, 0x7c, 0x5b, 0x12, 0x98, 0xef, 0x9a, 0xba, 0x81, 0xe8, 0x3d, 0x6e, 0x1c, 0x93, 0x60, 0xb2, + 0x06, 0x21, 0x49, 0xcb, 0x73, 0x17, 0xe6, 0x83, 0x94, 0x2b, 0x65, 0x18, 0x65, 0x76, 0x9a, 0x19, + 0x3a, 0x14, 0x20, 0x7a, 0xb5, 0x8c, 0xbe, 0x59, 0x5e, 0xc6, 0x2a, 0x70, 0x54, 0xc6, 0x07, 0x9b, + 0x87, 0xf8, 0x43, 0x10, 0xbc, 0x49, 0x14, 0x87, 0x92, 0x06, 0x22, 0xaf, 0x26, 0xde, 0x53, 0x9a, + 0xff, 0x66, 0x84, 0xb0, 0x57, 0x6f, 0x5b, 0xab, 0x93, 0x9a, 0xe9, 0x38, 0x54, 0x43, 0x98, 0x7b, + 0x34, 0x22, 0xe8, 0x38, 0x23, 0xa7, 0x9b, 0xb7, 0x58, 0x84, 0xf5, 0x2f, 0xa2, 0x08, 0xf3, 0x1f, + 0x8a, 0xfa, 0xee, 0x8b, 0x1b, 0x42, 0x34, 0xa0, 0xc0, 0xff, 0xb3, 0x77, 0x86, 0x6c, 0x9d, 0x94, + 0xad, 0xe3, 0x0b, 0x7b, 0xe6, 0x03, 0xb7, 0xfa, 0x0d, 0x37, 0xfb, 0x19, 0x37, 0xf3, 0x0d, 0x07, + 0xd7, 0x81, 0x2f, 0x4c, 0xfa, 0xb4, 0xa2, 0x7c, 0xac, 0x01, 0x2f, 0x09, 0x0a, 0x78, 0x00, 0x7c, + 0x90, 0x45, 0x76, 0xc4, 0x40, 0x0c, 0x44, 0x15, 0x66, 0xe6, 0x53, 0x98, 0xbd, 0xcf, 0x36, 0x9c, + 0xb9, 0x80, 0x9e, 0xd9, 0x6d, 0x92, 0x47, 0xdd, 0x03, 0xb7, 0x1f, 0x66, 0x5a, 0x0a, 0xab, 0xea, + 0xc3, 0x1f, 0xd3, 0xb7, 0x08, 0x98, 0x30, 0x17, 0x52, 0x62, 0x49, 0x61, 0x54, 0xc2, 0x19, 0x47, + 0x75, 0xe4, 0xa9, 0x88, 0xe1, 0x0e, 0x7d, 0x16, 0x61, 0x12, 0x21, 0x4b, 0xd3, 0x32, 0x51, 0x46, + 0x2c, 0xbc, 0xc2, 0x80, 0xfb, 0x82, 0xd8, 0x43, 0x52, 0x1e, 0x56, 0x84, 0x51, 0x77, 0xb0, 0x59, + 0x79, 0xc5, 0x60, 0x5a, 0x49, 0x62, 0x09, 0xe7, 0x44, 0xe5, 0x70, 0xb5, 0x1d, 0x31, 0x9c, 0x8d, + 0x9b, 0x70, 0x36, 0xd8, 0x29, 0xc5, 0x70, 0x2b, 0x48, 0x3d, 0x05, 0xd0, 0x30, 0x7e, 0x22, 0xc2, + 0x80, 0xca, 0xda, 0xab, 0x04, 0x55, 0xc8, 0x5e, 0x28, 0x08, 0xfd, 0x22, 0x82, 0xe7, 0x96, 0xf4, + 0xe1, 0x78, 0xc1, 0xcd, 0xf1, 0xcc, 0xcd, 0x35, 0xee, 0xd4, 0x3d, 0x59, 0xd0, 0x3d, 0x9e, 0xe8, + 0x1e, 0x2f, 0xeb, 0xee, 0xbd, 0x7f, 0xa2, 0x65, 0x18, 0xe6, 0x46, 0xe5, 0xb7, 0xb8, 0x55, 0x14, + 0xf0, 0x36, 0xd4, 0x7f, 0xeb, 0x88, 0x58, 0xc0, 0xd9, 0x6e, 0x26, 0x91, 0xac, 0x2b, 0xea, 0xc0, + 0xdb, 0x88, 0x0d, 0xdc, 0x93, 0x3f, 0xd6, 0xc6, 0xb2, 0xa9, 0xd4, 0x12, 0x84, 0x47, 0x5c, 0x9e, + 0x6d, 0x81, 0x72, 0x64, 0xeb, 0x06, 0x4e, 0x65, 0x76, 0x47, 0xbd, 0x67, 0x2f, 0x37, 0x2c, 0x4b, + 0x37, 0xb1, 0x63, 0x3a, 0x4d, 0xc3, 0x9d, 0x6d, 0xc3, 0x7d, 0xab, 0x1a, 0x58, 0x8f, 0xc2, 0x79, + 0xe7, 0xd0, 0x76, 0xad, 0xfe, 0x0c, 0xb1, 0x7a, 0x18, 0x00, 0x7d, 0x50, 0x35, 0x77, 0x03, 0x31, + 0x0c, 0x56, 0xb9, 0x9d, 0xca, 0x34, 0x95, 0xb7, 0x52, 0x70, 0x2e, 0x33, 0xe3, 0xa8, 0xa7, 0xdc, + 0x29, 0x89, 0x5f, 0x7c, 0xd6, 0x55, 0x76, 0xe0, 0xdf, 0x97, 0x99, 0x46, 0xc1, 0x87, 0x70, 0xb1, + 0x7e, 0x88, 0xf9, 0x62, 0x2e, 0x83, 0x91, 0x24, 0x29, 0x39, 0x23, 0x0e, 0x95, 0x62, 0x23, 0xc8, + 0xa1, 0xca, 0xea, 0x38, 0xc0, 0x64, 0xb9, 0x6e, 0x2d, 0x4c, 0xd6, 0x7d, 0x09, 0xfa, 0x6a, 0xc6, + 0x7b, 0x4c, 0x24, 0xe6, 0x04, 0xfa, 0x94, 0x53, 0xa6, 0xc0, 0x8a, 0x25, 0x35, 0x71, 0x85, 0x51, + 0x7d, 0x36, 0x97, 0x45, 0xcf, 0x6a, 0xc8, 0x66, 0xd0, 0x8a, 0x12, 0x4a, 0x20, 0x74, 0x82, 0xdc, + 0x0a, 0xac, 0x8e, 0x2c, 0x7a, 0x76, 0x5e, 0xd4, 0xc3, 0x42, 0xf6, 0xff, 0xfb, 0xd0, 0xff, 0x57, + 0xa5, 0x5a, 0xa6, 0xb0, 0xba, 0x9a, 0x41, 0x6f, 0x25, 0x60, 0xf4, 0xce, 0x05, 0x43, 0x75, 0x67, + 0xca, 0x86, 0xe3, 0x4f, 0x9b, 0x66, 0xb6, 0x70, 0x97, 0x29, 0x7e, 0x2c, 0xfa, 0x02, 0x6b, 0x62, + 0x9e, 0xe8, 0xa2, 0x2a, 0x05, 0xe8, 0xbc, 0x35, 0xfe, 0x98, 0x7d, 0x15, 0xaf, 0xa8, 0xf2, 0xa3, + 0x82, 0x92, 0x65, 0x98, 0x7e, 0xca, 0x3d, 0xf0, 0x29, 0xf7, 0xc0, 0x5b, 0x6e, 0x98, 0xf8, 0x3f, + 0xc4, 0xa0, 0x48, 0x82, 0x9e, 0xb5, 0xc1, 0x0a, 0x03, 0x2f, 0x4b, 0x80, 0x4b, 0x76, 0x9e, 0x37, + 0x29, 0xc5, 0x4b, 0xb1, 0xa4, 0xf8, 0xce, 0xd5, 0xbb, 0xef, 0x62, 0x13, 0x51, 0x10, 0x5d, 0xda, + 0xbe, 0x3c, 0x3b, 0x91, 0xe6, 0xb3, 0x99, 0x57, 0x1f, 0x4f, 0x16, 0x86, 0x8e, 0xe4, 0x82, 0x96, + 0x4e, 0x32, 0xc5, 0x8b, 0xec, 0xb7, 0x13, 0x36, 0x4d, 0xd9, 0xc0, 0xd8, 0x77, 0xc0, 0xc2, 0xcb, + 0xfe, 0x01, 0xb5, 0x8f, 0xb5, 0x95, 0xc4, 0x60, 0x0c, 0x84, 0x15, 0x1d, 0x33, 0x33, 0x54, 0x60, + 0x6a, 0xe1, 0x20, 0x3c, 0x2a, 0x40, 0xcf, 0xa0, 0xf5, 0x93, 0x49, 0xfe, 0x61, 0xa1, 0xfb, 0x65, + 0x5e, 0xa7, 0x97, 0x89, 0x66, 0xe6, 0x13, 0x18, 0x29, 0x42, 0xdd, 0xe3, 0x50, 0x30, 0x70, 0xff, + 0x30, 0x96, 0xb2, 0x0e, 0x2b, 0x96, 0x0a, 0x75, 0x98, 0xb0, 0x60, 0x54, 0x10, 0x3c, 0xb5, 0x4d, + 0x04, 0x47, 0x39, 0x1b, 0x83, 0xa5, 0x24, 0x54, 0xc2, 0x91, 0xa6, 0x9a, 0x42, 0x4b, 0x1a, 0xae, + 0x30, 0xd2, 0x9c, 0xa4, 0x84, 0xda, 0xf8, 0x81, 0x0f, 0xc7, 0x22, 0x25, 0x1a, 0xd2, 0x9d, 0xe1, + 0xea, 0x16, 0x0b, 0xd1, 0x21, 0x8a, 0x0d, 0x61, 0x3f, 0xf8, 0x27, 0x63, 0xf8, 0xb3, 0xa1, 0x1d, + 0x79, 0x7e, 0xfa, 0x58, 0x48, 0x9a, 0x72, 0x95, 0xe2, 0xdc, 0xe2, 0x7e, 0x66, 0x41, 0xa6, 0xae, + 0xa4, 0x0b, 0xfb, 0xf3, 0xcd, 0xf9, 0xda, 0x66, 0x37, 0xc0, 0xf7, 0xbf, 0xef, 0xc0, 0x6f, 0x84, + 0x7c, 0x6d, 0xb5, 0x8b, 0x02, 0x5e, 0x47, 0xa8, 0xaf, 0x5d, 0x6e, 0x31, 0x9c, 0x2e, 0x01, 0x9f, + 0x6e, 0xa4, 0x9e, 0x6a, 0x7f, 0xd1, 0x3c, 0xa0, 0x44, 0x13, 0x6a, 0x2c, 0x95, 0xb5, 0x70, 0xb9, + 0xf4, 0xc6, 0xf3, 0xda, 0xee, 0x6c, 0x26, 0xba, 0x5d, 0x5f, 0xcc, 0x45, 0x9b, 0x86, 0x60, 0x3d, + 0xd7, 0x75, 0xa9, 0x11, 0xfc, 0x7a, 0x84, 0xbc, 0xbd, 0x04, 0xbb, 0x3d, 0xe7, 0x1a, 0x00, 0x54, + 0x4e, 0x85, 0x4d, 0x45, 0x41, 0x10, 0xdc, 0x28, 0xb4, 0xf0, 0x88, 0xe1, 0xee, 0x56, 0x00, 0x25, + 0xe0, 0xd1, 0xc9, 0xc9, 0x42, 0x9e, 0xdc, 0xed, 0xd9, 0x97, 0xee, 0xbf, 0x6b, 0x7c, 0x7c, 0xa8, + 0x3c, 0xdc, 0xdf, 0xdf, 0x33, 0x12, 0x07, 0x56, 0x85, 0x13, 0x3a, 0x49, 0x0a, 0x52, 0x14, 0xbc, + 0x80, 0x87, 0x13, 0xc6, 0xab, 0x0e, 0x99, 0x7d, 0x96, 0x94, 0x69, 0x57, 0xc4, 0x21, 0x73, 0xea, + 0x73, 0xa2, 0x9d, 0x39, 0x49, 0xf4, 0x89, 0x37, 0x56, 0x0e, 0x1f, 0x4f, 0xf8, 0xe8, 0x85, 0xc4, + 0x8e, 0xf1, 0xf1, 0xfb, 0xf0, 0xf8, 0xfa, 0x08, 0x23, 0x2e, 0x28, 0x5f, 0x8b, 0x47, 0x52, 0xb6, + 0x16, 0xe8, 0x8e, 0xe9, 0x1a, 0x21, 0xd3, 0xc5, 0xa0, 0xfe, 0xab, 0xa9, 0x6a, 0xeb, 0x9c, 0xe1, + 0xc4, 0xb2, 0xa3, 0x6a, 0xb9, 0x4f, 0x07, 0xd0, 0xa9, 0xb6, 0xff, 0x0c, 0xce, 0xc1, 0xcb, 0x80, + 0x0e, 0x45, 0xcf, 0x07, 0xe1, 0x62, 0x29, 0xb4, 0xb7, 0xf1, 0xe0, 0x18, 0xdb, 0xda, 0x78, 0x8c, + 0x49, 0x40, 0x77, 0xe0, 0xb3, 0x5c, 0x01, 0x64, 0x39, 0x2f, 0x92, 0xb5, 0xbc, 0x70, 0x6b, 0x45, + 0xd1, 0xcc, 0xe2, 0x43, 0xb2, 0x47, 0x02, 0x4d, 0x8e, 0xcb, 0xdc, 0x8c, 0x96, 0xc6, 0x33, 0x02, + 0xc3, 0xb5, 0x24, 0xb1, 0x28, 0xde, 0xd9, 0x38, 0xbe, 0xb8, 0x02, 0xec, 0x17, 0x24, 0x23, 0x85, + 0xe0, 0x46, 0x4a, 0xa1, 0x05, 0x94, 0x80, 0x05, 0x71, 0xea, 0x00, 0xb5, 0x51, 0x45, 0x56, 0x04, + 0x51, 0x0c, 0xcc, 0x1a, 0xdb, 0xcd, 0x0e, 0x3b, 0x02, 0xce, 0x25, 0x70, 0xd8, 0xe7, 0xb5, 0xd4, + 0xd0, 0x69, 0x49, 0xfa, 0x25, 0x7f, 0xe4, 0xf6, 0xb8, 0xbb, 0xee, 0xcb, 0x8b, 0xce, 0x2a, 0xd1, + 0xe6, 0xc0, 0x4a, 0xf0, 0xfe, 0x3a, 0x17, 0xd6, 0xf5, 0x99, 0x8a, 0x29, 0x7b, 0x56, 0x8a, 0x71, + 0xde, 0x82, 0x96, 0x46, 0x91, 0xd3, 0x7c, 0x23, 0x11, 0xdc, 0x59, 0x89, 0x8e, 0xe6, 0x1d, 0x3d, + 0xb2, 0x04, 0xa7, 0x05, 0xb3, 0x8e, 0x03, 0x3e, 0x86, 0xd3, 0xec, 0xc6, 0xdb, 0xec, 0xe1, 0xa8, + 0x6d, 0x69, 0x29, 0xe0, 0x92, 0xb5, 0xb7, 0x5a, 0x4b, 0x13, 0x19, 0xca, 0x73, 0xc5, 0x93, 0x7b, + 0xc5, 0x01, 0xe7, 0x2e, 0xe5, 0x39, 0x18, 0xd1, 0xab, 0x4d, 0x68, 0xde, 0x9f, 0x58, 0xcc, 0x27, + 0x78, 0x27, 0x9a, 0xb5, 0x5c, 0x54, 0x59, 0xa2, 0xd9, 0xcd, 0xef, 0xe6, 0x1d, 0x70, 0x6b, 0x9e, + 0xb1, 0xa5, 0x8c, 0xd5, 0x18, 0xd3, 0x04, 0xbf, 0x56, 0x0b, 0x68, 0x98, 0xc7, 0xf3, 0x3a, 0xdb, + 0x6f, 0x8a, 0xaf, 0x72, 0x1f, 0xff, 0xda, 0x69, 0xe7, 0x57, 0x1f, 0xec, 0xbe, 0xcd, 0x46, 0xaa, + 0xe0, 0x8a, 0x74, 0x9f, 0xfc, 0x31, 0xf1, 0x25, 0x62, 0x08, 0xc9, 0xc8, 0xcc, 0xcb, 0xf6, 0xcb, + 0x4f, 0x01, 0xb4, 0x41, 0x0d, 0xb8, 0x15, 0x05, 0x51, 0xf4, 0x06, 0xb5, 0xed, 0x35, 0xd4, 0x8a, + 0x53, 0x33, 0xaa, 0xa3, 0x93, 0x67, 0xcf, 0x7d, 0x83, 0x8f, 0xe5, 0xf7, 0x9b, 0x63, 0x1f, 0x05, + 0x78, 0xbf, 0x44, 0x96, 0x68, 0x8d, 0xa2, 0xb1, 0x29, 0x68, 0x41, 0x05, 0x65, 0xe0, 0x98, 0x40, + 0xd1, 0x06, 0xd8, 0xd5, 0x81, 0xaa, 0x06, 0x0a, 0x85, 0x34, 0x86, 0x16, 0x99, 0x0b, 0xb4, 0x0f, + 0x68, 0x7b, 0xe4, 0x6e, 0xee, 0x1a, 0x1e, 0x17, 0xa5, 0x23, 0x12, 0x16, 0x50, 0x8e, 0x9f, 0x30, + 0xdc, 0x3c, 0x00, 0x1d, 0x54, 0xb4, 0xdb, 0x18, 0x6b, 0x49, 0x98, 0x80, 0x16, 0x6c, 0x34, 0x18, + 0xc4, 0x05, 0xd1, 0xcf, 0xd3, 0xdf, 0xc9, 0xd1, 0xc9, 0x39, 0x5c, 0xc7, 0x8f, 0x02, 0x3b, 0xe8, + 0x62, 0xff, 0x2f, 0xd5, 0x1a, 0x87, 0xb5, 0xe2, 0xf8, 0xf8, 0xf8, 0x08, 0x3d, 0xe1, 0xf4, 0x93, + 0xf0, 0xa7, 0x72, 0xbc, 0x2c, 0x50, 0x6f, 0xc7, 0x36, 0x88, 0x96, 0xac, 0x86, 0x1c, 0xbb, 0xeb, + 0x9c, 0x95, 0x03, 0x36, 0x0e, 0x04, 0x23, 0xa8, 0x37, 0xce, 0x84, 0x93, 0x43, 0x07, 0x42, 0xb3, + 0xd2, 0x43, 0x21, 0xcc, 0xdf, 0x37, 0x22, 0xb4, 0xe2, 0xaf, 0x0c, 0x40, 0xd3, 0x6f, 0xe2, 0x30, + 0x6e, 0x4f, 0xad, 0x0f, 0xf7, 0x50, 0x6a, 0xda, 0x6e, 0xaa, 0xd9, 0x6e, 0x66, 0x44, 0xd3, 0x03, + 0x1f, 0xa7, 0x7b, 0x9a, 0x66, 0x06, 0x48, 0x27, 0x2f, 0xec, 0xa1, 0x58, 0x7f, 0x82, 0xfe, 0x01, + 0x6d, 0x8e, 0x05, 0x72, 0x03, 0x31, 0x0c, 0x00, 0x55, 0x0e, 0x94, 0x99, 0xfb, 0x86, 0xd2, 0x23, + 0x3a, 0x50, 0xa6, 0x41, 0xcd, 0xf1, 0xe9, 0x48, 0x8e, 0xe4, 0xc0, 0xef, 0xc3, 0x9c, 0x0c, 0x98, + 0xbd, 0xbb, 0xf0, 0x4f, 0x0e, 0x63, 0x21, 0xe1, 0xf3, 0x03, 0x7a, 0x9c, 0x1b, 0xa7, 0x20, 0x2e, + 0x50, 0x1c, 0x9f, 0x18, 0xdd, 0x2c, 0x80, 0xab, 0xba, 0xba, 0x50, 0x4d, 0x49, 0x15, 0x73, 0xf6, + 0x83, 0x1c, 0xaa, 0x12, 0x98, 0xc0, 0xb1, 0x81, 0xc0, 0x11, 0xba, 0x42, 0x7e, 0x14, 0xa0, 0xe8, + 0xc3, 0xe3, 0x03, 0x6c, 0x33, 0xe7, 0xa8, 0x81, 0x34, 0x02, 0x81, 0x8d, 0xaf, 0xc7, 0xe5, 0x34, + 0x34, 0x0a, 0xe7, 0x3d, 0x64, 0x69, 0x0a, 0x59, 0x1a, 0x23, 0xf7, 0x47, 0xc8, 0x40, 0x4d, 0xc1, + 0x4d, 0x38, 0x60, 0x77, 0x40, 0xc4, 0xbc, 0x66, 0x2d, 0x94, 0xa7, 0x14, 0x47, 0x38, 0x7b, 0x57, + 0xd2, 0xa0, 0x50, 0x16, 0x28, 0xfd, 0xbd, 0xbc, 0x2d, 0x75, 0xdf, 0xf5, 0xdc, 0xd7, 0x61, 0x3d, + 0xcb, 0xe0, 0x5a, 0x03, 0x21, 0xa7, 0x3b, 0xf7, 0x4a, 0xae, 0x9b, 0x14, 0x12, 0x6c, 0x4f, 0xf5, + 0x6c, 0x1b, 0x47, 0x2c, 0xd9, 0xde, 0xf7, 0xa0, 0xd5, 0xe5, 0xf8, 0x5d, 0xdd, 0xf6, 0xb8, 0xf1, + 0x78, 0xae, 0xf1, 0x64, 0xb6, 0x63, 0x67, 0xaa, 0xf1, 0x6c, 0xae, 0x71, 0x7b, 0xd8, 0xb8, 0xdd, + 0xe5, 0xe4, 0x54, 0x38, 0x19, 0x7a, 0xb1, 0x63, 0xa1, 0x03, }; for (unsigned i = 0; i < sizeof(IDS) / sizeof(IDS[0]); i++) { if (IDS[i] != target_id) continue; @@ -439,7 +441,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x30d4e442, "Ebyte_EoRa-S3_companion_radio_usb" }, { 0x263a8d80, "Ebyte_EoRa-S3_kiss_modem" }, { 0x9070ae2c, "Ebyte_EoRa-S3_Repeater" }, + { 0xe0aea3d5, "Ebyte_EoRa-S3_repeater_observer_mqtt" }, { 0xf190d834, "Ebyte_EoRa-S3_room_server" }, + { 0x0773c386, "Ebyte_EoRa-S3_room_server_observer_mqtt" }, { 0x32890982, "Ebyte_EoRa-S3_terminal_chat" }, { 0xacb599d5, "GAT562_30S_Mesh_Kit_companion_radio_ble" }, { 0x82d3b273, "GAT562_30S_Mesh_Kit_companion_radio_usb" }, @@ -477,14 +481,18 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x2d2a4089, "Heltec_E213_kiss_modem" }, { 0x89ea5c4e, "Heltec_E213_repeater" }, { 0xc0eb712a, "Heltec_E213_repeater_bridge_espnow" }, + { 0x23b8a187, "Heltec_E213_repeater_observer_mqtt" }, { 0x67293f43, "Heltec_E213_room_server" }, + { 0x4cc23248, "Heltec_E213_room_server_observer_mqtt" }, { 0x3f364910, "Heltec_E290_companion_ble" }, { 0xac4b4eb5, "Heltec_E290_companion_usb" }, { 0x822e96ca, "Heltec_E290_companion_usb_ble" }, { 0xe077fb8d, "Heltec_E290_kiss_modem" }, { 0x2bbe7fd8, "Heltec_E290_repeater" }, { 0x9e40157c, "Heltec_E290_repeater_bridge_espnow" }, + { 0x8d8bde08, "Heltec_E290_repeater_observer_mqtt" }, { 0xb12e6e01, "Heltec_E290_room_server" }, + { 0x930c9fa0, "Heltec_E290_room_server_observer_mqtt" }, { 0xf6f835ec, "Heltec_mesh_solar_repeater" }, { 0x7fab017f, "heltec_rc32_companion_radio_ble" }, { 0x9e23928a, "heltec_rc32_companion_radio_full" }, @@ -493,7 +501,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x97574bc2, "heltec_rc32_kiss_modem" }, { 0x2d918a3c, "heltec_rc32_repeater" }, { 0xd1755faa, "heltec_rc32_repeater_bridge_espnow" }, + { 0x89c28271, "heltec_rc32_repeater_observer_mqtt" }, { 0x8a6ceaf0, "heltec_rc32_room_server" }, + { 0xba2943f4, "heltec_rc32_room_server_observer_mqtt" }, { 0x57f69746, "heltec_rc32_sensor" }, { 0xbc05d6a7, "heltec_rc32_terminal_chat" }, { 0x5bad80f1, "heltec_rc32_without_display_companion_radio_ble" }, @@ -502,7 +512,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xf10a26df, "heltec_rc32_without_display_companion_radio_wifi" }, { 0xc5409ced, "heltec_rc32_without_display_repeater" }, { 0xf9022545, "heltec_rc32_without_display_repeater_bridge_espnow" }, + { 0x75f74679, "heltec_rc32_without_display_repeater_observer_mqtt" }, { 0x2023036d, "heltec_rc32_without_display_room_server" }, + { 0x6600f959, "heltec_rc32_without_display_room_server_observer_mqtt" }, { 0x4abd24af, "heltec_rc32_without_display_sensor" }, { 0x35d42b35, "Heltec_t096_companion_radio_ble_femoff" }, { 0x9f736ca7, "Heltec_t096_companion_radio_ble_femon" }, @@ -657,14 +669,18 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x8ea86381, "Heltec_Wireless_Paper_kiss_modem" }, { 0x09f57289, "Heltec_Wireless_Paper_repeater" }, { 0x148aeaab, "Heltec_Wireless_Paper_repeater_bridge_espnow" }, + { 0xcbb2bf38, "Heltec_Wireless_Paper_repeater_observer_mqtt" }, { 0x6e9f7fec, "Heltec_Wireless_Paper_room_server" }, + { 0x9a340cd3, "Heltec_Wireless_Paper_room_server_observer_mqtt" }, { 0x6a25d024, "Heltec_Wireless_Tracker_companion_radio_ble" }, { 0xc5a2cb41, "Heltec_Wireless_Tracker_companion_radio_ble_ps" }, { 0xc4a11710, "Heltec_Wireless_Tracker_companion_radio_usb" }, { 0x753947a4, "Heltec_Wireless_Tracker_kiss_modem" }, { 0xe6fa98b4, "Heltec_Wireless_Tracker_repeater" }, { 0xf8c27cf4, "Heltec_Wireless_Tracker_repeater_bridge_espnow" }, + { 0xa139c6ae, "Heltec_Wireless_Tracker_repeater_observer_mqtt" }, { 0x90e227d9, "Heltec_Wireless_Tracker_room_server" }, + { 0x89e8d957, "Heltec_Wireless_Tracker_room_server_observer_mqtt" }, { 0x812f5e52, "Heltec_WSL3_companion_radio_ble" }, { 0x61f961c5, "Heltec_WSL3_companion_radio_ble_ps" }, { 0x1ddd15c8, "Heltec_WSL3_companion_radio_usb" }, @@ -716,6 +732,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xde7dc472, "KeepteenLT1_repeater" }, { 0x3625bb80, "KeepteenLT1_repeater_lora_ota_no_external_sensors" }, { 0x69df7765, "LilyGo_T-Echo-Lite_companion_radio_ble" }, + { 0x988cc4e1, "LilyGo_T-Echo-Lite_companion_radio_usb" }, { 0xe9fbc9be, "LilyGo_T-Echo-Lite_kiss_modem" }, { 0x40818863, "LilyGo_T-Echo-Lite_non_shell_companion_radio_ble" }, { 0x65deac33, "LilyGo_T-Echo-Lite_non_shell_companion_radio_usb" }, @@ -746,7 +763,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xa7d97db7, "LilyGo_T3S3_sx1276_kiss_modem" }, { 0x468a133e, "LilyGo_T3S3_sx1276_repeater" }, { 0xb59ea2ed, "LilyGo_T3S3_sx1276_repeater_bridge_espnow" }, + { 0xcb75495b, "LilyGo_T3S3_sx1276_repeater_observer_mqtt" }, { 0xbe372c6d, "LilyGo_T3S3_sx1276_room_server" }, + { 0xb762985a, "LilyGo_T3S3_sx1276_room_server_observer_mqtt" }, { 0x6215e9eb, "LilyGo_T3S3_sx1276_terminal_chat" }, { 0x07907102, "LilyGo_T_Impulse_Plus_repeater" }, { 0x3898ea56, "LilyGo_TBeam_1W_companion_radio_ble" }, @@ -768,7 +787,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xc23f8082, "LilyGo_TETH_Elite_sx1262_companion_radio_usb" }, { 0x0e74ae16, "LilyGo_TETH_Elite_sx1262_kiss_modem" }, { 0x8f256e56, "LilyGo_TETH_Elite_sx1262_repeater" }, + { 0x6915c6e1, "LilyGo_TETH_Elite_sx1262_repeater_observer_mqtt" }, { 0xf1407f39, "LilyGo_TETH_Elite_sx1262_room_server" }, + { 0x1b4dc5ab, "LilyGo_TETH_Elite_sx1262_room_server_observer_mqtt" }, { 0xe90a3181, "LilyGo_Tlora_C6_companion_radio_ble_" }, { 0x3b1b4237, "LilyGo_Tlora_C6_kiss_modem" }, { 0x21c6d08d, "LilyGo_Tlora_C6_repeater_" }, @@ -811,7 +832,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x17a7ed8c, "meshnology_w12_kiss_modem" }, { 0x07f5f5b5, "meshnology_w12_repeater" }, { 0xfd0d4b8f, "meshnology_w12_repeater_bridge_espnow" }, + { 0x8d88f33c, "meshnology_w12_repeater_observer_mqtt" }, { 0xf5afb7dd, "meshnology_w12_room_server" }, + { 0x3a0ea830, "meshnology_w12_room_server_observer_mqtt" }, { 0x6ebf28cb, "meshnology_w12_sensor" }, { 0x2d589527, "meshnology_w12_terminal_chat" }, { 0x77d0810c, "Minewsemi_me25ls01_repeater" }, @@ -906,12 +929,14 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xb38ebf22, "solarxiao_30S_repeater" }, { 0x7c89c73d, "solarxiao_30S_repeater_bridge_rs232" }, { 0x2d7c11e8, "solarxiao_30S_room_server" }, + { 0x5bb567cd, "solarxiao_30S_room_server_lora_ota" }, { 0x61b32037, "solarxiao_33S_companion_radio_ble" }, { 0xad1cd53d, "solarxiao_33S_companion_radio_usb" }, { 0x0e18b360, "solarxiao_33S_kiss_modem" }, { 0x3caf966e, "solarxiao_33S_repeater" }, { 0xcb59ea4e, "solarxiao_33S_repeater_bridge_rs232" }, { 0xe3a28284, "solarxiao_33S_room_server" }, + { 0xa8cb8e8b, "solarxiao_33S_room_server_lora_ota" }, { 0x05747eb6, "Station_G2_logging_repeater" }, { 0x75587f24, "Station_G2_logging_repeater_bridge_espnow" }, { 0xf2128c81, "Station_G2_repeater" }, @@ -1052,7 +1077,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x0a5dc760, "Xiao_S3_kiss_modem" }, { 0xd08496ce, "Xiao_S3_repeater" }, { 0x293ac2c8, "Xiao_S3_repeater_bridge_espnow" }, + { 0x23b70842, "Xiao_S3_repeater_observer_mqtt" }, { 0x60ebfa87, "Xiao_S3_room_server" }, + { 0x0ea78cab, "Xiao_S3_room_server_observer_mqtt" }, { 0x60f5fdc2, "Xiao_S3_sensor" }, { 0xe8f79d22, "Xiao_S3_WIO_companion_radio_ble" }, { 0xb97bbb15, "Xiao_S3_WIO_companion_radio_ble_ps" }, diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index f7c2a11ce..123240b9e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -451,6 +451,7 @@ public: _time_sync_needed = true; _time_sync_applied = false; _last_valid_time_sync = 0; + resetTimeSyncRequestState(); } void begin() override { } void stop() override { } @@ -1200,14 +1201,17 @@ void EnvironmentSensorManager::initBasicGPS() { MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on..."); #endif - // Give GPS a moment to power up and send data - delay(1000); - - // We'll consider GPS detected if we see any data on Serial1 + // Cold-start receivers can take several seconds to emit their first NMEA + // data. Check immediately, then wait at most five seconds; an already awake + // receiver and the explicit bypass need no extra startup delay. #ifdef ENV_SKIP_GPS_DETECT gps_detected = true; #else gps_detected = (Serial1.available() > 0); + for (uint8_t attempt = 0; attempt < 20 && !gps_detected; attempt++) { + delay(250); + gps_detected = (Serial1.available() > 0); + } #endif if (gps_detected) { diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index c4a8dca6d..599255227 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -3,6 +3,7 @@ #include "Mesh.h" #include #include +#include #ifndef GPS_POWERSAVING_ON_DURATION_SECS #define GPS_POWERSAVING_ON_DURATION_SECS (10UL * 60UL) @@ -22,12 +23,49 @@ protected: unsigned long _gps_on_duration_secs = GPS_POWERSAVING_ON_DURATION_SECS; unsigned long _gps_off_duration_secs = GPS_POWERSAVING_OFF_DURATION_SECS; unsigned long _last_valid_time_sync = 0; + uint32_t _last_time_sync_request_ms = 0; + uint32_t _last_time_sync_applied_ms = 0; + bool _time_sync_request_seen = false; + bool _time_sync_applied_seen = false; - void markTimeSyncApplied() { _time_sync_applied = true; } + void markTimeSyncApplied() { + _time_sync_applied = true; + _last_time_sync_applied_ms = static_cast(millis()); + _time_sync_applied_seen = true; + } + void resetTimeSyncRequestState() { + _last_time_sync_request_ms = 0; + _last_time_sync_applied_ms = 0; + _time_sync_request_seen = false; + _time_sync_applied_seen = false; + } public: virtual void syncTime() { _time_sync_needed = true; } virtual bool waitingTimeSync() { return _time_sync_needed; } + // Telemetry requests share an in-progress acquisition and rate-limit new + // ones using monotonic time, not an RTC which GPS/manual sync may correct. + // Direct syncTime() callers (startup, CLI, power-cycle policy) remain force + // requests. The argument is seconds, separately from position gps_interval. + bool requestTimeSync(uint64_t min_interval_secs) { + if (waitingTimeSync()) return false; + // Keep the interval below half the 32-bit millis range. Clamp before + // multiplying so even unusually large build overrides cannot overflow. + const uint32_t max_interval_ms = 0x7FFFFFFFUL; + const uint32_t interval_ms = min_interval_secs > max_interval_ms / 1000UL + ? max_interval_ms : static_cast(min_interval_secs) * 1000UL; + const uint32_t now = static_cast(millis()); + if ((_time_sync_applied_seen + && static_cast(now - _last_time_sync_applied_ms) < interval_ms) + || (_time_sync_request_seen + && static_cast(now - _last_time_sync_request_ms) < interval_ms)) { + return false; + } + _last_time_sync_request_ms = now; + _time_sync_request_seen = true; + syncTime(); + return true; + } // Edge-triggered notification for consumers that need to know a GPS time // was actually written, rather than merely seeing a valid location fix. bool consumeTimeSyncApplied() { diff --git a/test/fixtures/t1000e_sensor_power/Arduino.h b/test/fixtures/t1000e_sensor_power/Arduino.h new file mode 100644 index 000000000..a7980530a --- /dev/null +++ b/test/fixtures/t1000e_sensor_power/Arduino.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include +#include +#include "variant.h" + +#define HIGH 1 +#define LOW 0 +#define AR_INTERNAL_3_0 3 + +void digitalWrite(int pin, int level); +void analogReference(int reference); +void analogReadResolution(int bits); +int analogRead(int pin); +void delay(unsigned long milliseconds); diff --git a/test/fixtures/t1000e_sensor_power/WVariant.h b/test/fixtures/t1000e_sensor_power/WVariant.h new file mode 100644 index 000000000..dc7485e48 --- /dev/null +++ b/test/fixtures/t1000e_sensor_power/WVariant.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/test/fixtures/t1000e_sensor_power/power.cpp b/test/fixtures/t1000e_sensor_power/power.cpp new file mode 100644 index 000000000..2a53196cd --- /dev/null +++ b/test/fixtures/t1000e_sensor_power/power.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +static std::vector> writes; +static int batteryAdc = 2253, temperatureAdc = 2048, lightAdc = 2048; + +void digitalWrite(int pin, int level) { writes.emplace_back(pin, level); } +void analogReference(int reference) { assert(reference == AR_INTERNAL_3_0); } +void analogReadResolution(int bits) { assert(bits == 12); } +void delay(unsigned long milliseconds) { assert(milliseconds == 10); } +int analogRead(int pin) { + if (pin == BATTERY_PIN) return batteryAdc; + if (pin == TEMP_SENSOR) return temperatureAdc; + assert(pin == LUX_SENSOR); + return lightAdc; +} + +#include "t1000e_sensors.cpp" + +static void checkRailCycle() { + assert(writes.size() == 2); + assert(writes[0] == std::make_pair(38, HIGH)); + assert(writes[1] == std::make_pair(38, LOW)); + writes.clear(); +} + +int main() { + static_assert(PIN_3V3_EN == 38, "actual sensor rail must remain P1.06"); + static_assert(TEMP_SENSOR == 31 && LUX_SENSOR == 29, "retain real ADC pins"); + assert(isfinite(t1000e_get_temperature())); + checkRailCycle(); + assert(t1000e_get_light() <= 100); + checkRailCycle(); + assert(get_heater_temperature(3300, 0) == -30); + assert(get_heater_temperature(0, 1) == 105); + assert(get_heater_temperature(3300, 3300) == 105); + assert(get_heater_temperature(3300, 4000) == 105); + float previous = -30; + for (unsigned int sample = 0; sample <= 3300; ++sample) { + float temperature = get_heater_temperature(3300, sample); + assert(isfinite(temperature)); + assert(temperature >= -30 && temperature <= 105 && temperature >= previous); + previous = temperature; + } + temperatureAdc = 0; + assert(t1000e_get_temperature() == -30); + checkRailCycle(); + lightAdc = 0; + assert(t1000e_get_light() == 0); + checkRailCycle(); + lightAdc = 4095; + assert(t1000e_get_light() == 100); + checkRailCycle(); +} diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index 464164620..d7cdd87c0 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -517,6 +517,44 @@ for t1000_env in \ ) done +# Every added observer recipe must become the feature base for its exact +# normal-role Full image, without changing that normal target's mOTA identity. +# Only same-partition hardware may advertise an automatic observer successor; +# the four legacy-slot targets must retain their partition-expansion handoff. +for observer_hardware in \ + Ebyte_EoRa-S3 Heltec_E213 Heltec_E290 heltec_rc32 \ + heltec_rc32_without_display Heltec_Wireless_Tracker Heltec_Wireless_Paper \ + LilyGo_T3S3_sx1276 LilyGo_TETH_Elite_sx1262 meshnology_w12 Xiao_S3; do + for observer_role in repeater room_server; do + normal_target="${observer_hardware}_${observer_role}" + observer_target="${normal_target}_observer_mqtt" + if [ "$normal_target" = Ebyte_EoRa-S3_repeater ]; then + normal_target=Ebyte_EoRa-S3_Repeater + fi + is_mqtt_bridge_target "$observer_target" \ + || fail "$observer_target missing its MQTT feature base" + [ "${PIO_ENV_OTA_BY_NAME[$observer_target]:-0}" = 1 ] \ + || fail "$observer_target lost LoRa OTA" + [ "$(get_exact_identity_full_pio_env "$normal_target")" = "$observer_target" ] \ + || fail "$normal_target Full did not use its exact observer recipe" + case "$observer_hardware" in + Ebyte_EoRa-S3|LilyGo_T3S3_sx1276) + is_esp32_partition_migration_full_target "$normal_target" \ + || fail "$normal_target lost its partition-migration boundary" + if get_exact_identity_full_migration_target "$observer_target" >/dev/null; then + fail "$observer_target bypassed partition expansion with an OTA alias" + fi + ;; + *) + is_esp32_full_only_bulk_target "$observer_target" \ + || fail "$observer_target lost same-partition Full policy" + [ "$(get_exact_identity_full_migration_target "$observer_target")" = "$normal_target" ] \ + || fail "$observer_target lost its exact same-partition successor" + ;; + esac + done +done + # Arduino-ESP32 3.x OTA-only targets that have always declared larger A/B # slots must be checked against their real partition table, not the legacy # 1.25 MiB portable ceiling. Keep that exception OTA-only so ordinary RC32 @@ -928,42 +966,40 @@ BUILD_PROFILE_EXPLICIT=0 RESOLVED_BUILD_TARGETS=(nrf_repeater) configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[0]}" = \ - nrf_repeater_lora_ota_no_external_sensors ] \ - || fail "nRF52 complete OTA pass did not select the OTA identity" -[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \ - || fail "nRF52 complete first pass was not scheduled" -[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 1 ] \ - || fail "internal-flash nRF52 reduced second artifact was not scheduled" + nrf_repeater ] || fail "nRF52 profile configuration changed the selected target" +is_nrf52_sensor_ota_pair_target nrf_repeater \ + || fail "nRF52 repeater did not enter the required two-profile policy" +[ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \ + || fail "nRF52 retained the permissive size-fallback policy" BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 RESOLVED_BUILD_TARGETS=(nrf_room_server) configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[0]}" = \ - nrf_room_server_lora_ota_no_external_sensors ] \ - || fail "nRF52 room-server complete OTA pass did not select the OTA identity" -[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \ - || fail "nRF52 room-server complete first pass was not scheduled" -[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 1 ] \ - || fail "internal-flash nRF52 room-server reduced second artifact was not scheduled" + nrf_room_server ] || fail "nRF52 room profile configuration changed the selected target" +is_nrf52_sensor_ota_pair_target nrf_room_server \ + || fail "nRF52 room server did not enter the required two-profile policy" +[ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \ + || fail "nRF52 room server retained the permissive size-fallback policy" BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 RESOLVED_BUILD_TARGETS=(nrf_qspi_repeater) configure_effective_build_profile build-firmware >/dev/null -[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \ - || fail "external-storage nRF52 complete first pass was not scheduled" -[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \ - || fail "external-storage nRF52 incorrectly scheduled a redundant artifact" +is_nrf52_sensor_ota_pair_target nrf_qspi_repeater \ + || fail "external-storage nRF52 repeater did not enter the pair policy" +[ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \ + || fail "external-storage nRF52 retained the old fallback policy" BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 RESOLVED_BUILD_TARGETS=(nrf_qspi_room_server) configure_effective_build_profile build-firmware >/dev/null -[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \ - || fail "external-storage nRF52 room-server complete first pass was not scheduled" -[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \ - || fail "external-storage nRF52 room-server incorrectly scheduled a redundant artifact" +is_nrf52_sensor_ota_pair_target nrf_qspi_room_server \ + || fail "external-storage nRF52 room server did not enter the pair policy" +[ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \ + || fail "external-storage nRF52 room server retained the old fallback policy" BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 @@ -972,9 +1008,9 @@ configure_effective_build_profile build-firmware >/dev/null [ "$BUILD_PROFILE_EFFECTIVE" = auto ] \ || fail "nRF52 sensor did not retain its ordinary auto profile" [ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \ - || fail "nRF52 sensor incorrectly scheduled OTA profile passes" -[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \ - || fail "nRF52 sensor incorrectly scheduled two artifacts" + || fail "nRF52 sensor retained the permissive size-fallback policy" +is_nrf52_sensor_ota_pair_target nrf_sensor \ + || fail "nRF52 sensor was excluded from the required pair policy" # Ordinary USB-loggable roles compile their historical logging profile into # the canonical artifact. LoRa OTA repeaters, KISS, and BLE keep their distinct @@ -1105,6 +1141,7 @@ fi # Option 3 must not publish an oversized standard ESP32 image, but its expected # portable-slot status is satisfied by the mandatory expanded FULL pass. saved_has_esp32_full_profile=$(declare -f has_esp32_full_profile) +saved_build_firmware=$(declare -f build_firmware) has_esp32_full_profile() { [ "$1" = esp32_portable_overflow ]; } build_firmware() { return 42; } saved_output_dir=$OUTPUT_DIR @@ -1124,6 +1161,7 @@ fi OUTPUT_DIR=$saved_output_dir unset DEFER_ESP32_PORTABLE_OVERFLOW_TO_FULL eval "$saved_has_esp32_full_profile" +eval "$saved_build_firmware" reduced_full_alias=Tbeam_SX1262_repeater_lora_ota_no_external_sensors [ "$(get_esp32_full_profile_target "$reduced_full_alias")" = Tbeam_SX1262_repeater ] \ @@ -1132,8 +1170,9 @@ has_esp32_full_profile "$reduced_full_alias" \ || fail "reduced ESP32 LoRa OTA alias lost its expanded FULL replacement" calls=() -build_firmware() { - calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$FIRMWARE_OUTPUT_ENV_NAME") +saved_build_firmware_one_profile=$(declare -f build_firmware_one_profile) +build_firmware_one_profile() { + calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$NRF52_OTA_SENSOR_PROFILE") if [ "${#calls[@]}" -eq 1 ]; then return 42 fi @@ -1146,15 +1185,17 @@ AUTO_REDUCED_FALLBACK_TARGET=nrf_repeater_lora_ota_no_external_sensors BUILD_PROFILE_EFFECTIVE=auto FIRMWARE_FILENAME_INFIX="" SKIP_DECLARED_REDUCTIONS=0 -run_auto_two_pass_build nrf_repeater_lora_ota_no_external_sensors >/dev/null +if run_auto_two_pass_build nrf_repeater_lora_ota_no_external_sensors >/dev/null 2>&1; then + fail "failed full nRF52 option was hidden by a successful reduced fallback" +fi [ "${#calls[@]}" -eq 2 ] || fail "size overflow did not run exactly two passes" [[ "${calls[0]}" == *:auto:1:* ]] || fail "pass 1 unexpectedly applied reductions" -[[ "${calls[1]}" == *:standard:0:* ]] || fail "pass 2 did not apply standard reductions" +[[ "${calls[1]}" == *:auto:0:reduced ]] || fail "pass 2 did not apply reduced sensor policy" calls=() -build_firmware() { - calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$FIRMWARE_OUTPUT_ENV_NAME") +build_firmware_one_profile() { + calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$NRF52_OTA_SENSOR_PROFILE") return 0 } @@ -1167,10 +1208,11 @@ run_auto_two_pass_build nrf_repeater_lora_ota_no_external_sensors >/dev/null [ "${#calls[@]}" -eq 2 ] \ || fail "successful internal-flash nRF52 build did not publish both artifacts" -[[ "${calls[0]}" == *:auto:1:nrf_repeater ]] \ - || fail "complete nRF52 artifact did not use the feature-rich output name" -[[ "${calls[1]}" == *:standard:0: ]] \ - || fail "reduced nRF52 artifact did not use the reduced target name" +[[ "${calls[0]}" == *:auto:1:full ]] \ + || fail "complete nRF52 artifact did not declare the full sensor policy" +[[ "${calls[1]}" == *:auto:0:reduced ]] \ + || fail "reduced nRF52 artifact did not declare the reduced sensor policy" +eval "$saved_build_firmware_one_profile" is_rak_i2c_voltage_monitor_ota_target \ RAK_3401_repeater_lora_ota_no_external_sensors \ diff --git a/test/test_cascade_release_package.py b/test/test_cascade_release_package.py index ad9135fac..2aed18db7 100644 --- a/test/test_cascade_release_package.py +++ b/test/test_cascade_release_package.py @@ -7,11 +7,14 @@ import unittest import subprocess import sys import hashlib +from unittest.mock import patch +import zipfile ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location("package_cascade_release", ROOT / "scripts/package_cascade_release.py") package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) +from motalib import FwIdent, Nrf52Layout, ensure_endf, ensure_nrf52_layout, target_id_for_env class ReleaseQualificationTest(unittest.TestCase): @@ -62,6 +65,25 @@ class ReleaseQualificationTest(unittest.TestCase): def test_usb_companion_is_accepted(self): package.validate_manifest(self.manifest("test_companion_radio_usb", ota_update_verified=False)) + def test_nrf52_sensor_profiles_require_actual_qualified_lora_receiver(self): + for role in ('repeater', 'room_server', 'sensor'): + for sensor in ('full', 'reduced'): + manifest = self.manifest('test_' + role, platform='NRF52_PLATFORM', build_profile='auto', + capabilities=['sensor.profile.' + sensor, 'ota.update.lora'], ota_update_methods=['lora'], + verification=[dict(capability='ota.update.lora', present=True, source='linked image')]) + package.validate_manifest(manifest) + self.assertEqual(package.nrf52_sensor_profile(manifest), sensor) + self.assertIn('sensors + LoRa OTA', package.release_profile_label(manifest)) + self.assertEqual(package.category({'manifest': manifest}), 'lora-ota') + for change in (dict(platform='ESP32_PLATFORM'), dict(target='test_companion_radio_full'), + dict(ota_update_methods=['bluetooth']), dict(verification=[]), + dict(verification=[dict(capability='ota.update.lora', present=True, source='filename')])): + with self.subTest(change=change), self.assertRaisesRegex(ValueError, 'qualified nRF52'): + package.validate_manifest({**manifest, **change}) + with self.assertRaisesRegex(ValueError, 'ambiguous'): + package.validate_manifest({**manifest, 'capabilities': manifest['capabilities'] + + ['sensor.profile.' + ('reduced' if sensor == 'full' else 'full')]}) + def test_full_companion_requires_linked_source_implementation(self): manifest = self.manifest("test_companion_radio_full", ota_update_verified=False) with self.assertRaisesRegex(ValueError, "MOTA sending"): @@ -74,6 +96,91 @@ class ReleaseQualificationTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, "MOTA sending"): package.validate_manifest(manifest) + def sensor_record(self, directory, sensor, *, target='test_repeater', target_id=None, + hw_id='TEST', app_base=0x26000, flags=0x10, fw_version=0x01110107): + stem = target + '-' + sensor + '-ota-v1.17.1.7-dev-deadbeef' + application, _ = ensure_endf(ensure_nrf52_layout(b'OTA: status\0invalid in-place patch geometry\0', + Nrf52Layout(app_base, 0xED000, 0xED000, flags)), + FwIdent(fw_version, target_id_for_env(target) if target_id is None else target_id, hw_id)) + path = directory / (stem + '.zip') + with zipfile.ZipFile(path, 'w') as archive: + archive.writestr('application.bin', application) + archive.writestr('manifest.json', json.dumps({'manifest': { + 'application': {'bin_file': 'application.bin', 'dat_file': 'application.dat'}}})) + archive.writestr('application.dat', b'fixture init') + manifest = self.manifest(target, artifact_target=target + '-' + sensor + '-ota', + platform='NRF52_PLATFORM', build_profile='auto', capabilities=['sensor.profile.' + sensor, + 'ota.update.lora'], ota_update_methods=['lora'], + verification=[dict(capability='ota.update.lora', present=True, source='linked image')], + ota_update_requirements={'lora': {'storage': 'internal_flash_and_retained_ram'}}) + return {'manifest': manifest, 'files': [path]} + + def test_sensor_pair_requires_one_of_each_with_matching_packaged_identity_and_layout(self): + with tempfile.TemporaryDirectory() as temp: + directory = Path(temp) + full = self.sensor_record(directory, 'full') + reduced = self.sensor_record(directory, 'reduced') + package.validate_nrf52_sensor_pairs([full, reduced]) + for records in ([full], [reduced], [full, reduced, reduced]): + with self.subTest(count=len(records)), self.assertRaisesRegex(ValueError, 'exactly one full'): + package.validate_nrf52_sensor_pairs(records) + for change in (dict(target='another_repeater'), dict(target_id=456), dict(hw_id='OTHER'), + dict(app_base=0x27000), dict(flags=0x20), dict(fw_version=0x01110108)): + with self.subTest(change=change), self.assertRaisesRegex(ValueError, 'sensor pair'): + package.validate_nrf52_sensor_pairs([full, self.sensor_record(directory, 'reduced', **change)]) + reduced = self.sensor_record(directory, 'reduced') + reduced['manifest']['ota_update_requirements']['lora']['storage'] = 'external_qspi' + with self.assertRaisesRegex(ValueError, 'storage layout'): + package.validate_nrf52_sensor_pairs([full, reduced]) + with self.assertRaisesRegex(ValueError, 'target ID disagrees with logical target'): + package.validate_nrf52_sensor_pairs([self.sensor_record(directory, 'full', target_id=456), + self.sensor_record(directory, 'reduced', target_id=456)]) + + def test_sensor_pair_filename_does_not_replace_metadata_or_packaged_layout(self): + with tempfile.TemporaryDirectory() as temp: + directory = Path(temp) + full = self.sensor_record(directory, 'full') + missing = {**full, 'manifest': {**full['manifest'], 'capabilities': ['ota.update.lora']}} + with self.assertRaisesRegex(ValueError, 'lacks qualified metadata'): + package.validate_nrf52_sensor_pairs([missing]) + missing['manifest']['artifact_target'] = 'test_repeater' + with self.assertRaisesRegex(ValueError, 'lacks qualified metadata'): + package.validate_nrf52_sensor_pairs([missing]) + full['manifest']['artifact_target'] = 'test_repeater-reduced-ota' + with self.assertRaisesRegex(ValueError, 'disagrees with metadata'): + package.validate_nrf52_sensor_pairs([full]) + full['manifest']['artifact_target'] = 'different_repeater-full-ota' + with self.assertRaisesRegex(ValueError, 'filename disagrees'): + package.validate_nrf52_sensor_pairs([full]) + full = self.sensor_record(directory, 'full') + with zipfile.ZipFile(full['files'][0], 'w') as archive: + archive.writestr('application.bin', b'no EndF identity or layout') + archive.writestr('manifest.json', json.dumps({'manifest': { + 'application': {'bin_file': 'application.bin'}}})) + with self.assertRaisesRegex(ValueError, 'valid packaged EndF'): + package.validate_nrf52_sensor_pairs([full]) + # The new policy is not retroactively applied to historical files. + package.validate_nrf52_sensor_pairs([{'manifest': self.manifest('legacy_repeater', + platform='NRF52_PLATFORM'), 'files': [directory / 'legacy_repeater-ota-v1.0.zip']}]) + + def test_collect_artifacts_always_enforces_sensor_pair_even_for_partial_staging(self): + with tempfile.TemporaryDirectory() as temp: + directory = Path(temp) + record = self.sensor_record(directory, 'full') + stem = record['files'][0].stem + (directory / (stem + '.uf2')).write_bytes(b'validated fixture UF2') + (directory / (stem + '.capabilities.json')).write_text(json.dumps(record['manifest'])) + memory = dict(passed=True, available_internal_bytes=100000, + required_heap_bytes=10000, elf_sha256='a' * 64) + with patch.object(package, 'check_uf2'), patch.object(package, 'validate_package', return_value=memory), \ + patch.object(package, 'validate_artifact_notice', return_value=None): + with self.assertRaisesRegex(ValueError, 'exactly one full'): + package.collect_artifacts(directory, 'v1.17.1.7-dev-deadbeef') + reduced = self.sensor_record(directory, 'reduced') + other = reduced['files'][0].stem + (directory / (other + '.uf2')).write_bytes(b'validated fixture UF2') + (directory / (other + '.capabilities.json')).write_text(json.dumps(reduced['manifest'])) + self.assertEqual(len(package.collect_artifacts(directory, 'v1.17.1.7-dev-deadbeef')), 2) def test_missing_qualification_and_mixed_versions_are_rejected(self): with tempfile.TemporaryDirectory() as temp: directory = Path(temp) diff --git a/test/test_firmware_capabilities.py b/test/test_firmware_capabilities.py index 75d6dcde6..912ad88b8 100644 --- a/test/test_firmware_capabilities.py +++ b/test/test_firmware_capabilities.py @@ -273,11 +273,12 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase): details = manifest["ota_update_requirements"]["lora"] self.assertEqual(details["storage"], storage) self.assertEqual(details["package_types"], types) + self.assertTrue(details["bootloader_release"].endswith("v0.11.0-OTAFIX2.4.11")) + self.assertTrue(any("hardware-pending" in note for note in details["notes"])) if flags == 48: - self.assertIsNone(details["bootloader_release"]) - self.assertIn("Merged RAK", details["bootloader"]) - else: - self.assertTrue(details["bootloader_release"].endswith("0.11.0-OTAFIX2.4.6")) + self.assertIn("adaptive OTAFIX", details["bootloader"]) + self.assertTrue(any("*_AUTO_DFU" in note for note in details["notes"])) + self.assertFalse(any("self-update is unavailable" in note for note in details["notes"])) def test_nrf52_full_companion_cannot_claim_lora_self_update(self): result, manifest = self.run_checker( diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index e643682cc..e9a85ab74 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -5,7 +5,7 @@ const picker = require("../docs/_javascript/firmware_picker.js"); const bootloaderManifest = require("../docs/_data/bootloader_manifest.json"); const bootloaderCatalog = picker.buildBootloaderCatalog(bootloaderManifest); -assert.strictEqual(bootloaderCatalog.profiles.length, 24); +assert.strictEqual(bootloaderCatalog.profiles.length, 23); for (const profile of bootloaderCatalog.profiles) { for (const hardware of profile.meshcoreHardware) { assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, profile.id); @@ -16,7 +16,8 @@ for (const profile of bootloaderCatalog.profiles) { assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_t096", "nrf52").id, "heltec_t096"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_t114", "nrf52").id, "heltec_t114"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_tower_v2_sdcard", "nrf52").storage, "sd"); -assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_tower_v2", "nrf52").storage, "internal"); +assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_tower_v2", "nrf52"), null); +assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "Heltec_tower_v2", "nrf52").reason.includes("one-time local installation")); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "RAK_3401", "nrf52").storage, "adaptive"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "RAK_4631", "nrf52").id, "wiscore_rak4631_auto"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "ThinkNode_M8", "nrf52"), null); @@ -34,7 +35,7 @@ for (const hardware of nrf52Hardware) { } assert.strictEqual(picker.unavailableBootloaderForHardware(bootloaderCatalog, hardware, "esp32"), null); } -assert.strictEqual(mappedNrf52, 39); +assert.strictEqual(mappedNrf52, 38); for (const hardware of nrf52Hardware.filter(h => /^(ikoka_|solarxiao_)/.test(h))) { assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, "xiao_nrf52840_ble"); } @@ -42,8 +43,8 @@ for (const hardware of ["WioTrackerL1-1W", "WioTrackerL1Eink"]) { assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, "wio_tracker_l1"); } assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "GAT562_Mesh_Watch13", "nrf52").reason.includes("vibration motor")); -assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "wio_wm1110", "nrf52").reason.includes("does not make it XIAO")); -const nextBootloaderTag = "v0.11.0-OTAFIX2.4.11"; +assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "wio_wm1110", "nrf52").reason.includes("identity and installation route still need qualification")); +const nextBootloaderTag = "v0.11.0-OTAFIX2.4.12"; const nextBootloaderRelease = { tag_name: nextBootloaderTag, draft: false, prerelease: false, html_url: "https://github.com/" + bootloaderManifest.repository + "/releases/tag/" + nextBootloaderTag, @@ -56,7 +57,7 @@ const nextBootloaderRelease = { }), }; const nextBootloaderCatalog = picker.buildBootloaderCatalog(bootloaderManifest, nextBootloaderRelease); -assert.strictEqual(nextBootloaderCatalog.version, "2.4.11"); +assert.strictEqual(nextBootloaderCatalog.version, "2.4.12"); assert(picker.bootloaderForHardware(nextBootloaderCatalog, "t1000e", "nrf52").files.uf2.url.includes(nextBootloaderTag)); const ambiguousBootloader = structuredClone(nextBootloaderRelease); ambiguousBootloader.assets.push(ambiguousBootloader.assets.find(a => a.name.startsWith("update-heltec_t096_"))); @@ -1408,6 +1409,83 @@ assert.strictEqual(picker.firmwareProfileLabel({ota: 'lora-receiver', feature: ' assert(picker.firmwareProfileLabel({ota: 'lora-receiver', feature: 'standard', variant: 'w25q16'}) .includes('External storage board (W25Q16)')); +// New nRF52 releases keep both sensor policies, using verified release-bound +// controls rather than a publication suffix as evidence of compiled drivers. +const sensorSource = 'deadbeef' + '0'.repeat(32); +const sensorFamily = 'v1.17.1.7-halo-keymind-cascade-dev-deadbeef'; +const sensorAssets = []; +const sensorControls = {familyTag: sensorFamily, profiles: {}}; +for (const role of ['repeater', 'room_server', 'sensor']) { + for (const storage of ['', '_w25q16']) { + const logical = 'RAK_3401_' + role + '_lora_ota_no_external_sensors' + storage; + for (const policy of ['full', 'reduced']) { + const publication = logical + '-' + policy; + for (const extension of ['zip', 'uf2']) { + sensorAssets.push(asset(publication + '-ota-' + sensorFamily + '.' + extension)); + } + sensorControls.profiles[publication] = { + platform: 'NRF52_PLATFORM', otaRole: 'lora-receiver', updateMethods: ['lora'], + sensorProfile: policy, sensorProfileSource: sensorSource, + }; + } + } +} +const sensorReleases = [release(sensorFamily, '2026-10-02T00:00:00Z', sensorAssets)]; +const sensorCatalog = picker.buildCatalog(sensorReleases, sensorControls); +assert.strictEqual(sensorCatalog.profiles.length, 12); +for (const item of sensorCatalog.profiles) { + const policy = item.target.endsWith('-full') ? 'full' : 'reduced'; + assert.strictEqual(item.sensorProfile, policy); + assert.strictEqual(item.chipFamily, 'nrf52'); + assert.strictEqual(item.ota, 'lora-receiver'); + assert.strictEqual(item.feature, policy === 'full' ? 'full' : 'standard'); + assert(!item.variant.includes('-reduced')); + if (policy === 'full') assert(!item.variant.includes('no-external-sensors')); + const label = picker.firmwareProfileLabel(item); + assert(label.startsWith(policy === 'full' ? 'Full supported sensors + LoRa OTA' : 'Reduced sensors + LoRa OTA')); + if (item.target.includes('w25q16')) assert(label.includes('W25Q16')); + const choices = picker.firmwareProfileChoices(sensorCatalog.profiles, { + hardware: item.hardware, role: item.role, mode: item.mode, install: 'zip', + }); + const choice = choices.find(entry => entry.value === picker.firmwareProfileValue(item)); + assert(choice, item.target); + const selected = sensorCatalog.profiles.filter(profile => picker.profileMatchesFacets(profile, + {...choice.filters, hardware: item.hardware, role: item.role, mode: item.mode})); + assert.deepStrictEqual(selected.map(profile => profile.target), [item.target]); + assert.deepStrictEqual(item.installKinds, ['zip', 'uf2']); + const steps = picker.installSteps(item, 'zip'); + assert(steps.some(step => step.includes(policy === 'full' ? 'complete sensor drivers' : 'Omits selected optional'))); + if (policy === 'full') assert(!steps.some(step => step.includes('compact profile omits'))); +} +for (const change of [{sensorProfileSource: 'bad'}, {sensorProfileSource: 'f'.repeat(40)}, + {platform: 'ESP32_PLATFORM'}, {updateMethods: []}, {otaRole: 'none'}]) { + const changedControls = structuredClone(sensorControls); + Object.values(changedControls.profiles).forEach(info => Object.assign(info, change)); + assert(picker.buildCatalog(sensorReleases, changedControls).profiles.every(item => !item.sensorProfile)); +} +assert(picker.buildCatalog(sensorReleases).profiles.every(item => !item.sensorProfile)); +assert(picker.buildCatalog(sensorReleases, {...sensorControls, familyTag: 'v0.0.0'}).profiles + .every(item => !item.sensorProfile)); +for (const length of [7, 9, 40]) { + const hash = sensorSource.slice(0, length); + const tag = sensorFamily.replace(/-deadbeef$/, '-' + hash); + const files = sensorAssets.map(file => asset(file.name.replace(/-deadbeef\.(zip|uf2)$/, '-' + hash + '.$1'))); + const metadata = {...sensorControls, familyTag: tag}; + const profiles = picker.buildCatalog([release(tag, '2026-10-02T00:00:00Z', files)], metadata).profiles; + assert.strictEqual(profiles.length, 12, 'Git abbreviation length ' + length); + assert(profiles.every(item => item.sensorProfile), 'Git abbreviation length ' + length); + const wrong = hash.slice(0, -1) + (hash.endsWith('f') ? '1' : 'f'); + const badFiles = files.map(file => asset(file.name.replace('-' + hash + '.', '-' + wrong + '.'))); + const badProfiles = picker.buildCatalog([release(tag, '2026-10-02T00:00:00Z', badFiles)], metadata).profiles; + assert.strictEqual(badProfiles.length, 12); + assert(badProfiles.every(item => !item.sensorProfile), 'Mismatched source of length ' + length); +} +const mixedSourceFiles = sensorAssets.map(file => asset(file.name.endsWith('.uf2') + ? file.name.replace('-deadbeef.uf2', '-deadbeef1.uf2') : file.name)); +assert(picker.buildCatalog([release(sensorFamily, '2026-10-02T00:00:00Z', mixedSourceFiles)], sensorControls) + .profiles.every(item => !item.sensorProfile), 'Every file must bind to the exact source'); +console.log('nRF52 full and reduced sensor OTA profile pairs and qualification gating passed'); + // A combined profile can be replaced in one click even when its former // OTA/feature/variant facets have narrowed the catalog to a different image. const replacementChoices = picker.firmwareProfileChoices(catalog.profiles, { diff --git a/test/test_firmware_picker_package.py b/test/test_firmware_picker_package.py new file mode 100644 index 000000000..308b0fb6b --- /dev/null +++ b/test/test_firmware_picker_package.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Offline picker packaging preserves qualified full/reduced OTA choices.""" +import importlib.util +import json +from pathlib import Path +import re +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location('package_firmware_picker', ROOT / 'scripts/package_firmware_picker.py') +PACKAGE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PACKAGE) + + +class OfflinePickerPackageTests(unittest.TestCase): + def test_sensor_policy_pairs_and_individual_downloads_are_retained(self): + source = 'a' * 40 + family = 'v1.17.1.7-dev-aaaaaaaa' + logical = 'RAK_3401_repeater_lora_ota_no_external_sensors' + controls = dict(familyTag=family, source=source, profiles={}) + assets = [] + for policy in ('full', 'reduced'): + target = logical + '-' + policy + controls['profiles'][target] = dict(platform='NRF52_PLATFORM', otaRole='lora-receiver', + updateMethods=['lora'], sensorProfile=policy, sensorProfileSource=source) + for extension in ('.zip', '.uf2'): + name = target + '-ota-' + family + extension + assets.append(dict(name=name, size=100, + browser_download_url='https://github.com/mikecarper/MeshCore/releases/download/' + family + '/' + name, + uploader={'token': 'not part of the picker'})) + releases = [dict(name=family, tag_name=family, assets=assets, authorization='private')] + packaged = PACKAGE.package(releases, controls) + match = re.search(r'', packaged) + embedded = json.loads(match[1]) + self.assertEqual(embedded['controls'], controls) + self.assertEqual(len(embedded['releases'][0]['assets']), 4) + self.assertEqual({asset['name'] for asset in embedded['releases'][0]['assets']}, + {asset['name'] for asset in assets}) + self.assertNotIn('authorization', embedded['releases'][0]) + self.assertTrue(all('uploader' not in asset for asset in embedded['releases'][0]['assets'])) + self.assertIn('Full supported sensors', packaged) + self.assertIn('Reduced sensors', packaged) + + def test_controls_must_belong_to_the_packaged_release_family(self): + with self.assertRaisesRegex(ValueError, 'match the supplied release family'): + PACKAGE.package([dict(tag_name='v1.17.1.6', assets=[])], + dict(familyTag='v1.17.1.7', profiles={})) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_full_infrastructure_selection.sh b/test/test_full_infrastructure_selection.sh index 1f40b240c..fabfa6b71 100644 --- a/test/test_full_infrastructure_selection.sh +++ b/test/test_full_infrastructure_selection.sh @@ -89,7 +89,7 @@ for target in "${!PIO_ENV_PLATFORM_BY_NAME[@]}"; do full_only_count=$((full_only_count + 1)) fi done -[ "$full_only_count" -eq 129 ] || fail "expected 129 same-partition FULL-only targets, found $full_only_count" +[ "$full_only_count" -eq 147 ] || fail "expected 147 same-partition FULL-only targets, found $full_only_count" for target in heltec_rc32_repeater Station_G2_repeater_observer_mqtt \ Heltec_v3_repeater heltec_v4_r8_repeater \ @@ -228,7 +228,7 @@ done } build_firmware heltec_rc32_repeater >/dev/null [ "$observed_full" = 1 ] || fail "audited bulk target did not promote to FULL" - [[ "$observed_filename" == heltec_rc32_repeater-full-logging-ota-vtest-* ]] \ + [[ "$observed_filename" == heltec_rc32_repeater-full-usb-wifi-ota-vtest-* ]] \ || fail "FULL-only bulk target lost its exact identity" ) ( diff --git a/test/test_gps_upstream_adaptations.py b/test/test_gps_upstream_adaptations.py new file mode 100644 index 000000000..076ca0a7d --- /dev/null +++ b/test/test_gps_upstream_adaptations.py @@ -0,0 +1,401 @@ +"""Host regressions for bounded GPS discovery, query sync and BLE LED opt-out. + +Uses the real LocationProvider/SensorManager/MicroNMEA-provider headers and +SensorManager.cpp. The cold-start and sensor-request cases execute extracted +production methods, with only their hardware/library boundaries mocked. +""" +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] + +ARDUINO = r''' +#pragma once +#include +#include +#include +#include +#include +#include +#define HIGH 1 +#define LOW 0 +#define OUTPUT 1 +inline uint32_t now_ms=0; +inline uint32_t delay_count=0; +inline uint32_t millis(){return now_ms;} +inline void delay(uint32_t ms){now_ms+=ms;++delay_count;} +inline void yield(){++now_ms;} +inline void pinMode(int,int){} +inline void digitalWrite(int,int){} +inline int digitalRead(int){return HIGH;} +class Stream { +public: + virtual ~Stream()=default; + virtual int available(){return 0;} + virtual int read(){return -1;} + virtual size_t write(uint8_t){return 1;} +}; +using std::isnan; +''' + +MESH = r''' +#pragma once +#include +namespace mesh { +class RTCClock { + uint32_t utc=1790899200U; +public: + uint32_t getCurrentTime(){return utc;} + void setCurrentTime(uint32_t value){utc=value;} +}; +} +''' + +NMEA = r''' +#pragma once +#include +class MicroNMEA { + bool valid=false; +public: + inline static unsigned clears=0; + MicroNMEA(char*,size_t){} + void clear(){++clears;valid=false;} + bool process(char c){if(c=='\n'){valid=true;return true;}return false;} + static bool testChecksum(const char*){return true;} + static void sendSentence(Stream&,const char*){} + const char* getSentence()const{return "$GPRMC,valid*00";} + const char* getMessageID()const{return "RMC";} + bool isValid()const{return valid;} + long getLatitude()const{return 47610000;} + long getLongitude()const{return -122330000;} + bool getAltitude(long& value)const{value=125000;return valid;} + long getNumSatellites()const{return valid?8:0;} + uint16_t getYear()const{return valid?2026:0;} + uint8_t getMonth()const{return valid?10:0;} + uint8_t getDay()const{return valid?2:0;} + uint8_t getHour()const{return 12;} + uint8_t getMinute()const{return 0;} + uint8_t getSecond()const{return 0;} +}; +''' + +RTC = r''' +#pragma once +#include +class DateTime { +public: + DateTime(uint16_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t){} + uint32_t unixtime()const{return 1790942400U;} +}; +''' + +CAYENNE = r''' +#pragma once +#include +#include +class CayenneLPP { + uint8_t data[64]={}; +public: + unsigned resets=0,gps_count=0; + explicit CayenneLPP(size_t){} + void reset(){++resets;} + void addVoltage(uint8_t,float){} + void addTemperature(uint8_t,float){} + void addGPS(uint8_t,float,float,float){++gps_count;} + const uint8_t* getBuffer()const{return data;} + uint8_t getSize()const{return 0;} +}; +''' + +QUERY = r''' +#include +#include +#include +#include +void LocationProvider::sendSentence(const char*){} +struct Uart:Stream { + std::deque incoming; + int available()override{return static_cast(incoming.size());} + int read()override{if(incoming.empty())return -1;char c=incoming.front();incoming.pop_front();return c;} + void fix(){incoming.push_back('\n');} +}; +struct TestProvider:MicroNMEALocationProvider { + using MicroNMEALocationProvider::MicroNMEALocationProvider; + void applied(){markTimeSyncApplied();stopTimeSync();} + void resetQueryState(){resetTimeSyncRequestState();} +}; +struct TestSensors:SensorManager { + LocationProvider* provider; + bool detected=true,active=true; + unsigned queries=0; + explicit TestSensors(LocationProvider* p):provider(p){} + LocationProvider* getLocationProvider()override{return provider;} + bool telemetryGpsDetected()const override{return detected;} + bool telemetryGpsActive()const override{return active;} + void telemetryGpsStart()override{active=true;} + void telemetryGpsStop()override{active=false;} + void block(bool value){setGpsTelemetryTransportAvailable(!value);} + bool querySensors(uint8_t,CayenneLPP&)override{++queries;return true;} +}; +struct Board { + int getBattMilliVolts(){return 4200;} + float getMCUTemperature(){return 25.0f;} +}; +uint8_t getTelemetryPermissions(uint8_t perms,int){return perms;} +@CONFIG@ +struct SensorRequestHarness { + TestSensors& sensors; + CayenneLPP telemetry{64}; + Board board; + struct {int telemetry_access=0;} _prefs; + uint8_t reply_data[128]={}; + explicit SensorRequestHarness(TestSensors& s):sensors(s){} + uint8_t request(uint8_t perms,uint8_t mask=0,uint8_t length=1){ + uint8_t payload[1]={mask}; + size_t payload_len=length; + const unsigned req_type=3; +@REQUEST@ + return 0; + } +}; +int main(){ + // First acquisition already underway: burst requests must preserve a parsed + // fix, not call the NMEA provider's clearing force-sync method. + now_ms=1;Uart uart;mesh::RTCClock clock; + TestProvider gps(uart,&clock,-1,-1); + gps.setGPSPowerSaving(true);uart.fix();gps.loop();assert(gps.isValid()); + const unsigned initial_clears=MicroNMEA::clears; + for(unsigned i=0;i<100;++i){now_ms=i+1;assert(!gps.requestTimeSync(60));} + assert(MicroNMEA::clears==initial_clears&&gps.isValid()); + + // Successful production NMEA sync marks monotonic time and leaves the edge + // notification available for existing mesh clock consumers. + now_ms=1100;gps.loop();now_ms=2200;gps.loop(); + assert(!gps.waitingTimeSync()); + assert(gps.consumeTimeSyncApplied());assert(!gps.consumeTimeSyncApplied()); + now_ms=62199;assert(!gps.requestTimeSync(60)); + // A backward/forward RTC correction cannot underflow or defeat the throttle. + clock.setCurrentTime(1);assert(!gps.requestTimeSync(60)); + clock.setCurrentTime(UINT32_MAX);assert(!gps.requestTimeSync(60)); + now_ms=62200;assert(gps.requestTimeSync(60)); + assert(MicroNMEA::clears==initial_clears+1&&!gps.isValid()); + for(unsigned i=0;i<100;++i){++now_ms;assert(!gps.requestTimeSync(0));} + assert(MicroNMEA::clears==initial_clears+1); + // Aborted work is still protected by the last request, even without a fix. + gps.stopTimeSync();assert(!gps.requestTimeSync(60)); + now_ms=122200;assert(gps.requestTimeSync(60)); + gps.stopTimeSync();assert(gps.requestTimeSync(0)); // explicit zero disables pace only + + // Forced CLI/startup callers retain their previous behavior. + const unsigned force_clears=MicroNMEA::clears; + gps.syncTime();assert(MicroNMEA::clears==force_clears+1); + + // millis rollover, including a valid successful sync at zero, is safe. + TestProvider wrap(uart,&clock,-1,-1);now_ms=UINT32_MAX-1000;wrap.applied(); + now_ms=58998;assert(!wrap.requestTimeSync(60)); + now_ms=58999;assert(wrap.requestTimeSync(60)); + TestProvider zero(uart,&clock,-1,-1);now_ms=0;zero.applied(); + now_ms=59999;assert(!zero.requestTimeSync(60));now_ms=60000;assert(zero.requestTimeSync(60)); + + // Clamp huge/negative-converted overrides before multiplication. Signed-long + // deadline comparisons elsewhere can never see an overflowing long interval. + TestProvider huge(uart,&clock,-1,-1);now_ms=100;huge.applied(); + now_ms=100+0x7ffffffeU;assert(!huge.requestTimeSync(UINT64_MAX)); + now_ms=100+0x7fffffffU;assert(huge.requestTimeSync(UINT64_MAX)); + huge.stopTimeSync();assert(!huge.requestTimeSync(static_cast(-1))); + + // Requests denied by transport/presence/mode/null provider do not consume + // throttle. A powered external rail is not permission to reclaim the UART. + TestProvider owned(uart,&clock,-1,-1);owned.setGPSPowerSaving(true);owned.stopTimeSync(); + TestSensors sensors(&owned);now_ms=200;sensors.block(true); + assert(owned.isEnabled());assert(!sensors.requestGpsTelemetryTimeSync(60)); + sensors.block(false);sensors.detected=false;assert(!sensors.requestGpsTelemetryTimeSync(60)); + sensors.detected=true;owned.setGPSPowerSaving(false);assert(!sensors.requestGpsTelemetryTimeSync(60)); + owned.setGPSPowerSaving(true);sensors.provider=nullptr;assert(!sensors.requestGpsTelemetryTimeSync(60)); + sensors.provider=&owned;assert(sensors.requestGpsTelemetryTimeSync(60)); + // A RAK discovery reset intentionally forgets the old singleton's throttle. + owned.stopTimeSync();owned.resetQueryState();assert(sensors.requestGpsTelemetryTimeSync(60)); + + // Execute actual SensorMesh permission and query wiring, including the build + // override's seconds units. No-location/masked requests cannot stamp a query. + TestProvider request_gps(uart,&clock,-1,-1);request_gps.setGPSPowerSaving(true);request_gps.stopTimeSync(); + TestSensors request_sensors(&request_gps);SensorRequestHarness requests(request_sensors); + now_ms=1000;const unsigned before=MicroNMEA::clears; + requests.request(TELEM_PERM_BASE);requests.request(TELEM_PERM_LOCATION,TELEM_PERM_LOCATION); + requests.request(TELEM_PERM_LOCATION,0,0);assert(MicroNMEA::clears==before); + requests.request(TELEM_PERM_LOCATION);assert(MicroNMEA::clears==before+1); + request_gps.stopTimeSync(); + now_ms=1000+static_cast(GPS_READ_INTERVAL_SECS)*1000U-1; + requests.request(TELEM_PERM_LOCATION);assert(MicroNMEA::clears==before+1); + ++now_ms;requests.request(TELEM_PERM_LOCATION);assert(MicroNMEA::clears==before+2); +} +''' + +COLD = r''' +#include +#include +#define PIN_GPS_TX 8 +#define PIN_GPS_RX 7 +#define MESH_DEBUG_PRINTLN(...) ((void)0) +struct SerialMock { + uint32_t start=0,arrival=UINT32_MAX; + unsigned begins=0,polls=0; + void setPins(int,int){} + void begin(int){++begins;start=millis();} + int available(){++polls;return arrival!=UINT32_MAX&&static_cast(millis()-start)>=arrival;} +} Serial1; +struct Location { + unsigned begins=0,resets=0,stops=0; + void begin(){++begins;}void reset(){++resets;}void stop(){++stops;} +}; +struct EnvironmentSensorManager { + Location* _location; + bool gps_serial_transport_blocked=false,gps_serial_transport=false; + bool gps_detected=false,gps_active=false,user_enabled=false; + unsigned transport_resets=0; + explicit EnvironmentSensorManager(Location& p):_location(&p){} + void resetGpsTelemetryTransportState(){++transport_resets;} + void setGpsTelemetryUserEnabled(bool enabled){user_enabled=enabled;} + void initBasicGPS(); +}; +@INIT@ +int main(){ + for(uint32_t arrival:{0U,1U,250U,2600U,5000U,5001U,UINT32_MAX}){ + now_ms=UINT32_MAX-1000;delay_count=0;Serial1=SerialMock();Serial1.arrival=arrival; + Location location;EnvironmentSensorManager sensors(location);const uint32_t start=now_ms; + sensors.initBasicGPS(); +#ifdef ENV_SKIP_GPS_DETECT + assert(sensors.gps_detected&&now_ms==start&&delay_count==0&&Serial1.polls==0); +#else + const bool found=arrival<=5000; + const uint32_t elapsed=arrival==0?0:found?((arrival+249)/250)*250:5000; + assert(sensors.gps_detected==found&&static_cast(now_ms-start)==elapsed); + assert(delay_count==elapsed/250&&delay_count<=20); +#endif + assert(location.begins==1&&location.resets==1&&sensors.transport_resets==1); + assert(sensors.gps_serial_transport==sensors.gps_detected); +#ifdef PERSISTANT_GPS + assert(sensors.gps_active==sensors.gps_detected&&sensors.user_enabled==sensors.gps_detected); + assert(location.stops==static_cast(!sensors.gps_detected)); +#else + assert(!sensors.gps_active&&location.stops==1); +#endif + } + Location location;EnvironmentSensorManager blocked(location); + blocked.gps_serial_transport_blocked=true;delay_count=0;Serial1=SerialMock(); + blocked.initBasicGPS();assert(delay_count==0&&Serial1.begins==0&&Serial1.polls==0); + assert(location.begins==0&&blocked.transport_resets==0); +} +''' + + +class GpsUpstreamAdaptationsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.compiler = shutil.which("g++") or shutil.which("clang++") + if cls.compiler is None: + raise unittest.SkipTest("a host C++17 compiler is required") + + def compile_run(self, source, defines=(), extra_sources=()): + with tempfile.TemporaryDirectory(prefix="meshcore-gps-upstream-") as directory: + path = Path(directory) + for name, text in { + "Arduino.h": ARDUINO, "Mesh.h": MESH, "MicroNMEA.h": NMEA, + "RTClib.h": RTC, "CayenneLPP.h": CAYENNE, + "Wire.h": "#pragma once\nclass TwoWire {};\n", + }.items(): + (path / name).write_text(text, encoding="utf-8") + cpp = path / "test.cpp" + cpp.write_text(source, encoding="utf-8") + binary = path / "test" + subprocess.run([ + self.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", + "-Wno-unused-parameter", + *(["-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []), + *[f"-D{define}" for define in defines], + "-I", str(path), "-I", str(ROOT / "src"), str(cpp), + *[str(ROOT / name) for name in extra_sources], "-o", str(binary), + ], check=True) + subprocess.run([str(binary)], check=True) + + def query_harness(self): + source = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text() + config = re.search(r"#ifndef GPS_READ_INTERVAL_SECS\s*[\s\S]*?#endif", source).group() + request = extract_braced(source, "if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && payload_len >= 1)") + return QUERY.replace("@CONFIG@", config).replace("@REQUEST@", request).replace( + "const unsigned req_type=3;", "const unsigned req_type=3;\n#define REQ_TYPE_GET_TELEMETRY_DATA 3") + + def test_monotonic_query_throttle_permissions_and_ownership(self): + self.compile_run(self.query_harness(), ("ENV_INCLUDE_GPS=1",), + ("src/helpers/SensorManager.cpp",)) + + def test_build_override_keeps_seconds_separate_from_position_interval(self): + self.compile_run(self.query_harness(), ("ENV_INCLUDE_GPS=1", "GPS_READ_INTERVAL_SECS=2"), + ("src/helpers/SensorManager.cpp",)) + + def test_no_gps_build_keeps_safe_noop(self): + self.compile_run(r''' +#include +#include +void LocationProvider::sendSentence(const char*){} +int main(){SensorManager sensors;assert(!sensors.requestGpsTelemetryTimeSync(UINT64_MAX));} +''', ("ENV_INCLUDE_GPS=0",), ("src/helpers/SensorManager.cpp",)) + + def test_bounded_discovery_fast_cold_absent_bypass_and_uart_owner(self): + source = (ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp").read_text() + init = extract_braced(source, "void EnvironmentSensorManager::initBasicGPS()") + for defines in ((), ("ENV_SKIP_GPS_DETECT=1",), ("PERSISTANT_GPS=1",)): + with self.subTest(defines=defines): + self.compile_run(COLD.replace("@INIT@", init), defines) + + def test_ble_led_optout_preserves_default_and_begin_order(self): + source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text() + begin = extract_braced(source, "bool SerialBLEInterface::begin(") + block = re.search(r"#ifdef DISABLE_BLE_LED[\s\S]*?#endif", begin).group() + self.assertLess(begin.index(block), begin.index("Bluefruit.configPrphConn(")) + self.assertLess(begin.index(block), begin.index("if (!Bluefruit.begin())")) + self.assertLess(begin.index("if (_begin_attempted)"), begin.index(block)) + harness = r''' +#include +struct BluefruitMock { + bool controlled=true;unsigned calls=0; + void autoConnLed(bool enabled){controlled=enabled;++calls;} +} Bluefruit; +void configure(){ +@BLOCK@ +} +int main(){ + configure(); +#ifdef DISABLE_BLE_LED + assert(!Bluefruit.controlled&&Bluefruit.calls==1); +#else + assert(Bluefruit.controlled&&Bluefruit.calls==0); +#endif + Bluefruit.controlled=false;configure();assert(!Bluefruit.controlled); +} +'''.replace("@BLOCK@", block) + self.compile_run(harness) + self.compile_run(harness, ("DISABLE_BLE_LED=1",)) + + def test_shared_success_mark_and_rak_discovery_reset_are_wired(self): + provider = (ROOT / "src/helpers/sensors/MicroNMEALocationProvider.h").read_text() + rak = (ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp").read_text() + self.assertIn("markTimeSyncApplied();", provider) + rak_provider = rak[rak.index("class RAK12500LocationProvider"):rak.index("static RAK12500LocationProvider")] + self.assertIn("markTimeSyncApplied();", rak_provider) + self.assertIn("resetTimeSyncRequestState();", rak_provider) + # Preserve deliberate force-sync callers outside telemetry queries. + self.assertIn("void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }", provider) + self.assertIn("_location->syncTime();", extract_braced(rak, "void EnvironmentSensorManager::armGpsPowerSavingCycle()")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_local_release_full_selection.py b/test/test_local_release_full_selection.py index 0685c847c..522b2b95c 100644 --- a/test/test_local_release_full_selection.py +++ b/test/test_local_release_full_selection.py @@ -15,6 +15,17 @@ def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM") class ReleaseFullSelectionTest(unittest.TestCase): + def test_nrf52_full_and_reduced_ota_pair_is_never_collapsed(self): + for role in ('repeater', 'room_server', 'sensor'): + target = 'RAK_3401_' + role + variants = [] + for sensor in ('full', 'reduced'): + item = record(target, 'auto', 'NRF52_PLATFORM') + item['manifest'].update(artifact_target=target + '-' + sensor + '-ota', + capabilities=['sensor.profile.' + sensor]) + variants.append(item) + self.assertEqual(select_ordinary_full_records(variants), variants) + def test_resume_filters_sibling_full_images_only(self): inputs = [ record("Heltec_v3_repeater_observer_mqtt"), diff --git a/test/test_mqtt_connection_health/test_mqtt_connection_health.cpp b/test/test_mqtt_connection_health/test_mqtt_connection_health.cpp new file mode 100644 index 000000000..f4bfc2733 --- /dev/null +++ b/test/test_mqtt_connection_health/test_mqtt_connection_health.cpp @@ -0,0 +1,82 @@ +#include +#include "helpers/MQTTConnectionHealth.h" + +using namespace MQTTConnectionHealth; + +TEST(MQTTConnectionHealth, WaitingConfigurationIsNotMarkedAsDown) { + EXPECT_TRUE(isOutageSlot(true, true, false)); + EXPECT_FALSE(isOutageSlot(true, false, false)); + EXPECT_FALSE(isOutageSlot(false, true, false)); + EXPECT_FALSE(isOutageSlot(true, true, true)); +} + +TEST(MQTTConnectionHealth, OnlyPendingNeverConnectedAttemptIsAFailure) { + EXPECT_TRUE(disconnectFailedAttempt(true, false)); + EXPECT_FALSE(disconnectFailedAttempt(false, false)); // deliberate stop/duplicate callback + EXPECT_FALSE(disconnectFailedAttempt(true, true)); // late live-session bounce + EXPECT_FALSE(disconnectFailedAttempt(false, true)); // dropped established session +} + +TEST(MQTTConnectionHealth, RejectedReconnectPreservesOnlyExistingAttempt) { + EXPECT_TRUE(clearPendingAfterRejectedRequest(true, false)); + EXPECT_TRUE(clearPendingAfterRejectedRequest(true, true)); // failed start clears stale state + EXPECT_TRUE(clearPendingAfterRejectedRequest(false, false)); + EXPECT_FALSE(clearPendingAfterRejectedRequest(false, true)); // leave CURRENT state untouched +} + +TEST(MQTTConnectionHealth, UnreadyAndDisabledSlotsDoNotCreateFalseOutages) { + Totals totals; + addSlot(totals, true, true, true, false, 0, 10000, 2, 3); + addSlot(totals, true, false, false, true, 1, 10000, 5, 7); + addSlot(totals, false, true, false, true, 1, 10000, 11, 13); + EXPECT_EQ(1, totals.up); + EXPECT_EQ(1, totals.total); + EXPECT_EQ(0, totals.breakers); + EXPECT_FALSE(totals.outage_timed); + EXPECT_EQ(41u, totals.failures); // all history, even disabled slots +} + +TEST(MQTTConnectionHealth, DisablingSlotDoesNotReduceFailureHistory) { + Totals enabled, disabled; + addSlot(enabled, true, true, false, false, 1000, 9000, 31, 4); + addSlot(disabled, false, true, false, false, 1000, 9000, 31, 4); + EXPECT_EQ(enabled.failures, disabled.failures); + EXPECT_EQ(1, enabled.total); + EXPECT_EQ(0, disabled.total); +} + +TEST(MQTTConnectionHealth, ReadyOutagesChooseWorstAndIgnoreHealthyStaleTimer) { + Totals totals; + addSlot(totals, true, true, false, true, 0, 10000, 0, 0); // first attempt still pending + EXPECT_FALSE(totals.outage_timed); + addSlot(totals, true, true, false, false, 5000, 10000, 0, 0); + addSlot(totals, true, true, false, false, 2000, 10000, 0, 0); + addSlot(totals, true, true, true, false, 1, 10000, 0, 0); + EXPECT_TRUE(totals.outage_timed); + EXPECT_EQ(8000u, totals.worst_outage_ms); + EXPECT_EQ(1, totals.up); + EXPECT_EQ(4, totals.total); + EXPECT_EQ(1, totals.breakers); +} + +TEST(MQTTConnectionHealth, OutageElapsedIsWrapSafe) { + Totals totals; + addSlot(totals, true, true, false, false, UINT32_MAX - 499, 500, 0, 0); + EXPECT_EQ(1000u, totals.worst_outage_ms); +} + +TEST(MQTTConnectionHealth, LifetimeCountersSaturateInsteadOfWrapping) { + EXPECT_EQ(1u, incrementFailures(0)); + EXPECT_EQ(UINT32_MAX, incrementFailures(UINT32_MAX)); + Totals totals; + addSlot(totals, false, false, false, false, 0, 0, UINT32_MAX, UINT32_MAX); + addSlot(totals, false, false, false, false, 0, 0, UINT32_MAX, UINT32_MAX); + EXPECT_EQ(uint64_t(UINT32_MAX) * 4, totals.failures); + EXPECT_EQ(INT32_MAX, clampFailureTotal(totals.failures)); + EXPECT_EQ(31, clampFailureTotal(31)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp b/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp index d10e028e5..d901bf8e1 100644 --- a/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp +++ b/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp @@ -99,6 +99,93 @@ TEST(MQTTPayloadBuilder, StatusOmissionSentinelsRemainOmitted) { EXPECT_FALSE(parsed["repeat"].is()); } +TEST(MQTTPayloadBuilder, StatusIncludesAllConnectionHealthFields) { + JsonDocument scratch; + char buffer[1024]; + MQTTConnHealth health; + health.slots_up = 2; + health.slots_total = 3; + health.worst_outage_secs = 18432; + health.heap_largest = 16116; + health.connect_failures = 57; + health.slots_breaker = 1; + ASSERT_GT(MQTTPayloadBuilder::buildStatusMessage( + scratch, "node", "id", "model", "firmware", "radio", "client", "online", + kTimestamp, buffer, sizeof(buffer), -1, -1, -1, -1, -999, + -1, -1, -1, -1, -1, -1, nullptr, health), 0); + JsonDocument parsed; + ASSERT_FALSE(deserializeJson(parsed, buffer)); + JsonObject stats = parsed["stats"].as(); + EXPECT_EQ(2, stats["mqtt_slots_up"].as()); + EXPECT_EQ(3, stats["mqtt_slots_total"].as()); + EXPECT_EQ(18432, stats["mqtt_outage_secs"].as()); + EXPECT_EQ(16116, stats["heap_largest"].as()); + EXPECT_EQ(57, stats["mqtt_connect_failures"].as()); + EXPECT_EQ(1, stats["mqtt_slots_breaker"].as()); +} + +TEST(MQTTPayloadBuilder, HealthyStatusOmitsOutageAndZeroFailureKeys) { + JsonDocument scratch; + char buffer[1024]; + MQTTConnHealth health; + health.slots_up = 3; + health.slots_total = 3; + health.heap_largest = 54260; + health.connect_failures = 0; + health.slots_breaker = 0; + ASSERT_GT(MQTTPayloadBuilder::buildStatusMessage( + scratch, "node", "id", "model", "firmware", "radio", "client", "online", + kTimestamp, buffer, sizeof(buffer), -1, -1, -1, -1, -999, + -1, -1, -1, -1, -1, -1, nullptr, health), 0); + JsonDocument parsed; + ASSERT_FALSE(deserializeJson(parsed, buffer)); + JsonObject stats = parsed["stats"].as(); + EXPECT_EQ(3, stats["mqtt_slots_up"].as()); + EXPECT_FALSE(stats["mqtt_outage_secs"].is()); + EXPECT_FALSE(stats["mqtt_connect_failures"].is()); + EXPECT_FALSE(stats["mqtt_slots_breaker"].is()); +} + +TEST(MQTTPayloadBuilder, FailedStartsAloneCreateHealthStats) { + JsonDocument scratch; + char buffer[1024]; + MQTTConnHealth health; + health.connect_failures = 1; + ASSERT_GT(MQTTPayloadBuilder::buildStatusMessage( + scratch, "node", "id", "model", "firmware", "radio", "client", "online", + kTimestamp, buffer, sizeof(buffer), -1, -1, -1, -1, -999, + -1, -1, -1, -1, -1, -1, nullptr, health), 0); + JsonDocument parsed; + ASSERT_FALSE(deserializeJson(parsed, buffer)); + EXPECT_EQ(1, parsed["stats"]["mqtt_connect_failures"].as()); + EXPECT_FALSE(parsed["stats"]["mqtt_slots_up"].is()); +} + +TEST(MQTTPayloadBuilder, WorstCaseStatusWithHealthFitsProductionCeiling) { + // Actual bridge field maxima, including legal quote characters doubled by JSON. + const std::string origin(31, '"'), origin_id(64, 'A'), model(63, 'M'); + const std::string firmware(63, 'F'), client(63, 'C'); + JsonDocument scratch; + char buffer[1024]; + MQTTConnHealth health; + health.slots_up = 0; + health.slots_total = 5; + health.worst_outage_secs = 4294967; + health.heap_largest = 8388608; + health.connect_failures = INT32_MAX; + health.slots_breaker = 5; + const int len = MQTTPayloadBuilder::buildStatusMessage( + scratch, origin.c_str(), origin_id.c_str(), model.c_str(), firmware.c_str(), + "9999.999999,1000.0,12,8", client.c_str(), "online", kTimestamp, + buffer, sizeof(buffer), 4200, 2147483, INT32_MAX, 6, -128, + 2147483, 2147483, INT32_MAX, 8388608, INT32_MAX, INT32_MAX, "off", health); + ASSERT_GT(len, 0); + EXPECT_LT(len, 1024 - 32); + JsonDocument parsed; + ASSERT_FALSE(deserializeJson(parsed, buffer)); + EXPECT_EQ(INT32_MAX, parsed["stats"]["mqtt_connect_failures"].as()); +} + TEST(MQTTPayloadBuilder, StringsAreEscapedAndRoundTrip) { const char* origin = "node \"north\"\\rack\nline"; const char* model = "Heltec\tV3"; @@ -199,6 +286,27 @@ TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownRadioMetrics) { EXPECT_FALSE(parsed["score"].is()); } +TEST(MQTTPayloadBuilder, RxPacketOmitsOnlyTheUnknownRadioMetric) { + JsonDocument scratch; + char buffer[2048]; + ASSERT_GT(MQTTPayloadBuilder::buildPacketMessage( + scratch, "node", "id", kTimestamp, "rx", "12:34:56", "18/07/2026", + 42, 4, "D", 20, "A0B1", std::nanf(""), -93, std::nanf(""), "hash", + nullptr, 0, 0, 64, buffer, sizeof(buffer)), 0); + JsonDocument parsed; + ASSERT_FALSE(deserializeJson(parsed, buffer)); + EXPECT_FALSE(parsed["SNR"].is()); + EXPECT_STREQ("-93", parsed["RSSI"].as()); + ASSERT_GT(MQTTPayloadBuilder::buildPacketMessage( + scratch, "node", "id", kTimestamp, "rx", "12:34:56", "18/07/2026", + 42, 4, "D", 20, "A0B1", 7.2f, -999, std::nanf(""), "hash", + nullptr, 0, 0, 64, buffer, sizeof(buffer)), 0); + parsed.clear(); + ASSERT_FALSE(deserializeJson(parsed, buffer)); + EXPECT_STREQ("7.2", parsed["SNR"].as()); + EXPECT_FALSE(parsed["RSSI"].is()); +} + TEST(MQTTPayloadBuilder, RawMessageHasExactContractAndEscapesData) { char buffer[512]; JsonDocument doc; diff --git a/test/test_mqtt_transport_results.py b/test/test_mqtt_transport_results.py new file mode 100644 index 000000000..90d252fb5 --- /dev/null +++ b/test/test_mqtt_transport_results.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Compile the real adapter/bridge attempt methods against controllable SDK calls. + +No PlatformIO/network/hardware is used. IDF4 and IDF5 config layouts are covered. +""" +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +def method(path, signature): + source = (ROOT / path).read_text(encoding="utf-8") + start = source.index(signature) + opening = source.index("{", start) + depth = 1 + pos = opening + 1 + while depth: + depth += (source[pos] == "{") - (source[pos] == "}") + pos += 1 + return source[start:pos] + "\n" + + +PREAMBLE = r''' +#include +#include +#include +#include +#include "helpers/MQTTConnectionHealth.h" +using esp_err_t = int; +using esp_mqtt_client_handle_t = void*; +constexpr int ESP_OK=0, ESP_FAIL=-1, ESP_ERR_INVALID_STATE=2, ESP_ERR_NO_MEM=3; +constexpr int ESP_ERR_INVALID_ARG=4, MQTT_EVENT_ANY=0, RUNTIME_MQTT_SLOTS=2; +#define ESP_LOGE(...) ((void)0) +#define ESP_LOGW(...) ((void)0) +#define ESP_LOGI(...) ((void)0) +#define ESP_ERROR_CHECK_WITHOUT_ABORT(...) ((void)0) +#define MQTT_DEBUG_PRINTLN(...) ((void)0) +const char* esp_err_to_name(int) { return "mock"; } +struct Config { + struct { struct { const char* uri=nullptr; } address; } broker; + struct { int size=1024; } buffer; + const char* uri=nullptr; + int buffer_size=1024; +}; +static bool fail_malloc=false, fail_init=false; +static int register_result=ESP_OK, config_result=ESP_OK; +static int start_result=ESP_OK, reconnect_result=ESP_OK; +static int init_calls=0, register_calls=0, destroy_calls=0, config_calls=0; +static int start_calls=0, reconnect_calls=0; +static std::function sdk_callback; +void* test_malloc(size_t size) { return fail_malloc ? nullptr : std::malloc(size); } +#define malloc test_malloc +void* esp_mqtt_client_init(Config*) { + ++init_calls; return fail_init ? nullptr : reinterpret_cast(1); +} +int esp_mqtt_client_register_event(void*, int, void(*)(void*,int,int,void*), void*) { + ++register_calls; return register_result; +} +int esp_mqtt_client_destroy(void*) { ++destroy_calls; return ESP_OK; } +int esp_mqtt_set_config(void*, Config*) { ++config_calls; return config_result; } +int esp_mqtt_client_start(void*) { + ++start_calls; if(sdk_callback) sdk_callback(); return start_result; +} +int esp_mqtt_client_reconnect(void*) { + ++reconnect_calls; if(sdk_callback) sdk_callback(); return reconnect_result; +} +struct PsychicMqttClient { + Config _mqtt_cfg; + void* _client=nullptr; + bool _config_dirty=true, _started=false; + char* _buffer=nullptr; + size_t _buffer_capacity=0; + ~PsychicMqttClient() { std::free(_buffer); } + static void _onMqttEventStatic(void*,int,int,void*) {} + esp_err_t applyConfig(); + esp_err_t connect(); + esp_err_t reconnect(); + bool isStarted() const { return _started; } + void uri(const char* uri) { _mqtt_cfg.uri=uri; _mqtt_cfg.broker.address.uri=uri; } +}; +struct MQTTBridge { + struct MQTTSlot { + PsychicMqttClient* client=nullptr; + uint32_t start_failures=0; + }; + MQTTSlot _slots[RUNTIME_MQTT_SLOTS]; + volatile bool _slot_attempt_pending[RUNTIME_MQTT_SLOTS] = {}; + esp_err_t reconnectSlotClient(int index); +}; +''' + +MAIN = r''' +int main() { + PsychicMqttClient c; + assert(c.connect()==ESP_ERR_INVALID_STATE && start_calls==0 && init_calls==0); + c.uri("mqtts://broker"); + std::free(c._buffer); c._buffer=nullptr; + fail_malloc=true; + assert(c.connect()==ESP_ERR_NO_MEM && start_calls==0 && init_calls==0); + fail_malloc=false; + fail_init=true; + assert(c.connect()==ESP_ERR_NO_MEM && register_calls==0 && start_calls==0); + fail_init=false; + register_result=ESP_FAIL; + assert(c.connect()==ESP_FAIL && c._client==nullptr && c._config_dirty); + assert(destroy_calls==1 && start_calls==0); + register_result=ESP_OK; + start_result=ESP_FAIL; + assert(c.connect()==ESP_FAIL && !c.isStarted()); + start_result=ESP_OK; + assert(c.connect()==ESP_OK && c.isStarted()); + assert(register_calls==2); // failed registration then one successful registration + c._config_dirty=true; + config_result=ESP_FAIL; + int starts=start_calls; + assert(c.connect()==ESP_FAIL && start_calls==starts && c._config_dirty); + assert(c.reconnect()==ESP_FAIL && reconnect_calls==0 && c._config_dirty); + config_result=ESP_OK; + reconnect_result=ESP_FAIL; + assert(c.reconnect()==ESP_FAIL && !c._config_dirty && reconnect_calls==1); + reconnect_result=ESP_OK; + assert(c.reconnect()==ESP_OK && reconnect_calls==2); + + MQTTBridge b; + assert(b.reconnectSlotClient(-1)==ESP_ERR_INVALID_ARG); + assert(b.reconnectSlotClient(1)==ESP_ERR_INVALID_STATE); + b._slots[0].client=&c; + reconnect_result=ESP_FAIL; + assert(b.reconnectSlotClient(0)==ESP_FAIL); + assert(!b._slot_attempt_pending[0] && b._slots[0].start_failures==0); + b._slot_attempt_pending[0]=true; + assert(b.reconnectSlotClient(0)==ESP_FAIL); + assert(b._slot_attempt_pending[0] && b._slots[0].start_failures==0); + sdk_callback=[&]() { b._slot_attempt_pending[0]=false; }; + assert(b.reconnectSlotClient(0)==ESP_FAIL && !b._slot_attempt_pending[0]); + sdk_callback=nullptr; + c._started=false; + start_result=ESP_FAIL; + assert(b.reconnectSlotClient(0)==ESP_FAIL); + assert(!b._slot_attempt_pending[0] && b._slots[0].start_failures==1); + start_result=ESP_OK; + sdk_callback=[&]() { + assert(b._slot_attempt_pending[0]); // set before synchronous SDK callback + b._slot_attempt_pending[0]=false; // onConnect completes the attempt + }; + assert(b.reconnectSlotClient(0)==ESP_OK && !b._slot_attempt_pending[0]); + sdk_callback=nullptr; + reconnect_result=ESP_OK; + assert(b.reconnectSlotClient(0)==ESP_OK && b._slot_attempt_pending[0]); + return 0; +} +''' + + +class MqttTransportResultsTests(unittest.TestCase): + def test_real_adapter_and_bridge_methods_for_both_idf_layouts(self): + compiler = shutil.which("g++") or shutil.which("c++") + self.assertIsNotNone(compiler, "a host C++ compiler is required") + adapter = "lib/PsychicMqttClient/src/PsychicMqttClient.cpp" + source = PREAMBLE + "\n".join(method(adapter, signature) for signature in ( + "esp_err_t PsychicMqttClient::applyConfig()", + "esp_err_t PsychicMqttClient::connect()", + "esp_err_t PsychicMqttClient::reconnect()")) + source += method("src/helpers/bridges/MQTTBridge.cpp", + "esp_err_t MQTTBridge::reconnectSlotClient(int index)") + MAIN + with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-results-") as temp: + path = Path(temp) / "results.cpp" + path.write_text(source, encoding="utf-8") + for major in (4, 5): + with self.subTest(idf_major=major): + exe = Path(temp) / (f"results-{major}" + (".exe" if os.name == "nt" else "")) + result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", + f"-DESP_IDF_VERSION_MAJOR={major}", + "-I", str(ROOT / "src"), str(path), "-o", str(exe)], + text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + result = subprocess.run([str(exe)], text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_attempt_wiring_covers_stop_bounce_setup_and_both_status_paths(self): + source = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text(encoding="utf-8") + self.assertEqual(source.count("result = slot.client->connect();"), 1) + self.assertEqual(source.count("result = slot.client->reconnect();"), 1) + self.assertEqual(source.count("repeatStatus(), collectConnHealth()"), 2) + for signature in ("void MQTTBridge::teardownSlot(", "void MQTTBridge::destroySlotClients("): + body = method("src/helpers/bridges/MQTTBridge.cpp", signature) + self.assertIn("_slot_attempt_pending[", body) + self.assertNotIn("connect_failures =", body) + self.assertNotIn("start_failures =", body) + callback = source[source.index("slot.client->onDisconnect("):source.index("slot.client->onError(")] + self.assertIn("disconnectFailedAttempt", callback) + self.assertIn("incrementFailures", callback) + stats = method("src/helpers/bridges/MQTTBridge.cpp", "void MQTTBridge::formatMqttStatsReply(") + self.assertEqual(stats.count("MQTTConnectionHealth::isOutageSlot("), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_nrf52_ota_profiles.sh b/test/test_nrf52_ota_profiles.sh new file mode 100644 index 000000000..c339d1989 --- /dev/null +++ b/test/test_nrf52_ota_profiles.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source build.sh +fail() { echo "test_nrf52_ota_profiles: $*" >&2; exit 1; } + +# This suite deliberately has no PlatformIO dependency. A mocked one-profile +# worker exercises the actual single/bulk/matrix dispatch and fail-closed pair +# policy, while the flag/manifest contracts below use production helpers. +SUPPORTED_PIO_ENVS=( + nrf_repeater nrf_repeater_lora_ota_no_external_sensors + nrf_room_server nrf_room_server_lora_ota_no_external_sensors + nrf_sensor nrf_sensor_lora_ota_no_external_sensors + Xiao_nrf52_repeater Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors + RAK_3401_repeater_unified_lora_ota RAK_3401_sensor + RAK_3401_sensor_lora_ota_no_external_sensors nrf_kiss_modem + nrf_companion_radio_usb nrf_companion_radio_full nrf_terminal_chat + esp_repeater +) +for target in "${SUPPORTED_PIO_ENVS[@]}"; do + PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM +done +PIO_ENV_PLATFORM_BY_NAME[esp_repeater]=ESP32_PLATFORM +for base in nrf_repeater nrf_room_server nrf_sensor RAK_3401_sensor; do + alias=${base}_lora_ota_no_external_sensors + PIO_ENV_BUILD_BASE_BY_NAME[$alias]=$base + PIO_ENV_COMPLETE_OTA_BASE_BY_NAME[$alias]=$base + PIO_ENV_OTA_BY_NAME[$alias]=1 +done +PIO_ENV_QSPI_OTA_BY_NAME[Xiao_nrf52_repeater]=1 +PIO_ENV_QSPI_OTA_BY_NAME[RAK_3401_repeater_unified_lora_ota]=1 +PIO_ENV_SD_OTA_BY_NAME[Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors]=1 +PIO_CONFIG_JSON='[]' + +for target in nrf_repeater nrf_room_server nrf_sensor Xiao_nrf52_repeater \ + Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors \ + RAK_3401_repeater_unified_lora_ota; do + is_nrf52_sensor_ota_pair_target "$target" || fail "$target has no pair policy" +done +for target in nrf_kiss_modem nrf_companion_radio_usb nrf_companion_radio_full \ + nrf_terminal_chat esp_repeater; do + if is_nrf52_sensor_ota_pair_target "$target"; then + fail "$target incorrectly gained the nRF52 sensor/OTA manager policy" + fi +done +[ "$(get_reduced_lora_ota_target nrf_sensor)" = nrf_sensor_lora_ota_no_external_sensors ] \ + || fail "sensor reduced alias is not handled" +[ "$(get_nrf52_sensor_ota_pair_target nrf_repeater)" = nrf_repeater_lora_ota_no_external_sensors ] \ + || fail "ordinary internal target lost its historical OTA identity" +[ "$(get_nrf52_sensor_ota_pair_target Xiao_nrf52_repeater)" = Xiao_nrf52_repeater ] \ + || fail "external QSPI target changed its deployed ID" +[ "$(get_nrf52_sensor_ota_pair_target Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors)" = \ + Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors ] \ + || fail "SD target lost its exact storage identity" + +RESOLVED_BUILD_TARGETS=(nrf_repeater nrf_repeater_lora_ota_no_external_sensors \ + nrf_sensor nrf_sensor_lora_ota_no_external_sensors Xiao_nrf52_repeater \ + Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors) +normalize_nrf52_sensor_ota_pair_targets +[ "${#RESOLVED_BUILD_TARGETS[@]}" -eq 4 ] \ + || fail "ordinary/reduced aliases did not deduplicate into exactly one pair" + +for selected in auto standard full; do + BUILD_PROFILE_OVERRIDE=$selected + BUILD_PROFILE_EXPLICIT=1 + RESOLVED_BUILD_TARGETS=(nrf_sensor) + configure_effective_build_profile build-firmware >/dev/null + [ "$BUILD_PROFILE_EFFECTIVE" = auto ] || fail "$selected bypassed strict nRF52 pairing" + [ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] || fail "$selected retained the permissive fallback path" +done +BUILD_PROFILE_EXPLICIT=0 + +# Use the same explicit source prefix as canonical release packaging, not +# Git's repository-dependent/adaptive abbreviated hash length. +worker_source=$(declare -f build_firmware_one_profile) +[[ "$worker_source" == *'commit_hash=$(git rev-parse HEAD)'* ]] \ + || fail "builder does not derive publication prefix from full source identity" +[[ "$worker_source" == *'commit_hash=${commit_hash:0:8}'* ]] \ + || fail "builder publication hash disagrees with canonical release prefix" + +calls=() +failed_profile="" +build_firmware_one_profile() { + calls+=("$1:$NRF52_OTA_SENSOR_PROFILE:$FIRMWARE_FILENAME_INFIX:$SKIP_DECLARED_REDUCTIONS:$COMPLETE_OTA_FIRST_PASS:$REQUIRE_OTA_UPDATES") + if [ -n "$failed_profile" ] && [ "$NRF52_OTA_SENSOR_PROFILE" = "$failed_profile" ]; then return 42; fi + return 0 +} +ESP32_FULL_BUILD=1 +BUILD_PROFILE_EFFECTIVE=standard +REQUIRE_OTA_UPDATES=0 +FIRMWARE_FILENAME_INFIX=outer +SKIP_DECLARED_REDUCTIONS=0 +NRF52_OTA_SENSOR_PROFILE="" +for target in nrf_repeater nrf_room_server nrf_sensor Xiao_nrf52_repeater \ + Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors; do + calls=() + build_firmware "$target" >/dev/null + [ "${#calls[@]}" -eq 2 ] || fail "$target did not build exactly two options" + [[ "${calls[0]}" == *:full:full-ota:1:1:1 ]] || fail "$target full flags/OTA requirement are wrong" + [[ "${calls[1]}" == *:reduced:reduced-ota:0:0:1 ]] || fail "$target reduced flags/OTA requirement are wrong" + [ "${calls[0]%%:*}" = "${calls[1]%%:*}" ] || fail "$target profiles have different OTA IDs" +done +[ "$ESP32_FULL_BUILD:$BUILD_PROFILE_EFFECTIVE:$REQUIRE_OTA_UPDATES:$FIRMWARE_FILENAME_INFIX:$NRF52_OTA_SENSOR_PROFILE" = \ + '1:standard:0:outer:' ] || fail "pair state leaked into other platform/Companion builds" + +for failed_profile in full reduced; do + calls=() + if build_firmware nrf_repeater >/dev/null 2>&1; then + fail "$failed_profile failure was reported as a complete pair" + fi + [ "${#calls[@]}" -eq 2 ] || fail "$failed_profile failure hid the other option's qualification" +done +failed_profile="" + +# Both ordinary bulk and the logged matrix go through the same pair dispatcher. +calls=() +run_resolved_build_targets nrf_repeater nrf_sensor Xiao_nrf52_repeater >/dev/null +[ "${#calls[@]}" -eq 6 ] || fail "bulk builds did not publish every pair" +test_output=$(mktemp -d) +trap 'rm -rf -- "$test_output"' EXIT +OUTPUT_DIR=$test_output +FIRMWARE_FILENAME_INFIX="" +calls=() +run_logged_build_targets nrf_room_server Xiao_nrf52_repeater >/dev/null +[ "${#calls[@]}" -eq 4 ] || fail "logged matrix did not publish external/internal pairs" + +# KISS and Companion keep the original one-profile worker and do not gain an +# application receiver merely because another target used the paired overlay. +calls=() +build_firmware nrf_kiss_modem >/dev/null +build_firmware nrf_companion_radio_full >/dev/null +[ "${#calls[@]}" -eq 2 ] || fail "excluded roles gained profile pairs" +[[ "${calls[0]}" == nrf_kiss_modem::* ]] || fail "KISS inherited sensor OTA state" + +NRF52_OTA_SENSOR_PROFILE=reduced +BUILD_PROFILE_FOR_TARGET=standard +MQTT_BRIDGE_OVERRIDE=off +PACKET_LOGGING_OVERRIDE=on +MESHDEBUG_OVERRIDE=on +ESP32_FULL_BUILD=0 +is_lora_ota_build nrf_sensor || fail "logging/standard settings disabled required OTA" +if is_lora_ota_build nrf_kiss_modem; then fail "KISS gained an OTA application"; fi + +for profile in full reduced; do + NRF52_OTA_SENSOR_PROFILE=$profile + BUILD_CAPABILITIES=() + BUILD_EXPECTATIONS=() + BUILD_REDUCTIONS=() + BUILD_APPLICATION_EXPECTATIONS=() + declare_build_capability_contract nrf_sensor NRF52_PLATFORM + [[ " ${BUILD_CAPABILITIES[*]} " == *" sensor.profile.${profile} "* ]] \ + || fail "$profile manifest sensor label is missing" + [[ " ${BUILD_EXPECTATIONS[*]} " == *" ota.update.lora=invalid in-place patch geometry "* ]] \ + || fail "$profile does not verify actual LoRa apply support" +done + +# Raw external-storage names have no reduced-alias suffix, but still receive +# the explicit reduced policy; the full profile does not trim these drivers. +NRF52_OTA_SENSOR_PROFILE=reduced +SKIP_DECLARED_REDUCTIONS=0 +PLATFORMIO_BUILD_FLAGS="" +PLATFORMIO_BUILD_UNFLAGS="" +BUILD_REDUCTIONS=() +apply_lora_ota_no_external_sensors_profile Xiao_nrf52_repeater +[[ "$PLATFORMIO_BUILD_FLAGS" == *-UENV_INCLUDE_BME280* ]] \ + || fail "external QSPI reduced profile did not trim optional drivers" +NRF52_OTA_SENSOR_PROFILE=full +SKIP_DECLARED_REDUCTIONS=1 +PLATFORMIO_BUILD_FLAGS="" +apply_lora_ota_no_external_sensors_profile Xiao_nrf52_repeater +[ -z "$PLATFORMIO_BUILD_FLAGS" ] || fail "full sensor profile was silently trimmed" + +NRF52_OTA_SENSOR_PROFILE=reduced +is_rak_i2c_voltage_monitor_ota_target RAK_3401_repeater_unified_lora_ota \ + || fail "adaptive RAK reduced profile dropped the retained INA policy" +is_rak_gps_retaining_ota_target RAK_3401_repeater_unified_lora_ota \ + || fail "adaptive RAK reduced profile dropped compatible GPS" +if is_rak_gps_retaining_ota_target RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors; then + fail "Serial1 bridge incorrectly promises conflicting GPS" +fi + +# Resume must requalify the current sensor profile, not accept a legacy +# Bluetooth-only or unlabeled image merely because its hash/UF2 checks pass. +for profile in full reduced; do + for corruption in valid missing_sensor opposite_sensor ambiguous_sensor missing_lora_cap \ + no_lora_method unproven_lora unverified_ota unverified_manifest wrong_target wrong_artifact wrong_platform; do + resume_manifest=$test_output/resume.json + python3 - "$resume_manifest" "$profile" "$corruption" <<'PY' +import json, pathlib, sys +path, profile, corruption = sys.argv[1:] +manifest = {"platform": "NRF52_PLATFORM", "target": "nrf_sensor", + "artifact_target": "nrf_sensor-" + profile + "-ota", + "capabilities": ["sensor.profile." + profile, "ota.update.lora"], + "verification": [{"capability": "ota.update.lora", "present": True}], + "verified": True, "ota_update_verified": True, "ota_update_methods": ["lora", "bluetooth"]} +if corruption == "missing_sensor": manifest["capabilities"].pop(0) +elif corruption == "opposite_sensor": manifest["capabilities"][0] = "sensor.profile." + ("reduced" if profile == "full" else "full") +elif corruption == "ambiguous_sensor": manifest["capabilities"].append("sensor.profile." + ("reduced" if profile == "full" else "full")) +elif corruption == "missing_lora_cap": manifest["capabilities"].pop() +elif corruption == "no_lora_method": manifest["ota_update_methods"] = ["bluetooth"] +elif corruption == "unproven_lora": manifest["verification"][0]["present"] = False +elif corruption == "unverified_ota": manifest["ota_update_verified"] = False +elif corruption == "unverified_manifest": manifest["verified"] = False +elif corruption == "wrong_target": manifest["target"] = "other_sensor" +elif corruption == "wrong_artifact": manifest["artifact_target"] = "nrf_sensor" +elif corruption == "wrong_platform": manifest["platform"] = "ESP32_PLATFORM" +pathlib.Path(path).write_text(json.dumps(manifest, indent=2)) +PY + if nrf52_sensor_profile_manifest_matches "$resume_manifest" nrf_sensor "$profile" "nrf_sensor-${profile}-ota"; then + [ "$corruption" = valid ] || fail "resume accepted $profile/$corruption" + else + [ "$corruption" != valid ] || fail "resume rejected valid $profile metadata" + fi + done +done + +echo "test_nrf52_ota_profiles: OK" diff --git a/test/test_ota_target_names.py b/test/test_ota_target_names.py index 8b44aee19..0d6ffba47 100644 --- a/test/test_ota_target_names.py +++ b/test/test_ota_target_names.py @@ -1,5 +1,7 @@ """All OTA names survive front coding; original Companion API stays unchanged.""" import re +import os +import shutil import subprocess import sys import tempfile @@ -35,7 +37,8 @@ class OtaTargetNameTest(unittest.TestCase): (ROOT / "src/helpers/ota/tinf/tinf.h").read_text(), encoding="utf-8") decoder = Path(temp_dir) / "tinf.o" subprocess.run([ - "cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c", + os.environ.get("CC") or shutil.which("cc") or "gcc", + "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c", str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder), ], check=True) subprocess.run([ diff --git a/test/test_picker_controls_generator.py b/test/test_picker_controls_generator.py index aa260748f..d3d21a6d1 100644 --- a/test/test_picker_controls_generator.py +++ b/test/test_picker_controls_generator.py @@ -47,6 +47,58 @@ class PickerControlsTests(unittest.TestCase): manifest['ota_update_methods'] = ['lora'] self.assertEqual(GENERATOR.runtime_metadata(manifest, False)['otaRole'], 'lora-receiver') + def test_sensor_profile_is_qualified_nrf52_receiver_metadata_not_a_filename(self): + source = 'a' * 40 + manifest = dict(target='RAK_3401_repeater_lora_ota_no_external_sensors', + platform='NRF52_PLATFORM', verified=True, ota_update_verified=True, + capabilities=['ota.update.lora', 'sensor.profile.full'], + ota_update_methods=['lora'], verification=[ + dict(capability='ota.update.lora', present=True, source='linked image')]) + result = GENERATOR.runtime_metadata(manifest, False, source) + self.assertEqual(result['sensorProfile'], 'full') + self.assertEqual(result['sensorProfileSource'], source) + manifest['capabilities'][-1] = 'sensor.profile.reduced' + self.assertEqual(GENERATOR.runtime_metadata(manifest, False, source)['sensorProfile'], 'reduced') + for changes in (dict(platform='ESP32_PLATFORM'), dict(target='RAK_3401_companion_radio_full'), + dict(ota_update_verified=False), dict(verification=[]), + dict(verification=[dict(capability='ota.update.lora', present=True, source='filename')])): + with self.subTest(changes=changes), self.assertRaisesRegex(ValueError, 'qualified nRF52'): + GENERATOR.runtime_metadata({**manifest, **changes}, False, source) + with self.assertRaisesRegex(ValueError, 'exact source commit'): + GENERATOR.runtime_metadata(manifest, False, 'aaaaaaaa') + manifest['capabilities'].append('sensor.profile.full') + with self.assertRaisesRegex(ValueError, 'ambiguous'): + GENERATOR.runtime_metadata(manifest, False, source) + + def test_generates_independent_controls_for_same_identity_sensor_pair(self): + source = 'a' * 40 + family = 'v1.17.1.7-dev-aaaaaaaa' + logical = 'RAK_3401_repeater_lora_ota_no_external_sensors' + manifests = [] + for policy in ('full', 'reduced'): + manifests.append(dict(target=logical, artifact_target=logical + '-' + policy + '-ota', + platformio_env='sample', platform='NRF52_PLATFORM', verified=True, + ota_update_verified=True, capabilities=['sensor.profile.' + policy, 'ota.update.lora'], + ota_update_methods=['lora'], verification=[dict(capability='ota.update.lora', + present=True, source='linked image')], files=[logical + '-' + policy + '-ota-' + family + ext + for ext in ('.zip', '.uf2')])) + with tempfile.TemporaryDirectory() as temporary: + stage = Path(temporary) + for group in ('companion', 'lora-ota'): + (stage / group).mkdir() + (stage / 'release-plan.json').write_text(json.dumps(dict(source=source, groups=[ + dict(key='companion', tag=family), dict(key='lora-ota', tag='lora-ota-' + family)]))) + (stage / 'companion/TARGET-MANIFEST.json').write_text('[]') + (stage / 'lora-ota/TARGET-MANIFEST.json').write_text(json.dumps(manifests)) + controls = GENERATOR.generate(stage, [('env:sample', [('build_flags', [])])]) + self.assertEqual(set(controls['profiles']), {logical + '-full', logical + '-reduced'}) + for policy in ('full', 'reduced'): + profile = controls['profiles'][logical + '-' + policy] + self.assertEqual(profile['sensorProfile'], policy) + self.assertEqual(profile['sensorProfileSource'], source) + self.assertEqual(profile['otaRole'], 'lora-receiver') + self.assertEqual(profile['updateMethods'], ['lora']) + def generate(self, reductions, *, source=None, flags=(), verified=True): initial = 'a' * 40 manifest = dict(target='sample_companion_radio_full', platformio_env='sample', diff --git a/test/test_t1000e_sensor_power.py b/test/test_t1000e_sensor_power.py new file mode 100644 index 000000000..c7ca3501e --- /dev/null +++ b/test/test_t1000e_sensor_power.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Verify the upstream sensor-rail correction without changing local GPS/OTA.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +VARIANT = ROOT / "variants/t1000-e" +FIXTURES = ROOT / "test/fixtures/t1000e_sensor_power" + + +class T1000eSensorPowerTests(unittest.TestCase): + def test_obsolete_p0_04_control_is_removed_everywhere(self): + for path in VARIANT.iterdir(): + if path.suffix in (".h", ".cpp", ".ini"): + with self.subTest(path=path.name): + self.assertNotIn("SENSOR_EN", path.read_text()) + self.assertRegex((VARIANT / "variant.h").read_text(), r"PIN_3V3_EN\s+\(38\)") + + def test_real_rail_and_existing_shutdown_and_gps_are_preserved(self): + board = (VARIANT / "T1000eBoard.h").read_text() + self.assertIn("void shutdownPeripherals() override", board) + self.assertIn("NRF52Board::shutdownPeripherals();", board) + for pin in ("GPS_VRTC_EN", "GPS_RESET", "GPS_SLEEP_INT", "GPS_RTC_INT", + "GPS_EN", "PIN_3V3_EN", "PIN_3V3_ACC_EN", "BUZZER_EN"): + self.assertIn(f"digitalWrite({pin}, LOW);", board) + target = (VARIANT / "target.cpp").read_text() + for method in ("armGpsPowerSavingCycle", "loopGpsTelemetry", "processGpsTelemetryFix"): + self.assertIn(method, target) + startup = (VARIANT / "variant.cpp").read_text() + self.assertIn("digitalWrite(GPS_EN, HIGH);", startup) + self.assertIn("digitalWrite(GPS_VRTC_EN, HIGH);", startup) + self.assertIn("pinMode(PIN_3V3_EN, OUTPUT);", startup) + + def test_actual_sensor_functions_only_switch_p1_06_and_keep_bounds(self): + with tempfile.TemporaryDirectory(prefix="t1000e-sensor-power-") as temp: + binary = Path(temp) / "power" + compiled = subprocess.run([ + "c++", "-std=c++11", "-Wall", "-Wextra", "-Wno-unused-variable", + "-I", str(FIXTURES), "-I", str(VARIANT), str(FIXTURES / "power.cpp"), + "-o", str(binary), + ], text=True, capture_output=True) + self.assertEqual(compiled.returncode, 0, compiled.stderr) + executed = subprocess.run([str(binary)], text=True, capture_output=True) + self.assertEqual(executed.returncode, 0, executed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_upstream_observer_recipes.py b/test/test_upstream_observer_recipes.py new file mode 100644 index 000000000..259a7ce5b --- /dev/null +++ b/test/test_upstream_observer_recipes.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Host-only contracts for the observer recipes imported from 33f1b1096. + +Resolve inheritance and interpolation without starting PlatformIO or a build. +The existing board recipes remain authoritative for hardware and OTA policy. +""" +import configparser +import fnmatch +from pathlib import Path +import re +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BOARDS = { + "ebyte_eora_s3": ("Ebyte_EoRa-S3",), + "heltec_e213": ("Heltec_E213",), + "heltec_e290": ("Heltec_E290",), + "heltec_rc32": ("heltec_rc32", "heltec_rc32_without_display"), + "heltec_tracker": ("Heltec_Wireless_Tracker",), + "heltec_wireless_paper": ("Heltec_Wireless_Paper",), + "lilygo_t3s3_sx1276": ("LilyGo_T3S3_sx1276",), + "lilygo_teth_elite": ("LilyGo_TETH_Elite_sx1262",), + "meshnology_w12": ("meshnology_w12",), + "xiao_s3": ("Xiao_S3",), +} +NAMES = tuple( + f"{board}_{role}_observer_mqtt" + for boards in BOARDS.values() for board in boards + for role in ("repeater", "room_server") +) + + +class Recipes: + def __init__(self): + self.config = configparser.ConfigParser(interpolation=None, strict=True) + self.config.read([ROOT / "platformio.ini", *sorted(ROOT.glob("variants/*/platformio.ini"))]) + + def value(self, section, key, seen=()): + pair = (section, key) + if pair in seen: + raise AssertionError(f"cyclic recipe reference: {pair}") + seen = (*seen, pair) + if not self.config.has_option(section, key): + for parent in self.config.get(section, "extends", fallback="").split(","): + parent = parent.strip() + if parent: + value = self.value(parent, key, seen) + if value: + return value + return "" + value = self.config.get(section, key) + return re.sub(r"\$\{([^}.]+)\.([^}]+)\}", + lambda m: self.value(m[1], m[2], seen), value) + + def includes(self, section, source): + included = False + for sign, pattern in re.findall(r"([+-])<([^>]+)>", self.value(section, "build_src_filter")): + # PlatformIO's file glob does not let a root *.cpp select a file + # outside src. Match path components, then handle directory filters. + source_parts, pattern_parts = source.split("/"), pattern.split("/") + file_match = len(source_parts) == len(pattern_parts) and all( + fnmatch.fnmatchcase(part, glob) + for part, glob in zip(source_parts, pattern_parts) + ) + if file_match or source.startswith(pattern.rstrip("/") + "/"): + included = sign == "+" + return included + + +class UpstreamObserverRecipeTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.recipes = Recipes() + + def test_all_22_named_additions_exist_once(self): + self.assertEqual(len(NAMES), 22) + for family, boards in BOARDS.items(): + text = (ROOT / "variants" / family / "platformio.ini").read_text() + for board in boards: + for role in ("repeater", "room_server"): + name = f"{board}_{role}_observer_mqtt" + with self.subTest(env=name): + self.assertEqual(text.count(f"[env:{name}]"), 1) + self.assertTrue(self.recipes.config.has_section(f"env:{name}")) + + def test_observers_keep_inherited_ota_and_complete_tls_hooks(self): + expected_hooks = ( + "generate_webconfig_html.py", "meshcore_image_identity.py", + "portable_esp32_link.py", "esp32_ota_heap_context.py", "merge-bin.py", + "tools/mota/pio_endf.py", "check_esp32_dram.py", "check_firmware_ram.py", + "generate_cert_bundle.py", + ) + for name in NAMES: + section = f"env:{name}" + with self.subTest(env=name): + flags = self.recipes.value(section, "build_flags") + for flag in ("ESP32_PLATFORM", "ENABLE_OTA=1", "OTA_FLASH_STORE=1", + "OTA_FOLDER_SERIAL", "WITH_MQTT_BRIDGE=1", "MQTT_MAX_PACKET_SIZE=1024"): + self.assertIn(flag, flags) + hooks = self.recipes.value(section, "extra_scripts") + for hook in expected_hooks: + self.assertIn(hook, hooks) + self.assertEqual(self.recipes.value(section, "board_ssl_cert_source"), "adafruit-full") + self.assertEqual(self.recipes.value(section, "board_build.embed_files"), + "src/certs/x509_crt_bundle.bin") + self.assertTrue((ROOT / "scripts/generate_cert_bundle.py").is_file()) + + def test_mqtt_sources_dependencies_and_role_entrypoints(self): + for name in NAMES: + section = f"env:{name}" + role = "simple_room_server" if "_room_server_" in name else "simple_repeater" + other = "simple_repeater" if role == "simple_room_server" else "simple_room_server" + with self.subTest(env=name): + for source in ("helpers/bridges/MQTTBridge.cpp", "helpers/MQTTMessageBuilder.cpp", + "helpers/JWTHelper.cpp", f"../examples/{role}/main.cpp", + "helpers/ota/OtaTinf.c"): + self.assertTrue(self.recipes.includes(section, source), source) + self.assertFalse(self.recipes.includes(section, f"../examples/{other}/main.cpp")) + deps = self.recipes.value(section, "lib_deps") + for dependency in ("PsychicMqttClient", "ArduinoJson @ 7.4.3", "NTPClient", + "JChristensen/Timezone", "paulstoffregen/Time@1.6.1"): + self.assertIn(dependency, deps) + if "WITH_SNMP=1" in self.recipes.value(section, "build_flags"): + self.assertTrue(self.recipes.includes(section, "helpers/SNMPAgent.cpp")) + self.assertIn("0neblock/SNMP_Agent", deps) + if "_room_server_" in name: + self.assertIn("ROOM_PASSWORD=", self.recipes.value(section, "build_flags")) + + def test_small_flash_and_non_psram_profiles_keep_upstream_limits(self): + for board in ("Ebyte_EoRa-S3", "LilyGo_T3S3_sx1276"): + for role in ("repeater", "room_server"): + section = f"env:{board}_{role}_observer_mqtt" + self.assertEqual(self.recipes.value(section, "board_build.partitions"), "min_spiffs.csv") + self.assertNotIn("WITH_SNMP", self.recipes.value(section, "build_flags")) + for board in ("Heltec_Wireless_Tracker", "Heltec_Wireless_Paper"): + for role in ("repeater", "room_server"): + section = f"env:{board}_{role}_observer_mqtt" + self.assertIn("MQTT_NEIGHBORS_WITHOUT_PSRAM=1", self.recipes.value(section, "build_flags")) + + def test_rc32_local_startup_platform_and_safety_remain_inherited(self): + for board in ("heltec_rc32", "heltec_rc32_without_display"): + for role in ("repeater", "room_server"): + section = f"env:{board}_{role}_observer_mqtt" + flags = self.recipes.value(section, "build_flags") + self.assertIn("ESP32_POST_BOOT_CPU_FREQ=160", flags) + self.assertIn("RC32_PERIPHERAL_WARMUP_MS=100", flags) + self.assertIn("-UENV_INCLUDE_GPS", flags) + self.assertNotIn("-D ESP32_CPU_FREQ=", flags) + self.assertNotIn("PIN_GPS_EN=45", flags) + self.assertIn("55.03.311", self.recipes.value(section, "platform")) + self.assertIn("3.3.11", self.recipes.value(section, "platform_packages")) + self.assertEqual(self.recipes.value(section, "board_build.partitions"), "default_16MB.csv") + self.assertEqual("HELTEC_RC32_WITH_DISPLAY" in flags, board == "heltec_rc32") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_xiao_qspi_combined.sh b/test/test_xiao_qspi_combined.sh index 98701eeea..ee348e710 100644 --- a/test/test_xiao_qspi_combined.sh +++ b/test/test_xiao_qspi_combined.sh @@ -8,7 +8,7 @@ fail() { echo "test_xiao_qspi_combined: $*" >&2; exit 1; } # Use the resolved PlatformIO recipes, not a board-name allowlist: each # canonical XIAO repeater must retain the internal bootloader scratch bank, -# raw external-QSPI OTA staging, and packet logging in one application. +# raw external-QSPI OTA staging, and packet logging in both sensor profiles. init_project_context BUILD_PROFILE_FOR_TARGET=standard BUILD_PROFILE_EFFECTIVE=standard @@ -33,11 +33,14 @@ for target in \ is_xiao_qspi_canonical_repeater_build "$target" \ || fail "$target lost its combined-image classification" if is_supported_build_env "${target}_lora_ota_no_external_sensors"; then - fail "$target still publishes a redundant lean OTA sibling" + fail "$target invented a separate lean OTA identity instead of pairing its deployed ID" fi if get_reduced_lora_ota_target "$target" >/dev/null; then - fail "$target still falls back to a reduced OTA sibling" + fail "$target still changes its deployed OTA identity for the reduced option" fi + is_nrf52_sensor_ota_pair_target "$target" || fail "$target lacks the required sensor pair policy" + [ "$(get_nrf52_sensor_ota_pair_target "$target")" = "$target" ] \ + || fail "$target pair changed its deployed QSPI identity" pio_env_option_contains "$target" board_build.ldscript \ nrf52840_s140_v7_xiao_bootloader_ota.ld \ || fail "$target lost its 40 KiB internal bootloader scratch reservation" @@ -68,7 +71,8 @@ for target in \ || fail "$target no longer verifies the packaged USB logging control" done -# This consolidation is specific to the external-QSPI XIAO repeater layout. +# The deployed QSPI layout/identity classification stays specific to XIAO +# repeaters, while the new sensor pairing policy also covers other roles. is_xiao_qspi_combined_repeater_target Xiao_nrf52_room_server \ && fail "non-QSPI room server was incorrectly consolidated" is_xiao_qspi_combined_repeater_target RAK_4631_repeater \ diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini index 3853a819c..89b8ba473 100644 --- a/variants/ebyte_eora_s3/platformio.ini +++ b/variants/ebyte_eora_s3/platformio.ini @@ -145,3 +145,80 @@ lib_deps = extends = Ebyte_EoRa-S3 build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} +<../examples/kiss_modem/> + +[env:Ebyte_EoRa-S3_repeater_observer_mqtt] +extends = Ebyte_EoRa-S3 +board_build.partitions = min_spiffs.csv +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + +[env:Ebyte_EoRa-S3_room_server_observer_mqtt] +extends = Ebyte_EoRa-S3 +board_build.partitions = min_spiffs.csv +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"EoRa-S3 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 diff --git a/variants/heltec_e213/platformio.ini b/variants/heltec_e213/platformio.ini index a0b286200..90c8b32c1 100644 --- a/variants/heltec_e213/platformio.ini +++ b/variants/heltec_e213/platformio.ini @@ -172,3 +172,88 @@ lib_deps = extends = Heltec_E213_base build_src_filter = ${Heltec_E213_base.build_src_filter} +<../examples/kiss_modem/> + +[env:Heltec_E213_repeater_observer_mqtt] +extends = Heltec_E213_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_E213_base.build_flags} + -D DISPLAY_CLASS=E213Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_E213_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_E213_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:Heltec_E213_room_server_observer_mqtt] +extends = Heltec_E213_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_E213_base.build_flags} + -D DISPLAY_CLASS=E213Display + -D ADVERT_NAME='"Heltec E213 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_E213_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_E213_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/heltec_e290/platformio.ini b/variants/heltec_e290/platformio.ini index c806e0c5e..edfc6c00f 100644 --- a/variants/heltec_e290/platformio.ini +++ b/variants/heltec_e290/platformio.ini @@ -173,3 +173,88 @@ lib_deps = extends = Heltec_E290_base build_src_filter = ${Heltec_E290_base.build_src_filter} +<../examples/kiss_modem/> + +[env:Heltec_E290_repeater_observer_mqtt] +extends = Heltec_E290_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_E290_base.build_flags} + -D DISPLAY_CLASS=E290Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_E290_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_E290_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:Heltec_E290_room_server_observer_mqtt] +extends = Heltec_E290_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_E290_base.build_flags} + -D DISPLAY_CLASS=E290Display + -D ADVERT_NAME='"Heltec E290 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_E290_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_E290_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 6001638a0..9ac08f570 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -370,3 +370,161 @@ lib_deps = extends = Heltec_RC32 build_src_filter = ${Heltec_RC32.build_src_filter} +<../examples/kiss_modem/> + +[env:heltec_rc32_repeater_observer_mqtt] +extends = Heltec_RC32_with_display +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:heltec_rc32_room_server_observer_mqtt] +extends = Heltec_RC32_with_display +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:heltec_rc32_without_display_repeater_observer_mqtt] +extends = Heltec_RC32 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:heltec_rc32_without_display_room_server_observer_mqtt] +extends = Heltec_RC32 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 976344872..c2798e888 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -196,3 +196,92 @@ lib_deps = extends = Heltec_tracker_base build_src_filter = ${Heltec_tracker_base.build_src_filter} +<../examples/kiss_modem/> + +[env:Heltec_Wireless_Tracker_repeater_observer_mqtt] +extends = Heltec_tracker_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_tracker_base.build_flags} + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 +; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h). + -D MQTT_NEIGHBORS_WITHOUT_PSRAM=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=240 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_tracker_base.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:Heltec_Wireless_Tracker_room_server_observer_mqtt] +extends = Heltec_tracker_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_tracker_base.build_flags} + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D ADVERT_NAME='"Heltec Tracker Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 +; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h). + -D MQTT_NEIGHBORS_WITHOUT_PSRAM=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=240 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_tracker_base.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 9e9c5ec16..32c74eb37 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -174,3 +174,92 @@ lib_deps = extends = Heltec_Wireless_Paper_base build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} +<../examples/kiss_modem/> + +[env:Heltec_Wireless_Paper_repeater_observer_mqtt] +extends = Heltec_Wireless_Paper_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_Wireless_Paper_base.build_flags} + -D DISPLAY_CLASS=E213Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 +; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h). + -D MQTT_NEIGHBORS_WITHOUT_PSRAM=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_Wireless_Paper_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:Heltec_Wireless_Paper_room_server_observer_mqtt] +extends = Heltec_Wireless_Paper_base +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Heltec_Wireless_Paper_base.build_flags} + -D DISPLAY_CLASS=E213Display + -D ADVERT_NAME='"Heltec WP Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 +; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h). + -D MQTT_NEIGHBORS_WITHOUT_PSRAM=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D MQTT_WIFI_TX_POWER=WIFI_POWER_11dBm + -D WITH_SNMP=1 +build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_Wireless_Paper_base.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index 0a9978729..7c6ae139c 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -177,3 +177,80 @@ lib_deps = extends = LilyGo_T3S3_sx1276 build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} +<../examples/kiss_modem/> + +[env:LilyGo_T3S3_sx1276_repeater_observer_mqtt] +extends = LilyGo_T3S3_sx1276 +board_build.partitions = min_spiffs.csv +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + +[env:LilyGo_T3S3_sx1276_room_server_observer_mqtt] +extends = LilyGo_T3S3_sx1276 +board_build.partitions = min_spiffs.csv +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3S3-1276 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 diff --git a/variants/lilygo_teth_elite/platformio.ini b/variants/lilygo_teth_elite/platformio.ini index 9c001af1a..fc132ac52 100644 --- a/variants/lilygo_teth_elite/platformio.ini +++ b/variants/lilygo_teth_elite/platformio.ini @@ -103,3 +103,80 @@ lib_deps = extends = LilyGo_TETH_Elite_sx1262 build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} +<../examples/kiss_modem/> + +[env:LilyGo_TETH_Elite_sx1262_repeater_observer_mqtt] +extends = LilyGo_TETH_Elite_sx1262 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:LilyGo_TETH_Elite_sx1262_room_server_observer_mqtt] +extends = LilyGo_TETH_Elite_sx1262 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index c99d9daca..beea12c5b 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -223,3 +223,86 @@ lib_deps = extends = meshnology_w12 build_src_filter = ${meshnology_w12.build_src_filter} +<../examples/kiss_modem/> + +[env:meshnology_w12_repeater_observer_mqtt] +extends = meshnology_w12 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${meshnology_w12.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${meshnology_w12.build_src_filter} + + + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:meshnology_w12_room_server_observer_mqtt] +extends = meshnology_w12 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${meshnology_w12.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Meshnology W12 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${meshnology_w12.build_src_filter} + + + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index 84021fac2..a08fc4e4f 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -71,10 +71,6 @@ public: #ifdef PIN_3V3_ACC_EN digitalWrite(PIN_3V3_ACC_EN, LOW); #endif - #ifdef SENSOR_EN - digitalWrite(SENSOR_EN, LOW); - #endif - // set led on and wait for button release before poweroff #ifdef LED_PIN digitalWrite(LED_PIN, HIGH); diff --git a/variants/t1000-e/t1000e_sensors.cpp b/variants/t1000-e/t1000e_sensors.cpp index 5dda444d5..cd43512ac 100644 --- a/variants/t1000-e/t1000e_sensors.cpp +++ b/variants/t1000-e/t1000e_sensors.cpp @@ -81,7 +81,6 @@ float t1000e_get_temperature(void) { unsigned int ntc_v, vcc_v; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); @@ -89,7 +88,6 @@ float t1000e_get_temperature(void) { vcc_v = (rail_v > NTC_REF_VCC) ? NTC_REF_VCC : rail_v; ntc_v = (1000.0 * AREF_VOLTAGE * analogRead(TEMP_SENSOR)) / 4096; digitalWrite(PIN_3V3_EN, LOW); - digitalWrite(SENSOR_EN, LOW); return get_heater_temperature(vcc_v, ntc_v); } @@ -99,13 +97,11 @@ uint32_t t1000e_get_light(void) { unsigned int lux_v = 0; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); lux_v = 1000 * analogRead(LUX_SENSOR) * AREF_VOLTAGE / 4096; lux = get_light_lv(lux_v); - digitalWrite(SENSOR_EN, LOW); digitalWrite(PIN_3V3_EN, LOW); return lux; diff --git a/variants/t1000-e/variant.cpp b/variants/t1000-e/variant.cpp index fc30a6197..6530d171c 100644 --- a/variants/t1000-e/variant.cpp +++ b/variants/t1000-e/variant.cpp @@ -14,7 +14,7 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 1, // P0.01 2, // P0.02, AIN0 BATTERY_PIN 3, // P0.03 - 4, // P0.04, SENSOR_EN + 4, // P0.04 5, // P0.05, EXT_PWR_DETEC 6, // P0.06, PIN_BUTTON1 7, // P0.07, LORA_BUSY @@ -77,7 +77,6 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); pinMode(PIN_3V3_ACC_EN, OUTPUT); pinMode(BUZZER_EN, OUTPUT); - pinMode(SENSOR_EN, OUTPUT); pinMode(GPS_EN, OUTPUT); pinMode(GPS_RESET, OUTPUT); pinMode(GPS_VRTC_EN, OUTPUT); @@ -88,7 +87,6 @@ void initVariant() digitalWrite(PIN_3V3_EN, LOW); digitalWrite(PIN_3V3_ACC_EN, LOW); digitalWrite(BUZZER_EN, LOW); - digitalWrite(SENSOR_EN, LOW); digitalWrite(GPS_EN, HIGH); digitalWrite(GPS_RESET, LOW); digitalWrite(GPS_VRTC_EN, HIGH); diff --git a/variants/t1000-e/variant.h b/variants/t1000-e/variant.h index b95e1e4d8..bd5090733 100644 --- a/variants/t1000-e/variant.h +++ b/variants/t1000-e/variant.h @@ -120,7 +120,6 @@ //////////////////////////////////////////////////////////////////////////////// // Temp+Lux Sensor -#define SENSOR_EN (4) // P0.4 #define TEMP_SENSOR (31) // P0.31/AIN7 #define LUX_SENSOR (29) // P0.29/AIN5 diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index f5a12935a..232bc1693 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -161,3 +161,80 @@ lib_deps = extends = Xiao_S3 build_src_filter = ${Xiao_S3.build_src_filter} +<../examples/kiss_modem/> + +[env:Xiao_S3_repeater_observer_mqtt] +extends = Xiao_S3 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent + +[env:Xiao_S3_room_server_observer_mqtt] +extends = Xiao_S3 +extra_scripts = + ${esp32_base.extra_scripts} + pre:scripts/generate_cert_bundle.py +board_ssl_cert_source = adafruit-full +board_build.embed_files = src/certs/x509_crt_bundle.bin +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 + -D WITH_SNMP=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + 0neblock/SNMP_Agent