mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
Merge upstream feat/newRadios (radios, MQTT, SPI)
- Take upstream radio-presets.json and radio-settings.json (Femtofox, AIO v2, CH341). - config.py: upstream get_radio_for_board (_parse_int, sx1262_ch341, GPIO) + re-add KISS branch. - config.yaml.example: upstream radio/ch341 + our identities.companions. - manage.sh: upstream SPI warning prompt. - main.py: single try/finally with companion shutdown and _shutdown(). - letsmesh_handler: upstream UTC fallback and MQTT v5 reason code handling. - storage_collector, api_endpoints: upstream storage_dir_cfg; apply_setup_wizard supports KISS and sx1262_ch341/ch341. - airtime.py: upstream bw_hz fix for symbol time. - pyproject.toml: keep pymc_core dependency (no git pin). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,6 +34,7 @@ debian/pymc-repeater.substvars
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv_new/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -74,6 +74,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
|
||||
|
||||
@@ -89,7 +89,6 @@ identities:
|
||||
# longitude: 0.0
|
||||
# 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"
|
||||
@@ -117,6 +116,18 @@ identities:
|
||||
# node_name: "meshcore-bot"
|
||||
# tcp_port: 5001
|
||||
|
||||
# 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
|
||||
@@ -151,12 +162,16 @@ radio:
|
||||
# baud_rate: 9600
|
||||
|
||||
# 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
|
||||
@@ -171,6 +186,8 @@ sx1262:
|
||||
rxled_pin: -1
|
||||
|
||||
use_dio3_tcxo: false
|
||||
dio3_tcxo_voltage: 1.8
|
||||
use_dio2_rf: false
|
||||
|
||||
# Waveshare hardware flag
|
||||
is_waveshare: false
|
||||
|
||||
29
manage.sh
29
manage.sh
@@ -70,6 +70,11 @@ is_running() {
|
||||
systemctl is-active "$SERVICE_NAME" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to check if service is enabled
|
||||
is_enabled() {
|
||||
systemctl is-enabled "$SERVICE_NAME" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to get current version
|
||||
get_version() {
|
||||
# Try to read from _version.py first (generated by setuptools_scm)
|
||||
@@ -178,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=""
|
||||
@@ -194,16 +200,28 @@ 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)"
|
||||
|
||||
@@ -719,6 +737,9 @@ manage_service() {
|
||||
|
||||
case $action in
|
||||
"start")
|
||||
if ! is_enabled; then
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
fi
|
||||
systemctl start "$SERVICE_NAME"
|
||||
if is_running; then
|
||||
show_info "Service Started" "\n✓ pyMC Repeater service has been started successfully."
|
||||
|
||||
@@ -1 +1,151 @@
|
||||
{"config":{"connect_screen":{"info_message":"The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings."},"remote_management":{"repeaters":{"guest_login_enabled":true,"guest_login_disabled_message":"Guest login has been temporarily disabled. Please try again later.","guest_login_passwords":[""],"flood_routed_guest_login_enabled":true,"flood_routed_guest_login_disabled_message":"To avoid overwhelming the mesh with flood packets, please set a path to log in to a repeater as a guest."}},"suggested_radio_settings":{"info_message":"These radio settings have been suggested by the community.","entries":[{"title":"Australia","description":"915.800MHz / SF10 / BW250 / CR5","frequency":"915.800","spreading_factor":"10","bandwidth":"250","coding_rate":"5"},{"title":"Australia: Victoria","description":"916.575MHz / SF7 / BW62.5 / CR8","frequency":"916.575","spreading_factor":"7","bandwidth":"62.5","coding_rate":"8"},{"title":"EU/UK (Narrow)","description":"869.618MHz / SF8 / BW62.5 / CR8","frequency":"869.618","spreading_factor":"8","bandwidth":"62.5","coding_rate":"8"},{"title":"EU/UK (Long Range)","description":"869.525MHz / SF11 / BW250 / CR5","frequency":"869.525","spreading_factor":"11","bandwidth":"250","coding_rate":"5"},{"title":"EU/UK (Medium Range)","description":"869.525MHz / SF10 / BW250 / CR5","frequency":"869.525","spreading_factor":"10","bandwidth":"250","coding_rate":"5"},{"title":"Czech Republic (Narrow)","description":"869.525MHz / SF7 / BW62.5 / CR5","frequency":"869.525","spreading_factor":"7","bandwidth":"62.5","coding_rate":"5"},{"title":"EU 433MHz (Long Range)","description":"433.650MHz / SF11 / BW250 / CR5","frequency":"433.650","spreading_factor":"11","bandwidth":"250","coding_rate":"5"},{"title":"New Zealand","description":"917.375MHz / SF11 / BW250 / CR5","frequency":"917.375","spreading_factor":"11","bandwidth":"250","coding_rate":"5"},{"title":"New Zealand (Narrow)","description":"917.375MHz / SF7 / BW62.5 / CR5","frequency":"917.375","spreading_factor":"7","bandwidth":"62.5","coding_rate":"5"},{"title":"Portugal 433","description":"433.375MHz / SF9 / BW62.5 / CR6","frequency":"433.375","spreading_factor":"9","bandwidth":"62.5","coding_rate":"6"},{"title":"Portugal 868","description":"869.618MHz / SF7 / BW62.5 / CR6","frequency":"869.618","spreading_factor":"7","bandwidth":"62.5","coding_rate":"6"},{"title":"Switzerland","description":"869.618MHz / SF8 / BW62.5 / CR8","frequency":"869.618","spreading_factor":"8","bandwidth":"62.5","coding_rate":"8"},{"title":"USA/Canada (Recommended)","description":"910.525MHz / SF7 / BW62.5 / CR5","frequency":"910.525","spreading_factor":"7","bandwidth":"62.5","coding_rate":"5"},{"title":"USA/Canada (Alternate)","description":"910.525MHz / SF11 / BW250 / CR5","frequency":"910.525","spreading_factor":"11","bandwidth":"250","coding_rate":"5"},{"title":"Vietnam","description":"920.250MHz / SF11 / BW250 / CR5","frequency":"920.250","spreading_factor":"11","bandwidth":"250","coding_rate":"5"}]}}}
|
||||
{
|
||||
"config": {
|
||||
"connect_screen": {
|
||||
"info_message": "The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings."
|
||||
},
|
||||
"remote_management": {
|
||||
"repeaters": {
|
||||
"guest_login_enabled": true,
|
||||
"guest_login_disabled_message": "Guest login has been temporarily disabled. Please try again later.",
|
||||
"guest_login_passwords": [
|
||||
""
|
||||
],
|
||||
"flood_routed_guest_login_enabled": true,
|
||||
"flood_routed_guest_login_disabled_message": "To avoid overwhelming the mesh with flood packets, please set a path to log in to a repeater as a guest."
|
||||
}
|
||||
},
|
||||
"suggested_radio_settings": {
|
||||
"info_message": "These radio settings have been suggested by the community.",
|
||||
"entries": [
|
||||
{
|
||||
"title": "Australia",
|
||||
"description": "915.800MHz / SF10 / BW250 / CR5",
|
||||
"frequency": "915.800",
|
||||
"spreading_factor": "10",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "Australia (Narrow)",
|
||||
"description": "916.575MHz / SF7 / BW62.5 / CR8",
|
||||
"frequency": "916.575",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "8"
|
||||
},
|
||||
{
|
||||
"title": "Australia: SA, WA, QLD",
|
||||
"description": "923.125MHz / SF8 / BW62.5 / CR8",
|
||||
"frequency": "923.125",
|
||||
"spreading_factor": "8",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "8"
|
||||
},
|
||||
{
|
||||
"title": "EU/UK (Narrow)",
|
||||
"description": "869.618MHz / SF8 / BW62.5 / CR8",
|
||||
"frequency": "869.618",
|
||||
"spreading_factor": "8",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "8"
|
||||
},
|
||||
{
|
||||
"title": "EU/UK (Long Range)",
|
||||
"description": "869.525MHz / SF11 / BW250 / CR5",
|
||||
"frequency": "869.525",
|
||||
"spreading_factor": "11",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "EU/UK (Medium Range)",
|
||||
"description": "869.525MHz / SF10 / BW250 / CR5",
|
||||
"frequency": "869.525",
|
||||
"spreading_factor": "10",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "Czech Republic (Narrow)",
|
||||
"description": "869.525MHz / SF7 / BW62.5 / CR5",
|
||||
"frequency": "869.525",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "EU 433MHz (Long Range)",
|
||||
"description": "433.650MHz / SF11 / BW250 / CR5",
|
||||
"frequency": "433.650",
|
||||
"spreading_factor": "11",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "New Zealand",
|
||||
"description": "917.375MHz / SF11 / BW250 / CR5",
|
||||
"frequency": "917.375",
|
||||
"spreading_factor": "11",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "New Zealand (Narrow)",
|
||||
"description": "917.375MHz / SF7 / BW62.5 / CR5",
|
||||
"frequency": "917.375",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "Portugal 433",
|
||||
"description": "433.375MHz / SF9 / BW62.5 / CR6",
|
||||
"frequency": "433.375",
|
||||
"spreading_factor": "9",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "6"
|
||||
},
|
||||
{
|
||||
"title": "Portugal 868",
|
||||
"description": "869.618MHz / SF7 / BW62.5 / CR6",
|
||||
"frequency": "869.618",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "6"
|
||||
},
|
||||
{
|
||||
"title": "Switzerland",
|
||||
"description": "869.618MHz / SF8 / BW62.5 / CR8",
|
||||
"frequency": "869.618",
|
||||
"spreading_factor": "8",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "8"
|
||||
},
|
||||
{
|
||||
"title": "USA/Canada (Recommended)",
|
||||
"description": "910.525MHz / SF7 / BW62.5 / CR5",
|
||||
"frequency": "910.525",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "USA/Canada (Alternate)",
|
||||
"description": "910.525MHz / SF11 / BW250 / CR5",
|
||||
"frequency": "910.525",
|
||||
"spreading_factor": "11",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
},
|
||||
{
|
||||
"title": "Vietnam",
|
||||
"description": "920.250MHz / SF11 / BW250 / CR5",
|
||||
"frequency": "920.250",
|
||||
"spreading_factor": "11",
|
||||
"bandwidth": "250",
|
||||
"coding_rate": "5"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)",
|
||||
@@ -111,6 +128,24 @@
|
||||
"use_dio2_rf": true,
|
||||
"preamble_length": 17
|
||||
},
|
||||
"femtofox-1W-SX": {
|
||||
"name": "FemtoFox SX1262 (1W)",
|
||||
"bus_id": 0,
|
||||
"cs_id": 0,
|
||||
"cs_pin": 16,
|
||||
"gpio_chip": 1,
|
||||
"use_gpiod_backend": true,
|
||||
"reset_pin": 25,
|
||||
"busy_pin": 22,
|
||||
"irq_pin": 23,
|
||||
"txen_pin": -1,
|
||||
"rxen_pin": 24,
|
||||
"txled_pin": -1,
|
||||
"rxled_pin": -1,
|
||||
"tx_power": 30,
|
||||
"use_dio3_tcxo": true,
|
||||
"preamble_length": 17
|
||||
},
|
||||
"nebrahat": {
|
||||
"name": "NebraHat-2W",
|
||||
"bus_id": 0,
|
||||
@@ -127,6 +162,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,17 +53,17 @@ class AirtimeManager:
|
||||
Airtime in milliseconds
|
||||
"""
|
||||
sf = spreading_factor or self.spreading_factor
|
||||
bw_khz = (bandwidth_hz or self.bandwidth) / 1000
|
||||
bw_hz = (bandwidth_hz or self.bandwidth)
|
||||
cr = coding_rate or self.coding_rate
|
||||
preamble_len = preamble_len or self.preamble_length
|
||||
crc = 1 if crc_enabled else 0
|
||||
h = 0 if explicit_header else 1 # H=0 for explicit, H=1 for implicit
|
||||
|
||||
# Low data rate optimization: required for SF11/SF12 at 125kHz
|
||||
de = 1 if (sf >= 11 and bandwidth_hz <= 125000) else 0
|
||||
de = 1 if (sf >= 11 and bw_hz <= 125000) else 0
|
||||
|
||||
# Symbol time in milliseconds: T_sym = 2^SF / BW_kHz
|
||||
t_sym = (2**sf) / bw_khz
|
||||
t_sym = (2 ** sf) / (bw_hz / 1000)
|
||||
|
||||
# Preamble time: T_preamble = (n_preamble + 4.25) * T_sym
|
||||
t_preamble = (preamble_len + 4.25) * t_sym
|
||||
|
||||
@@ -197,11 +197,20 @@ 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().strip()
|
||||
if radio_type == "kiss-modem":
|
||||
radio_type = "kiss"
|
||||
|
||||
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
|
||||
@@ -213,19 +222,35 @@ 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"],
|
||||
"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),
|
||||
"bus_id": _parse_int(spi_config["bus_id"]),
|
||||
"cs_id": _parse_int(spi_config["cs_id"]),
|
||||
"cs_pin": _parse_int(spi_config["cs_pin"]),
|
||||
"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:
|
||||
@@ -293,7 +325,6 @@ def get_radio_for_board(board_config: dict):
|
||||
|
||||
return radio
|
||||
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Unknown radio type: {radio_type}. Supported: sx1262, kiss (or kiss-modem)"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Unknown radio type: {radio_type}. Supported: sx1262, sx1262_ch341, kiss (or kiss-modem)"
|
||||
)
|
||||
|
||||
@@ -3,13 +3,20 @@ import binascii
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
from nacl.signing import SigningKey
|
||||
|
||||
from .. import __version__
|
||||
# Try to import datetime.UTC (Python 3.11+) otherwise fallback to timezone.utc
|
||||
try:
|
||||
from datetime import UTC
|
||||
except Exception:
|
||||
from datetime import timezone
|
||||
UTC = timezone.utc
|
||||
|
||||
from repeater import __version__
|
||||
|
||||
# Try to import paho-mqtt error code mappings
|
||||
try:
|
||||
@@ -601,32 +608,86 @@ 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
|
||||
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}")
|
||||
|
||||
@@ -18,8 +18,13 @@ class StorageCollector:
|
||||
def __init__(self, config: dict, local_identity=None, repeater_handler=None):
|
||||
self.config = config
|
||||
self.repeater_handler = repeater_handler
|
||||
storage_cfg = config.get("storage", {})
|
||||
self.storage_dir = Path(storage_cfg.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")
|
||||
|
||||
152
repeater/main.py
152
repeater/main.py
@@ -63,7 +63,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)
|
||||
|
||||
@@ -747,69 +748,104 @@ 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...")
|
||||
for frame_server in getattr(self, "companion_frame_servers", []):
|
||||
try:
|
||||
await frame_server.stop()
|
||||
except Exception as e:
|
||||
logger.debug(f"Companion frame server stop: {e}")
|
||||
if hasattr(self, "companion_bridges"):
|
||||
for bridge in self.companion_bridges.values():
|
||||
if hasattr(bridge, "stop"):
|
||||
try:
|
||||
await bridge.stop()
|
||||
except Exception as e:
|
||||
logger.debug(f"Companion bridge stop: {e}")
|
||||
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...")
|
||||
for frame_server in getattr(self, "companion_frame_servers", []):
|
||||
try:
|
||||
await frame_server.stop()
|
||||
except Exception as e:
|
||||
logger.debug(f"Companion frame server stop: {e}")
|
||||
if hasattr(self, "companion_bridges"):
|
||||
for bridge in self.companion_bridges.values():
|
||||
if hasattr(bridge, "stop"):
|
||||
try:
|
||||
await bridge.stop()
|
||||
except Exception as e:
|
||||
logger.debug(f"Companion bridge stop: {e}")
|
||||
if self.router:
|
||||
await self.router.stop()
|
||||
if self.http_server:
|
||||
self.http_server.stop()
|
||||
finally:
|
||||
await self._shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -287,8 +287,12 @@ class APIEndpoints:
|
||||
import json
|
||||
|
||||
# Check config-based location first, then development location
|
||||
storage_cfg = self.config.get("storage", {})
|
||||
config_dir = Path(storage_cfg.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")
|
||||
|
||||
@@ -333,8 +337,12 @@ class APIEndpoints:
|
||||
import json
|
||||
|
||||
# Check config-based location first, then development location
|
||||
storage_cfg = self.config.get("storage", {})
|
||||
config_dir = Path(storage_cfg.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")
|
||||
|
||||
@@ -390,6 +398,29 @@ class APIEndpoints:
|
||||
|
||||
import json
|
||||
|
||||
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")
|
||||
hardware_file = str(installed_path) if installed_path.exists() else dev_path
|
||||
|
||||
if hardware_key != "kiss":
|
||||
if not os.path.exists(hardware_file):
|
||||
logger.error(f"Hardware file not found. Tried: {installed_path}, {dev_path}")
|
||||
return {"success": False, "error": "Hardware configuration file not found"}
|
||||
with open(hardware_file, "r") as f:
|
||||
hardware_data = json.load(f)
|
||||
hardware_configs = hardware_data.get("hardware", {})
|
||||
hw_config = hardware_configs.get(hardware_key, {})
|
||||
if not hw_config:
|
||||
return {"success": False, "error": f"Hardware configuration not found: {hardware_key}"}
|
||||
else:
|
||||
hw_config = {}
|
||||
|
||||
import yaml
|
||||
|
||||
# Read current config first so we can update it
|
||||
@@ -425,27 +456,23 @@ class APIEndpoints:
|
||||
if "preamble_length" not in config_yaml["radio"]:
|
||||
config_yaml["radio"]["preamble_length"] = 17
|
||||
else:
|
||||
# SX1262: load hardware config from radio-settings.json
|
||||
storage_cfg = self.config.get("storage", {})
|
||||
config_dir = Path(storage_cfg.get("storage_dir", "/var/lib/pymc_repeater"))
|
||||
installed_path = config_dir / "radio-settings.json"
|
||||
dev_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "radio-settings.json"
|
||||
)
|
||||
hardware_file = str(installed_path) if installed_path.exists() else dev_path
|
||||
if not os.path.exists(hardware_file):
|
||||
return {"success": False, "error": "Hardware configuration file not found"}
|
||||
with open(hardware_file, "r") as f:
|
||||
hardware_data = json.load(f)
|
||||
hardware_configs = hardware_data.get("hardware", {})
|
||||
hw_config = hardware_configs.get(hardware_key, {})
|
||||
if not hw_config:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Hardware configuration not found: {hardware_key}",
|
||||
}
|
||||
# SX1262 / sx1262_ch341: radio_type and optional CH341 from hw_config
|
||||
if "radio_type" in hw_config:
|
||||
config_yaml["radio_type"] = hw_config.get("radio_type")
|
||||
else:
|
||||
config_yaml["radio_type"] = "sx1262"
|
||||
|
||||
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
|
||||
|
||||
config_yaml["radio_type"] = "sx1262"
|
||||
if "tx_power" in hw_config:
|
||||
config_yaml["radio"]["tx_power"] = hw_config.get("tx_power", 22)
|
||||
if "preamble_length" in hw_config:
|
||||
@@ -475,11 +502,12 @@ class APIEndpoints:
|
||||
config_yaml["sx1262"]["rxled_pin"] = hw_config.get("rxled_pin", -1)
|
||||
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:
|
||||
config_yaml["sx1262"]["is_waveshare"] = hw_config.get("is_waveshare", False)
|
||||
|
||||
# Write updated config
|
||||
with open(self._config_path, "w") as f:
|
||||
yaml.dump(config_yaml, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
@@ -267,6 +267,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')
|
||||
@@ -281,6 +285,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"
|
||||
@@ -345,6 +372,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
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user