Merge branch 'feat/newRadios' into dev

This commit is contained in:
Shaun Walker
2026-02-22 21:45:22 +10:00
committed by GitHub
12 changed files with 398 additions and 104 deletions
+1
View File
@@ -34,6 +34,7 @@ debian/pymc-repeater.substvars
# Virtual environments
.venv/
.venv_new/
env/
ENV/
+10
View File
@@ -69,6 +69,16 @@ Frequency Labs meshadv
TX Power: Up to 22dBm
SPI Bus: SPI0
GPIO Pins: CS=21, Reset=18, Busy=20, IRQ=16, TXEN=13, RXEN=12, use_dio3_tcxo=True
HT-RA62 module
Hardware: Heltec HT-RA62 LoRa module
Platform: Raspberry Pi (or compatible single-board computer)
Frequency: 868MHz (EU) or 915MHz (US)
TX Power: Up to 22dBm
SPI Bus: SPI0
GPIO Pins: CS=21, Reset=18, Busy=20, IRQ=16, use_dio3_tcxo=True, use_dio2_rf=True
...
## Screenshots
+30 -12
View File
@@ -88,17 +88,29 @@ identities:
# admin_password: "room_admin_password"
# guest_password: "room_guest_password"
# Add more room servers as needed
# - name: "SocialHub"
# identity_key: "another_identity_key_hex_here"
# type: "room_server"
# settings:
# node_name: "Social Hub"
# latitude: 0.0
# longitude: 0.0
# admin_password: "social_admin_123"
# guest_password: "social_guest_123"
# Add more room servers as needed
# - name: "SocialHub"
# identity_key: "another_identity_key_hex_here"
# type: "room_server"
# settings:
# node_name: "Social Hub"
# latitude: 0.0
# longitude: 0.0
# admin_password: "social_admin_123"
# guest_password: "social_guest_123"
# Radio hardware type
# Supported:
# - sx1262 (Linux spidev + system GPIO)
# - sx1262_ch341 (CH341 USB-to-SPI + CH341 GPIO 0-7)
radio_type: sx1262
# CH341 USB-to-SPI adapter settings (only used when radio_type: sx1262_ch341)
# NOTE: VID/PID are integers. Hex is also accepted in YAML, e.g. 0x1A86.
ch341:
vid: 6790 # 0x1A86
pid: 21778 # 0x5512
radio:
# Frequency in Hz (869.618 MHz for EU)
frequency: 869618000
@@ -128,12 +140,16 @@ radio:
implicit_header: false
# SX1262 Hardware Configuration
# NOTE:
# - When radio_type: sx1262, these pins are BCM GPIO numbers.
# - When radio_type: sx1262_ch341, these pins are CH341 GPIO numbers (0-7).
sx1262:
# SPI bus and chip select
# NOTE: For CH341 these are not used but are still required parameters.
bus_id: 0
cs_id: 0
# GPIO pins (BCM numbering)
# GPIO pins
cs_pin: 21
reset_pin: 18
busy_pin: 20
@@ -148,6 +164,8 @@ sx1262:
rxled_pin: -1
use_dio3_tcxo: false
dio3_tcxo_voltage: 1.8
use_dio2_rf: false
# Waveshare hardware flag
is_waveshare: false
+17 -4
View File
@@ -183,6 +183,7 @@ install_repeater() {
$DIALOG --backtitle "pyMC Repeater Management" --title "Welcome" --msgbox "\nWelcome to pyMC Repeater Setup\n\nThis installer will configure your Linux system as a LoRa mesh network repeater.\n\nPress OK to continue..." 12 70
# SPI Check - Universal approach that works on all boards
SPI_MISSING=0
if ! ls /dev/spidev* >/dev/null 2>&1; then
# SPI devices not found, check if we're on a Raspberry Pi and can enable it
CONFIG_FILE=""
@@ -199,15 +200,27 @@ install_repeater() {
show_info "SPI Enabled" "\nSPI has been enabled in $CONFIG_FILE\n\nSystem will reboot now. Please run this script again after reboot."
reboot
else
show_error "SPI is required for LoRa radio operation.\n\nPlease enable SPI manually and run this script again."
return
if ask_yes_no "Continue Without SPI?" "\nSPI is required for LoRa radio operation and is not enabled.\n\nYou can continue the installation, but the radio will not work until SPI is enabled.\n\nContinue anyway?"; then
SPI_MISSING=1
else
show_error "SPI is required for LoRa radio operation.\n\nPlease enable SPI manually and run this script again."
return
fi
fi
else
# Not a Raspberry Pi - provide generic instructions
show_error "SPI interface is required but not detected (/dev/spidev* not found).\n\nPlease enable SPI in your system's configuration and ensure the SPI kernel module is loaded.\n\nFor Raspberry Pi: sudo raspi-config -> Interfacing Options -> SPI -> Enable"
return
if ask_yes_no "SPI Not Detected" "\nSPI interface is required but not detected (/dev/spidev* not found).\n\nPlease enable SPI in your system's configuration and ensure the SPI kernel module is loaded.\n\nFor Raspberry Pi: sudo raspi-config -> Interfacing Options -> SPI -> Enable\n\nContinue installation anyway?"; then
SPI_MISSING=1
else
show_error "SPI interface is required but not detected (/dev/spidev* not found).\n\nPlease enable SPI in your system's configuration and ensure the SPI kernel module is loaded.\n\nFor Raspberry Pi: sudo raspi-config -> Interfacing Options -> SPI -> Enable"
return
fi
fi
fi
if [ "$SPI_MISSING" -eq 1 ]; then
show_info "Warning" "\nContinuing without SPI enabled.\n\nLoRa radio will not work until SPI is enabled and /dev/spidev* is available."
fi
# Get script directory for file copying during installation
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+1 -1
View File
@@ -31,7 +31,7 @@ keywords = ["mesh", "networking", "lora", "repeater", "daemon", "iot"]
dependencies = [
"pymc_core[hardware] @ git+https://github.com/theshaun/pyMC_core.git@dev",
"pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@feat/newRadios",
"pyyaml>=6.0.0",
"cherrypy>=18.0.0",
"paho-mqtt>=1.6.0",
+45 -5
View File
@@ -16,8 +16,8 @@
"preamble_length": 17,
"is_waveshare": true
},
"uconsole": {
"name": "uConsole LoRa Module",
"uconsole_aiov1": {
"name": "uConsole LoRa Module aio v1",
"bus_id": 1,
"cs_id": 0,
"cs_pin": -1,
@@ -30,6 +30,23 @@
"rxled_pin": -1,
"tx_power": 22,
"preamble_length": 17
},
"uconsole_aio_v2": {
"name": "uConsole LoRa Module aio v2",
"bus_id": 1,
"cs_id": 0,
"cs_pin": -1,
"reset_pin": 25,
"busy_pin": 24,
"irq_pin": 26,
"txen_pin": -1,
"rxen_pin": -1,
"txled_pin": -1,
"rxled_pin": -1,
"tx_power": 22,
"preamble_length": 17,
"use_dio3_tcxo": true,
"use_dio2_rf": true
},
"pimesh-1w-usa": {
"name": "PiMesh-1W (USA)",
@@ -125,9 +142,9 @@
"rxen_pin": 24,
"txled_pin": -1,
"rxled_pin": -1,
"tx_power": 22,
"use_dio2_rf": true,
"use_dio3_tcxo": true
"tx_power": 30,
"use_dio3_tcxo": true,
"preamble_length": 17
},
"femtofox-2W-SX": {
"name": "FemtoFox SX1262 (2W)",
@@ -163,6 +180,29 @@
"use_dio3_tcxo": true,
"use_dio2_rf": true,
"preamble_length": 17
},
"ch341-usb-sx1262": {
"name": "CH341 USB-SPI + SX1262 (example)",
"description": "SX1262 via CH341 USB-to-SPI adapter. NOTE: pin numbers are CH341 GPIO 0-7, not BCM.",
"radio_type": "sx1262_ch341",
"vid": 6790,
"pid": 21778,
"bus_id": 0,
"cs_id": 0,
"cs_pin": 0,
"reset_pin": 2,
"busy_pin": 4,
"irq_pin": 6,
"txen_pin": -1,
"rxen_pin": 1,
"txled_pin": -1,
"rxled_pin": -1,
"tx_power": 22,
"use_dio2_rf": true,
"use_dio3_tcxo": true,
"dio3_tcxo_voltage": 1.8,
"preamble_length": 17,
"is_waveshare": false
}
}
}
+44 -11
View File
@@ -197,9 +197,18 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes:
def get_radio_for_board(board_config: dict):
def _parse_int(value, *, default=None) -> int:
if value is None:
return default
if isinstance(value, int):
return value
if isinstance(value, str):
return int(value.strip().rstrip(','), 0)
raise ValueError(f"Invalid int value type: {type(value)}")
radio_type = board_config.get("radio_type", "sx1262").lower()
if radio_type == "sx1262":
if radio_type in ("sx1262", "sx1262_ch341"):
from pymc_core.hardware.sx1262_wrapper import SX1262Radio
# Get radio and SPI configuration - all settings must be in config file
@@ -211,21 +220,37 @@ def get_radio_for_board(board_config: dict):
if not radio_config:
raise ValueError("Missing 'radio' section in configuration file")
# Build config with required fields - no defaults
# CH341 integration: swap SPI transport + GPIO backend to CH341
if radio_type == "sx1262_ch341":
ch341_cfg = board_config.get("ch341")
if not ch341_cfg:
raise ValueError("Missing 'ch341' section in configuration file")
from pymc_core.hardware.lora.LoRaRF.SX126x import set_spi_transport
from pymc_core.hardware.transports.ch341_spi_transport import CH341SPITransport
vid = _parse_int(ch341_cfg.get("vid"), default=0x1A86)
pid = _parse_int(ch341_cfg.get("pid"), default=0x5512)
# Create CH341 transport (also configures CH341 GPIO manager globally)
ch341_spi = CH341SPITransport(vid=vid, pid=pid, auto_setup_gpio=True)
set_spi_transport(ch341_spi)
combined_config = {
"bus_id": spi_config["bus_id"],
"cs_id": spi_config["cs_id"],
"cs_pin": spi_config["cs_pin"],
"gpio_chip": spi_config.get("gpio_chip", 0),
"use_gpiod_backend": spi_config.get("use_gpiod_backend", False),
"reset_pin": spi_config["reset_pin"],
"busy_pin": spi_config["busy_pin"],
"irq_pin": spi_config["irq_pin"],
"txen_pin": spi_config["txen_pin"],
"rxen_pin": spi_config["rxen_pin"],
"txled_pin": spi_config.get("txled_pin", -1),
"rxled_pin": spi_config.get("rxled_pin", -1),
"reset_pin": _parse_int(spi_config["reset_pin"]),
"busy_pin": _parse_int(spi_config["busy_pin"]),
"irq_pin": _parse_int(spi_config["irq_pin"]),
"txen_pin": _parse_int(spi_config["txen_pin"]),
"rxen_pin": _parse_int(spi_config["rxen_pin"]),
"txled_pin": _parse_int(spi_config.get("txled_pin", -1), default=-1),
"rxled_pin": _parse_int(spi_config.get("rxled_pin", -1), default=-1),
"use_dio3_tcxo": spi_config.get("use_dio3_tcxo", False),
"dio3_tcxo_voltage": float(spi_config.get("dio3_tcxo_voltage", 1.8)),
"use_dio2_rf": spi_config.get("use_dio2_rf", False),
"is_waveshare": spi_config.get("is_waveshare", False),
"frequency": int(radio_config["frequency"]),
@@ -237,6 +262,13 @@ def get_radio_for_board(board_config: dict):
"sync_word": radio_config["sync_word"],
}
# Add optional GPIO parameters if specified in config
# These wont be supported by older versions of pymc_core
if "gpio_chip" in spi_config:
combined_config["gpio_chip"] = _parse_int(spi_config["gpio_chip"], default=0)
if "use_gpiod_backend" in spi_config:
combined_config["use_gpiod_backend"] = spi_config["use_gpiod_backend"]
radio = SX1262Radio.get_instance(**combined_config)
if hasattr(radio, "_initialized") and not radio._initialized:
@@ -247,5 +279,6 @@ def get_radio_for_board(board_config: dict):
return radio
else:
raise RuntimeError(f"Unknown radio type: {radio_type}. Supported: sx1262")
raise RuntimeError(
f"Unknown radio type: {radio_type}. Supported: sx1262, sx1262_ch341"
)
+77 -21
View File
@@ -17,6 +17,7 @@ except Exception:
from datetime import timezone
UTC = timezone.utc
# Try to import paho-mqtt error code mappings
try:
from paho.mqtt.reasoncodes import ReasonCode
@@ -598,33 +599,88 @@ def get_mqtt_error_message(rc: int, is_disconnect: bool = False) -> str:
"""
if HAS_REASON_CODES:
try:
reason = ReasonCode(rc)
return f"{reason.name}: {reason.value}"
except (ValueError, AttributeError):
pass
# ReasonCode object has getName() method and value property
from paho.mqtt.reasoncodes import ReasonCode
reason = ReasonCode(mqtt.CONNACK if not is_disconnect else mqtt.DISCONNECT, identifier=rc)
name = reason.getName() if hasattr(reason, 'getName') else str(reason)
return f"{name} (code {rc})"
except Exception as e:
# Log the exception for debugging
logger.debug(f"Could not decode reason code {rc}: {e}")
# Fallback to manual mappings
# Fallback to manual mappings - Extended with MQTT v5 codes
connect_errors = {
0: "connection accepted",
1: "incorrect protocol version",
2: "invalid client identifier",
3: "server unavailable",
4: "bad username or password (JWT invalid)",
5: "not authorized (JWT signature/format invalid)",
6: "reserved error code",
0: "Connection accepted",
1: "Incorrect protocol version",
2: "Invalid client identifier",
3: "Server unavailable",
4: "Bad username or password (JWT invalid)",
5: "Not authorized (JWT signature/format invalid)",
# MQTT v5 codes
128: "Unspecified error",
129: "Malformed packet",
130: "Protocol error",
131: "Implementation specific error",
132: "Unsupported protocol version",
133: "Client identifier not valid",
134: "Bad username or password",
135: "Not authorized",
136: "Server unavailable",
137: "Server busy",
138: "Banned",
140: "Bad authentication method",
144: "Topic name invalid",
149: "Packet too large",
151: "Quota exceeded",
153: "Payload format invalid",
154: "Retain not supported",
155: "QoS not supported",
156: "Use another server",
157: "Server moved",
159: "Connection rate exceeded",
}
disconnect_errors = {
0: "normal disconnect",
1: "unacceptable protocol version",
2: "identifier rejected",
3: "server unavailable",
4: "bad username or password",
5: "not authorized",
16: "connection lost / protocol error",
17: "client timeout",
0: "Normal disconnect",
1: "Unacceptable protocol version",
2: "Identifier rejected",
3: "Server unavailable",
4: "Bad username or password",
5: "Not authorized",
7: "Connection lost / network error",
16: "Connection lost / protocol error",
17: "Client timeout",
# MQTT v5 codes
4: "Disconnect with Will message",
128: "Unspecified error",
129: "Malformed packet",
130: "Protocol error",
131: "Implementation specific error",
135: "Not authorized",
137: "Server busy",
139: "Server shutting down",
141: "Keep alive timeout",
142: "Session taken over",
143: "Topic filter invalid",
144: "Topic name invalid",
147: "Receive maximum exceeded",
148: "Topic alias invalid",
149: "Packet too large",
150: "Message rate too high",
151: "Quota exceeded",
152: "Administrative action",
153: "Payload format invalid",
154: "Retain not supported",
155: "QoS not supported",
156: "Use another server",
157: "Server moved",
158: "Shared subscriptions not supported",
159: "Connection rate exceeded",
160: "Maximum connect time",
161: "Subscription identifiers not supported",
162: "Wildcard subscriptions not supported",
}
error_dict = disconnect_errors if is_disconnect else connect_errors
return error_dict.get(rc, f"unknown error code {rc}")
return error_dict.get(rc, f"Unknown error code {rc}")
@@ -19,7 +19,13 @@ class StorageCollector:
def __init__(self, config: dict, local_identity=None, repeater_handler=None):
self.config = config
self.repeater_handler = repeater_handler
self.storage_dir = Path(config.get("storage_dir", "/var/lib/pymc_repeater"))
storage_dir_cfg = (
config.get("storage", {}).get("storage_dir")
or config.get("storage_dir")
or "/var/lib/pymc_repeater"
)
self.storage_dir = Path(storage_dir_cfg)
self.storage_dir.mkdir(parents=True, exist_ok=True)
node_name = config.get("repeater", {}).get("node_name", "unknown")
+79 -46
View File
@@ -53,7 +53,8 @@ class RepeaterDaemon:
logger.info(f"Initializing repeater: {self.config['repeater']['node_name']}")
if self.radio is None:
logger.info("Initializing radio hardware...")
radio_type = self.config.get("radio_type", "sx1262")
logger.info(f"Initializing radio hardware... (radio_type={radio_type})")
try:
self.radio = get_radio_for_board(self.config)
@@ -458,57 +459,89 @@ class RepeaterDaemon:
logger.error(f"Failed to send advert: {e}", exc_info=True)
return False
async def _shutdown(self):
"""Best-effort shutdown: stop background services and release hardware."""
# Stop router
if self.router:
try:
await self.router.stop()
except Exception as e:
logger.warning(f"Error stopping router: {e}")
# Stop HTTP server
if self.http_server:
try:
self.http_server.stop()
except Exception as e:
logger.warning(f"Error stopping HTTP server: {e}")
# Release radio resources
if self.radio and hasattr(self.radio, "cleanup"):
try:
self.radio.cleanup()
except Exception as e:
logger.warning(f"Error cleaning up radio: {e}")
# Release CH341 USB device if in use
try:
if self.config.get("radio_type", "sx1262").lower() == "sx1262_ch341":
from pymc_core.hardware.ch341.ch341_async import CH341Async
CH341Async.reset_instance()
except Exception as e:
logger.debug(f"CH341 reset skipped/failed: {e}")
async def run(self):
logger.info("Repeater daemon started")
await self.initialize()
# Start HTTP stats server
http_port = self.config.get("http", {}).get("port", 8000)
http_host = self.config.get("http", {}).get("host", "0.0.0.0")
node_name = self.config.get("repeater", {}).get("node_name", "Repeater")
# Format public key for display
pub_key_formatted = ""
if self.local_identity:
pub_key_hex = self.local_identity.get_public_key().hex()
# Format as <first8...last8>
if len(pub_key_hex) >= 16:
pub_key_formatted = f"{pub_key_hex[:8]}...{pub_key_hex[-8:]}"
else:
pub_key_formatted = pub_key_hex
current_loop = asyncio.get_event_loop()
self.http_server = HTTPStatsServer(
host=http_host,
port=http_port,
stats_getter=self.get_stats,
node_name=node_name,
pub_key=pub_key_formatted,
send_advert_func=self.send_advert,
config=self.config,
event_loop=current_loop,
daemon_instance=self,
config_path=getattr(self, 'config_path', '/etc/pymc_repeater/config.yaml'),
)
try:
self.http_server.start()
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
await self.initialize()
# Run dispatcher (handles RX/TX via pymc_core)
try:
await self.dispatcher.run_forever()
except KeyboardInterrupt:
logger.info("Shutting down...")
if self.router:
await self.router.stop()
if self.http_server:
self.http_server.stop()
# Start HTTP stats server
http_port = self.config.get("http", {}).get("port", 8000)
http_host = self.config.get("http", {}).get("host", "0.0.0.0")
node_name = self.config.get("repeater", {}).get("node_name", "Repeater")
# Format public key for display
pub_key_formatted = ""
if self.local_identity:
pub_key_hex = self.local_identity.get_public_key().hex()
# Format as <first8...last8>
if len(pub_key_hex) >= 16:
pub_key_formatted = f"{pub_key_hex[:8]}...{pub_key_hex[-8:]}"
else:
pub_key_formatted = pub_key_hex
current_loop = asyncio.get_event_loop()
self.http_server = HTTPStatsServer(
host=http_host,
port=http_port,
stats_getter=self.get_stats,
node_name=node_name,
pub_key=pub_key_formatted,
send_advert_func=self.send_advert,
config=self.config,
event_loop=current_loop,
daemon_instance=self,
config_path=getattr(self, 'config_path', '/etc/pymc_repeater/config.yaml'),
)
try:
self.http_server.start()
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
# Run dispatcher (handles RX/TX via pymc_core)
try:
await self.dispatcher.run_forever()
except KeyboardInterrupt:
logger.info("Shutting down...")
finally:
await self._shutdown()
def main():
+36 -3
View File
@@ -262,7 +262,12 @@ class APIEndpoints:
import json
# Check config-based location first, then development location
config_dir = Path(self.config.get("storage_dir", "/var/lib/pymc_repeater"))
storage_dir_cfg = (
self.config.get("storage", {}).get("storage_dir")
or self.config.get("storage_dir")
or "/var/lib/pymc_repeater"
)
config_dir = Path(storage_dir_cfg)
installed_path = config_dir / 'radio-settings.json'
dev_path = os.path.join(os.path.dirname(__file__), '..', '..', 'radio-settings.json')
@@ -301,7 +306,12 @@ class APIEndpoints:
import json
# Check config-based location first, then development location
config_dir = Path(self.config.get("storage_dir", "/var/lib/pymc_repeater"))
storage_dir_cfg = (
self.config.get("storage", {}).get("storage_dir")
or self.config.get("storage_dir")
or "/var/lib/pymc_repeater"
)
config_dir = Path(storage_dir_cfg)
installed_path = config_dir / 'radio-presets.json'
dev_path = os.path.join(os.path.dirname(__file__), '..', '..', 'radio-presets.json')
@@ -353,7 +363,12 @@ class APIEndpoints:
# Load hardware configuration - check installed path first, then dev path
import json
config_dir = Path(self.config.get("storage_dir", "/var/lib/pymc_repeater"))
storage_dir_cfg = (
self.config.get("storage", {}).get("storage_dir")
or self.config.get("storage_dir")
or "/var/lib/pymc_repeater"
)
config_dir = Path(storage_dir_cfg)
installed_path = config_dir / 'radio-settings.json'
dev_path = os.path.join(os.path.dirname(__file__), '..', '..', 'radio-settings.json')
@@ -388,6 +403,22 @@ class APIEndpoints:
config_yaml['repeater']['security'] = {}
config_yaml['repeater']['security']['admin_password'] = admin_password
# Update radio backend selection (radio_type + optional CH341 USB adapter info)
if 'radio_type' in hw_config:
config_yaml['radio_type'] = hw_config.get('radio_type')
# CH341 settings can be provided as top-level fields or nested object
ch341_cfg = hw_config.get('ch341') if isinstance(hw_config.get('ch341'), dict) else None
vid = (ch341_cfg or {}).get('vid', hw_config.get('vid'))
pid = (ch341_cfg or {}).get('pid', hw_config.get('pid'))
if vid is not None or pid is not None:
if 'ch341' not in config_yaml:
config_yaml['ch341'] = {}
if vid is not None:
config_yaml['ch341']['vid'] = vid
if pid is not None:
config_yaml['ch341']['pid'] = pid
# Update radio settings - convert MHz/kHz to Hz
if 'radio' not in config_yaml:
config_yaml['radio'] = {}
@@ -439,6 +470,8 @@ class APIEndpoints:
# Hardware flags
if 'use_dio3_tcxo' in hw_config:
config_yaml['sx1262']['use_dio3_tcxo'] = hw_config.get('use_dio3_tcxo', False)
if 'dio3_tcxo_voltage' in hw_config:
config_yaml['sx1262']['dio3_tcxo_voltage'] = hw_config.get('dio3_tcxo_voltage', 1.8)
if 'use_dio2_rf' in hw_config:
config_yaml['sx1262']['use_dio2_rf'] = hw_config.get('use_dio2_rf', False)
if 'is_waveshare' in hw_config:
+51
View File
@@ -206,6 +206,10 @@ if [ -z "$hw_config" ] || [ "$hw_config" == "null" ]; then
echo "Warning: Could not extract hardware config from JSON, using defaults"
else
# Extract each field and update config.yaml
radio_type=$(echo "$hw_config" | jq -r '.radio_type // empty')
vid=$(echo "$hw_config" | jq -r '.vid // empty')
pid=$(echo "$hw_config" | jq -r '.pid // empty')
bus_id=$(echo "$hw_config" | jq -r '.bus_id // empty')
cs_id=$(echo "$hw_config" | jq -r '.cs_id // empty')
cs_pin=$(echo "$hw_config" | jq -r '.cs_pin // empty')
@@ -220,6 +224,29 @@ else
preamble_length=$(echo "$hw_config" | jq -r '.preamble_length // empty')
is_waveshare=$(echo "$hw_config" | jq -r '.is_waveshare // empty')
use_dio3_tcxo=$(echo "$hw_config" | jq -r '.use_dio3_tcxo // empty')
dio3_tcxo_voltage=$(echo "$hw_config" | jq -r '.dio3_tcxo_voltage // empty')
use_dio2_rf=$(echo "$hw_config" | jq -r '.use_dio2_rf // empty')
# Update radio_type + optional CH341 section
if [ -n "$radio_type" ]; then
if grep -q "^radio_type:" "$CONFIG_FILE"; then
sed "${SED_OPTS[@]}" "s/^radio_type:.*/radio_type: $radio_type/" "$CONFIG_FILE"
else
# Append if missing (config.yaml.example includes it, so this is a fallback)
printf "\nradio_type: %s\n" "$radio_type" >> "$CONFIG_FILE"
fi
fi
if [ -n "$vid" ] || [ -n "$pid" ]; then
# Ensure ch341 section exists
if ! grep -q "^ch341:" "$CONFIG_FILE"; then
# Append if missing (fallback)
printf "\nch341:\n vid: %s\n pid: %s\n" "${vid:-6790}" "${pid:-21778}" >> "$CONFIG_FILE"
fi
[ -n "$vid" ] && sed "${SED_OPTS[@]}" "s/^ vid:.*/ vid: $vid/" "$CONFIG_FILE"
[ -n "$pid" ] && sed "${SED_OPTS[@]}" "s/^ pid:.*/ pid: $pid/" "$CONFIG_FILE"
fi
# Update sx1262 section in config.yaml (2-space indentation)
[ -n "$bus_id" ] && sed "${SED_OPTS[@]}" "s/^ bus_id:.*/ bus_id: $bus_id/" "$CONFIG_FILE"
@@ -284,6 +311,30 @@ else
fi
fi
fi
# Update dio3_tcxo_voltage (only meaningful if use_dio3_tcxo is true)
if [ -n "$dio3_tcxo_voltage" ]; then
if grep -q "^ dio3_tcxo_voltage:" "$CONFIG_FILE"; then
sed "${SED_OPTS[@]}" "s/^ dio3_tcxo_voltage:.*/ dio3_tcxo_voltage: $dio3_tcxo_voltage/" "$CONFIG_FILE"
else
# Add after use_dio3_tcxo
sed "${SED_OPTS[@]}" "/^ use_dio3_tcxo:.*/a\\ dio3_tcxo_voltage: $dio3_tcxo_voltage" "$CONFIG_FILE"
fi
fi
# Update use_dio2_rf flag
if [ "$use_dio2_rf" == "true" ] || [ "$use_dio2_rf" == "false" ]; then
if grep -q "^ use_dio2_rf:" "$CONFIG_FILE"; then
sed "${SED_OPTS[@]}" "s/^ use_dio2_rf:.*/ use_dio2_rf: $use_dio2_rf/" "$CONFIG_FILE"
else
# Add after dio3_tcxo_voltage if present, otherwise after use_dio3_tcxo
if grep -q "^ dio3_tcxo_voltage:" "$CONFIG_FILE"; then
sed "${SED_OPTS[@]}" "/^ dio3_tcxo_voltage:.*/a\\ use_dio2_rf: $use_dio2_rf" "$CONFIG_FILE"
else
sed "${SED_OPTS[@]}" "/^ use_dio3_tcxo:.*/a\\ use_dio2_rf: $use_dio2_rf" "$CONFIG_FILE"
fi
fi
fi
fi
# Cleanup