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:
agessaman
2026-02-03 16:20:51 -08:00
parent 076d87dcde
commit 878ff8dc51
11 changed files with 329 additions and 174 deletions

View File

@@ -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:

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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",

View File

@@ -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)"
)

View File

@@ -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")

View File

@@ -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", {})

View File

@@ -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

View File

@@ -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"