mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-05-18 07:16:20 +02:00
Add support for new radio hardware configurations and CH341 USB adapter
This commit is contained in:
@@ -34,6 +34,7 @@ debian/pymc-repeater.substvars
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv_new/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
|
||||
+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
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ keywords = ["mesh", "networking", "lora", "repeater", "daemon", "iot"]
|
||||
|
||||
|
||||
dependencies = [
|
||||
"pymc_core[hardware] @ git+https://github.com/rightup/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",
|
||||
|
||||
@@ -127,6 +127,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-4
@@ -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(), 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,7 +220,22 @@ 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"],
|
||||
@@ -224,6 +248,7 @@ def get_radio_for_board(board_config: dict):
|
||||
"txled_pin": spi_config.get("txled_pin", -1),
|
||||
"rxled_pin": spi_config.get("rxled_pin", -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"]),
|
||||
@@ -245,5 +270,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"
|
||||
)
|
||||
|
||||
@@ -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