diff --git a/.gitignore b/.gitignore index 253ea02b..fba7c559 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ config.local.js config.local.json config.json +# Rig control daemon config (user-specific settings) +rig-control/rig-config.json + # Test files *.test.js.snap diff --git a/RIG_CONTROL_COMPARISON.md b/RIG_CONTROL_COMPARISON.md new file mode 100644 index 00000000..c8c788d0 --- /dev/null +++ b/RIG_CONTROL_COMPARISON.md @@ -0,0 +1,292 @@ +# 📻 OpenHamClock Rig Control Solutions — Comparison Guide + +OpenHamClock offers **three different solutions** for connecting your radio to the web application. Each serves different use cases and technical requirements. + +--- + +## Quick Decision Guide + +**Choose based on your setup:** + +| Your Situation | Recommended Solution | +|----------------|---------------------| +| 🎯 **I want the simplest setup** | **Rig Listener** — One download, one click | +| 🔌 **I already use flrig or rigctld** | **Rig Control Daemon** — Works with existing setup | +| 🌐 **I need a web UI to configure my radio** | **Rig Bridge** — Browser-based configuration | +| 🏠 **Radio is on a different computer** | **Rig Bridge** or **Rig Control Daemon** — Network accessible | +| 🐧 **Running on Raspberry Pi** | **Rig Control Daemon** — Lightweight, proven | + +--- + +## Solution Comparison + +### 1️⃣ Rig Listener (Newest — Recommended for Most Users) + +**What it is:** A standalone executable that connects directly to your radio via USB. No dependencies, no configuration files, no web server. + +**Best for:** +- First-time users who want zero hassle +- Users who don't need flrig/rigctld for other apps +- Portable/field operation (single executable) + +**Pros:** +- ✅ **Easiest setup** — Interactive wizard on first run +- ✅ **Single executable** — No Node.js installation required +- ✅ **Direct USB connection** — No intermediate software needed +- ✅ **Remembers settings** — Config saved automatically +- ✅ **Small footprint** — ~30MB executable + +**Cons:** +- ❌ No web UI for configuration (CLI wizard only) +- ❌ **USB port exclusivity** — Cannot share radio with other apps simultaneously (WSJT-X, fldigi, etc.) +- ❌ Must run on same computer as radio USB connection + +> [!WARNING] +> **USB Port Limitation**: When using direct USB connection, only ONE application can access the serial port at a time. If you need to use WSJT-X, fldigi, or other CAT control software alongside OpenHamClock, use **Rig Control Daemon** with flrig/rigctld instead — they can share the radio among multiple applications. + +**Supported Radios:** +- Yaesu (FT-991A, FT-891, FT-710, FT-DX10, FT-817/818) +- Kenwood (TS-590, TS-890, TS-480, TS-2000) +- Elecraft (K3, K4, KX3, KX2) +- Icom (IC-7300, IC-7610, IC-705, IC-9700) + +**Setup:** +```bash +# Download executable, then: +./rig-listener-mac-arm64 # Mac +rig-listener-win-x64.exe # Windows +./rig-listener-linux-x64 # Linux + +# Follow wizard prompts +# Done! Runs on http://localhost:5555 +``` + +**When to use:** +- You want to get started in under 2 minutes +- You don't use flrig/rigctld for other applications +- You value simplicity over advanced features + +--- + +### 2️⃣ Rig Bridge (Feature-Rich) + +**What it is:** A web-based rig control server with a browser configuration UI. Connects directly to your radio via USB **or** can proxy to flrig/rigctld. + +**Best for:** +- Users who want a web UI to configure their radio +- Network setups (radio on one computer, OpenHamClock on another) +- Users who need to switch between radios frequently + +**Pros:** +- ✅ **Web-based configuration** — Configure via browser at http://localhost:5555 +- ✅ **Direct USB or proxy mode** — Works with or without flrig/rigctld +- ✅ **Network accessible** — Can run on a different computer +- ✅ **Visual port selection** — See all available COM ports in browser +- ✅ **Live status display** — Real-time frequency/mode display in web UI + +**Cons:** +- ❌ Requires Node.js (or download pre-built executable) +- ❌ More complex than Rig Listener +- ❌ Slightly larger resource footprint +- ❌ **USB port exclusivity** (when using direct USB mode) — Cannot share radio with other apps + +**Supported Radios:** +- Same as Rig Listener (Yaesu, Kenwood, Icom, Elecraft) +- **Plus:** Can proxy to flrig/rigctld for any radio they support + +**Setup:** +```bash +cd rig-bridge +npm install +node rig-bridge.js + +# Open http://localhost:5555 in browser +# Select radio type and COM port +# Click "Save & Connect" +``` + +**When to use:** +- You prefer GUI configuration over CLI +- You want to access rig control from multiple devices on your network +- You need to frequently switch between different radios +- You want a visual confirmation of connection status + +--- + +### 3️⃣ Rig Control Daemon (Original — Most Flexible) + +**What it is:** A lightweight Node.js service that acts as a bridge between OpenHamClock and **existing** rig control software (flrig or rigctld). + +**Best for:** +- Users who already run flrig or rigctld for other applications +- Advanced users who want maximum control via config files +- Raspberry Pi / headless server deployments +- Integration with existing HAMlib-based workflows + +**Pros:** +- ✅ **Works with existing setup** — No need to change your current rig control +- ✅ **Lightweight** — Minimal resource usage +- ✅ **Flexible configuration** — JSON config file with all options +- ✅ **Battle-tested** — Original solution, most mature codebase +- ✅ **Remote access** — Binds to 0.0.0.0 by default for network access + +**Cons:** +- ❌ **Requires flrig or rigctld** — Cannot connect directly to radio +- ❌ Requires Node.js installation +- ❌ Manual configuration (edit JSON file) +- ❌ No built-in web UI + +**Supported Backends:** +- **rigctld** (HAMlib) — Supports 300+ radio models +- **flrig** — Popular GUI rig control software +- **mock** — Simulation mode for testing + +**Setup:** +```bash +cd rig-control +npm install + +# Edit rig-config.json: +{ + "radio": { + "type": "flrig", // or "rigctld" + "host": "127.0.0.1", + "port": 12345 // flrig default, rigctld uses 4532 + } +} + +node rig-daemon.js +``` + +**When to use:** +- You already use flrig or rigctld for WSJT-X, fldigi, etc. +- You want to share radio control across multiple applications +- You're running on a Raspberry Pi or headless server +- You need maximum flexibility and don't mind config files + +--- + +## Technical Comparison + +| Feature | Rig Listener | Rig Bridge | Rig Control Daemon | +|---------|--------------|------------|-------------------| +| **Direct USB** | ✅ Yes | ✅ Yes | ❌ No (needs flrig/rigctld) | +| **Web UI** | ❌ No | ✅ Yes | ❌ No | +| **Standalone Executable** | ✅ Yes | ✅ Yes (optional) | ❌ No | +| **Requires Node.js** | ❌ No | ❌ No (if using exe) | ✅ Yes | +| **Config Method** | CLI Wizard | Web UI | JSON File | +| **Network Access** | ✅ Yes | ✅ Yes | ✅ Yes | +| **Resource Usage** | Low | Medium | Very Low | +| **Setup Time** | 2 minutes | 5 minutes | 10 minutes | +| **Proxy to flrig/rigctld** | ❌ No | ✅ Yes | ✅ Yes (only) | + +--- + +## API Compatibility + +**All three solutions expose the same HTTP API**, so OpenHamClock works identically with any of them: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/status` | GET | Current freq, mode, PTT, connection status | +| `/stream` | GET | Server-Sent Events (SSE) real-time updates | +| `/freq` | POST | Set frequency: `{ "freq": 14074000 }` | +| `/mode` | POST | Set mode: `{ "mode": "USB" }` | +| `/ptt` | POST | Set PTT: `{ "ptt": true }` | + +**Additional endpoints (Rig Bridge only):** +- `/api/ports` — List available serial ports +- `/api/config` — Get/set configuration via web UI + +--- + +## Migration Guide + +### From Rig Control Daemon → Rig Listener +1. Stop `rig-daemon.js` +2. Stop `flrig` or `rigctld` +3. Download and run Rig Listener +4. Follow the wizard +5. No changes needed in OpenHamClock settings (same port 5555) + +### From Rig Listener → Rig Bridge +1. Stop Rig Listener +2. Download/run Rig Bridge +3. Open http://localhost:5555 and configure +4. No changes needed in OpenHamClock settings + +### From Rig Bridge → Rig Control Daemon +1. Install and start `flrig` or `rigctld` +2. Stop Rig Bridge +3. Configure `rig-control/rig-config.json` +4. Run `node rig-daemon.js` +5. No changes needed in OpenHamClock settings + +--- + +## Troubleshooting + +### All Solutions +- **Port 5555 in use:** Another rig control service is running. Stop it first. +- **OpenHamClock can't connect:** Check firewall, ensure service is running +- **No frequency updates:** Verify radio is connected and powered on + +### Rig Listener / Rig Bridge (Direct USB) +- **No COM ports found:** Install USB driver (Silicon Labs CP210x for Yaesu) +- **Port opens but no data:** Baud rate mismatch — check radio's CAT settings +- **Linux permission denied:** `sudo usermod -a -G dialout $USER` then log out/in + +### Rig Control Daemon +- **Connection refused:** Ensure flrig/rigctld is running first +- **Wrong port:** Check `rig-config.json` matches flrig/rigctld port + +--- + +## Recommendations by Use Case + +### 🏕️ Field Operation / Portable +**Use Rig Listener** — Single executable, no dependencies, works offline + +### 🏠 Home Station (Single Radio) +**Use Rig Listener** — Simplest setup, direct USB connection + +### 🏠 Home Station (Multiple Apps) +**Use Rig Control Daemon** — Share flrig/rigctld with WSJT-X, fldigi, etc. + +> **Why?** Direct USB solutions (Rig Listener/Rig Bridge) lock the serial port exclusively. If you run WSJT-X, fldigi, or other CAT control software, they cannot access the radio simultaneously. The Rig Control Daemon works with flrig/rigctld, which act as a "hub" that multiple applications can connect to at once. + +### 🌐 Network Setup (Radio on Different Computer) +**Use Rig Bridge** — Web UI makes remote configuration easy + +### 🐧 Raspberry Pi / Headless Server +**Use Rig Control Daemon** — Lightweight, proven, easy to automate + +### 🔧 Frequent Radio Changes +**Use Rig Bridge** — Web UI makes switching radios quick + +### 🆕 First-Time User +**Use Rig Listener** — Get running in under 2 minutes + +--- + +## Summary + +| Solution | Best For | Complexity | Setup Time | +|----------|----------|------------|------------| +| **Rig Listener** | Most users, simplicity | ⭐ Easy | 2 min | +| **Rig Bridge** | Web UI lovers, network setups | ⭐⭐ Moderate | 5 min | +| **Rig Control Daemon** | Advanced users, existing flrig/rigctld | ⭐⭐⭐ Advanced | 10 min | + +**Still unsure?** Start with **Rig Listener**. You can always switch later — all three use the same API, so OpenHamClock doesn't need reconfiguration. + +--- + +## Getting Help + +- **Documentation:** Each solution has its own README in its folder +- **Issues:** [GitHub Issues](https://github.com/K0CJH/openhamclock/issues) +- **Community:** Check the OpenHamClock community forums + +--- + +**73 de K0CJH** 📻 diff --git a/rig-control/README.md b/rig-control/README.md index 69381b77..db4ed106 100644 --- a/rig-control/README.md +++ b/rig-control/README.md @@ -27,7 +27,7 @@ npm install ## Configuration -Configuration is loaded from `rig-config.json`. A default file is provided: +Configuration is loaded from `rig-config.json`. On first run, this file is automatically created from `rig-config.json.example`: ```json { @@ -46,6 +46,8 @@ Configuration is loaded from `rig-config.json`. A default file is provided: } ``` +**Important:** Your `rig-config.json` customizations are preserved during updates. The file is excluded from git tracking, so your local changes won't be overwritten when pulling new versions. + ### Configuration Options - **server.host**: IP to bind to (default 0.0.0.0) @@ -109,6 +111,23 @@ The daemon listens on port `5555` (configurable) and provides the following endp - **CORS Errors**: The daemon enables CORS for all origins by default (`*`) to allow local development. - **Port Conflicts**: If port 5555 is in use, change `server.port` in `rig-config.json`. +### Mixed Content Issues (HTTPS → HTTP) + +**Problem:** If OpenHamClock is accessed via **HTTPS** (e.g., `https://yourdomain.com` or `https://openhamclock.com`), browsers will block HTTP requests to the rig daemon (`http://localhost:5555`) due to **Mixed Content** security policies. + +**Browser Behavior:** + +| Browser | Behavior | Workaround | +|---------|----------|------------| +| **Safari (macOS/iOS)** | ❌ **Strictly blocks** all mixed content. No override option. | No workaround available. Use Chrome/Firefox/Edge or run OpenHamClock locally via HTTP. | +| **Chrome** | ⚠️ Blocks by default. Shows shield icon in address bar to allow insecure content. | Click shield icon → "Load unsafe scripts" | +| **Firefox** | ⚠️ Blocks by default. Shows shield icon in address bar. | Click shield icon → "Disable protection for this session" | +| **Edge** | ⚠️ Blocks by default. Similar to Chrome. | Click shield icon → Allow | + +**Recommendation:** For the best experience, run OpenHamClock locally using HTTP (e.g., `http://localhost:3000`) to avoid mixed content issues entirely. See the [User Guide](./UserGuide.md) for detailed setup instructions. + + + ## Experimental Scripts The `scripts/` folder contains experimental installation and utility scripts. These are currently **in testing** and may not function properly on all systems. Use them with caution. diff --git a/rig-control/UserGuide.md b/rig-control/UserGuide.md index 6ab9dc93..c95cf435 100644 --- a/rig-control/UserGuide.md +++ b/rig-control/UserGuide.md @@ -11,9 +11,33 @@ This feature allows you to: --- +## 📋 Choose Your Setup Scenario + +There are **two ways** to use Rig Control with OpenHamClock: + +### Scenario 1: Local Installation (Full Setup) +You install **both** OpenHamClock and the Rig Control daemon on your local computer. This is ideal for: +- Running everything on one machine (laptop, desktop, Raspberry Pi) +- Development and testing +- Offline operation + +👉 **[Jump to Scenario 1 Instructions](#scenario-1-local-installation)** + +### Scenario 2: Remote UI with Local Daemon +You use the OpenHamClock web interface from **openhamclock.com** (or another hosted instance) but run **only the Rig Control daemon** locally on the computer connected to your radio. This is ideal for: +- Using the hosted version without maintaining your own installation +- Accessing from multiple devices while keeping rig control local +- Simpler setup with fewer components to manage + +👉 **[Jump to Scenario 2 Instructions](#scenario-2-remote-ui-with-local-daemon)** + +--- + +# Scenario 1: Local Installation + ## 🛠 Prerequisites -You need three things installed on the computer connected to your radio (e.g., Raspberry Pi, Mac, or PC): +You need the following installed on the computer connected to your radio (e.g., Raspberry Pi, Mac, or PC): 1. **Git:** To download the software. - [Download Git](https://git-scm.com/downloads) @@ -28,8 +52,6 @@ You need three things installed on the computer connected to your radio (e.g., R ## 📦 Step 1: Install OpenHamClock -If you haven't installed OpenHamClock yet, follow these steps. - 1. Open your terminal/command prompt. 2. Download the code: ```bash @@ -47,27 +69,27 @@ If you haven't installed OpenHamClock yet, follow these steps. --- -## 🚀 Step 2: Install the Rig Control Bridge +## 🚀 Step 2: Install the Rig Control Daemon -The "Rig Control Bridge" (daemon) is a separate small program that sits between OpenHamClock and your radio software. +The "Rig Control Daemon" is a separate small program that sits between OpenHamClock and your radio software. 1. Navigate to the `rig-control` folder: ```bash cd rig-control ``` _(If you are in the main folder, just type `cd rig-control`)_ -2. Install the bridge libraries: +2. Install the daemon libraries: ```bash npm install ``` --- -## ⚙️ Step 3: Configure the Bridge +## ⚙️ Step 3: Configure the Daemon -Tell the bridge which radio software you use. +Tell the daemon which radio software you use. -1. Find `rig-config.json` in the `rig-control` folder. +1. Find `rig-config.json` in the `rig-control` folder. If it doesn't exist, it will be created automatically from `rig-config.json.example` when you first start the daemon. 2. Edit it with any text editor. ### If using FLRIG (Easiest) @@ -76,12 +98,18 @@ Ensure FLRIG is running and **XML-RPC** is enabled in its settings (Config > Set ```json { - "rigType": "flrig", - "flrig": { - "host": "127.0.0.1", - "port": 12345 + "server": { + "host": "0.0.0.0", + "port": 5555, + "cors": "*" }, - "serverPort": 5555 + "radio": { + "type": "flrig", + "host": "127.0.0.1", + "port": 12345, + "pollInterval": 1000, + "pttEnabled": false + } } ``` @@ -89,12 +117,18 @@ Ensure FLRIG is running and **XML-RPC** is enabled in its settings (Config > Set ```json { - "rigType": "rigctld", - "rigctld": { - "host": "127.0.0.1", - "port": 4532 + "server": { + "host": "0.0.0.0", + "port": 5555, + "cors": "*" }, - "serverPort": 5555 + "radio": { + "type": "rigctld", + "host": "127.0.0.1", + "port": 4532, + "pollInterval": 1000, + "pttEnabled": false + } } ``` @@ -124,7 +158,7 @@ In the `openhamclock/rig-control` folder: node rig-daemon.js ``` -- This starts the **Bridge**. +- This starts the **Daemon**. - Standard Port: **5555** - _Note: You do NOT visit this port in your browser. It runs in the background._ @@ -136,9 +170,10 @@ node rig-daemon.js 2. Go to **Settings** (Gear Icon) > **Station Settings**. 3. Scroll to **Rig Control**. 4. Check **Enable Rig Control**. -5. Set **Host URL** to: `http://localhost:5555` - - _(This points the Dashboard on port 3000 to the Bridge on port 5555)_. +5. Set **Daemon URL** to: `http://localhost:5555` + - _(This points the Dashboard on port 3000 to the Daemon on port 5555)_. 6. **Optional:** Check **"Tune Button Enabled"** if you want to trigger your ATU. +7. Click **Save**. --- @@ -158,4 +193,189 @@ Navigate to the dashboard. You should see the Rig Control panel (if enabled). - **Radio won't tune:** Ensure FLRIG is running and connected to the radio. - **Double check ports:** - Browser URL: `http://localhost:3000` - - Settings Rig URL: `http://localhost:5555` + - Settings Daemon URL: `http://localhost:5555` + +--- + +# Scenario 2: Remote UI with Local Daemon + +In this scenario, you use the OpenHamClock web interface from **openhamclock.com** (or another HTTPS-hosted instance) while running only the Rig Control daemon locally on the computer connected to your radio. + +## 🛠 Prerequisites + +You need the following installed on the computer connected to your radio: + +1. **Git:** To download the daemon software. + - [Download Git](https://git-scm.com/downloads) +2. **Node.js:** The engine that runs the daemon. + - **Check:** Open a terminal and type `node -v`. (You want version 18 or higher). + - **Install:** [Download Node.js LTS](https://nodejs.org/). +3. **Radio Software:** One of the following must be running and connected to your radio: + - **FLRIG (Recommended):** [Download FLRIG](http://www.w1hkj.com/files/flrig/) + - **Hamlib (rigctld):** For advanced users. + +--- + +## 📦 Step 1: Install the Rig Control Daemon Only + +1. Open your terminal/command prompt. +2. Download the OpenHamClock repository (we only need the `rig-control` folder): + ```bash + git clone https://github.com/HAMDevs/openhamclock.git + cd openhamclock/rig-control + ``` +3. Install the daemon dependencies: + ```bash + npm install + ``` + +--- + +## ⚙️ Step 2: Configure the Daemon + +1. Find `rig-config.json` in the `rig-control` folder. If it doesn't exist, it will be created automatically from `rig-config.json.example` when you first start the daemon. +2. Edit it with any text editor. + +### If using FLRIG (Easiest) + +Ensure FLRIG is running and **XML-RPC** is enabled in its settings (Config > Setup > UI > XML-RPC). + +```json +{ + "server": { + "host": "0.0.0.0", + "port": 5555, + "cors": "*" + }, + "radio": { + "type": "flrig", + "host": "127.0.0.1", + "port": 12345, + "pollInterval": 1000, + "pttEnabled": false + } +} +``` + +### If using Hamlib (rigctld) + +```json +{ + "server": { + "host": "0.0.0.0", + "port": 5555, + "cors": "*" + }, + "radio": { + "type": "rigctld", + "host": "127.0.0.1", + "port": 4532, + "pollInterval": 1000, + "pttEnabled": false + } +} +``` + +--- + +## ▶️ Step 3: Start the Daemon + +In the `openhamclock/rig-control` folder: + +```bash +node rig-daemon.js +``` + +- This starts the **Daemon**. +- Standard Port: **5555** +- The daemon will run in the background and communicate with your radio software. + +--- + +## 🔗 Step 4: Configure OpenHamClock Web UI + +### Important: HTTPS → HTTP Mixed Content Issue + +When you access OpenHamClock via **HTTPS** (e.g., `https://openhamclock.com`), your browser will **block** direct HTTP connections to your local daemon (`http://localhost:5555`) for security reasons. This is called a "Mixed Content" security policy. + +**Different browsers handle this differently:** + +| Browser | Behavior | Workaround Available? | +|---------|----------|----------------------| +| **Safari (macOS/iOS)** | ❌ Strictly blocks all mixed content | ❌ No workaround available | +| **Chrome** | ⚠️ Blocks by default | ✅ Click shield icon in address bar → "Load unsafe scripts" | +| **Firefox** | ⚠️ Blocks by default | ✅ Click shield icon in address bar → "Disable protection for this session" | +| **Edge** | ⚠️ Blocks by default | ✅ Click shield icon in address bar → Allow | + +### ⚠️ Current Limitations + +**Safari Users:** Unfortunately, Safari does not provide any way to override mixed content blocking. If you're using Safari, you have two options: +1. **Use a different browser** (Chrome, Firefox, or Edge) that allows mixed content overrides +2. **Run OpenHamClock locally** using Scenario 1 (both UI and daemon on HTTP) + +**Chrome/Firefox/Edge Users:** You can use the browser's mixed content override feature (shield icon in address bar), but you'll need to re-enable it each time you reload the page. + +### 📝 Configuration Steps + +1. Open **https://openhamclock.com** (or your hosted instance) in your browser. +2. Go to **Settings** (Gear Icon) > **Station Settings**. +3. Scroll to **Rig Control**. +4. Check **Enable Rig Control**. +5. Set **Daemon URL** to: `http://localhost:5555` +6. **Optional:** Check **"Tune Button Enabled"** if you want to trigger your ATU. +7. Click **Save**. +8. **If using Chrome/Firefox/Edge:** Look for the shield icon in your browser's address bar and click it to allow mixed content. + +> **💡 Recommendation:** For the best experience with rig control, consider using **Scenario 1** (local installation) where both the UI and daemon run on HTTP, avoiding mixed content issues entirely. + +--- + +## ✅ You're Done! + +Navigate to the dashboard at **https://openhamclock.com**. You should see the Rig Control panel (if enabled). + +**Try it out:** +- Click a spot on the **World Map**. +- Click a row in the **DX Cluster** list. +- Click a **POTA** or **SOTA** spot. +- Works across **Classic**, **Modern**, **Tablet**, and **Compact** layouts! + +### Troubleshooting + +- **"Connection Failed":** + - Ensure `node rig-daemon.js` is running in a terminal. + - Verify the daemon is listening on port 5555 (check terminal output). + - **If using HTTPS UI:** Check for mixed content blocking (see browser-specific workarounds above). + +- **Radio won't tune:** + - Ensure FLRIG or rigctld is running and connected to the radio. + - Check the daemon terminal output for error messages. + +- **Mixed Content Errors (Console):** + - **Safari:** No workaround available. Use Chrome/Firefox/Edge or switch to Scenario 1. + - **Chrome/Firefox/Edge:** Click the shield icon in the address bar to allow mixed content. + - **Best Solution:** Consider using Scenario 1 (local installation) to avoid this issue entirely. + +- **Firewall Issues:** + - If the daemon is on a different machine than your browser, ensure port 5555 is open in your firewall. + - Update the **Daemon URL** to use the daemon machine's IP address (e.g., `http://192.168.1.50:5555`). + +--- + +## 🎯 Quick Reference + +### Scenario 1 (Local Installation) +- **What you install:** OpenHamClock + Rig Control Daemon +- **What you run:** `npm start` (OpenHamClock) + `node rig-daemon.js` (Daemon) +- **Where you access UI:** `http://localhost:3000` +- **Daemon URL in Settings:** `http://localhost:5555` +- **Browser compatibility:** All browsers ✅ +- **Mixed content issues:** None (both on HTTP) + +### Scenario 2 (Remote UI) +- **What you install:** Rig Control Daemon only +- **What you run:** `node rig-daemon.js` (Daemon) +- **Where you access UI:** `https://openhamclock.com` (or your hosted instance) +- **Daemon URL in Settings:** `http://localhost:5555` (or `http://your-daemon-ip:5555`) +- **Browser compatibility:** Chrome/Firefox/Edge ✅ (with manual override) | Safari ❌ +- **Mixed content issues:** Requires browser override on each page load (Chrome/Firefox/Edge only) diff --git a/rig-control/rig-config.json.example b/rig-control/rig-config.json.example new file mode 100644 index 00000000..73d6506b --- /dev/null +++ b/rig-control/rig-config.json.example @@ -0,0 +1,24 @@ +{ + "server": { + "host": "0.0.0.0", + "port": 5555, + "cors": "*" + }, + "radio": { + "type": "flrig", + "host": "127.0.0.1", + "port": 12345, + "pollInterval": 1000, + "timeout": 5000, + "tuneDelay": 3000, + "pttEnabled": false, + "_comment": "Set type to 'rigctld' (TCP), 'flrig' (XML-RPC), or 'mock' (Simulation)" + }, + "rotator": { + "enabled": false, + "type": "rotctld", + "host": "127.0.0.1", + "port": 4533, + "pollInterval": 1000 + } +} diff --git a/rig-control/rig-daemon.js b/rig-control/rig-daemon.js index 78498707..f2f7f1a8 100644 --- a/rig-control/rig-daemon.js +++ b/rig-control/rig-daemon.js @@ -32,6 +32,18 @@ let CONFIG = { // Load Config File const configPath = path.join(__dirname, "rig-config.json"); +const exampleConfigPath = path.join(__dirname, "rig-config.json.example"); + +// Auto-create config from example if it doesn't exist +if (!fs.existsSync(configPath) && fs.existsSync(exampleConfigPath)) { + try { + fs.copyFileSync(exampleConfigPath, configPath); + console.log(`[Config] Created ${configPath} from example template`); + } catch (e) { + console.warn(`[Config] Could not create config from example: ${e.message}`); + } +} + if (fs.existsSync(configPath)) { try { const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf8")); diff --git a/scripts/update.sh b/scripts/update.sh index 937ed3ae..9d9bd18c 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -140,6 +140,12 @@ if [ -f "config.json" ]; then echo " ✓ config.json → config.json.backup" fi +# Backup rig control daemon config +if [ -f "rig-control/rig-config.json" ]; then + cp rig-control/rig-config.json rig-control/rig-config.json.backup + echo " ✓ rig-control/rig-config.json → rig-config.json.backup" +fi + echo "" echo "⬇️ Pulling latest changes..." @@ -208,6 +214,16 @@ if [ -f "config.json.backup" ] && [ ! -f "config.json" ]; then echo " ✓ config.json restored from backup" fi +# Restore rig control daemon config +if [ -f "rig-control/rig-config.json.backup" ]; then + cp rig-control/rig-config.json.backup rig-control/rig-config.json + echo " ✓ rig-control/rig-config.json restored from backup" +elif [ ! -f "rig-control/rig-config.json" ] && [ -f "rig-control/rig-config.json.example" ]; then + # First-time setup: copy example to actual config + cp rig-control/rig-config.json.example rig-control/rig-config.json + echo " ✓ rig-control/rig-config.json created from example template" +fi + # Patch kiosk.sh if present — fix --incognito flag that wipes localStorage on reboot if [ -f "kiosk.sh" ]; then if grep -q "\-\-incognito" kiosk.sh; then diff --git a/src/DockableApp.jsx b/src/DockableApp.jsx index 81395773..9d26d207 100644 --- a/src/DockableApp.jsx +++ b/src/DockableApp.jsx @@ -186,11 +186,19 @@ export const DockableApp = ({ if (!spot) return; // 1. Tune Rig if frequency is available and rig control is enabled - // Spot freq is usually in kHz or MHz string - if (enabled && (spot.freq || spot.freqMHz)) { - const freq = spot.freq || (parseFloat(spot.freqMHz) * 1000); // Normalize to kHz for tuneTo (which handles units) - // tuneTo handles unit detection (MHz vs kHz vs Hz) so just pass the raw value - tuneTo(spot.freq || spot.freqMHz, spot.mode); + if (enabled && (spot.freq || spot.freqMHz || spot.dialFrequency)) { + let freqToSend; + + // WSJT-X decodes have dialFrequency (the VFO frequency to tune to) + // The freq field is just the audio delta offset within the passband + if (spot.dialFrequency) { + freqToSend = spot.dialFrequency; // Use dial frequency directly + } else { + // For other spot types (DX Cluster, POTA, etc.), use freq or freqMHz as-is + freqToSend = spot.freq || spot.freqMHz; + } + + tuneTo(freqToSend, spot.mode); } // 2. Set DX Location if location data is available @@ -561,10 +569,11 @@ export const DockableApp = ({ showLabelsOnMap={mapLayersEff.showPOTALabels} onToggleLabelsOnMap={togglePOTALabelsEff} + onSpotClick={handleSpotClick} /> ); break; - + case 'wwff': content = ( ); break; diff --git a/src/components/POTAPanel.jsx b/src/components/POTAPanel.jsx index 04d66c6f..d12b0240 100644 --- a/src/components/POTAPanel.jsx +++ b/src/components/POTAPanel.jsx @@ -3,8 +3,6 @@ * Displays Parks on the Air activations with ON/OFF toggle */ import React from 'react'; -import { detectMode } from '../utils/callsign.js'; -import { useRig } from '../contexts/RigContext.jsx'; import CallsignLink from './CallsignLink.jsx'; export const POTAPanel = ({ @@ -14,8 +12,8 @@ export const POTAPanel = ({ onToggleMap, showLabelsOnMap = true, onToggleLabelsOnMap, + onSpotClick, }) => { - const { tuneTo, tuneEnabled } = useRig(); return (
{ - if (spot.freq) { - const freqVal = parseFloat(spot.freq); - let freqHz = freqVal; - if (freqVal < 1000) freqHz = freqVal * 1000000; - else if (freqVal < 100000) freqHz = freqVal * 1000; - - const mode = spot.mode || detectMode(spot.locationDesc || spot.comment || ''); - tuneTo(freqHz, mode); - } + onSpotClick?.(spot); }} > @@ -102,7 +92,17 @@ export const POTAPanel = ({ {spot.locationDesc || spot.ref} - {spot.freq} + {(() => { + if (!spot.freq) return '?'; + const freqVal = parseFloat(spot.freq); + if (freqVal > 1000) { + // It's in kHz, convert to MHz + return (freqVal / 1000).toFixed(3); + } else { + // Already in MHz + return freqVal.toFixed(3); + } + })()} {spot.time} diff --git a/src/components/PotaSotaPanel.jsx b/src/components/PotaSotaPanel.jsx index 6a830ba2..90aa9f7f 100644 --- a/src/components/PotaSotaPanel.jsx +++ b/src/components/PotaSotaPanel.jsx @@ -13,7 +13,10 @@ const TABS = ['pota', 'sota']; export const PotaSotaPanel = ({ potaData, potaLoading, showPOTA, onTogglePOTA, wwffData, wwffLoading, showWWFF, onToggleWWFF, - sotaData, sotaLoading, showSOTA, onToggleSOTA + sotaData, sotaLoading, showSOTA, onToggleSOTA, + onPOTASpotClick, + onWWFFSpotClick, + onSOTASpotClick }) => { const [activeTab, setActiveTab] = useState(() => { try { @@ -24,7 +27,7 @@ export const PotaSotaPanel = ({ const handleTabChange = (tab) => { setActiveTab(tab); - try { localStorage.setItem('openhamclock_potaSotaTab', tab); } catch {} + try { localStorage.setItem('openhamclock_potaSotaTab', tab); } catch { } }; const tabStyle = (tab) => ({ @@ -32,10 +35,10 @@ export const PotaSotaPanel = ({ padding: '3px 0', background: activeTab === tab ? 'rgba(255, 255, 255, 0.08)' : 'transparent', border: 'none', - borderBottom: activeTab === tab - ? `2px solid ${tab === 'pota' ? '#44cc44' : '#ff9632'}` + borderBottom: activeTab === tab + ? `2px solid ${tab === 'pota' ? '#44cc44' : '#ff9632'}` : '2px solid transparent', - color: activeTab === tab + color: activeTab === tab ? (tab === 'pota' ? '#44cc44' : '#ff9632') : '#666', fontSize: '10px', @@ -48,8 +51,8 @@ export const PotaSotaPanel = ({ return (
{/* Tab bar */} -
@@ -72,6 +75,7 @@ export const PotaSotaPanel = ({ loading={potaLoading} showOnMap={showPOTA} onToggleMap={onTogglePOTA} + onSpotClick={onPOTASpotClick} /> ) : activeTab === 'sota' ? ( ) : ( )}
diff --git a/src/components/SOTAPanel.jsx b/src/components/SOTAPanel.jsx index 2fd016be..7115e6c2 100644 --- a/src/components/SOTAPanel.jsx +++ b/src/components/SOTAPanel.jsx @@ -65,7 +65,17 @@ export const SOTAPanel = ({ data, loading, showOnMap, onToggleMap, onSpotClick } {spot.ref} - {spot.freq} + {(() => { + if (!spot.freq) return '?'; + const freqVal = parseFloat(spot.freq); + if (freqVal > 1000) { + // It's in kHz, convert to MHz + return (freqVal / 1000).toFixed(3); + } else { + // Already in MHz + return freqVal.toFixed(3); + } + })()} {spot.time} diff --git a/src/components/WWFFPanel.jsx b/src/components/WWFFPanel.jsx index 964ed215..ed6fc48a 100644 --- a/src/components/WWFFPanel.jsx +++ b/src/components/WWFFPanel.jsx @@ -12,13 +12,14 @@ export const WWFFPanel = ({ onToggleMap, showLabelsOnMap = true, onToggleLabelsOnMap, + onSpotClick, }) => { return (
-
@@ -61,7 +62,7 @@ export const WWFFPanel = ({ )}
- +
{loading ? (
@@ -70,14 +71,16 @@ export const WWFFPanel = ({ ) : data && data.length > 0 ? (
{data.map((spot, i) => ( -
onSpotClick?.(spot)} + style={{ display: 'grid', gridTemplateColumns: '62px 62px 58px 1fr', gap: '4px', padding: '3px 0', - borderBottom: i < data.length - 1 ? '1px solid var(--border-color)' : 'none' + borderBottom: i < data.length - 1 ? '1px solid var(--border-color)' : 'none', + cursor: 'pointer' }} > @@ -87,7 +90,17 @@ export const WWFFPanel = ({ {spot.ref} - {spot.freq} + {(() => { + if (!spot.freq) return '?'; + const freqVal = parseFloat(spot.freq); + if (freqVal > 1000) { + // It's in kHz, convert to MHz + return (freqVal / 1000).toFixed(3); + } else { + // Already in MHz + return freqVal.toFixed(3); + } + })()} {spot.time} diff --git a/src/contexts/RigContext.jsx b/src/contexts/RigContext.jsx index 0dd1c37b..ce644657 100644 --- a/src/contexts/RigContext.jsx +++ b/src/contexts/RigContext.jsx @@ -182,7 +182,17 @@ export const RigProvider = ({ children, rigConfig }) => { // Handle spot object (recursive call) if (typeof freqInput === 'object' && freqInput !== null) { const spot = freqInput; - const f = spot.freq || spot.freqMHz; + let f; + + // WSJT-X decodes have dialFrequency (VFO frequency) + // The freq field is just the audio delta offset, not part of the tune frequency + if (spot.dialFrequency) { + f = spot.dialFrequency; // Use dial frequency directly + } else { + // For other spot types (DX Cluster, POTA, etc.) + f = spot.freq || spot.freqMHz; + } + const m = spot.mode || modeInput; if (f) { tuneTo(f, m); diff --git a/src/hooks/usePOTASpots.js b/src/hooks/usePOTASpots.js index 3a2ec7bd..1ef10a3b 100644 --- a/src/hooks/usePOTASpots.js +++ b/src/hooks/usePOTASpots.js @@ -9,23 +9,23 @@ import { apiFetch } from '../utils/apiFetch'; // Convert grid square to lat/lon function gridToLatLon(grid) { if (!grid || grid.length < 4) return null; - + const g = grid.toUpperCase(); const lon = (g.charCodeAt(0) - 65) * 20 - 180; const lat = (g.charCodeAt(1) - 65) * 10 - 90; const lonMin = parseInt(g[2]) * 2; const latMin = parseInt(g[3]) * 1; - + let finalLon = lon + lonMin + 1; let finalLat = lat + latMin + 0.5; - + if (grid.length >= 6) { - const lonSec = (g.charCodeAt(4) - 65) * (2/24); - const latSec = (g.charCodeAt(5) - 65) * (1/24); - finalLon = lon + lonMin + lonSec + (1/24); - finalLat = lat + latMin + latSec + (0.5/24); + const lonSec = (g.charCodeAt(4) - 65) * (2 / 24); + const latSec = (g.charCodeAt(5) - 65) * (1 / 24); + finalLon = lon + lonMin + lonSec + (1 / 24); + finalLat = lat + latMin + latSec + (0.5 / 24); } - + return { lat: finalLat, lon: finalLon }; } @@ -41,17 +41,17 @@ export const usePOTASpots = () => { const res = await apiFetch('/api/pota/spots'); if (res?.ok) { const spots = await res.json(); - + // Filter out QRT spots and nearly-expired spots, then sort by most recent const validSpots = spots .filter(s => { // Filter out QRT (operator signed off) const comments = (s.comments || '').toUpperCase().trim(); if (comments === 'QRT' || comments.startsWith('QRT ') || comments.startsWith('QRT,')) return false; - + // Filter out spots expiring within 60 seconds if (typeof s.expire === 'number' && s.expire < 60) return false; - + return true; }) .sort((a, b) => { @@ -60,12 +60,12 @@ export const usePOTASpots = () => { const timeB = b.spotTime ? new Date(b.spotTime).getTime() : 0; return timeB - timeA; }); - + setData(validSpots.map(s => { // Use API coordinates, fall back to grid square let lat = s.latitude ? parseFloat(s.latitude) : null; let lon = s.longitude ? parseFloat(s.longitude) : null; - + if ((!lat || !lon) && s.grid6) { const loc = gridToLatLon(s.grid6); if (loc) { lat = loc.lat; lon = loc.lon; } @@ -74,28 +74,34 @@ export const usePOTASpots = () => { const loc = gridToLatLon(s.grid4); if (loc) { lat = loc.lat; lon = loc.lon; } } - + + + // POTA API returns frequency in kHz as a string (e.g., "7160" or "433240") + // Convert to MHz for consistency with SOTA and proper rig control + const freqKhz = parseFloat(s.frequency); + const freqMhz = !isNaN(freqKhz) ? freqKhz / 1000 : null; + return { - call: s.activator, - ref: s.reference, - freq: s.frequency, + call: s.activator, + ref: s.reference, + freq: freqMhz ? freqMhz.toString() : s.frequency, // Convert to MHz string mode: s.mode, name: s.name || s.locationDesc, locationDesc: s.locationDesc, lat, lon, - time: s.spotTime ? new Date(s.spotTime).toISOString().substr(11,5)+'z' : '', + time: s.spotTime ? new Date(s.spotTime).toISOString().substr(11, 5) + 'z' : '', expire: s.expire || 0 }; })); } - } catch (err) { - console.error('POTA error:', err); - } finally { - setLoading(false); + } catch (err) { + console.error('POTA error:', err); + } finally { + setLoading(false); } }; - + fetchPOTA(); const interval = setInterval(fetchPOTA, 120 * 1000); // 2 minutes fetchRefPOTA.current = fetchPOTA; diff --git a/src/hooks/useWWFFSpots.js b/src/hooks/useWWFFSpots.js index e0417c74..005e8d43 100644 --- a/src/hooks/useWWFFSpots.js +++ b/src/hooks/useWWFFSpots.js @@ -18,7 +18,7 @@ export const useWWFFSpots = () => { const res = await apiFetch('/api/wwff/spots'); if (res.ok) { const spots = await res.json(); - + // Filter out QRT spots and nearly-expired spots, then sort by most recent const validSpots = spots .filter(s => { @@ -28,7 +28,7 @@ export const useWWFFSpots = () => { // We should also time it out if it's more than 60 minutes old if (Math.floor(Date.now() / 1000) - s.spot_time > 3600) return false; - + return true; }) .sort((a, b) => { @@ -37,33 +37,38 @@ export const useWWFFSpots = () => { const timeB = b.spot_time ? b.spot_time : 0; return timeB - timeA; }); - + setData(validSpots.map(s => { // Use API coordinates let lat = s.latitude ? parseFloat(s.latitude) : null; let lon = s.longitude ? parseFloat(s.longitude) : null; - + + // WWFF API returns frequency_khz as a number (e.g., 7160 or 433240) + // Convert to MHz for consistency with POTA/SOTA and proper rig control + const freqKhz = parseFloat(s.frequency_khz); + const freqMhz = !isNaN(freqKhz) ? freqKhz / 1000 : null; + return { - call: s.activator, - ref: s.reference, - freq: s.frequency_khz, + call: s.activator, + ref: s.reference, + freq: freqMhz ? freqMhz.toString() : s.frequency_khz, // Convert to MHz string mode: s.mode, name: s.reference_name, remarks: s.remarks, lat, lon, - time: s.spot_time ? s.spot_time_formatted.substr(11,5)+'z' : '', + time: s.spot_time ? s.spot_time_formatted.substr(11, 5) + 'z' : '', expire: 0 }; })); } - } catch (err) { - console.error('WWFF error:', err); - } finally { - setLoading(false); + } catch (err) { + console.error('WWFF error:', err); + } finally { + setLoading(false); } }; - + fetchWWFF(); fetchRef.current = fetchWWFF; const interval = setInterval(fetchWWFF, 120 * 1000); // 2 minutes - reduced from 1 to save bandwidth diff --git a/src/layouts/ClassicLayout.jsx b/src/layouts/ClassicLayout.jsx index 73c3b986..b27eeb4d 100644 --- a/src/layouts/ClassicLayout.jsx +++ b/src/layouts/ClassicLayout.jsx @@ -54,6 +54,12 @@ export default function ClassicLayout(props) { const { tuneTo } = useRig(); + // Handler for POTA/WWFF/SOTA spot clicks + const handleParkSpotClick = (spot) => { + // tuneTo() in RigContext handles spot objects and all frequency conversions + tuneTo(spot); + }; + return config.layout === 'classic' ? (
{ + // tuneTo() in RigContext handles spot objects and all frequency conversions + tuneTo(spot); + }; + return (
)}