diff --git a/README.md b/README.md index af59ad9..da5f638 100644 --- a/README.md +++ b/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://: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: diff --git a/config.yaml.example b/config.yaml.example index 9f96c83..e1498b3 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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 diff --git a/data/repeater.db b/data/repeater.db new file mode 100644 index 0000000..48bc38a Binary files /dev/null and b/data/repeater.db differ diff --git a/manage.sh b/manage.sh index e37ce5b..57087d7 100755 --- a/manage.sh +++ b/manage.sh @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 075c7e8..7bfc376 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/repeater/config.py b/repeater/config.py index ffe9268..5bd9ffa 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -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)" + ) diff --git a/repeater/data_acquisition/storage_collector.py b/repeater/data_acquisition/storage_collector.py index 1019415..f139151 100644 --- a/repeater/data_acquisition/storage_collector.py +++ b/repeater/data_acquisition/storage_collector.py @@ -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") diff --git a/repeater/main.py b/repeater/main.py index dd9c785..365e7f9 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -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", {}) diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index e3c3155..857f403 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -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)}") diff --git a/repeater/web/html/assets/Setup-CSawSnc5.js b/repeater/web/html/assets/Setup-CSawSnc5.js index 82e1677..6ce3729 100644 --- a/repeater/web/html/assets/Setup-CSawSnc5.js +++ b/repeater/web/html/assets/Setup-CSawSnc5.js @@ -1 +1 @@ -import{d as A,r as l,c as P,a as W,o as I,b as a,e,f as B,_ as Y,t as i,u as o,n as J,g as k,F as N,h as z,i as K,w as h,v as C,j as _,k as V,l as q,T,m as Q,p as n,q as E,s as X,x as Z}from"./index-sHch0610.js";const ee=A("setup",()=>{const m=l(1),r=l(5),y=l(`pyRpt${Math.floor(Math.random()*1e4).toString().padStart(4,"0")}`),b=l(null),v=l(null),x=l(""),f=l(""),w=l(!1),c=l({frequency:"915.0",spreading_factor:"7",bandwidth:"125",coding_rate:"5"}),R=l([]),j=l([]),g=l(!1),S=l(!1),u=l(null),t=P(()=>{switch(m.value){case 1:return!0;case 2:return y.value.trim().length>0;case 3:return b.value!==null;case 4:return w.value?c.value.frequency&&c.value.spreading_factor&&c.value.bandwidth&&c.value.coding_rate:v.value!==null;case 5:return x.value.length>=6&&x.value===f.value;default:return!1}}),s=P(()=>m.value>1),M=P(()=>m.value===r.value);async function F(){g.value=!0,u.value=null;try{const p=await(await fetch("/api/hardware_options")).json();if(p.error)throw new Error(p.error);R.value=p.hardware||[]}catch(d){u.value=d instanceof Error?d.message:"Failed to load hardware options",console.error("Error fetching hardware options:",d)}finally{g.value=!1}}async function H(){g.value=!0,u.value=null;try{const p=await(await fetch("/api/radio_presets")).json();if(p.error)throw new Error(p.error);j.value=p.presets||[]}catch(d){u.value=d instanceof Error?d.message:"Failed to load radio presets",console.error("Error fetching radio presets:",d)}finally{g.value=!1}}async function U(){if(!t.value)return{success:!1,error:"Please complete all required fields"};S.value=!0,u.value=null;try{const d=w.value?{title:"Custom Configuration",description:"Custom radio settings",frequency:c.value.frequency,spreading_factor:c.value.spreading_factor,bandwidth:c.value.bandwidth,coding_rate:c.value.coding_rate}:v.value,L=await(await fetch("/api/setup_wizard",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({node_name:y.value.trim(),hardware_key:b.value?.key,radio_preset:d,admin_password:x.value})})).json();if(!L.success)throw new Error(L.error||"Setup failed");return{success:!0,data:L}}catch(d){const p=d instanceof Error?d.message:"Failed to complete setup";return u.value=p,{success:!1,error:p}}finally{S.value=!1}}function O(){t.value&&m.value=1&&d<=r.value&&(m.value=d)}function D(){m.value=1,y.value=`pyRpt${Math.floor(Math.random()*1e4).toString().padStart(4,"0")}`,b.value=null,v.value=null,w.value=!1,c.value={frequency:"915.0",spreading_factor:"7",bandwidth:"125",coding_rate:"5"},x.value="",f.value="",u.value=null}return{currentStep:m,totalSteps:r,nodeName:y,selectedHardware:b,selectedRadioPreset:v,useCustomRadio:w,customRadio:c,adminPassword:x,confirmPassword:f,hardwareOptions:R,radioPresets:j,isLoading:g,isSubmitting:S,error:u,canGoNext:t,canGoBack:s,isLastStep:M,fetchHardwareOptions:F,fetchRadioPresets:H,completeSetup:U,nextStep:O,previousStep:$,goToStep:G,reset:D}}),te={class:"min-h-screen bg-background dark:bg-background overflow-hidden relative flex items-center justify-center p-4"},re={class:"absolute top-4 right-4 z-20"},oe={class:"w-full max-w-4xl relative z-10"},se={class:"mb-8"},ae={class:"flex justify-between mb-2"},ne={class:"text-content-secondary dark:text-content-muted text-sm"},de={class:"text-content-secondary dark:text-content-muted text-sm"},ie={class:"h-2 bg-stroke-subtle dark:bg-stroke/10 rounded-full overflow-hidden"},le={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[20px] p-6 sm:p-8 md:p-12"},ue={class:"flex justify-center mb-8"},ce={class:"flex gap-2"},pe={class:"mb-8"},me={class:"text-2xl sm:text-3xl font-bold text-content-primary dark:text-content-primary mb-2 text-center"},be={key:0,class:"space-y-6 mt-8"},fe={key:1,class:"space-y-6 mt-8"},xe={class:"max-w-md mx-auto"},ke={key:2,class:"space-y-6 mt-8"},ve={key:0,class:"text-center text-content-secondary dark:text-content-muted"},ge={key:1,class:"text-center text-content-secondary dark:text-content-muted"},ye={key:2,class:"grid grid-cols-1 md:grid-cols-2 gap-4 max-w-3xl mx-auto"},he=["onClick"],we={class:"font-medium text-content-primary dark:text-content-primary mb-1"},_e={class:"text-sm text-content-secondary dark:text-content-muted"},Se={key:3,class:"space-y-6 mt-8"},Ce={key:0,class:"text-center text-content-secondary dark:text-content-muted"},Re={key:1,class:"text-center text-content-secondary dark:text-content-muted"},je={key:2,class:"max-w-5xl mx-auto"},Pe={class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4"},Me=["onClick"],Le={class:"relative z-10"},Be={class:"font-medium text-content-primary dark:text-content-primary mb-1 flex items-start justify-between gap-2"},Ne={class:"flex items-center gap-2"},ze={class:"text-2xl"},Ve={key:0,class:"text-primary flex-shrink-0"},qe={class:"text-xs text-content-secondary dark:text-content-muted mb-3"},Te={class:"grid grid-cols-2 gap-2 text-xs"},Ee={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},Fe={class:"text-content-primary dark:text-content-primary/80 font-medium"},He={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},Ue={class:"text-content-primary dark:text-content-primary/80 font-medium"},Oe={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},$e={class:"text-content-primary dark:text-content-primary/80 font-medium"},Ge={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},De={class:"text-content-primary dark:text-content-primary/80 font-medium"},Ae={class:"border-t border-stroke-subtle dark:border-stroke/10 pt-6"},We={class:"flex items-center justify-between mb-2"},Ie={key:0,class:"text-primary"},Ye={key:0,class:"mt-4 grid grid-cols-2 gap-4"},Je={key:4,class:"space-y-6 mt-8"},Ke={class:"max-w-md mx-auto space-y-4"},Qe={key:0,class:"text-red-600 dark:text-red-400 text-sm"},Xe={key:0,class:"mb-6 bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-600 dark:text-red-200"},Ze={class:"flex justify-between gap-4"},et={key:1},tt=["disabled"],rt={key:0,class:"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"},ot={key:1},st={key:2},at={key:3},nt={key:4,class:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},dt={class:"flex justify-center mb-6"},it={key:0,class:"w-16 h-16 rounded-full bg-green-100 dark:bg-green-500/20 flex items-center justify-center"},lt={key:1,class:"w-16 h-16 rounded-full bg-red-100 dark:bg-red-500/20 flex items-center justify-center"},ut={class:"text-2xl font-bold text-content-primary dark:text-content-primary text-center mb-4"},ct={class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"},pt=W({name:"SetupView",__name:"Setup",setup(m){const r=ee(),y=Q(),b=l(!1),v=l(""),x=l(""),f=l("success"),w=u=>{const t=u.toLowerCase();return t.includes("australia")?"🇦🇺":t.includes("eu")||t.includes("uk")?"🇪🇺":t.includes("czech")?"🇨🇿":t.includes("new zealand")?"🇳🇿":t.includes("portugal")?"🇵🇹":t.includes("switzerland")?"🇨🇭":t.includes("usa")||t.includes("canada")?"🇺🇸":t.includes("vietnam")?"🇻🇳":"🌍"};I(async()=>{await Promise.all([r.fetchHardwareOptions(),r.fetchRadioPresets()])});const c=P(()=>r.currentStep/r.totalSteps*100);async function R(){if(r.isLastStep){const u=await r.completeSetup();u.success?(f.value="success",v.value="Setup Complete!",x.value="Your repeater has been configured successfully. The service is restarting now...",b.value=!0,setTimeout(()=>{b.value=!1,y.push("/login")},5e3)):(f.value="error",v.value="Setup Failed",x.value=u.error||"An unknown error occurred",b.value=!0)}else r.nextStep()}function j(){r.previousStep()}function g(){b.value=!1,f.value==="success"&&y.push("/login")}const S=["Welcome","Repeater Name","Hardware Selection","Radio Configuration","Security Setup"];return(u,t)=>(n(),a("div",te,[e("div",re,[B(Y)]),t[36]||(t[36]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-80 animate-pulse-slow -top-[79px] left-[575px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),t[37]||(t[37]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-75 animate-pulse-slower -top-[94px] -left-[92px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),t[38]||(t[38]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-80 animate-pulse-slowest top-[373px] left-[246px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),e("div",oe,[e("div",se,[e("div",ae,[e("span",ne,"Step "+i(o(r).currentStep)+" of "+i(o(r).totalSteps),1),e("span",de,i(Math.round(c.value))+"% Complete",1)]),e("div",ie,[e("div",{class:"h-full bg-gradient-to-r from-primary to-primary/80 transition-all duration-500",style:J({width:`${c.value}%`})},null,4)])]),e("div",le,[e("div",ue,[e("div",ce,[(n(!0),a(N,null,z(o(r).totalSteps,s=>(n(),a("div",{key:s,class:_(["w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-all",s===o(r).currentStep?"bg-primary text-white":s

