mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
Enhance configuration and setup for KISS modem support
- Added support for Meshcore KISS modem firmware in configuration, allowing users to set `radio_type: kiss` and configure serial port and baud rate. - Updated `config.yaml.example` to include KISS modem settings. - Modified `manage.sh` to install with hardware extras for KISS support. - Enhanced `setup-radio-config.sh` to prompt for radio type and KISS modem settings. - Updated API endpoints to handle KISS modem configurations and hardware options. - Improved error handling for missing configuration sections. This update improves flexibility for users utilizing KISS modems alongside SX1262 hardware.
This commit is contained in:
12
README.md
12
README.md
@@ -30,7 +30,12 @@ The repeater daemon runs continuously as a background process, forwarding LoRa p
|
||||
|
||||
## Supported Hardware (Out of the Box)
|
||||
|
||||
The following hardware is currently supported out-of-the-box:
|
||||
The repeater supports two radio backends:
|
||||
|
||||
- **SX1262 (SPI)** — Direct connection to LoRa modules (HATs, etc.) as listed below.
|
||||
- **KISS modem** — Serial TNC using the KISS protocol. Requires a pyMC_core build with KISS support (e.g. [agessaman/pyMC_core (dev)](https://github.com/agessaman/pyMC_core/tree/dev)). Set `radio_type: kiss` in config and configure `kiss.port` and `kiss.baud_rate`. The setup script (`./setup-radio-config.sh`) offers a "KISS modem" option when configuring the repeater.
|
||||
|
||||
The following SX1262 hardware is currently supported out-of-the-box:
|
||||
|
||||
Waveshare LoRaWAN/GNSS HAT (SPI Version Only)
|
||||
|
||||
@@ -149,6 +154,11 @@ http://<repeater-ip>:8000
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
On **macOS** (or when using only the KISS modem), the base install is enough. On **Raspberry Pi** with SX1262 hardware, install with the optional hardware extra so SPI/spidev is available:
|
||||
```bash
|
||||
pip install -e .[hardware]
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration file is created and configured during installation at:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Default Repeater Configuration
|
||||
# radio_type: sx1262 | kiss (use kiss for serial KISS TNC modem)
|
||||
radio_type: sx1262
|
||||
|
||||
repeater:
|
||||
# Node name for logging and identification
|
||||
@@ -127,6 +129,11 @@ radio:
|
||||
# Use implicit header mode
|
||||
implicit_header: false
|
||||
|
||||
# KISS modem (when radio_type: kiss). Requires pyMC_core with KISS support.
|
||||
# kiss:
|
||||
# port: "/dev/ttyUSB0"
|
||||
# baud_rate: 9600
|
||||
|
||||
# SX1262 Hardware Configuration
|
||||
sx1262:
|
||||
# SPI bus and chip select
|
||||
@@ -212,7 +219,8 @@ mqtt:
|
||||
|
||||
# Storage Configuration
|
||||
storage:
|
||||
# Directory for persistent storage files (SQLite, RRD)
|
||||
# Directory for persistent storage files (SQLite, RRD).
|
||||
# Use a writable path for local/dev (e.g. "./var/pymc_repeater" or "~/var/pymc_repeater").
|
||||
storage_dir: "/var/lib/pymc_repeater"
|
||||
|
||||
# Data retention settings
|
||||
|
||||
BIN
data/repeater.db
Normal file
BIN
data/repeater.db
Normal file
Binary file not shown.
@@ -336,7 +336,7 @@ EOF
|
||||
echo "Note: Using optimized binary wheels for faster installation"
|
||||
echo ""
|
||||
|
||||
if pip install --break-system-packages --no-cache-dir .; then
|
||||
if pip install --break-system-packages --no-cache-dir .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Python package installation completed successfully!"
|
||||
|
||||
@@ -589,7 +589,7 @@ EOF
|
||||
echo ""
|
||||
|
||||
# Upgrade packages (uses cache for unchanged dependencies - much faster)
|
||||
if python3 -m pip install --break-system-packages --upgrade --upgrade-strategy eager .; then
|
||||
if python3 -m pip install --break-system-packages --upgrade --upgrade-strategy eager .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Package and dependencies updated successfully!"
|
||||
else
|
||||
|
||||
@@ -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",
|
||||
"pyyaml>=6.0.0",
|
||||
"cherrypy>=18.0.0",
|
||||
"paho-mqtt>=1.6.0",
|
||||
@@ -44,6 +44,10 @@ dependencies = [
|
||||
|
||||
|
||||
[project.optional-dependencies]
|
||||
# SX1262/SPI support (Linux only; required for Raspberry Pi HATs)
|
||||
hardware = [
|
||||
"pymc_core[hardware]",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.4.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
|
||||
@@ -197,7 +197,9 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes:
|
||||
|
||||
def get_radio_for_board(board_config: dict):
|
||||
|
||||
radio_type = board_config.get("radio_type", "sx1262").lower()
|
||||
radio_type = board_config.get("radio_type", "sx1262").lower().strip()
|
||||
if radio_type == "kiss-modem":
|
||||
radio_type = "kiss"
|
||||
|
||||
if radio_type == "sx1262":
|
||||
from pymc_core.hardware.sx1262_wrapper import SX1262Radio
|
||||
@@ -245,5 +247,51 @@ def get_radio_for_board(board_config: dict):
|
||||
|
||||
return radio
|
||||
|
||||
elif radio_type == "kiss":
|
||||
try:
|
||||
from pymc_core.hardware.kiss_modem_wrapper import KissModemWrapper
|
||||
except ImportError:
|
||||
try:
|
||||
from pymc_core.hardware.kiss_serial_wrapper import KissSerialWrapper as KissModemWrapper
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"KISS modem support requires pyMC_core with KISS support. "
|
||||
"Install your fork with: pip install -e /path/to/pyMC_core"
|
||||
) from None
|
||||
|
||||
kiss_config = board_config.get("kiss")
|
||||
if not kiss_config:
|
||||
raise ValueError("Missing 'kiss' section in configuration file for radio_type: kiss")
|
||||
|
||||
port = kiss_config.get("port")
|
||||
if not port:
|
||||
raise ValueError("Missing 'port' in 'kiss' section (e.g. /dev/ttyUSB0)")
|
||||
|
||||
baudrate = int(kiss_config.get("baud_rate", 115200))
|
||||
radio_cfg = board_config.get("radio") or {}
|
||||
radio_config = {
|
||||
"frequency": int(radio_cfg.get("frequency", 869618000)),
|
||||
"bandwidth": int(radio_cfg.get("bandwidth", 62500)),
|
||||
"spreading_factor": int(radio_cfg.get("spreading_factor", 8)),
|
||||
"coding_rate": int(radio_cfg.get("coding_rate", 8)),
|
||||
"tx_power": int(radio_cfg.get("tx_power", 14)),
|
||||
}
|
||||
radio = KissModemWrapper(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
radio_config=radio_config,
|
||||
auto_configure=True,
|
||||
)
|
||||
|
||||
if hasattr(radio, "begin"):
|
||||
try:
|
||||
radio.begin()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to initialize KISS modem: {e}") from e
|
||||
|
||||
return radio
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Unknown radio type: {radio_type}. Supported: sx1262")
|
||||
raise RuntimeError(
|
||||
f"Unknown radio type: {radio_type}. Supported: sx1262, kiss (or kiss-modem)"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,8 @@ 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_cfg = config.get("storage", {})
|
||||
self.storage_dir = Path(storage_cfg.get("storage_dir", "/var/lib/pymc_repeater"))
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
node_name = config.get("repeater", {}).get("node_name", "unknown")
|
||||
|
||||
@@ -56,7 +56,11 @@ class RepeaterDaemon:
|
||||
logger.info("Initializing radio hardware...")
|
||||
try:
|
||||
self.radio = get_radio_for_board(self.config)
|
||||
|
||||
|
||||
# KISS modem: schedule RX callbacks on the event loop for thread safety
|
||||
if hasattr(self.radio, "set_event_loop"):
|
||||
self.radio.set_event_loop(asyncio.get_running_loop())
|
||||
|
||||
if hasattr(self.radio, 'set_custom_cad_thresholds'):
|
||||
# Load CAD settings from config, with defaults
|
||||
cad_config = self.config.get("radio", {}).get("cad", {})
|
||||
|
||||
@@ -232,20 +232,17 @@ class APIEndpoints:
|
||||
def needs_setup(self):
|
||||
"""Check if the repeater needs initial setup configuration"""
|
||||
try:
|
||||
# Check if config has default values that indicate first-time setup
|
||||
config = self.config
|
||||
|
||||
# Check for default node name
|
||||
|
||||
# Check for default values that indicate first-time setup
|
||||
node_name = config.get('repeater', {}).get('node_name', '')
|
||||
has_default_name = node_name in ['mesh-repeater-01', '']
|
||||
|
||||
# Check for default admin password
|
||||
|
||||
admin_password = config.get('repeater', {}).get('security', {}).get('admin_password', '')
|
||||
has_default_password = admin_password in ['admin123', '']
|
||||
|
||||
# Needs setup if either condition is true
|
||||
|
||||
needs_setup = has_default_name or has_default_password
|
||||
|
||||
|
||||
return {'needs_setup': needs_setup, 'reasons': {
|
||||
'default_name': has_default_name,
|
||||
'default_password': has_default_password
|
||||
@@ -262,32 +259,35 @@ 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_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):
|
||||
logger.error(f"Hardware file not found. Tried: {installed_path}, {dev_path}")
|
||||
return {'error': 'Hardware configuration file not found', 'hardware': []}
|
||||
|
||||
with open(hardware_file, 'r') as f:
|
||||
hardware_data = json.load(f)
|
||||
|
||||
# Parse hardware options from the "hardware" key
|
||||
hardware_list = []
|
||||
hardware_configs = hardware_data.get('hardware', {})
|
||||
|
||||
for hw_key, hw_config in hardware_configs.items():
|
||||
if isinstance(hw_config, dict):
|
||||
hardware_list.append({
|
||||
'key': hw_key,
|
||||
'name': hw_config.get('name', hw_key),
|
||||
'description': hw_config.get('description', ''),
|
||||
'config': hw_config
|
||||
})
|
||||
|
||||
|
||||
if os.path.exists(hardware_file):
|
||||
with open(hardware_file, 'r') as f:
|
||||
hardware_data = json.load(f)
|
||||
hardware_configs = hardware_data.get('hardware', {})
|
||||
for hw_key, hw_config in hardware_configs.items():
|
||||
if isinstance(hw_config, dict):
|
||||
hardware_list.append({
|
||||
'key': hw_key,
|
||||
'name': hw_config.get('name', hw_key),
|
||||
'description': hw_config.get('description', ''),
|
||||
'config': hw_config
|
||||
})
|
||||
|
||||
# Add MeshCore KISS modem option (serial TNC)
|
||||
hardware_list.append({
|
||||
'key': 'kiss',
|
||||
'name': 'KISS modem (serial)',
|
||||
'description': 'MeshCore KISS modem over serial – requires pyMC_core with KISS support',
|
||||
'config': {}
|
||||
})
|
||||
|
||||
return {'hardware': hardware_list}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading hardware options: {e}")
|
||||
@@ -301,7 +301,8 @@ 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_cfg = self.config.get("storage", {})
|
||||
config_dir = Path(storage_cfg.get("storage_dir", "/var/lib/pymc_repeater"))
|
||||
installed_path = config_dir / 'radio-presets.json'
|
||||
dev_path = os.path.join(os.path.dirname(__file__), '..', '..', 'radio-presets.json')
|
||||
|
||||
@@ -351,105 +352,100 @@ class APIEndpoints:
|
||||
if not admin_password or len(admin_password) < 6:
|
||||
return {'success': False, 'error': 'Admin password must be at least 6 characters'}
|
||||
|
||||
# Load hardware configuration - check installed path first, then dev path
|
||||
import json
|
||||
config_dir = Path(self.config.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):
|
||||
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)
|
||||
|
||||
# Get hardware config from nested "hardware" key
|
||||
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}'}
|
||||
|
||||
# Prepare configuration updates
|
||||
import yaml
|
||||
|
||||
# Read current config
|
||||
|
||||
# Read current config first so we can update it
|
||||
with open(self._config_path, 'r') as f:
|
||||
config_yaml = yaml.safe_load(f)
|
||||
|
||||
|
||||
# Update repeater settings
|
||||
if 'repeater' not in config_yaml:
|
||||
config_yaml['repeater'] = {}
|
||||
config_yaml['repeater']['node_name'] = node_name
|
||||
|
||||
|
||||
if 'security' not in config_yaml['repeater']:
|
||||
config_yaml['repeater']['security'] = {}
|
||||
config_yaml['repeater']['security']['admin_password'] = admin_password
|
||||
|
||||
# Update radio settings - convert MHz/kHz to Hz
|
||||
|
||||
# Update radio settings - convert MHz/kHz to Hz (used for both SX1262 and KISS modem)
|
||||
if 'radio' not in config_yaml:
|
||||
config_yaml['radio'] = {}
|
||||
|
||||
freq_mhz = float(radio_preset.get('frequency', 0))
|
||||
bw_khz = float(radio_preset.get('bandwidth', 0))
|
||||
|
||||
config_yaml['radio']['frequency'] = int(freq_mhz * 1000000)
|
||||
config_yaml['radio']['spreading_factor'] = int(radio_preset.get('spreading_factor', 7))
|
||||
config_yaml['radio']['bandwidth'] = int(bw_khz * 1000)
|
||||
config_yaml['radio']['coding_rate'] = int(radio_preset.get('coding_rate', 5))
|
||||
|
||||
# Handle hardware-specific TX power (can be overridden by user later)
|
||||
if 'tx_power' in hw_config:
|
||||
config_yaml['radio']['tx_power'] = hw_config.get('tx_power', 22)
|
||||
|
||||
# Handle preamble length (goes in radio section)
|
||||
if 'preamble_length' in hw_config:
|
||||
config_yaml['radio']['preamble_length'] = hw_config.get('preamble_length', 17)
|
||||
|
||||
# Update hardware-specific settings under sx1262 section
|
||||
if 'sx1262' not in config_yaml:
|
||||
config_yaml['sx1262'] = {}
|
||||
|
||||
# SPI configuration
|
||||
if 'bus_id' in hw_config:
|
||||
config_yaml['sx1262']['bus_id'] = hw_config.get('bus_id', 0)
|
||||
if 'cs_id' in hw_config:
|
||||
config_yaml['sx1262']['cs_id'] = hw_config.get('cs_id', 0)
|
||||
|
||||
# Pin configuration
|
||||
if 'reset_pin' in hw_config:
|
||||
config_yaml['sx1262']['reset_pin'] = hw_config.get('reset_pin', 22)
|
||||
if 'busy_pin' in hw_config:
|
||||
config_yaml['sx1262']['busy_pin'] = hw_config.get('busy_pin', 17)
|
||||
if 'irq_pin' in hw_config:
|
||||
config_yaml['sx1262']['irq_pin'] = hw_config.get('irq_pin', 16)
|
||||
if 'txen_pin' in hw_config:
|
||||
config_yaml['sx1262']['txen_pin'] = hw_config.get('txen_pin', -1)
|
||||
if 'rxen_pin' in hw_config:
|
||||
config_yaml['sx1262']['rxen_pin'] = hw_config.get('rxen_pin', -1)
|
||||
if 'cs_pin' in hw_config:
|
||||
config_yaml['sx1262']['cs_pin'] = hw_config.get('cs_pin', -1)
|
||||
if 'txled_pin' in hw_config:
|
||||
config_yaml['sx1262']['txled_pin'] = hw_config.get('txled_pin', -1)
|
||||
if 'rxled_pin' in hw_config:
|
||||
config_yaml['sx1262']['rxled_pin'] = hw_config.get('rxled_pin', -1)
|
||||
|
||||
# Hardware flags
|
||||
if 'use_dio3_tcxo' in hw_config:
|
||||
config_yaml['sx1262']['use_dio3_tcxo'] = hw_config.get('use_dio3_tcxo', False)
|
||||
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)
|
||||
|
||||
|
||||
if hardware_key == 'kiss':
|
||||
# KISS modem: set radio_type and kiss section (port/baud from request or defaults)
|
||||
config_yaml['radio_type'] = 'kiss'
|
||||
kiss_port = (data.get('kiss_port') or '').strip() or '/dev/ttyUSB0'
|
||||
kiss_baud = int(data.get('kiss_baud_rate', data.get('kiss_baud', 115200)))
|
||||
config_yaml['kiss'] = {'port': kiss_port, 'baud_rate': kiss_baud}
|
||||
config_yaml['radio']['tx_power'] = int(radio_preset.get('tx_power', 14))
|
||||
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}'}
|
||||
|
||||
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:
|
||||
config_yaml['radio']['preamble_length'] = hw_config.get('preamble_length', 17)
|
||||
|
||||
if 'sx1262' not in config_yaml:
|
||||
config_yaml['sx1262'] = {}
|
||||
if 'bus_id' in hw_config:
|
||||
config_yaml['sx1262']['bus_id'] = hw_config.get('bus_id', 0)
|
||||
if 'cs_id' in hw_config:
|
||||
config_yaml['sx1262']['cs_id'] = hw_config.get('cs_id', 0)
|
||||
if 'reset_pin' in hw_config:
|
||||
config_yaml['sx1262']['reset_pin'] = hw_config.get('reset_pin', 22)
|
||||
if 'busy_pin' in hw_config:
|
||||
config_yaml['sx1262']['busy_pin'] = hw_config.get('busy_pin', 17)
|
||||
if 'irq_pin' in hw_config:
|
||||
config_yaml['sx1262']['irq_pin'] = hw_config.get('irq_pin', 16)
|
||||
if 'txen_pin' in hw_config:
|
||||
config_yaml['sx1262']['txen_pin'] = hw_config.get('txen_pin', -1)
|
||||
if 'rxen_pin' in hw_config:
|
||||
config_yaml['sx1262']['rxen_pin'] = hw_config.get('rxen_pin', -1)
|
||||
if 'cs_pin' in hw_config:
|
||||
config_yaml['sx1262']['cs_pin'] = hw_config.get('cs_pin', -1)
|
||||
if 'txled_pin' in hw_config:
|
||||
config_yaml['sx1262']['txled_pin'] = hw_config.get('txled_pin', -1)
|
||||
if 'rxled_pin' in hw_config:
|
||||
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 '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)
|
||||
|
||||
logger.info(f"Setup wizard completed: node_name={node_name}, hardware={hardware_key}, freq={freq_mhz}MHz")
|
||||
|
||||
logger.info(
|
||||
f"Setup wizard completed: node_name={node_name}, hardware={hardware_key}, freq={freq_mhz}MHz"
|
||||
)
|
||||
|
||||
# Trigger service restart after setup
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -467,18 +463,19 @@ class APIEndpoints:
|
||||
restart_thread = threading.Thread(target=delayed_restart, daemon=True)
|
||||
restart_thread.start()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Setup completed successfully. Service is restarting...',
|
||||
'config': {
|
||||
'node_name': node_name,
|
||||
'hardware': hardware_key,
|
||||
'frequency': freq_mhz,
|
||||
'spreading_factor': radio_preset.get('spreading_factor'),
|
||||
'bandwidth': radio_preset.get('bandwidth'),
|
||||
'coding_rate': radio_preset.get('coding_rate')
|
||||
}
|
||||
result_config = {
|
||||
'node_name': node_name,
|
||||
'hardware': hardware_key,
|
||||
'radio_type': config_yaml.get('radio_type', 'sx1262'),
|
||||
'frequency': freq_mhz,
|
||||
'spreading_factor': radio_preset.get('spreading_factor'),
|
||||
'bandwidth': radio_preset.get('bandwidth'),
|
||||
'coding_rate': radio_preset.get('coding_rate')
|
||||
}
|
||||
if hardware_key == 'kiss':
|
||||
result_config['kiss_port'] = config_yaml.get('kiss', {}).get('port')
|
||||
result_config['kiss_baud_rate'] = config_yaml.get('kiss', {}).get('baud_rate')
|
||||
return {'success': True, 'message': 'Setup completed successfully. Service is restarting...', 'config': result_config}
|
||||
|
||||
except cherrypy.HTTPError:
|
||||
raise
|
||||
@@ -1307,15 +1304,31 @@ class APIEndpoints:
|
||||
return self._error("Advert interval must be 0 (off) or 1-10080 minutes")
|
||||
self.config["repeater"]["advert_interval_minutes"] = mins
|
||||
applied.append(f"advert.interval={mins}m")
|
||||
|
||||
# KISS modem settings (only when radio_type is kiss)
|
||||
if "kiss_port" in data or "kiss_baud_rate" in data:
|
||||
if self.config.get("radio_type") != "kiss":
|
||||
return self._error("KISS settings only apply when radio_type is kiss")
|
||||
if "kiss" not in self.config:
|
||||
self.config["kiss"] = {}
|
||||
if "kiss_port" in data:
|
||||
self.config["kiss"]["port"] = str(data["kiss_port"]).strip()
|
||||
applied.append("kiss.port")
|
||||
if "kiss_baud_rate" in data:
|
||||
self.config["kiss"]["baud_rate"] = int(data["kiss_baud_rate"])
|
||||
applied.append("kiss.baud_rate")
|
||||
|
||||
if not applied:
|
||||
return self._error("No valid settings provided")
|
||||
|
||||
live_sections = ['repeater', 'delays', 'radio']
|
||||
if "kiss" in self.config:
|
||||
live_sections.append("kiss")
|
||||
# Save to config file and live update daemon in one operation
|
||||
result = self.config_manager.update_and_save(
|
||||
updates={}, # Updates already applied to self.config above
|
||||
live_update=True,
|
||||
live_update_sections=['repeater', 'delays', 'radio']
|
||||
live_update_sections=live_sections
|
||||
)
|
||||
|
||||
logger.info(f"Radio config updated: {', '.join(applied)}")
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -43,56 +43,76 @@ repeater_name=${repeater_name:-$default_name}
|
||||
|
||||
echo "Repeater name: $repeater_name"
|
||||
echo ""
|
||||
echo "=== Step 1: Select Hardware ==="
|
||||
|
||||
# Step 0.5: Radio type (SX1262 hardware vs KISS modem)
|
||||
echo "=== Step 0.5: Select Radio Type ==="
|
||||
echo ""
|
||||
echo " 1) SX1262 hardware (SPI LoRa module - Raspberry Pi HAT, etc.)"
|
||||
echo " 2) KISS modem (serial TNC - requires pyMC_core with KISS support)"
|
||||
echo ""
|
||||
read -p "Select radio type (1 or 2): " radio_type_sel
|
||||
|
||||
if [ ! -f "$HARDWARE_CONFIG" ]; then
|
||||
echo "Error: Hardware configuration file not found at $HARDWARE_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$radio_type_sel" = "2" ]; then
|
||||
RADIO_TYPE="kiss"
|
||||
hw_key="kiss"
|
||||
hw_name="KISS modem"
|
||||
echo "Selected: $hw_name"
|
||||
echo ""
|
||||
else
|
||||
RADIO_TYPE="sx1262"
|
||||
echo "Selected: SX1262 hardware"
|
||||
echo ""
|
||||
echo "=== Step 1: Select Hardware ==="
|
||||
echo ""
|
||||
|
||||
# Parse hardware options from radio-settings.json
|
||||
hw_index=0
|
||||
declare -a hw_keys
|
||||
declare -a hw_names
|
||||
|
||||
# Extract hardware keys and names using grep and sed
|
||||
hw_data=$(grep -o '"[^"]*":\s*{' "$HARDWARE_CONFIG" | grep -v hardware | sed 's/"\([^"]*\)".*/\1/' | while read hw_key; do
|
||||
hw_name=$(grep -A 1 "\"$hw_key\"" "$HARDWARE_CONFIG" | grep "\"name\"" | sed 's/.*"name":\s*"\([^"]*\)".*/\1/')
|
||||
if [ -n "$hw_name" ]; then
|
||||
echo "$hw_key|$hw_name"
|
||||
if [ ! -f "$HARDWARE_CONFIG" ]; then
|
||||
echo "Error: Hardware configuration file not found at $HARDWARE_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
done)
|
||||
|
||||
while IFS='|' read -r hw_key hw_name; do
|
||||
if [ -n "$hw_key" ] && [ -n "$hw_name" ]; then
|
||||
echo " $((hw_index + 1))) $hw_name ($hw_key)"
|
||||
hw_keys[$hw_index]="$hw_key"
|
||||
hw_names[$hw_index]="$hw_name"
|
||||
((hw_index++))
|
||||
# Parse hardware options from radio-settings.json
|
||||
hw_index=0
|
||||
declare -a hw_keys
|
||||
declare -a hw_names
|
||||
|
||||
# Extract hardware keys and names using grep and sed
|
||||
hw_data=$(grep -o '"[^"]*":\s*{' "$HARDWARE_CONFIG" | grep -v hardware | sed 's/"\([^"]*\)".*/\1/' | while read hw_key; do
|
||||
hw_name=$(grep -A 1 "\"$hw_key\"" "$HARDWARE_CONFIG" | grep "\"name\"" | sed 's/.*"name":\s*"\([^"]*\)".*/\1/')
|
||||
if [ -n "$hw_name" ]; then
|
||||
echo "$hw_key|$hw_name"
|
||||
fi
|
||||
done)
|
||||
|
||||
while IFS='|' read -r hw_key hw_name; do
|
||||
if [ -n "$hw_key" ] && [ -n "$hw_name" ]; then
|
||||
echo " $((hw_index + 1))) $hw_name ($hw_key)"
|
||||
hw_keys[$hw_index]="$hw_key"
|
||||
hw_names[$hw_index]="$hw_name"
|
||||
((hw_index++))
|
||||
fi
|
||||
done <<< "$hw_data"
|
||||
|
||||
if [ "$hw_index" -eq 0 ]; then
|
||||
echo "Error: No hardware configurations found"
|
||||
exit 1
|
||||
fi
|
||||
done <<< "$hw_data"
|
||||
|
||||
if [ "$hw_index" -eq 0 ]; then
|
||||
echo "Error: No hardware configurations found"
|
||||
exit 1
|
||||
echo ""
|
||||
read -p "Select hardware (1-$hw_index): " hw_selection
|
||||
|
||||
if ! [ "$hw_selection" -ge 1 ] 2>/dev/null || [ "$hw_selection" -gt "$hw_index" ]; then
|
||||
echo "Error: Invalid selection"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
selected_hw=$((hw_selection - 1))
|
||||
hw_key="${hw_keys[$selected_hw]}"
|
||||
hw_name="${hw_names[$selected_hw]}"
|
||||
|
||||
echo "Selected: $hw_name"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
read -p "Select hardware (1-$hw_index): " hw_selection
|
||||
|
||||
if ! [ "$hw_selection" -ge 1 ] 2>/dev/null || [ "$hw_selection" -gt "$hw_index" ]; then
|
||||
echo "Error: Invalid selection"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
selected_hw=$((hw_selection - 1))
|
||||
hw_key="${hw_keys[$selected_hw]}"
|
||||
hw_name="${hw_names[$selected_hw]}"
|
||||
|
||||
echo "Selected: $hw_name"
|
||||
echo ""
|
||||
|
||||
# Step 2: Radio Settings Selection
|
||||
echo "=== Step 2: Select Radio Settings ==="
|
||||
echo ""
|
||||
@@ -179,14 +199,44 @@ echo "Selected: $title"
|
||||
echo "Frequency: ${freq}MHz, SF: $sf, BW: $bw, CR: $cr"
|
||||
echo ""
|
||||
|
||||
# Update config.yaml
|
||||
# KISS modem: prompt for serial port and baud rate
|
||||
if [ "$RADIO_TYPE" = "kiss" ]; then
|
||||
echo "=== KISS Modem Settings ==="
|
||||
echo ""
|
||||
default_port="/dev/ttyUSB0"
|
||||
read -p "Serial port [$default_port]: " kiss_port
|
||||
kiss_port=${kiss_port:-$default_port}
|
||||
default_baud="9600"
|
||||
read -p "Baud rate [$default_baud]: " kiss_baud
|
||||
kiss_baud=${kiss_baud:-$default_baud}
|
||||
echo "KISS: port=$kiss_port, baud_rate=$kiss_baud"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Ensure config file exists (create from example if missing)
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: Config file not found at $CONFIG_FILE"
|
||||
exit 1
|
||||
if [ -f "$CONFIG_DIR/config.yaml.example" ]; then
|
||||
cp "$CONFIG_DIR/config.yaml.example" "$CONFIG_FILE"
|
||||
echo "Created $CONFIG_FILE from config.yaml.example"
|
||||
elif [ -f "$SCRIPT_DIR/config.yaml.example" ]; then
|
||||
cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_FILE"
|
||||
echo "Created $CONFIG_FILE from $SCRIPT_DIR/config.yaml.example"
|
||||
else
|
||||
echo "Error: Config file not found at $CONFIG_FILE"
|
||||
echo "Copy config.yaml.example to config.yaml or run from a directory that has it."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Updating configuration..."
|
||||
|
||||
# Radio type (sx1262 or kiss)
|
||||
if grep -q "^radio_type:" "$CONFIG_FILE"; then
|
||||
sed "${SED_OPTS[@]}" "s/^radio_type:.*/radio_type: $RADIO_TYPE/" "$CONFIG_FILE"
|
||||
else
|
||||
{ echo "radio_type: $RADIO_TYPE"; cat "$CONFIG_FILE"; } > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Repeater name
|
||||
sed "${SED_OPTS[@]}" "s/^ node_name:.*/ node_name: \"$repeater_name\"/" "$CONFIG_FILE"
|
||||
|
||||
@@ -196,7 +246,18 @@ sed "${SED_OPTS[@]}" "s/^ spreading_factor:.*/ spreading_factor: $sf/" "$CONFI
|
||||
sed "${SED_OPTS[@]}" "s/^ bandwidth:.*/ bandwidth: $bw_hz/" "$CONFIG_FILE"
|
||||
sed "${SED_OPTS[@]}" "s/^ coding_rate:.*/ coding_rate: $cr/" "$CONFIG_FILE"
|
||||
|
||||
# Extract hardware-specific settings from radio-settings.json
|
||||
# KISS modem: update kiss section
|
||||
if [ "$RADIO_TYPE" = "kiss" ]; then
|
||||
if grep -q "^kiss:" "$CONFIG_FILE"; then
|
||||
sed "${SED_OPTS[@]}" "s/^ port:.*/ port: \"$kiss_port\"/" "$CONFIG_FILE"
|
||||
sed "${SED_OPTS[@]}" "s/^ baud_rate:.*/ baud_rate: $kiss_baud/" "$CONFIG_FILE"
|
||||
else
|
||||
printf '\nkiss:\n port: "%s"\n baud_rate: %s\n' "$kiss_port" "$kiss_baud" >> "$CONFIG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract hardware-specific settings from radio-settings.json (SX1262 only)
|
||||
if [ "$RADIO_TYPE" = "sx1262" ]; then
|
||||
echo "Extracting hardware configuration from $HARDWARE_CONFIG..."
|
||||
|
||||
# Use jq to extract all fields from the selected hardware
|
||||
@@ -285,6 +346,7 @@ else
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/radio_*_* "$CONFIG_FILE.bak"
|
||||
@@ -293,14 +355,19 @@ echo "Configuration updated successfully!"
|
||||
echo ""
|
||||
echo "Applied Configuration:"
|
||||
echo " Repeater Name: $repeater_name"
|
||||
echo " Radio Type: $RADIO_TYPE"
|
||||
echo " Hardware: $hw_name ($hw_key)"
|
||||
echo " Frequency: ${freq}MHz (${freq_hz}Hz)"
|
||||
echo " Spreading Factor: $sf"
|
||||
echo " Bandwidth: ${bw}kHz (${bw_hz}Hz)"
|
||||
echo " Coding Rate: $cr"
|
||||
if [ "$RADIO_TYPE" = "kiss" ]; then
|
||||
echo " KISS Port: $kiss_port"
|
||||
echo " KISS Baud Rate: $kiss_baud"
|
||||
fi
|
||||
echo ""
|
||||
echo "Hardware GPIO Configuration:"
|
||||
if [ -n "$bus_id" ]; then
|
||||
if [ "$RADIO_TYPE" = "sx1262" ] && [ -n "$bus_id" ]; then
|
||||
echo " Bus ID: $bus_id"
|
||||
echo " Chip Select: $cs_id (pin $cs_pin)"
|
||||
echo " Reset Pin: $reset_pin"
|
||||
|
||||
Reference in New Issue
Block a user