Add SenseCAP Indicator LoRa repeater profiles
This commit is contained in:
1 parent
624efe152d
commit
3236164d21
5 files changed
+218
-3
No files matched your search
@@ -416,6 +416,7 @@ jobs:
|
||||
python3 -B test/test_udp_packet_origin.py -v
|
||||
python3 -B test/test_udp_bridge.py -v
|
||||
python3 -B test/test_udp_bridge_integration.py -v
|
||||
python3 -B test/test_indicator_repeater_profile.py -v
|
||||
|
||||
- name: Verify saved repeat-off trace exception and relay guards
|
||||
run: |
|
||||
|
||||
+26
-3
@@ -2564,8 +2564,11 @@ supports_esp32_full_shared_espnow() {
|
||||
&& { is_mqtt_bridge_target "$env_name" || is_repeater_role_target "$env_name"; } || return 1
|
||||
# Primary ESP-NOW radios already own the SDK callbacks and packet transport.
|
||||
# A second bridge instance would conflict with that radio, not add a mode.
|
||||
! pio_env_option_contains "$env_name" build_flags "MESH_PRIMARY_ESPNOW" \
|
||||
&& ! pio_env_option_contains "$env_name" build_flags "MESH_ESPNOW_RADIO"
|
||||
# The Indicator LoRa board inherits the ESP-NOW board's original declaration
|
||||
# followed by -UMESH_PRIMARY_ESPNOW. Inspect effective definitions rather
|
||||
# than mistaking that overridden declaration for a live radio owner.
|
||||
! pio_env_has_effective_define "$env_name" MESH_PRIMARY_ESPNOW \
|
||||
&& ! pio_env_has_effective_define "$env_name" MESH_ESPNOW_RADIO
|
||||
}
|
||||
|
||||
apply_esp32_full_shared_bridge_profile() {
|
||||
@@ -3402,7 +3405,7 @@ is_esp32_full_only_bulk_target() {
|
||||
station_g3_esp32_repeater|station_g3_esp32_logging_repeater|station_g3_esp32_room_server|station_g3_esp32_sensor|station_g3_esp32_r2_sensor|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|\
|
||||
lilygo_tdeck_repeater|\
|
||||
lilygo_tdeck_repeater|sensecapindicator-lora_repeater|sensecapindicator-lora-n16r2_repeater|\
|
||||
thinknode_m7_repeater|thinknode_m7_room_server|thinknode_m7_repeater_observer_mqtt|thinknode_m7_room_server_observer_mqtt|\
|
||||
heltec_v4_r8_repeater|heltec_v4_r8_repeater_observer_mqtt|heltec_v4_r8_room_server|heltec_v4_r8_room_server_observer_mqtt|heltec_v4_r8_sensor|\
|
||||
heltec_v4_r8_tft_repeater|heltec_v4_r8_tft_repeater_observer_mqtt|heltec_v4_r8_tft_portrait_repeater_observer_mqtt|heltec_v4_r8_tft_room_server|heltec_v4_r8_tft_room_server_observer_mqtt|heltec_v4_r8_tft_portrait_room_server_observer_mqtt|heltec_v4_r8_tft_sensor)
|
||||
@@ -4143,6 +4146,26 @@ sys.exit(1)
|
||||
' "$env_name" "$option_name" "$needle" <<<"$PIO_CONFIG_JSON"
|
||||
}
|
||||
|
||||
pio_env_has_effective_define() {
|
||||
local env_name=$1
|
||||
local define_name=$2
|
||||
python3 -c '
|
||||
import json, re, sys
|
||||
rows = {name: dict(options) for name, options in json.load(sys.stdin)}
|
||||
options = rows.get("env:" + sys.argv[1], {})
|
||||
flags = options.get("build_flags", [])
|
||||
if not isinstance(flags, list): flags = str(flags).splitlines()
|
||||
definitions = {}
|
||||
for flag in flags:
|
||||
match = re.fullmatch(r"-([DU])\s*(\w+)(?:=(.*))?", str(flag).strip())
|
||||
if not match: continue
|
||||
kind, name, value = match.groups()
|
||||
if kind == "U": definitions.pop(name, None)
|
||||
else: definitions[name] = "1" if value is None else value
|
||||
sys.exit(0 if sys.argv[2] in definitions else 1)
|
||||
' "$env_name" "$define_name" <<<"$PIO_CONFIG_JSON"
|
||||
}
|
||||
|
||||
append_platformio_build_src_filter() {
|
||||
local rule=$1
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ WiFi network using the normal [WiFi settings](WiFi.md). Read `get wifi.status`
|
||||
on each board to find its local address once connected. The commands below use
|
||||
example addresses; substitute the actual reserved addresses.
|
||||
|
||||
For a LoRa-capable SenseCAP Indicator, use the Full
|
||||
`SenseCapIndicator-LoRa_repeater` target on the original 8 MiB/OPI-PSRAM board.
|
||||
The distinct `SenseCapIndicator-LoRa-N16R2_repeater` target is for 16 MiB/QSPI
|
||||
PSRAM hardware. Both retain the LoRa expander and display; their PSRAM modes
|
||||
are selected before app startup and the images are not interchangeable.
|
||||
The Indicator Companion and primary-ESP-NOW images do not provide this bridge.
|
||||
|
||||
Both boards use the existing `bridge.secret` as their shared authentication
|
||||
secret. Set the same strong secret on both before enabling the transport. Do
|
||||
not paste it into shared diagnostic captures. The transport authenticates
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Resolve the real Indicator repeater recipes without PlatformIO or hardware."""
|
||||
import configparser
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
N8 = "SenseCapIndicator-LoRa_repeater"
|
||||
N16 = "SenseCapIndicator-LoRa-N16R2_repeater"
|
||||
|
||||
|
||||
class IndicatorRepeaterProfileTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.config = configparser.ConfigParser(interpolation=None, strict=False)
|
||||
cls.config.read([ROOT / "platformio.ini",
|
||||
ROOT / "variants/sensecap_indicator-espnow/platformio.ini"], encoding="ascii")
|
||||
|
||||
def option(self, section, option, depth=0):
|
||||
self.assertLess(depth, 30)
|
||||
if self.config.has_option(section, option):
|
||||
value = self.config.get(section, option)
|
||||
return re.sub(r"\$\{([^}]+)\}", lambda match:
|
||||
self.option(*match[1].rsplit(".", 1), depth=depth + 1), value)
|
||||
for parent in self.config.get(section, "extends", fallback="").split(","):
|
||||
if parent.strip():
|
||||
try:
|
||||
return self.option(parent.strip(), option, depth + 1)
|
||||
except KeyError:
|
||||
pass
|
||||
raise KeyError((section, option))
|
||||
|
||||
def flags(self, target):
|
||||
return [line.strip() for line in self.option("env:" + target, "build_flags").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith((";", "#"))]
|
||||
|
||||
def definitions(self, target):
|
||||
definitions = {}
|
||||
for flag in self.flags(target):
|
||||
match = re.fullmatch(r"-([DU])\s*(\w+)(?:=(.*))?", flag)
|
||||
if not match:
|
||||
continue
|
||||
kind, name, value = match.groups()
|
||||
if kind == "U":
|
||||
definitions.pop(name, None)
|
||||
else:
|
||||
definitions[name] = (value or "1").strip("'")
|
||||
return definitions
|
||||
|
||||
def selected(self, target, source):
|
||||
result = False
|
||||
rules = self.option("env:" + target, "build_src_filter")
|
||||
for action, pattern in re.findall(r"([+-])<([^>]+)>", rules):
|
||||
if "/" not in pattern and "/" in source:
|
||||
continue
|
||||
match = fnmatch.fnmatchcase(source, pattern)
|
||||
if not any(character in pattern for character in "*?["):
|
||||
match = match or source.startswith(pattern.rstrip("/") + "/")
|
||||
if match:
|
||||
result = action == "+"
|
||||
return result
|
||||
|
||||
def test_real_radio_and_display_are_retained_in_both_memory_variants(self):
|
||||
for target in (N8, N16):
|
||||
with self.subTest(target=target):
|
||||
definitions = self.definitions(target)
|
||||
self.assertIn("SENSECAP_INDICATOR_LORA", definitions)
|
||||
self.assertNotIn("MESH_PRIMARY_ESPNOW", definitions)
|
||||
self.assertNotIn("DISABLE_WIFI_OTA", definitions)
|
||||
self.assertEqual(definitions["WRAPPER_CLASS"], "IndicatorSX1262Wrapper")
|
||||
self.assertEqual(definitions["DISPLAY_CLASS"], "SCIndicatorDisplay")
|
||||
self.assertEqual(definitions["INDICATOR_WIFI_FONT_RECOVERY"], "1")
|
||||
self.assertEqual(definitions["MOTA_HW_ID"], '"' + target[:-9] + '"')
|
||||
self.assertEqual([definitions[key] for key in ("P_LORA_NSS", "P_LORA_RESET",
|
||||
"P_LORA_BUSY", "P_LORA_DIO_1")], ["64", "65", "66", "67"])
|
||||
for source in ("../examples/simple_repeater/main.cpp", "../examples/simple_repeater/MyMesh.cpp",
|
||||
"../variants/sensecap_indicator-espnow/target.cpp",
|
||||
"../variants/sensecap_indicator-espnow/IndicatorRadioHal.cpp",
|
||||
"../variants/sensecap_indicator-espnow/IndicatorFontClient.cpp",
|
||||
"helpers/ui/LGFXDisplay.cpp", "helpers/ota/OtaContext.cpp",
|
||||
"helpers/esp32/WebConfigServer.cpp"):
|
||||
self.assertTrue(self.selected(target, source), source)
|
||||
for source in ("helpers/esp32/ESPNOWRadio.cpp", "../examples/companion_radio/main.cpp"):
|
||||
self.assertFalse(self.selected(target, source), source)
|
||||
self.assertIn("AsyncElegantOTA", self.option("env:" + target, "lib_deps"))
|
||||
|
||||
def test_flash_partition_and_psram_modes_are_explicit(self):
|
||||
for target, flash, mode, psram, table, size in (
|
||||
(N8, "8MB", "qio_opi", "opi", "dual_ota_2560k_preserve_spiffs.csv", "2621440"),
|
||||
(N16, "16MB", "qio_qspi", "qspi", "dual_ota_6400k_preserve_spiffs.csv", "6553600")):
|
||||
with self.subTest(target=target):
|
||||
for key, expected in (("board_upload.flash_size", flash),
|
||||
("board_build.arduino.memory_type", mode), ("board_build.psram_type", psram),
|
||||
("board_upload.maximum_size", size)):
|
||||
self.assertEqual(self.option("env:" + target, key), expected)
|
||||
self.assertEqual(self.option("env:" + target, "board_build.partitions"),
|
||||
"variants/sensecap_indicator-espnow/" + table)
|
||||
|
||||
def test_actual_full_overlay_uses_lora_not_overridden_primary_espnow_flag(self):
|
||||
rows = [["env:" + target, [["build_flags", self.flags(target)]]] for target in (N8, N16)]
|
||||
environment = dict(os.environ, INDICATOR_TEST_CONFIG=json.dumps(rows))
|
||||
script = r'''
|
||||
set -euo pipefail
|
||||
source build.sh
|
||||
PIO_CONFIG_JSON=$INDICATOR_TEST_CONFIG
|
||||
for target in SenseCapIndicator-LoRa_repeater SenseCapIndicator-LoRa-N16R2_repeater; do
|
||||
PIO_ENV_PLATFORM_BY_NAME[$target]=ESP32_PLATFORM
|
||||
PIO_ENV_BOARD_BY_NAME[$target]=esp32-s3-devkitc-1
|
||||
PIO_ENV_FULL_BUILD_BY_NAME[$target]=1
|
||||
PIO_ENV_MQTT_BY_NAME[$target]=0
|
||||
supports_esp32_full_shared_espnow "$target"
|
||||
is_esp32_full_only_bulk_target "$target"
|
||||
[[ $(get_exact_identity_full_pio_env "$target") == "$target" ]]
|
||||
ESP32_FULL_BUILD=1
|
||||
PLATFORMIO_BUILD_FLAGS=''
|
||||
PLATFORMIO_BUILD_SRC_FILTER=''
|
||||
apply_esp32_full_shared_bridge_profile "$target"
|
||||
[[ $PLATFORMIO_BUILD_FLAGS == *-DWITH_UDP_BRIDGE=1* ]]
|
||||
[[ $PLATFORMIO_BUILD_FLAGS == *-DWITH_ESPNOW_BRIDGE=1* ]]
|
||||
[[ $PLATFORMIO_BUILD_SRC_FILTER == *UDPBridgeSettings.cpp* ]]
|
||||
done
|
||||
PIO_CONFIG_JSON='[["env:bad", [["build_flags", ["-DMESH_PRIMARY_ESPNOW=1", "-UMESH_PRIMARY_ESPNOW", "-DMESH_PRIMARY_ESPNOW=1"]]]]]'
|
||||
pio_env_has_effective_define bad MESH_PRIMARY_ESPNOW
|
||||
PIO_CONFIG_JSON='[["env:bad", [["build_flags", ["-DMESH_ESPNOW_RADIO=1"]]]]]'
|
||||
pio_env_has_effective_define bad MESH_ESPNOW_RADIO
|
||||
'''
|
||||
result = subprocess.run(["bash", "-c", script], cwd=ROOT, env=environment,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -201,3 +201,49 @@ board_build.psram_type = qspi
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.partitions = variants/sensecap_indicator-espnow/dual_ota_6400k_preserve_spiffs.csv
|
||||
|
||||
; Production LoRa repeater on the original N8R8 Indicator. Retain the actual
|
||||
; SX1262 expander, RGB display/touch and RP2040 font link from the LoRa board.
|
||||
; Full builds add runtime-selectable ESP-NOW and eight-peer WiFi UDP through
|
||||
; the common repeater overlay. These are not primary-ESP-NOW radio images.
|
||||
[env:SenseCapIndicator-LoRa_repeater]
|
||||
extends = SenseCapIndicator-LoRa
|
||||
board_build.partitions = variants/sensecap_indicator-espnow/dual_ota_2560k_preserve_spiffs.csv
|
||||
board_upload.maximum_size = 2621440
|
||||
build_unflags =
|
||||
-D MESH_PRIMARY_ESPNOW=1
|
||||
-D DISABLE_WIFI_OTA=1
|
||||
build_flags =
|
||||
${SenseCapIndicator-LoRa.build_flags}
|
||||
-UMESH_PRIMARY_ESPNOW
|
||||
-UDISABLE_WIFI_OTA
|
||||
-D ADVERT_NAME='"Indicator Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D MOTA_HW_ID='"SenseCapIndicator-LoRa"'
|
||||
-D INDICATOR_WIFI_FONT_RECOVERY=1
|
||||
build_src_filter =
|
||||
${SenseCapIndicator-LoRa.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/ESPNOWRadio.cpp>
|
||||
lib_deps =
|
||||
${SenseCapIndicator-LoRa.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
; PSRAM line mode is fixed before app startup: never flash this QSPI image on
|
||||
; an original OPI N8R8 Indicator merely because both use the same SX1262 pins.
|
||||
[env:SenseCapIndicator-LoRa-N16R2_repeater]
|
||||
extends = env:SenseCapIndicator-LoRa_repeater
|
||||
board_build.arduino.memory_type = qio_qspi
|
||||
board_build.flash_mode = qio
|
||||
board_build.psram_type = qspi
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 6553600
|
||||
board_build.partitions = variants/sensecap_indicator-espnow/dual_ota_6400k_preserve_spiffs.csv
|
||||
build_flags =
|
||||
${env:SenseCapIndicator-LoRa_repeater.build_flags}
|
||||
-UMOTA_HW_ID
|
||||
-D MOTA_HW_ID='"SenseCapIndicator-LoRa-N16R2"'
|
||||
Reference in new issue
Block a user