Welcome to your pyMC Repeater! Let's get you set up in just a few steps.

You'll configure:

  • Repeater name and identification
  • Hardware board selection
  • Radio frequency and settings
  • Admin password for secure access
',1)]))):o(r).currentStep===2?(n(),a("div",fe,[t[12]||(t[12]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Choose a unique name for your repeater. This will be used for identification on the mesh network. ",-1)),e("div",xe,[t[10]||(t[10]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Repeater Name",-1)),h(e("input",{"onUpdate:modelValue":t[0]||(t[0]=s=>o(r).nodeName=s),type:"text",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"e.g., pyRpt0001",maxlength:"32"},null,512),[[C,o(r).nodeName]]),t[11]||(t[11]=e("p",{class:"text-content-secondary dark:text-content-muted text-xs mt-2"}," Use letters, numbers, hyphens, or underscores (3-32 characters) ",-1))])])):o(r).currentStep===3?(n(),a("div",ke,[t[13]||(t[13]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Select your hardware board type ",-1)),o(r).isLoading?(n(),a("div",ve," Loading hardware options... ")):o(r).hardwareOptions.length===0?(n(),a("div",ge," No hardware options available ")):(n(),a("div",ye,[(n(!0),a(N,null,z(o(r).hardwareOptions,s=>(n(),a("button",{key:s.key,onClick:M=>o(r).selectedHardware=s,class:_(["p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm",o(r).selectedHardware?.key===s.key?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",we,i(s.name),1),e("div",_e,i(s.description||s.key),1)],10,he))),128))]))])):o(r).currentStep===4?(n(),a("div",Se,[t[28]||(t[28]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Choose a radio configuration preset for your region or create a custom configuration ",-1)),o(r).isLoading?(n(),a("div",Ce," Loading radio presets... ")):o(r).radioPresets.length===0?(n(),a("div",Re," No radio presets available ")):(n(),a("div",je,[e("div",Pe,[(n(!0),a(N,null,z(o(r).radioPresets,s=>(n(),a("button",{key:s.title,onClick:M=>{o(r).selectedRadioPreset=s,o(r).useCustomRadio=!1},class:_(["p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm relative overflow-hidden",!o(r).useCustomRadio&&o(r).selectedRadioPreset?.title===s.title?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",Le,[e("div",Be,[e("span",Ne,[e("span",ze,i(w(s.title)),1),e("span",null,i(s.title),1)]),!o(r).useCustomRadio&&o(r).selectedRadioPreset?.title===s.title?(n(),a("div",Ve,t[14]||(t[14]=[e("svg",{class:"w-5 h-5",fill:"currentColor",viewBox:"0 0 20 20"},[e("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z","clip-rule":"evenodd"})],-1)]))):k("",!0)]),e("div",qe,i(s.description),1),e("div",Te,[e("div",Ee,[t[15]||(t[15]=e("div",{class:"text-content-muted dark:text-content-muted"},"Freq",-1)),e("div",Fe,i(s.frequency),1)]),e("div",He,[t[16]||(t[16]=e("div",{class:"text-content-muted dark:text-content-muted"},"BW",-1)),e("div",Ue,i(s.bandwidth),1)]),e("div",Oe,[t[17]||(t[17]=e("div",{class:"text-content-muted dark:text-content-muted"},"SF",-1)),e("div",$e,i(s.spreading_factor),1)]),e("div",Ge,[t[18]||(t[18]=e("div",{class:"text-content-muted dark:text-content-muted"},"CR",-1)),e("div",De,i(s.coding_rate),1)])])])],10,Me))),128))]),e("div",Ae,[e("button",{onClick:t[1]||(t[1]=s=>{o(r).useCustomRadio=!o(r).useCustomRadio,o(r).useCustomRadio&&(o(r).selectedRadioPreset=null)}),class:_(["w-full p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm",o(r).useCustomRadio?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",We,[t[20]||(t[20]=e("div",{class:"font-medium text-content-primary dark:text-content-primary flex items-center gap-2"},[e("svg",{class:"w-5 h-5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"})]),V(" Custom Configuration ")],-1)),o(r).useCustomRadio?(n(),a("div",Ie,t[19]||(t[19]=[e("svg",{class:"w-5 h-5",fill:"currentColor",viewBox:"0 0 20 20"},[e("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z","clip-rule":"evenodd"})],-1)]))):k("",!0)]),t[21]||(t[21]=e("div",{class:"text-xs text-content-secondary dark:text-content-muted"},"Manually configure frequency, bandwidth, spreading factor, and coding rate",-1))],2),B(T,{name:"slide"},{default:q(()=>[o(r).useCustomRadio?(n(),a("div",Ye,[e("div",null,[t[22]||(t[22]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Frequency (MHz)",-1)),h(e("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>o(r).customRadio.frequency=s),type:"number",step:"0.1",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all",placeholder:"915.0"},null,512),[[C,o(r).customRadio.frequency]])]),e("div",null,[t[23]||(t[23]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Bandwidth (kHz)",-1)),h(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>o(r).customRadio.bandwidth=s),type:"number",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all",placeholder:"125"},null,512),[[C,o(r).customRadio.bandwidth]])]),e("div",null,[t[25]||(t[25]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Spreading Factor",-1)),h(e("select",{"onUpdate:modelValue":t[4]||(t[4]=s=>o(r).customRadio.spreading_factor=s),class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all"},t[24]||(t[24]=[e("option",{value:"7"},"7",-1),e("option",{value:"8"},"8",-1),e("option",{value:"9"},"9",-1),e("option",{value:"10"},"10",-1),e("option",{value:"11"},"11",-1),e("option",{value:"12"},"12",-1)]),512),[[E,o(r).customRadio.spreading_factor]])]),e("div",null,[t[27]||(t[27]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Coding Rate",-1)),h(e("select",{"onUpdate:modelValue":t[5]||(t[5]=s=>o(r).customRadio.coding_rate=s),class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all"},t[26]||(t[26]=[e("option",{value:"5"},"4/5",-1),e("option",{value:"6"},"4/6",-1),e("option",{value:"7"},"4/7",-1),e("option",{value:"8"},"4/8",-1)]),512),[[E,o(r).customRadio.coding_rate]])])])):k("",!0)]),_:1})])]))])):o(r).currentStep===5?(n(),a("div",Je,[t[32]||(t[32]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Set a secure admin password to protect your repeater ",-1)),e("div",Ke,[e("div",null,[t[29]||(t[29]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Admin Password",-1)),h(e("input",{"onUpdate:modelValue":t[6]||(t[6]=s=>o(r).adminPassword=s),type:"password",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"Enter password (min 6 characters)",minlength:"6"},null,512),[[C,o(r).adminPassword]])]),e("div",null,[t[30]||(t[30]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Confirm Password",-1)),h(e("input",{"onUpdate:modelValue":t[7]||(t[7]=s=>o(r).confirmPassword=s),type:"password",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"Confirm password"},null,512),[[C,o(r).confirmPassword]])]),o(r).adminPassword&&o(r).confirmPassword&&o(r).adminPassword!==o(r).confirmPassword?(n(),a("div",Qe," Passwords do not match ")):k("",!0),t[31]||(t[31]=e("div",{class:"bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3 text-sm text-yellow-800 dark:text-yellow-200"},[e("strong",null,"Important:"),V(" Remember this password - you'll need it to access the dashboard. ")],-1))])])):k("",!0)]),o(r).error?(n(),a("div",Xe,i(o(r).error),1)):k("",!0),e("div",Ze,[o(r).canGoBack?(n(),a("button",{key:0,onClick:j,class:"px-6 py-3 rounded-[12px] bg-background-mute dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 text-content-primary dark:text-content-primary hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20 transition-all duration-300 font-medium"}," Back ")):(n(),a("div",et)),e("button",{onClick:R,disabled:!o(r).canGoNext||o(r).isSubmitting,class:_(["px-8 py-3 rounded-[12px] font-semibold transition-all duration-300 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed",o(r).canGoNext&&!o(r).isSubmitting?"bg-gradient-to-r from-primary/20 to-primary/10 hover:from-primary/30 hover:to-primary/20 text-white border border-primary/30 hover:border-primary/50":"bg-background-mute dark:bg-stroke/5 text-content-muted dark:text-content-muted border border-stroke-subtle dark:border-stroke/10"])},[o(r).isSubmitting?(n(),a("div",rt)):k("",!0),o(r).isSubmitting?(n(),a("span",ot,"Setting up...")):o(r).isLastStep?(n(),a("span",st,"Complete Setup")):(n(),a("span",at,"Next")),!o(r).isSubmitting&&!o(r).isLastStep?(n(),a("svg",nt,t[33]||(t[33]=[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M9 5l7 7-7 7"},null,-1)]))):k("",!0)],10,tt)])])]),B(T,{name:"modal"},{default:q(()=>[b.value?(n(),a("div",{key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:g},[e("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl max-w-md w-full p-8 rounded-[24px] border border-stroke-subtle dark:border-white/20 shadow-[0_8px_32px_0_rgba(0,0,0,0.37)]",onClick:t[8]||(t[8]=X(()=>{},["stop"]))},[e("div",dt,[f.value==="success"?(n(),a("div",it,t[34]||(t[34]=[e("svg",{class:"w-8 h-8 text-green-600 dark:text-green-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})],-1)]))):(n(),a("div",lt,t[35]||(t[35]=[e("svg",{class:"w-8 h-8 text-red-600 dark:text-red-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])))]),e("h3",ut,i(v.value),1),e("p",ct,i(x.value),1),e("button",{onClick:g,class:_(["w-full px-6 py-3 rounded-lg font-medium transition-all",f.value==="success"?"bg-gradient-to-r from-primary/20 to-primary/10 hover:from-primary/30 hover:to-primary/20 text-white":"bg-gradient-to-r from-red-500/20 to-red-500/10 hover:from-red-500/30 hover:to-red-500/20 text-white"])},i(f.value==="success"?"Continue to Login":"Close"),3)])])):k("",!0)]),_:1})]))}}),bt=Z(pt,[["__scopeId","data-v-20a8772f"]]);export{bt as default}; +import{d as A,r as l,c as P,a as W,o as I,b as a,e,f as B,_ as Y,t as i,u as o,n as J,g as k,F as N,h as z,i as K,w as h,v as C,j as _,k as V,l as q,T,m as Q,p as n,q as E,s as X,x as Z}from"./index-sHch0610.js";const ee=A("setup",()=>{const m=l(1),r=l(5),y=l(`pyRpt${Math.floor(Math.random()*1e4).toString().padStart(4,"0")}`),b=l(null),v=l(null),x=l(""),f=l(""),w=l(!1),c=l({frequency:"915.0",spreading_factor:"7",bandwidth:"125",coding_rate:"5"}),R=l([]),j=l([]),g=l(!1),S=l(!1),u=l(null),kp=l("/dev/ttyUSB0"),kb=l("115200"),t=P(()=>{switch(m.value){case 1:return!0;case 2:return y.value.trim().length>0;case 3:return b.value!==null&&(b.value?.key!=="kiss"||(kp.value&&String(kp.value).trim().length>0));case 4:return w.value?c.value.frequency&&c.value.spreading_factor&&c.value.bandwidth&&c.value.coding_rate:v.value!==null;case 5:return x.value.length>=6&&x.value===f.value;default:return!1}}),s=P(()=>m.value>1),M=P(()=>m.value===r.value);async function F(){g.value=!0,u.value=null;try{const p=await(await fetch("/api/hardware_options")).json();if(p.error)throw new Error(p.error);R.value=p.hardware||[]}catch(d){u.value=d instanceof Error?d.message:"Failed to load hardware options",console.error("Error fetching hardware options:",d)}finally{g.value=!1}}async function H(){g.value=!0,u.value=null;try{const p=await(await fetch("/api/radio_presets")).json();if(p.error)throw new Error(p.error);j.value=p.presets||[]}catch(d){u.value=d instanceof Error?d.message:"Failed to load radio presets",console.error("Error fetching radio presets:",d)}finally{g.value=!1}}async function U(){if(!t.value)return{success:!1,error:"Please complete all required fields"};S.value=!0,u.value=null;try{const d=w.value?{title:"Custom Configuration",description:"Custom radio settings",frequency:c.value.frequency,spreading_factor:c.value.spreading_factor,bandwidth:c.value.bandwidth,coding_rate:c.value.coding_rate}:v.value;const payload={node_name:y.value.trim(),hardware_key:b.value?.key,radio_preset:d,admin_password:x.value};if(b.value?.key==="kiss"){payload.kiss_port=kp.value||"/dev/ttyUSB0";payload.kiss_baud_rate=Number(kb.value)||115200;}const L=await(await fetch("/api/setup_wizard",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)})).json();if(!L.success)throw new Error(L.error||"Setup failed");return{success:!0,data:L}}catch(d){const p=d instanceof Error?d.message:"Failed to complete setup";return u.value=p,{success:!1,error:p}}finally{S.value=!1}}function O(){t.value&&m.value=1&&d<=r.value&&(m.value=d)}function D(){m.value=1,y.value=`pyRpt${Math.floor(Math.random()*1e4).toString().padStart(4,"0")}`,b.value=null,v.value=null,w.value=!1,c.value={frequency:"915.0",spreading_factor:"7",bandwidth:"125",coding_rate:"5"},x.value="",f.value="",u.value=null,kp.value="/dev/ttyUSB0",kb.value="115200"}return{currentStep:m,totalSteps:r,nodeName:y,selectedHardware:b,selectedRadioPreset:v,useCustomRadio:w,customRadio:c,adminPassword:x,confirmPassword:f,hardwareOptions:R,radioPresets:j,isLoading:g,isSubmitting:S,error:u,canGoNext:t,canGoBack:s,isLastStep:M,fetchHardwareOptions:F,fetchRadioPresets:H,completeSetup:U,nextStep:O,previousStep:$,goToStep:G,reset:D,kissPort:kp,kissBaud:kb}}),te={class:"min-h-screen bg-background dark:bg-background overflow-hidden relative flex items-center justify-center p-4"},re={class:"absolute top-4 right-4 z-20"},oe={class:"w-full max-w-4xl relative z-10"},se={class:"mb-8"},ae={class:"flex justify-between mb-2"},ne={class:"text-content-secondary dark:text-content-muted text-sm"},de={class:"text-content-secondary dark:text-content-muted text-sm"},ie={class:"h-2 bg-stroke-subtle dark:bg-stroke/10 rounded-full overflow-hidden"},le={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[20px] p-6 sm:p-8 md:p-12"},ue={class:"flex justify-center mb-8"},ce={class:"flex gap-2"},pe={class:"mb-8"},me={class:"text-2xl sm:text-3xl font-bold text-content-primary dark:text-content-primary mb-2 text-center"},be={key:0,class:"space-y-6 mt-8"},fe={key:1,class:"space-y-6 mt-8"},xe={class:"max-w-md mx-auto"},ke={key:2,class:"space-y-6 mt-8"},ve={key:0,class:"text-center text-content-secondary dark:text-content-muted"},ge={key:1,class:"text-center text-content-secondary dark:text-content-muted"},ye={key:2,class:"grid grid-cols-1 md:grid-cols-2 gap-4 max-w-3xl mx-auto"},he=["onClick"],we={class:"font-medium text-content-primary dark:text-content-primary mb-1"},_e={class:"text-sm text-content-secondary dark:text-content-muted"},Se={key:3,class:"space-y-6 mt-8"},Ce={key:0,class:"text-center text-content-secondary dark:text-content-muted"},Re={key:1,class:"text-center text-content-secondary dark:text-content-muted"},je={key:2,class:"max-w-5xl mx-auto"},Pe={class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4"},Me=["onClick"],Le={class:"relative z-10"},Be={class:"font-medium text-content-primary dark:text-content-primary mb-1 flex items-start justify-between gap-2"},Ne={class:"flex items-center gap-2"},ze={class:"text-2xl"},Ve={key:0,class:"text-primary flex-shrink-0"},qe={class:"text-xs text-content-secondary dark:text-content-muted mb-3"},Te={class:"grid grid-cols-2 gap-2 text-xs"},Ee={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},Fe={class:"text-content-primary dark:text-content-primary/80 font-medium"},He={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},Ue={class:"text-content-primary dark:text-content-primary/80 font-medium"},Oe={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},$e={class:"text-content-primary dark:text-content-primary/80 font-medium"},Ge={class:"bg-gray-50 dark:bg-white/5 rounded px-2 py-1"},De={class:"text-content-primary dark:text-content-primary/80 font-medium"},Ae={class:"border-t border-stroke-subtle dark:border-stroke/10 pt-6"},We={class:"flex items-center justify-between mb-2"},Ie={key:0,class:"text-primary"},Ye={key:0,class:"mt-4 grid grid-cols-2 gap-4"},Je={key:4,class:"space-y-6 mt-8"},Ke={class:"max-w-md mx-auto space-y-4"},Qe={key:0,class:"text-red-600 dark:text-red-400 text-sm"},Xe={key:0,class:"mb-6 bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-600 dark:text-red-200"},Ze={class:"flex justify-between gap-4"},et={key:1},tt=["disabled"],rt={key:0,class:"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"},ot={key:1},st={key:2},at={key:3},nt={key:4,class:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},dt={class:"flex justify-center mb-6"},it={key:0,class:"w-16 h-16 rounded-full bg-green-100 dark:bg-green-500/20 flex items-center justify-center"},lt={key:1,class:"w-16 h-16 rounded-full bg-red-100 dark:bg-red-500/20 flex items-center justify-center"},ut={class:"text-2xl font-bold text-content-primary dark:text-content-primary text-center mb-4"},ct={class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"},pt=W({name:"SetupView",__name:"Setup",setup(m){const r=ee(),y=Q(),b=l(!1),v=l(""),x=l(""),f=l("success"),w=u=>{const t=u.toLowerCase();return t.includes("australia")?"🇦🇺":t.includes("eu")||t.includes("uk")?"🇪🇺":t.includes("czech")?"🇨🇿":t.includes("new zealand")?"🇳🇿":t.includes("portugal")?"🇵🇹":t.includes("switzerland")?"🇨🇭":t.includes("usa")||t.includes("canada")?"🇺🇸":t.includes("vietnam")?"🇻🇳":"🌍"};I(async()=>{await Promise.all([r.fetchHardwareOptions(),r.fetchRadioPresets()])});const c=P(()=>r.currentStep/r.totalSteps*100);async function R(){if(r.isLastStep){const u=await r.completeSetup();u.success?(f.value="success",v.value="Setup Complete!",x.value="Your repeater has been configured successfully. The service is restarting now...",b.value=!0,setTimeout(()=>{b.value=!1,y.push("/login")},5e3)):(f.value="error",v.value="Setup Failed",x.value=u.error||"An unknown error occurred",b.value=!0)}else r.nextStep()}function j(){r.previousStep()}function g(){b.value=!1,f.value==="success"&&y.push("/login")}const S=["Welcome","Repeater Name","Hardware Selection","Radio Configuration","Security Setup"];return(u,t)=>(n(),a("div",te,[e("div",re,[B(Y)]),t[36]||(t[36]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-80 animate-pulse-slow -top-[79px] left-[575px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),t[37]||(t[37]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-75 animate-pulse-slower -top-[94px] -left-[92px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),t[38]||(t[38]=e("div",{class:"bg-gradient-light dark:bg-gradient-dark absolute rounded-full -rotate-[24.22deg] w-[705px] h-[512px] blur-[120px] opacity-80 animate-pulse-slowest top-[373px] left-[246px] mix-blend-multiply dark:mix-blend-screen pointer-events-none"},null,-1)),e("div",oe,[e("div",se,[e("div",ae,[e("span",ne,"Step "+i(o(r).currentStep)+" of "+i(o(r).totalSteps),1),e("span",de,i(Math.round(c.value))+"% Complete",1)]),e("div",ie,[e("div",{class:"h-full bg-gradient-to-r from-primary to-primary/80 transition-all duration-500",style:J({width:`${c.value}%`})},null,4)])]),e("div",le,[e("div",ue,[e("div",ce,[(n(!0),a(N,null,z(o(r).totalSteps,s=>(n(),a("div",{key:s,class:_(["w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-all",s===o(r).currentStep?"bg-primary text-white":s

Welcome to your pyMC Repeater! Let's get you set up in just a few steps.

You'll configure:

  • Repeater name and identification
  • Hardware board selection
  • Radio frequency and settings
  • Admin password for secure access
',1)]))):o(r).currentStep===2?(n(),a("div",fe,[t[12]||(t[12]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Choose a unique name for your repeater. This will be used for identification on the mesh network. ",-1)),e("div",xe,[t[10]||(t[10]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Repeater Name",-1)),h(e("input",{"onUpdate:modelValue":t[0]||(t[0]=s=>o(r).nodeName=s),type:"text",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"e.g., pyRpt0001",maxlength:"32"},null,512),[[C,o(r).nodeName]]),t[11]||(t[11]=e("p",{class:"text-content-secondary dark:text-content-muted text-xs mt-2"}," Use letters, numbers, hyphens, or underscores (3-32 characters) ",-1))])])):o(r).currentStep===3?(n(),a("div",ke,[t[13]||(t[13]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Select your hardware board type ",-1)),o(r).isLoading?(n(),a("div",ve," Loading hardware options... ")):o(r).hardwareOptions.length===0?(n(),a("div",ge," No hardware options available ")):(n(),a("div",ye,[(n(!0),a(N,null,z(o(r).hardwareOptions,s=>(n(),a("button",{key:s.key,onClick:M=>o(r).selectedHardware=s,class:_(["p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm",o(r).selectedHardware?.key===s.key?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",we,i(s.name),1),e("div",_e,i(s.description||s.key),1)],10,he))),128))])),o(r).selectedHardware?.key==="kiss"?(n(),a("div",{class:"mt-6 max-w-md mx-auto space-y-4 border-t border-stroke-subtle dark:border-stroke/10 pt-6"},[e("div",null,[e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"MeshCore KISS modem – Serial port",-1),h(e("input",{"onUpdate:modelValue":M=>o(r).kissPort=M,class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"/dev/ttyUSB0"},null,512),[[C,o(r).kissPort]])]),e("div",null,[e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Baud rate",-1),h(e("input",{"onUpdate:modelValue":M=>o(r).kissBaud=M,type:"number",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"115200"},null,512),[[C,o(r).kissBaud]])]) ])):k("",!0)])):o(r).currentStep===4?(n(),a("div",Se,[t[28]||(t[28]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Choose a radio configuration preset for your region or create a custom configuration ",-1)),o(r).isLoading?(n(),a("div",Ce," Loading radio presets... ")):o(r).radioPresets.length===0?(n(),a("div",Re," No radio presets available ")):(n(),a("div",je,[e("div",Pe,[(n(!0),a(N,null,z(o(r).radioPresets,s=>(n(),a("button",{key:s.title,onClick:M=>{o(r).selectedRadioPreset=s,o(r).useCustomRadio=!1},class:_(["p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm relative overflow-hidden",!o(r).useCustomRadio&&o(r).selectedRadioPreset?.title===s.title?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",Le,[e("div",Be,[e("span",Ne,[e("span",ze,i(w(s.title)),1),e("span",null,i(s.title),1)]),!o(r).useCustomRadio&&o(r).selectedRadioPreset?.title===s.title?(n(),a("div",Ve,t[14]||(t[14]=[e("svg",{class:"w-5 h-5",fill:"currentColor",viewBox:"0 0 20 20"},[e("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z","clip-rule":"evenodd"})],-1)]))):k("",!0)]),e("div",qe,i(s.description),1),e("div",Te,[e("div",Ee,[t[15]||(t[15]=e("div",{class:"text-content-muted dark:text-content-muted"},"Freq",-1)),e("div",Fe,i(s.frequency),1)]),e("div",He,[t[16]||(t[16]=e("div",{class:"text-content-muted dark:text-content-muted"},"BW",-1)),e("div",Ue,i(s.bandwidth),1)]),e("div",Oe,[t[17]||(t[17]=e("div",{class:"text-content-muted dark:text-content-muted"},"SF",-1)),e("div",$e,i(s.spreading_factor),1)]),e("div",Ge,[t[18]||(t[18]=e("div",{class:"text-content-muted dark:text-content-muted"},"CR",-1)),e("div",De,i(s.coding_rate),1)])])])],10,Me))),128))]),e("div",Ae,[e("button",{onClick:t[1]||(t[1]=s=>{o(r).useCustomRadio=!o(r).useCustomRadio,o(r).useCustomRadio&&(o(r).selectedRadioPreset=null)}),class:_(["w-full p-4 rounded-[12px] border transition-all duration-300 text-left backdrop-blur-sm",o(r).useCustomRadio?"bg-gradient-to-r from-primary/20 to-primary/10 border-primary/50 shadow-lg shadow-primary/20":"bg-background-mute dark:bg-white/5 border-stroke-subtle dark:border-stroke/10 hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20"])},[e("div",We,[t[20]||(t[20]=e("div",{class:"font-medium text-content-primary dark:text-content-primary flex items-center gap-2"},[e("svg",{class:"w-5 h-5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"})]),V(" Custom Configuration ")],-1)),o(r).useCustomRadio?(n(),a("div",Ie,t[19]||(t[19]=[e("svg",{class:"w-5 h-5",fill:"currentColor",viewBox:"0 0 20 20"},[e("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z","clip-rule":"evenodd"})],-1)]))):k("",!0)]),t[21]||(t[21]=e("div",{class:"text-xs text-content-secondary dark:text-content-muted"},"Manually configure frequency, bandwidth, spreading factor, and coding rate",-1))],2),B(T,{name:"slide"},{default:q(()=>[o(r).useCustomRadio?(n(),a("div",Ye,[e("div",null,[t[22]||(t[22]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Frequency (MHz)",-1)),h(e("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>o(r).customRadio.frequency=s),type:"number",step:"0.1",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all",placeholder:"915.0"},null,512),[[C,o(r).customRadio.frequency]])]),e("div",null,[t[23]||(t[23]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Bandwidth (kHz)",-1)),h(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>o(r).customRadio.bandwidth=s),type:"number",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all",placeholder:"125"},null,512),[[C,o(r).customRadio.bandwidth]])]),e("div",null,[t[25]||(t[25]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Spreading Factor",-1)),h(e("select",{"onUpdate:modelValue":t[4]||(t[4]=s=>o(r).customRadio.spreading_factor=s),class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all"},t[24]||(t[24]=[e("option",{value:"7"},"7",-1),e("option",{value:"8"},"8",-1),e("option",{value:"9"},"9",-1),e("option",{value:"10"},"10",-1),e("option",{value:"11"},"11",-1),e("option",{value:"12"},"12",-1)]),512),[[E,o(r).customRadio.spreading_factor]])]),e("div",null,[t[27]||(t[27]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Coding Rate",-1)),h(e("select",{"onUpdate:modelValue":t[5]||(t[5]=s=>o(r).customRadio.coding_rate=s),class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-[12px] px-4 py-2.5 text-content-primary dark:text-content-primary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-transparent transition-all"},t[26]||(t[26]=[e("option",{value:"5"},"4/5",-1),e("option",{value:"6"},"4/6",-1),e("option",{value:"7"},"4/7",-1),e("option",{value:"8"},"4/8",-1)]),512),[[E,o(r).customRadio.coding_rate]])])])):k("",!0)]),_:1})])]))])):o(r).currentStep===5?(n(),a("div",Je,[t[32]||(t[32]=e("p",{class:"text-content-secondary dark:text-content-primary/70 text-center mb-6"}," Set a secure admin password to protect your repeater ",-1)),e("div",Ke,[e("div",null,[t[29]||(t[29]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Admin Password",-1)),h(e("input",{"onUpdate:modelValue":t[6]||(t[6]=s=>o(r).adminPassword=s),type:"password",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"Enter password (min 6 characters)",minlength:"6"},null,512),[[C,o(r).adminPassword]])]),e("div",null,[t[30]||(t[30]=e("label",{class:"block text-content-primary dark:text-content-primary/90 text-sm font-medium mb-2"},"Confirm Password",-1)),h(e("input",{"onUpdate:modelValue":t[7]||(t[7]=s=>o(r).confirmPassword=s),type:"password",class:"w-full bg-white dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg px-4 py-3 text-content-primary dark:text-content-primary placeholder-gray-500 dark:placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",placeholder:"Confirm password"},null,512),[[C,o(r).confirmPassword]])]),o(r).adminPassword&&o(r).confirmPassword&&o(r).adminPassword!==o(r).confirmPassword?(n(),a("div",Qe," Passwords do not match ")):k("",!0),t[31]||(t[31]=e("div",{class:"bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3 text-sm text-yellow-800 dark:text-yellow-200"},[e("strong",null,"Important:"),V(" Remember this password - you'll need it to access the dashboard. ")],-1))])])):k("",!0)]),o(r).error?(n(),a("div",Xe,i(o(r).error),1)):k("",!0),e("div",Ze,[o(r).canGoBack?(n(),a("button",{key:0,onClick:j,class:"px-6 py-3 rounded-[12px] bg-background-mute dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 text-content-primary dark:text-content-primary hover:bg-stroke-subtle dark:hover:bg-white/10 hover:border-stroke dark:hover:border-stroke/20 transition-all duration-300 font-medium"}," Back ")):(n(),a("div",et)),e("button",{onClick:R,disabled:!o(r).canGoNext||o(r).isSubmitting,class:_(["px-8 py-3 rounded-[12px] font-semibold transition-all duration-300 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed",o(r).canGoNext&&!o(r).isSubmitting?"bg-gradient-to-r from-primary/20 to-primary/10 hover:from-primary/30 hover:to-primary/20 text-white border border-primary/30 hover:border-primary/50":"bg-background-mute dark:bg-stroke/5 text-content-muted dark:text-content-muted border border-stroke-subtle dark:border-stroke/10"])},[o(r).isSubmitting?(n(),a("div",rt)):k("",!0),o(r).isSubmitting?(n(),a("span",ot,"Setting up...")):o(r).isLastStep?(n(),a("span",st,"Complete Setup")):(n(),a("span",at,"Next")),!o(r).isSubmitting&&!o(r).isLastStep?(n(),a("svg",nt,t[33]||(t[33]=[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M9 5l7 7-7 7"},null,-1)]))):k("",!0)],10,tt)])])]),B(T,{name:"modal"},{default:q(()=>[b.value?(n(),a("div",{key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:g},[e("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl max-w-md w-full p-8 rounded-[24px] border border-stroke-subtle dark:border-white/20 shadow-[0_8px_32px_0_rgba(0,0,0,0.37)]",onClick:t[8]||(t[8]=X(()=>{},["stop"]))},[e("div",dt,[f.value==="success"?(n(),a("div",it,t[34]||(t[34]=[e("svg",{class:"w-8 h-8 text-green-600 dark:text-green-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})],-1)]))):(n(),a("div",lt,t[35]||(t[35]=[e("svg",{class:"w-8 h-8 text-red-600 dark:text-red-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])))]),e("h3",ut,i(v.value),1),e("p",ct,i(x.value),1),e("button",{onClick:g,class:_(["w-full px-6 py-3 rounded-lg font-medium transition-all",f.value==="success"?"bg-gradient-to-r from-primary/20 to-primary/10 hover:from-primary/30 hover:to-primary/20 text-white":"bg-gradient-to-r from-red-500/20 to-red-500/10 hover:from-red-500/30 hover:to-red-500/20 text-white"])},i(f.value==="success"?"Continue to Login":"Close"),3)])])):k("",!0)]),_:1})]))}}),bt=Z(pt,[["__scopeId","data-v-20a8772f"]]);export{bt as default}; diff --git a/setup-radio-config.sh b/setup-radio-config.sh index a0141fa..f85d8bc 100644 --- a/setup-radio-config.sh +++ b/setup-radio-config.sh @@ -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"