mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-06 08:53:46 +02:00
Merge branch 'feat/newRadios' into dev
This commit is contained in:
@@ -34,6 +34,7 @@ debian/pymc-repeater.substvars
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv_new/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user