diff --git a/build.sh b/build.sh index 745c2e33..b6d60ddb 100755 --- a/build.sh +++ b/build.sh @@ -270,8 +270,9 @@ for section, options in data: # Keep each ordinary repeater environment feature-rich (including external # sensors), and expose a separately named no-external-sensors OTA build for - # every ESP32/nRF52 repeater role. These two platforms have a complete apply - # path; RP2040 and STM32 do not yet have the required bootloader/apply path. + # ESP32/nRF52 repeaters that need a lean staging profile. These two platforms + # have a complete apply path; RP2040 and STM32 do not yet have the required + # bootloader/apply path. local env_name ota_env full_env usb_env ble_env local -a base_envs=("${SUPPORTED_PIO_ENVS[@]}") for env_name in "${base_envs[@]}"; do @@ -286,6 +287,12 @@ for section, options in data: *_repeater|*_repeater_|*_repeatr|*_repeatr_) ;; *) continue ;; esac + # SolarXiao 30S/33S repeaters already use matched external QSPI staging, + # so their normal full-sensor image is install-capable. A second lean + # no-external-sensors target provides no additional OTA capability. + case "${env_name,,}" in + solarxiao_30s_repeater|solarxiao_33s_repeater) continue ;; + esac ota_env="${env_name%_}_lora_ota_no_external_sensors" if [ -n "${PIO_ENV_PLATFORM_BY_NAME[$ota_env]+x}" ]; then continue diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index ec723a2a..cbe9b4e6 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -40,10 +40,9 @@ }); const OTA_LABELS = Object.freeze({ - none: "No explicit OTA profile", - "ota-enabled": "OTA-enabled profile", - "lora-receiver": "LoRa OTA receiver", - "lora-source": "LoRa OTA source (Full Companion)", + none: "No LoRa OTA", + "lora-receiver": "LoRa OTA enabled / receiver", + "lora-source": "LoRa OTA source only (Full Companion)", }); const MODE_LABELS = Object.freeze({ @@ -84,11 +83,8 @@ // Generated Full Companion artifact name. It uses the standard Heltec V4 // board definition and auto-detects the V4.2 and V4.3 FEM hardware. "heltec_v4_2_v4_3": "heltec_v4", - // These target prefixes select a radio profile on otherwise identical - // hardware. Keep that distinction in the firmware variant facet instead. - "heltec_v4_3": "heltec_v4", - "heltec_v4_3_expansionkit_tft": "heltec_v4_expansionkit_tft", - "heltec_v4_3_tft": "heltec_v4_tft", + // These Station target prefixes select a firmware/radio profile on the + // same physical hardware. "Station_G2_logging": "Station_G2", "Station_G3_ESP32_logging": "Station_G3_ESP32", }); @@ -100,6 +96,14 @@ "Station_G3_ESP32_logging", ]); + const HIDDEN_PROFILE_TARGETS = Object.freeze([ + // SolarXiao repeaters already stage full-sensor LoRa OTA images in their + // matched external QSPI flash. Older releases contain redundant lean + // siblings; keep those files searchable without recommending them. + "solarxiao_30S_repeater_lora_ota_no_external_sensors", + "solarxiao_33S_repeater_lora_ota_no_external_sensors", + ]); + let pickerInstanceCount = 0; function isSafeGithubUrl(value) { @@ -368,6 +372,9 @@ } const knownLabels = { + "heltec_v4_3": "V4.3", + "heltec_v4_3_expansionkit_tft": "V4.3 + expansion kit + TFT", + "heltec_v4_3_tft": "V4.3 + TFT", "heltec_v4_expansionkit": "Expansion kit", "heltec_v4_expansionkit_tft": "Expansion kit + TFT", "heltec_v4_r8": "R8", @@ -427,7 +434,7 @@ const profiles = Array.from(grouped.values()).map(function (profile) { profile.ota = profile.explicitOta || - (profile.otaPackaged ? "ota-enabled" : "none"); + (profile.otaPackaged ? "lora-receiver" : "none"); profile.installKinds = INSTALL_ORDER.filter(function (kind) { return Boolean(canonicalAsset(profile.files, kind)); }); @@ -436,7 +443,8 @@ }); return profile; }).filter(function (profile) { - return !HIDDEN_PROFILE_HARDWARE.includes(profile.sourceHardware); + return !HIDDEN_PROFILE_HARDWARE.includes(profile.sourceHardware) && + !HIDDEN_PROFILE_TARGETS.includes(profile.target); }).sort(function (a, b) { return a.target.localeCompare(b.target, undefined, { numeric: true, @@ -559,7 +567,7 @@ function optionSort(field, a, b) { const roleOrder = ["companion", "repeater", "room", "sensor", "terminal", "kiss", "other"]; const loggingOrder = ["none", "usb", "wifi"]; - const otaOrder = ["none", "ota-enabled", "lora-receiver", "lora-source"]; + const otaOrder = ["none", "lora-receiver", "lora-source"]; const featureOrder = ["standard", "full"]; const modeOrder = ["standard", "full", "ble", "usb", "wifi", "serial", "ethernet", "mqtt", "espnow", "rs232"]; const orders = { diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 18e08b72..3001ead5 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -135,9 +135,8 @@ from the published firmware assets. | USB logging / USB-connected MQTT | Node remains attached to a computer over a data-capable USB cable | | Wi-Fi MQTT observer | Firmware connects directly to MQTT over Wi-Fi; this is not USB logging | | No logging | Normal standalone operation without the dedicated logging/MQTT profile | -| LoRa OTA receiver | Repeater profile that can stage an exact matching update received over LoRa | -| LoRa OTA source | Full Companion serving a host-supplied update to another node | -| OTA-enabled profile | Build uses an OTA-capable application/partition profile, but is not necessarily an explicit LoRa receiver | +| LoRa OTA enabled / receiver | Install-capable profile that can stage an exact matching update received over LoRa | +| LoRa OTA source only | Full Companion serving a host-supplied update to another node without self-installing it | Connection and bridge choices depend on the selected role. Companion firmware may offer Full, Bluetooth, USB, Wi-Fi, serial, or Ethernet transports. diff --git a/docs/ota_easy.md b/docs/ota_easy.md index 6102bd1d..4a6e9e2e 100644 --- a/docs/ota_easy.md +++ b/docs/ota_easy.md @@ -74,8 +74,10 @@ RAK13302 radio's BUSY/DIO1 lines. The RAK4631 OTA repeater retains GPS except for its Serial1 RS232 bridge, which uses the same UART. Selected nRF52 boards with matched external QSPI application and bootloader support can instead make the normal full-sensor repeater install-capable; those targets do not need to reserve internal flash -for the downloaded container. Other normal repeaters can still serve as -intermediate relays but cannot necessarily install an update themselves. ESP32 +for the downloaded container. SolarXiao 30S and 33S use this matched external-QSPI +path and therefore do not emit redundant no-external-sensors siblings. Other +normal repeaters can still serve as intermediate relays but cannot necessarily +install an update themselves. ESP32 `-ota-` siblings also retain the lightweight browser WiFi uploader (`start ota`), the complete CLI, and a 254-entry neighbor table. RP2040 and STM32 repeaters do not currently have a safe self-apply path, but current repeater diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index 8973cece..26aebcca 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -125,9 +125,12 @@ artifacts, including room, sensor, and repeater roles, must fit the legacy slot limit and the target's actual app partition. The ESP32-C6 `no_external_sensors` OTA siblings are the narrow exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app -partition. For every standalone ESP32 and nRF52 repeater, `build.sh` also exposes an explicit -`*_lora_ota_no_external_sensors` artifact: the ordinary repeater remains sensor-enabled, while that sibling -disables optional external environmental-sensor drivers for LoRa distribution. Integrated GPS and other +partition. For standalone ESP32 and nRF52 repeaters that need a lean staging +profile, `build.sh` also exposes an explicit `*_lora_ota_no_external_sensors` +artifact: the ordinary repeater remains sensor-enabled, while that sibling +disables optional external environmental-sensor drivers for LoRa distribution. SolarXiao 30S and 33S use +matched external QSPI staging, so their ordinary full-sensor repeater is already install-capable and no +redundant lean sibling is generated. Integrated GPS and other board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The RAK3401 OTA repeater also retains RAK12501 GPS support; install that module in sensor slot A because slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines. RAK4631 OTA repeaters retain GPS except for the Serial1 RS232 diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index 2ea5de7a..7bd04609 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -44,6 +44,8 @@ const releases = [ asset("Station_G2_repeater-" + family + "-deadbee.bin"), asset("Station_G2_repeater-" + family + "-merged.bin"), asset("Station_G2_repeater-" + family + ".bin"), + asset("solarxiao_33S_repeater-ota-" + family + ".uf2"), + asset("solarxiao_33S_repeater-ota-" + family + ".zip"), asset("wio-e5-repeater_bridge_rs232-" + family + ".hex"), ]), release("utility-" + family, "2026-08-23T12:00:04Z", [ @@ -63,6 +65,14 @@ const releases = [ "Station_G2_repeater_lora_ota_no_external_sensors-ota-" + family + ".bin" ), + asset( + "solarxiao_33S_repeater_lora_ota_no_external_sensors-ota-" + + family + ".uf2" + ), + asset( + "solarxiao_33S_repeater_lora_ota_no_external_sensors-ota-" + + family + ".zip" + ), ]), release("full-profiles-" + family, "2026-08-23T12:00:01Z", [ asset( @@ -96,8 +106,8 @@ assert(!releaseSet.releases.some(function (item) { const catalog = picker.buildCatalog(releases); assert.strictEqual(catalog.releaseSet.familyTag, family); -assert.strictEqual(catalog.profiles.length, 11); -assert.strictEqual(catalog.rows.length, 25); +assert.strictEqual(catalog.profiles.length, 12); +assert.strictEqual(catalog.rows.length, 29); function profile(target) { const found = catalog.profiles.find(function (item) { @@ -153,23 +163,23 @@ assert.strictEqual( ); assert.strictEqual( picker.canonicalHardware("heltec_v4_3_tft"), - "heltec_v4_tft" + "heltec_v4_3_tft" ); assert.strictEqual( picker.canonicalHardware("heltec_v4_3_expansionkit_tft"), - "heltec_v4_expansionkit_tft" + "heltec_v4_3_expansionkit_tft" ); const heltecV4FemOff = picker.parseTargetProfile( "heltec_v4_3_companion_radio_ble_femoff" ); -assert.strictEqual(heltecV4FemOff.hardware, "heltec_v4"); +assert.strictEqual(heltecV4FemOff.hardware, "heltec_v4_3"); assert.strictEqual(heltecV4FemOff.variant, "femoff"); const heltecV4TftFemOff = picker.parseTargetProfile( "heltec_v4_3_tft_companion_radio_wifi_femoff" ); -assert.strictEqual(heltecV4TftFemOff.hardware, "heltec_v4_tft"); +assert.strictEqual(heltecV4TftFemOff.hardware, "heltec_v4_3_tft"); assert.strictEqual(heltecV4TftFemOff.variant, "femoff"); const g2RxBoosted = profile("Station_G2_logging_repeater"); @@ -204,20 +214,53 @@ assert.strictEqual( const fullLogging = profile("Station_G2_repeater-full-logging"); assert.strictEqual(fullLogging.logging, "usb"); -assert.strictEqual(fullLogging.ota, "ota-enabled"); +assert.strictEqual(fullLogging.ota, "lora-receiver"); assert.strictEqual(fullLogging.feature, "full"); assert.strictEqual(fullLogging.variant, "default"); const mqtt = profile("Station_G2_repeater_observer_mqtt-full"); assert.strictEqual(mqtt.logging, "wifi"); assert.strictEqual(mqtt.mode, "mqtt"); -assert.strictEqual(mqtt.ota, "ota-enabled"); +assert.strictEqual(mqtt.ota, "lora-receiver"); const lora = profile( "Station_G2_repeater_lora_ota_no_external_sensors" ); assert.strictEqual(lora.ota, "lora-receiver"); assert.strictEqual(lora.variant, "no-external-sensors"); +assert.strictEqual( + picker.OTA_LABELS["lora-receiver"], + "LoRa OTA enabled / receiver" +); +assert(!Object.prototype.hasOwnProperty.call( + picker.OTA_LABELS, + "ota-enabled" +)); +assert(!picker.uniqueValues(catalog.profiles, "ota").includes("ota-enabled")); + +const solarXiao = profile("solarxiao_33S_repeater"); +assert.strictEqual(solarXiao.ota, "lora-receiver"); +assert.strictEqual(solarXiao.variant, "default"); +assert(!catalog.profiles.some(function (item) { + return item.target === + "solarxiao_33S_repeater_lora_ota_no_external_sensors"; +})); +assert(catalog.rows.some(function (item) { + return item.target === + "solarxiao_33S_repeater_lora_ota_no_external_sensors"; +})); +assert.deepStrictEqual( + picker.facetValues( + catalog.profiles, + { + hardware: "solarxiao_33S", + role: "repeater", + logging: "none", + }, + "ota" + ), + ["lora-receiver"] +); const wio = profile("wio-e5-repeater_bridge_rs232"); assert.strictEqual(wio.hardware, "wio-e5"); @@ -269,11 +312,8 @@ assert.strictEqual( "Base / standard (V4.2 / V4.3 auto-detect)" ); assert.strictEqual( - picker.humanizeHardwareVariant( - picker.canonicalHardware("heltec_v4_3_tft"), - "heltec_v4" - ), - "TFT" + picker.humanizeHardwareVariant("heltec_v4_3_tft", "heltec_v4"), + "V4.3 + TFT" ); const groupedHardwareProfiles = [ { @@ -353,6 +393,7 @@ assert.deepStrictEqual( "RAK_4631", "Station_G2", "Station_G3_ESP32", + "solarxiao_33S", "wio-e5", ].sort() );