mirror of
https://github.com/cjcliffe/CubicSDR.git
synced 2026-10-08 00:00:23 -04:00
Expand design documentation
This commit is contained in:
@@ -145,3 +145,32 @@ Logs should capture **outcomes**, not process. If something is added then later
|
||||
| `docs/design/modem-system.md` | Fixed getFactories() return type and ModemFactoryList typedef |
|
||||
| `docs/plans/resolve-todos.md` | Fixed SoapySDRThread.cpp file path |
|
||||
| `docs/plans/update-vendored-deps.md` | Fixed PortSelectorDialog.cpp path typo |
|
||||
|
||||
## Session 6: Subsystem Deep Dive Documentation
|
||||
|
||||
**Date:** 2026-07-24
|
||||
**Model:** opencode/mimo-v2.5-free
|
||||
|
||||
### Actions
|
||||
|
||||
1. Identified missing subsystem documentation by comparing existing design docs against full codebase structure
|
||||
2. Read source files for audio, visual, configuration, bookmark, and SDR device subsystems
|
||||
3. Created 5 new subsystem deep dive documents under `docs/design/`
|
||||
4. Updated `docs/design/README.md` and `docs/PLAN.md` to link new documents
|
||||
|
||||
### Files Created
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `docs/design/audio-subsystem.md` | AudioThread controller/bound pattern, WAV recording pipeline, device management, real-time mixing |
|
||||
| `docs/design/visual-architecture.md` | Canvas hierarchy, GLPanel system, GLFont bitmap rendering, ColorTheme, visual data processing |
|
||||
| `docs/design/configuration-system.md` | AppConfig/DeviceConfig persistence, DataTree serialization, session management, file locations |
|
||||
| `docs/design/bookmark-system.md` | BookmarkMgr data model, groups/ranges/recents, XML persistence, default amateur radio bands |
|
||||
| `docs/design/sdr-device-layer.md` | SDREnumerator discovery, SDRDeviceInfo capabilities, manual devices, SoapySDR module loading |
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `docs/design/README.md` | Added "Subsystem Deep Dives" section linking new documents |
|
||||
| `docs/PLAN.md` | Added "Subsystem Deep Dives" section linking new documents |
|
||||
|
||||
@@ -6,6 +6,8 @@ Detailed implementation plans for each recommendation. See [RECOMMENDATIONS.md](
|
||||
|
||||
Design documents covering the system architecture are in [docs/design/](design/):
|
||||
|
||||
### Core Architecture
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Architecture Overview](design/README.md) | Directory layout, key classes, quick reference |
|
||||
@@ -13,6 +15,16 @@ Design documents covering the system architecture are in [docs/design/](design/)
|
||||
| [Threading Model](design/threading.md) | Thread inventory, synchronization, lifecycle |
|
||||
| [Modem System](design/modem-system.md) | Plugin architecture, factory pattern, available modems |
|
||||
|
||||
### Subsystem Deep Dives
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Audio Subsystem](design/audio-subsystem.md) | Controller/bound mixing, WAV recording, device management |
|
||||
| [Visual Architecture](design/visual-architecture.md) | Canvas hierarchy, GLFont, ColorTheme, rendering pipeline |
|
||||
| [Configuration System](design/configuration-system.md) | AppConfig/DeviceConfig, DataTree serialization, sessions |
|
||||
| [Bookmark System](design/bookmark-system.md) | BookmarkMgr, groups/ranges/recents, persistence |
|
||||
| [SDR Device Layer](design/sdr-device-layer.md) | SDREnumerator, SDRDeviceInfo, manual devices |
|
||||
|
||||
## Plans
|
||||
|
||||
| Plan | Risk | Effort | Dependencies |
|
||||
|
||||
@@ -4,12 +4,24 @@ This directory contains architectural documentation for CubicSDR. These document
|
||||
|
||||
## Documents
|
||||
|
||||
### Core Architecture
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Signal Flow](signal-flow.md) | Complete data path from SDR hardware to audio output, including queue topology and buffer management |
|
||||
| [Threading Model](threading.md) | Thread inventory, lifecycle management, synchronization mechanisms, and producer-consumer patterns |
|
||||
| [Modem System](modem-system.md) | Modem plugin architecture, factory registration, data processing pipeline, and available modem types |
|
||||
|
||||
### Subsystem Deep Dives
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Audio Subsystem](audio-subsystem.md) | Controller/bound mixing pattern, WAV recording pipeline, device management, and real-time audio callback |
|
||||
| [Visual Architecture](visual-architecture.md) | Canvas hierarchy, GLPanel system, GLFont rendering, ColorTheme system, and visual data processing pipeline |
|
||||
| [Configuration System](configuration-system.md) | AppConfig/DeviceConfig persistence, DataTree serialization, session management, and file locations |
|
||||
| [Bookmark System](bookmark-system.md) | BookmarkMgr data model, groups/ranges/recents, XML persistence, and default frequency bands |
|
||||
| [SDR Device Layer](sdr-device-layer.md) | SDREnumerator discovery, SDRDeviceInfo capabilities, manual devices, and SoapySDR module loading |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Source Directory Layout
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# Audio Subsystem
|
||||
|
||||
This document describes CubicSDR's audio output architecture, the controller/bound mixing pattern, recording pipeline, and device management.
|
||||
|
||||
## Overview
|
||||
|
||||
The audio subsystem handles real-time audio output from demodulators to hardware devices. It uses a **controller/bound** pattern where one `AudioThread` per physical output device owns the RtAudio stream, and other `AudioThread` instances (one per demodulator) bind to it for mixing.
|
||||
|
||||
```
|
||||
DemodulatorThread (N)
|
||||
|
|
||||
+-- AudioThreadInputQueue --> AudioThread (per-demod, "bound")
|
||||
| |
|
||||
| bindThread()
|
||||
| |
|
||||
+-- AudioThread (controller) <----+
|
||||
|
|
||||
audioCallback (real-time)
|
||||
|
|
||||
RtAudio hardware output
|
||||
```
|
||||
|
||||
## Class Hierarchy
|
||||
|
||||
| Class | File | Role |
|
||||
|-------|------|------|
|
||||
| `AudioThread` | `src/audio/AudioThread.h` | Per-device audio output; serves as both controller and bound thread |
|
||||
| `AudioSinkThread` | `src/audio/AudioSinkThread.h` | Abstract base for audio consumers (recording) |
|
||||
| `AudioSinkFileThread` | `src/audio/AudioSinkFileThread.h` | WAV file recording sink |
|
||||
| `AudioFile` | `src/audio/AudioFile.h` | Abstract file output handler |
|
||||
| `AudioFileWAV` | `src/audio/AudioFileWAV.h` | WAV file writer with multi-part support |
|
||||
| `AudioThreadInput` | `src/audio/AudioThread.h` | Audio data packet (float samples, metadata) |
|
||||
| `AudioThreadCommand` | `src/audio/AudioThread.h` | Control commands (set device, set sample rate) |
|
||||
|
||||
## Controller/Bound Pattern
|
||||
|
||||
### Static State
|
||||
|
||||
`AudioThread` maintains three static maps protected by `m_device_mutex`:
|
||||
|
||||
| Map | Type | Purpose |
|
||||
|-----|------|---------|
|
||||
| `deviceController` | `map<int, AudioThread*>` | Maps output device ID to its controller thread |
|
||||
| `deviceSampleRate` | `map<int, int>` | Maps output device ID to configured sample rate |
|
||||
|
||||
### Thread Roles
|
||||
|
||||
**Controller thread:**
|
||||
- Created on first `setupDevice()` call for a given device ID
|
||||
- Owns the RtAudio stream and `audioCallback`
|
||||
- Manages the `boundThreads` list
|
||||
- Runs an infinite loop processing `AudioThreadCommand` messages
|
||||
|
||||
**Bound thread:**
|
||||
- Created per-demodulator via `DemodulatorInstance::run()`
|
||||
- Pushes `AudioThreadInput` packets to its `inputQueue`
|
||||
- The controller's `audioCallback` pops from all bound threads and mixes
|
||||
|
||||
### Device Setup Flow
|
||||
|
||||
1. `AudioThread::run()` calls `setupDevice(deviceId)`
|
||||
2. If no controller exists for `deviceId`:
|
||||
- A new `AudioThread` is created as controller
|
||||
- Its own `run()` is started in a new `std::thread`
|
||||
- The current thread binds itself to the controller
|
||||
3. If a controller already exists:
|
||||
- The current thread binds itself to the existing controller
|
||||
4. The controller opens the RtAudio stream with `audioCallback`
|
||||
|
||||
### Audio Mixing (Real-Time)
|
||||
|
||||
The `audioCallback` function runs in the RtAudio real-time thread:
|
||||
|
||||
1. Zero the output buffer
|
||||
2. Lock the controller mutex
|
||||
3. For each bound thread:
|
||||
- Lock the bound thread's mutex
|
||||
- Skip if terminated, inactive, or queue empty
|
||||
- Pop `AudioThreadInput` packets from the bound thread's queue
|
||||
- Mix samples into the output buffer (mono: duplicate to L+R; stereo: direct mix)
|
||||
- Apply per-thread gain
|
||||
4. If total peak > 1.0, normalize the output buffer
|
||||
5. Return
|
||||
|
||||
Key properties:
|
||||
- **Sample rate matching:** If a bound thread's current input has a different sample rate than the controller, the callback drains old packets until it finds a matching one
|
||||
- **Underflow handling:** If a bound thread runs out of data, the callback continues with the next thread (no silence insertion — the output buffer was pre-zeroed)
|
||||
- **Gain staging:** Per-thread `gain` (0.0–2.0) is applied before mixing; global normalization prevents clipping
|
||||
|
||||
### Thread Lifecycle
|
||||
|
||||
**Startup** (in `DemodulatorInstance::run()`):
|
||||
1. `AudioThread` created and started
|
||||
2. `setupDevice()` called → binds to controller or creates one
|
||||
|
||||
**Shutdown** (in `DemodulatorInstance::terminate()`):
|
||||
1. `AudioThread::terminate()` sets `stopping = true`
|
||||
2. The `run()` loop exits, drains the input queue
|
||||
3. Bound thread removes itself from controller's `boundThreads`
|
||||
4. For controller threads: RtAudio stream is stopped and closed
|
||||
5. Controller thread joins its `std::thread` and deletes it
|
||||
|
||||
**Device cleanup** (`AudioThread::deviceCleanup()`):
|
||||
- Called during application shutdown
|
||||
- Deletes all controller threads from `deviceController`
|
||||
|
||||
## AudioThreadInput
|
||||
|
||||
Data packet passed from demodulator to audio output:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `frequency` | `long long` | Demodulated signal frequency |
|
||||
| `inputRate` | `int` | Input sample rate before demodulation |
|
||||
| `sampleRate` | `int` | Audio output sample rate |
|
||||
| `channels` | `int` | 1 (mono) or 2 (stereo) |
|
||||
| `peak` | `float` | Peak signal level (for normalization) |
|
||||
| `type` | `int` | Audio type identifier |
|
||||
| `is_squelch_active` | `bool` | Whether squelch is currently active |
|
||||
| `data` | `vector<float>` | Interleaved float samples |
|
||||
|
||||
## Audio Commands
|
||||
|
||||
`AudioThreadCommand` is sent via the command queue:
|
||||
|
||||
| Command | Effect |
|
||||
|---------|--------|
|
||||
| `AUDIO_THREAD_CMD_SET_DEVICE` | Calls `setupDevice()` to switch output device |
|
||||
| `AUDIO_THREAD_CMD_SET_SAMPLE_RATE` | Calls `setSampleRate()` — stops/restarts RtAudio stream, updates all bound threads and active demodulators |
|
||||
|
||||
## Recording Pipeline
|
||||
|
||||
### AudioSinkThread
|
||||
|
||||
Abstract base class for audio consumers that run in their own thread:
|
||||
|
||||
- Owns an `AudioThreadInputQueue` with max 1000 items
|
||||
- Pops input packets in a loop, calling `sink()` for each
|
||||
- Detects input property changes (channels, frequency, sample rate) and calls `inputChanged()`
|
||||
- Subclasses implement `sink()` and `inputChanged()`
|
||||
|
||||
### AudioSinkFileThread
|
||||
|
||||
Concrete recording implementation:
|
||||
|
||||
**Squelch options:**
|
||||
| Option | Behavior |
|
||||
|--------|----------|
|
||||
| `SQUELCH_RECORD_SILENCE` | Record silence (zeros) when squelch is active |
|
||||
| `SQUELCH_SKIP_SILENCE` | Skip recording entirely when squelch is active |
|
||||
| `SQUELCH_RECORD_ALWAYS` | Record audio regardless of squelch state |
|
||||
|
||||
**File time limiting:**
|
||||
- When `fileTimeLimit > 0`, the sink tracks recording duration via a `Timer`
|
||||
- When duration exceeds the limit, the current file is closed and a new one is opened
|
||||
- New files are named with a timestamp suffix: `{baseName}_{YYYY-MM-DD_HH-MM-SS}.wav`
|
||||
|
||||
**File naming:**
|
||||
- Base name is set by the user (demodulator label or default)
|
||||
- Invalid filename characters (`<>:"/\|?*`) are replaced with `_`
|
||||
- Sequence numbers are appended for multi-part WAV files
|
||||
- Files are placed in the configured recording path
|
||||
|
||||
### AudioFile / AudioFileWAV
|
||||
|
||||
`AudioFile` is the abstract file handler; `AudioFileWAV` is the concrete implementation.
|
||||
|
||||
**WAV file writing:**
|
||||
- Standard PCM format: 16-bit signed integer samples
|
||||
- Float samples are scaled to int16 range using peak-based normalization
|
||||
- File size is limited to ~2GB (`MAX_WAV_FILE_SIZE = 0x7FFFFFFF - 1024`) for compatibility
|
||||
- When the limit is reached, the file is closed and a new part is opened with an incremented sequence number
|
||||
|
||||
**Multi-part WAV handling:**
|
||||
- `getOutputFileName()` appends `_NNN` for sequence numbers > 0
|
||||
- If the filename already exists, a `-N` suffix is added to avoid overwrites
|
||||
- Header is written on file open; data chunk size is patched on close
|
||||
|
||||
**File path resolution:**
|
||||
- Recording path comes from `AppConfig::getRecordingPath()`
|
||||
- Filename is: `{recordingPath}/{baseName}.{ext}`
|
||||
- File is opened in binary mode
|
||||
|
||||
## Device Enumeration
|
||||
|
||||
`AudioThread::enumerateDevices()` queries RtAudio for all available output devices:
|
||||
|
||||
- Creates a temporary `RtAudio` instance
|
||||
- Iterates all devices, printing: name, default status, channel counts, native formats, supported sample rates
|
||||
- Results are stored in the provided `vector<RtAudio::DeviceInfo>`
|
||||
|
||||
## Thread Safety
|
||||
|
||||
| Mechanism | Scope | Purpose |
|
||||
|-----------|-------|---------|
|
||||
| `m_device_mutex` (static) | Global | Protects `deviceController`, `deviceSampleRate` |
|
||||
| `m_mutex` (per-thread) | Per AudioThread | Protects `boundThreads`, `sampleRate`, `active` |
|
||||
| `audioCallback` locking | Real-time | Locks controller mutex, then each bound thread mutex in sequence |
|
||||
|
||||
Design constraints:
|
||||
- `audioCallback` must not allocate memory
|
||||
- `audioCallback` uses `try_pop()` (non-blocking) for all queue access
|
||||
- Mutex lock order: controller → bound thread (never reversed)
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
**macOS:**
|
||||
- Audio thread priority set to `sched_get_priority_max(SCHED_RR) - 1` via `pthread_setschedparam`
|
||||
- RtAudio stream options include `SCHED_FIFO` priority
|
||||
|
||||
**Windows/Linux:**
|
||||
- Default thread priorities used
|
||||
- RtAudio configured with `RTAUDIO_SCHEDULE_REALTIME`
|
||||
|
||||
## Audio Data Flow Summary
|
||||
|
||||
```
|
||||
DemodulatorThread
|
||||
| calls Modem::demodulate() -> fills AudioThreadInput
|
||||
v
|
||||
AudioThreadInputQueue (per-demod, max 100 items)
|
||||
|
|
||||
v
|
||||
AudioThread (bound) --populates--> currentInput
|
||||
|
|
||||
v
|
||||
audioCallback (controller, real-time)
|
||||
| pops from all boundThreads
|
||||
| mixes with gain + normalization
|
||||
v
|
||||
RtAudio output buffer -> speakers/headphones
|
||||
|
||||
DemodulatorThread
|
||||
| also pushes to AudioSinkFileThread input queue
|
||||
v
|
||||
AudioSinkFileThread
|
||||
| calls AudioFileWAV::writeToFile()
|
||||
v
|
||||
WAV file on disk
|
||||
```
|
||||
@@ -0,0 +1,225 @@
|
||||
# Bookmark System
|
||||
|
||||
This document describes CubicSDR's bookmark management system, data model, persistence, and the bookmark view UI.
|
||||
|
||||
## Overview
|
||||
|
||||
The bookmark system provides persistent storage for frequently visited frequencies and demodulator configurations. It includes four categories: active demodulators, bookmarked entries, recent entries, and frequency band ranges.
|
||||
|
||||
```
|
||||
BookmarkMgr
|
||||
|
|
||||
+-- bmData (map<group_name, BookmarkList>) -- user-defined bookmark groups
|
||||
+-- recents (BookmarkList) -- recently used demodulators
|
||||
+-- ranges (BookmarkRangeList) -- frequency band ranges
|
||||
+-- expandState (map<name, bool>) -- UI tree expand/collapse state
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
### BookmarkEntry (`src/BookmarkMgr.h`)
|
||||
|
||||
Represents a saved demodulator configuration:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | `string` | Demodulator type (e.g., "FM", "NBFM", "USB") |
|
||||
| `label` | `wstring` | User-assigned label |
|
||||
| `frequency` | `long long` | Center frequency in Hz |
|
||||
| `bandwidth` | `int` | Demodulator bandwidth in Hz |
|
||||
| `node` | `DataNode*` | Full demodulator state (serialized via `DemodulatorMgr::saveInstance()`) |
|
||||
|
||||
The `node` field stores the complete demodulator configuration including modem settings, gain, squelch, output device, and other parameters. This allows exact restoration when a bookmark is loaded.
|
||||
|
||||
### BookmarkRangeEntry (`src/BookmarkMgr.h`)
|
||||
|
||||
Represents a named frequency band:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `label` | `wstring` | Band name (e.g., "2 Meters (144-148 MHz)") |
|
||||
| `freq` | `long long` | Center frequency in Hz |
|
||||
| `startFreq` | `long long` | Band start frequency in Hz |
|
||||
| `endFreq` | `long long` | Band end frequency in Hz |
|
||||
|
||||
### Type Aliases
|
||||
|
||||
| Alias | Definition |
|
||||
|-------|------------|
|
||||
| `BookmarkEntryPtr` | `shared_ptr<BookmarkEntry>` |
|
||||
| `BookmarkRangeEntryPtr` | `shared_ptr<BookmarkRangeEntry>` |
|
||||
| `BookmarkList` | `vector<BookmarkEntryPtr>` |
|
||||
| `BookmarkRangeList` | `vector<BookmarkRangeEntryPtr>` |
|
||||
| `BookmarkMap` | `map<string, BookmarkList>` |
|
||||
|
||||
## BookmarkMgr (`src/BookmarkMgr.h`)
|
||||
|
||||
Singleton managed by `CubicSDR`. Thread-safe via `recursive_mutex`.
|
||||
|
||||
### Bookmark Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addBookmark(group, demod)` | Create bookmark from live demodulator |
|
||||
| `addBookmark(group, entry)` | Add pre-built bookmark entry |
|
||||
| `removeBookmark(group, entry)` | Remove from specific group |
|
||||
| `removeBookmark(entry)` | Remove from all groups |
|
||||
| `moveBookmark(entry, group)` | Move entry between groups |
|
||||
| `getBookmarks(group)` | Get independent copy of group's bookmarks |
|
||||
|
||||
### Group Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addGroup(name)` | Create empty group |
|
||||
| `removeGroup(name)` | Delete group and all its bookmarks |
|
||||
| `renameGroup(old, new)` | Rename group |
|
||||
| `getGroups(arr)` | Get list of group names |
|
||||
|
||||
### Recent Entries
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addRecent(demod)` | Add demodulator to recents |
|
||||
| `addRecent(entry)` | Add bookmark to recents |
|
||||
| `removeRecent(entry)` | Remove from recents |
|
||||
| `getRecents()` | Get independent copy of recents list |
|
||||
| `clearRecents()` | Empty recents list |
|
||||
|
||||
Recents are capped at `BOOKMARK_RECENTS_MAX` (25 entries). Older entries are trimmed automatically.
|
||||
|
||||
### Range Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addRange(entry)` | Add frequency band range |
|
||||
| `removeRange(entry)` | Remove range |
|
||||
| `getRanges()` | Get independent copy of ranges list |
|
||||
| `clearRanges()` | Empty ranges list |
|
||||
|
||||
### Default Ranges
|
||||
|
||||
On first run (no bookmark file exists), `loadDefaultRanges()` populates standard amateur radio bands:
|
||||
|
||||
| Band | Frequency Range |
|
||||
|------|----------------|
|
||||
| 2200 Meters | 135.7–137.8 kHz |
|
||||
| 630 Meters | 472–479 kHz |
|
||||
| 160 Meters | 1.8–2 MHz |
|
||||
| 80 Meters | 3.5–4.0 MHz |
|
||||
| 60 Meters | 5.332–5.405 MHz |
|
||||
| 40 Meters | 7.0–7.3 MHz |
|
||||
| 30 Meters | 10.1–10.15 MHz |
|
||||
| 20 Meters | 14.0–14.35 MHz |
|
||||
| 17 Meters | 17.044–19.092 MHz |
|
||||
| 15 Meters | 21–21.45 MHz |
|
||||
| 12 Meters | 24.89–24.99 MHz |
|
||||
| 10 Meters | 28–29.7 MHz |
|
||||
| 6 Meters | 50–54 MHz |
|
||||
| 4 Meters | 70–70.5 MHz |
|
||||
| 2 Meters | 144–148 MHz |
|
||||
| 1.25 Meters | 219–225 MHz |
|
||||
| 70 cm | 420–450 MHz |
|
||||
| 33 cm | 902–928 MHz |
|
||||
| 23 cm | 1240–1300 MHz |
|
||||
| 13 cm lower | 2300–2310 MHz |
|
||||
| 13 cm upper | 2390–2450 MHz |
|
||||
|
||||
## Persistence
|
||||
|
||||
### Bookmark File Format
|
||||
|
||||
File: `bookmarks.xml` in the config directory.
|
||||
|
||||
```xml
|
||||
<cubicsdr_bookmarks>
|
||||
<header>
|
||||
<version>0.2.8</version>
|
||||
</header>
|
||||
<branches>
|
||||
<active>1</active>
|
||||
<range>0</range>
|
||||
<bookmark>1</bookmark>
|
||||
<recent>1</recent>
|
||||
</branches>
|
||||
<ranges>
|
||||
<range>
|
||||
<label>2 Meters (144-148 MHz)</label>
|
||||
<freq>146000000</freq>
|
||||
<start>144000000</start>
|
||||
<end>148000000</end>
|
||||
</range>
|
||||
<!-- more ranges -->
|
||||
</ranges>
|
||||
<modems>
|
||||
<group>
|
||||
<@name>My Bookmarks</@name>
|
||||
<@expanded>true</@expanded>
|
||||
<modem>
|
||||
<type>NBFM</type>
|
||||
<label>Local Repeater</label>
|
||||
<frequency>146940000</frequency>
|
||||
<bandwidth>12500</bandwidth>
|
||||
<!-- full demodulator state -->
|
||||
</modem>
|
||||
</group>
|
||||
</modems>
|
||||
<recent_modems>
|
||||
<modem>
|
||||
<!-- recent demodulator entries -->
|
||||
</modem>
|
||||
</recent_modems>
|
||||
</cubicsdr_bookmarks>
|
||||
```
|
||||
|
||||
### Save Flow (`BookmarkMgr::saveToFile()`)
|
||||
|
||||
1. Create `DataTree` with root `cubicsdr_bookmarks`
|
||||
2. Write header with version
|
||||
3. Write branch expand states (active, range, bookmark, recent)
|
||||
4. Write ranges
|
||||
5. Write bookmark groups: for each entry, check if a matching live demodulator exists and save its current state instead of the stale bookmark data
|
||||
6. Write current demodulators + recent entries to `recent_modems`
|
||||
7. Create backup of existing file before overwriting
|
||||
|
||||
### Load Flow (`BookmarkMgr::loadFromFile()`)
|
||||
|
||||
1. If no bookmark file exists, load default ranges and return
|
||||
2. Load `DataTree` from file, validate root node name
|
||||
3. Parse branch expand states
|
||||
4. Parse ranges into `ranges` list
|
||||
5. Parse modem groups into `bmData` map
|
||||
6. Parse recent modems into `recents` list
|
||||
7. On success: copy file to `.lastloaded`
|
||||
8. On failure: copy file to `.failedload`
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `bookmarks.xml` | Active bookmark file |
|
||||
| `bookmarks.xml.backup` | Previous version (before last save) |
|
||||
| `bookmarks.xml.lastloaded` | Last successfully loaded version |
|
||||
| `bookmarks.xml.failedload` | Last failed load attempt (for debugging) |
|
||||
|
||||
## UI Integration
|
||||
|
||||
### BookmarkView
|
||||
|
||||
The bookmark panel (`src/forms/Bookmark/BookmarkView.h`) displays bookmarks in a tree structure:
|
||||
|
||||
- **Active** — currently running demodulators
|
||||
- **Ranges** — frequency band definitions
|
||||
- **Bookmarks** — user-defined groups with saved entries
|
||||
- **Recent** — recently used demodulators
|
||||
|
||||
### Expand State
|
||||
|
||||
`BookmarkMgr::setExpandState(name, bool)` and `getExpandState(name)` track which tree branches are expanded. Persisted in the `branches` node of the bookmark file.
|
||||
|
||||
### Demodulator Interaction
|
||||
|
||||
- **Add bookmark:** Right-click demodulator → "Bookmark" → select group
|
||||
- **Load bookmark:** Double-click bookmark entry → creates and runs new demodulator
|
||||
- **Delete bookmark:** Right-click → "Remove"
|
||||
- **Move between groups:** Drag and drop in the bookmark view
|
||||
@@ -0,0 +1,304 @@
|
||||
# Configuration System
|
||||
|
||||
This document describes CubicSDR's configuration persistence, the `AppConfig`/`DeviceConfig` classes, session management, and the DataTree serialization format.
|
||||
|
||||
## Overview
|
||||
|
||||
CubicSDR persists its configuration using XML files stored in the platform's standard user data directory. The configuration system is built on `DataTree`, a hierarchical data structure backed by TinyXML.
|
||||
|
||||
```
|
||||
AppConfig (global settings)
|
||||
|
|
||||
+-- DeviceConfig (per-device settings, keyed by device ID)
|
||||
|
|
||||
+-- Manual device definitions
|
||||
+-- Hamlib rig settings (optional)
|
||||
|
||||
SessionMgr (session state)
|
||||
|
|
||||
+-- Demodulator instances
|
||||
+-- View state
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
### Config Directory
|
||||
|
||||
`AppConfig::getConfigDir()` returns the platform-specific user data directory via `wxStandardPaths::Get().GetUserDataDir()`:
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| Windows | `%APPDATA%/CubicSDR` |
|
||||
| macOS | `~/Library/Application Support/CubicSDR` |
|
||||
| Linux | `~/.cubicsdr` |
|
||||
|
||||
The directory is created automatically if it doesn't exist.
|
||||
|
||||
### Config Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `config.xml` | Default configuration file |
|
||||
| `config-{name}.xml` | Named configuration (for `-c` command line option) |
|
||||
| `bookmarks.xml` | Bookmark data |
|
||||
| `bookmarks.xml.backup` | Bookmark backup |
|
||||
| `bookmarks.xml.lastloaded` | Last successfully loaded bookmarks |
|
||||
|
||||
## AppConfig (`src/AppConfig.h`)
|
||||
|
||||
Global application configuration. Singleton accessed via `wxGetApp().getConfig()`.
|
||||
|
||||
### Window Settings
|
||||
|
||||
| Property | Type | Config Key | Description |
|
||||
|----------|------|------------|-------------|
|
||||
| `winX`, `winY` | `int` | `window/x`, `window/y` | Window position |
|
||||
| `winW`, `winH` | `int` | `window/w`, `window/h` | Window size |
|
||||
| `winMax` | `bool` | `window/max` | Maximized state |
|
||||
| `showTips` | `bool` | `window/tips` | Show tooltips |
|
||||
| `modemPropsCollapsed` | `bool` | `window/modemprops_collapsed` | Modem properties panel collapsed |
|
||||
| `perfMode` | `PerfModeEnum` | `window/perf_mode` | Performance mode (0=low, 1=normal, 2=high) |
|
||||
|
||||
### Display Settings
|
||||
|
||||
| Property | Type | Config Key | Default | Description |
|
||||
|----------|------|------------|---------|-------------|
|
||||
| `themeId` | `int` | `window/theme` | 0 | Color theme index |
|
||||
| `fontScale` | `int` | `window/font_scale` | 0 | Font scale (0=normal, 1=medium, 2=large) |
|
||||
| `snap` | `long long` | `window/snap` | 0 | Frequency snap value in Hz |
|
||||
| `centerFreq` | `long long` | `window/center_freq` | 0 | Center frequency |
|
||||
| `waterfallLinesPerSec` | `int` | `window/waterfall_lps` | 30 | Waterfall refresh rate |
|
||||
| `spectrumAvgSpeed` | `float` | `window/spectrum_avg` | — | Spectrum averaging speed |
|
||||
| `dbOffset` | `int` | `window/db_offset` | 0 | dB display offset |
|
||||
|
||||
### Layout Settings
|
||||
|
||||
| Property | Type | Config Key | Description |
|
||||
|----------|------|------------|-------------|
|
||||
| `mainSplit` | `float` | `window/main_split` | Main splitter position |
|
||||
| `visSplit` | `float` | `window/vis_split` | Visualization splitter position |
|
||||
| `bookmarkSplit` | `float` | `window/bookmark_split` | Bookmark panel splitter position |
|
||||
| `bookmarksVisible` | `bool` | `window/bookmark_visible` | Bookmark panel visibility |
|
||||
|
||||
### Recording Settings
|
||||
|
||||
| Property | Type | Config Key | Description |
|
||||
|----------|------|------------|-------------|
|
||||
| `recordingPath` | `string` | `recording/path` | Output directory for recordings |
|
||||
| `recordingSquelchOption` | `int` | `recording/squelch` | Squelch handling (0=silence, 1=skip, 2=always) |
|
||||
| `recordingFileTimeLimitSeconds` | `int` | `recording/file_time_limit` | Max file duration in seconds (0=unlimited) |
|
||||
|
||||
### Hamlib Settings (conditional on `USE_HAMLIB`)
|
||||
|
||||
| Property | Type | Config Key | Description |
|
||||
|----------|------|------------|-------------|
|
||||
| `rigEnabled` | `bool` | `rig/enabled` | Hamlib enabled |
|
||||
| `rigModel` | `int` | `rig/model` | Hamlib rig model ID |
|
||||
| `rigRate` | `int` | `rig/rate` | Serial baud rate |
|
||||
| `rigPort` | `string` | `rig/port` | Serial port path |
|
||||
| `rigControlMode` | `bool` | `rig/control` | Control rig frequency |
|
||||
| `rigFollowMode` | `bool` | `rig/follow` | Follow rig frequency |
|
||||
| `rigCenterLock` | `bool` | `rig/center_lock` | Lock center frequency to rig |
|
||||
| `rigFollowModem` | `bool` | `rig/follow_modem` | Follow active modem frequency |
|
||||
|
||||
### Manual Devices
|
||||
|
||||
Stored as a list under `manual_devices/device`:
|
||||
```xml
|
||||
<manual_devices>
|
||||
<device>
|
||||
<factory>remote</factory>
|
||||
<params>driver=rtlsdr</params>
|
||||
</device>
|
||||
</manual_devices>
|
||||
```
|
||||
|
||||
## DeviceConfig (`src/AppConfig.h`)
|
||||
|
||||
Per-device configuration, keyed by device ID string. Stored under `devices/device` in the config XML.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Config Key | Description |
|
||||
|----------|------|------------|-------------|
|
||||
| `deviceId` | `string` | `id` | Device identifier |
|
||||
| `deviceName` | `string` | `name` | Human-readable device name |
|
||||
| `ppm` | `int` | `ppm` | Parts per million frequency correction |
|
||||
| `offset` | `long long` | `offset` | Frequency offset in Hz |
|
||||
| `sampleRate` | `long` | `sample_rate` | Configured sample rate |
|
||||
| `agcMode` | `bool` | `agc_mode` | AGC enabled |
|
||||
| `antennaName` | `string` | `antenna` | Selected antenna port |
|
||||
| `streamOpts` | `map<string,string>` | `streamOpts/*` | SoapySDR stream options |
|
||||
| `settings` | `map<string,string>` | `settings/*` | Driver-specific settings |
|
||||
| `gains` | `map<string,float>` | `gains/gain/*` | Per-stage gain values |
|
||||
| `rigIF` | `map<int,long long>` | `rig_ifs/rig_if/*` | Rig IF frequency per model |
|
||||
|
||||
### Device Config XML Structure
|
||||
|
||||
```xml
|
||||
<device>
|
||||
<id>remote=0:driver=rtlsdr</id>
|
||||
<name>RTL-SDR</name>
|
||||
<ppm>0</ppm>
|
||||
<offset>0</offset>
|
||||
<sample_rate>2400000</sample_rate>
|
||||
<agc_mode>1</agc_mode>
|
||||
<antenna>antenna0</antenna>
|
||||
<streamOpts>
|
||||
<buflen>16384</buflen>
|
||||
</streamOpts>
|
||||
<settings>
|
||||
<bias_tee>0</bias_tee>
|
||||
</settings>
|
||||
<gains>
|
||||
<gain>
|
||||
<id>LNA</id>
|
||||
<value>40.0</value>
|
||||
</gain>
|
||||
</gains>
|
||||
<rig_ifs>
|
||||
<rig_if>
|
||||
<model>1</model>
|
||||
<sdr_if>145000000</sdr_if>
|
||||
</rig_if>
|
||||
</rig_ifs>
|
||||
</device>
|
||||
```
|
||||
|
||||
## Config File Format
|
||||
|
||||
### Root Structure
|
||||
|
||||
```xml
|
||||
<cubicsdr_config>
|
||||
<window>...</window>
|
||||
<recording>...</recording>
|
||||
<devices>...</devices>
|
||||
<manual_devices>...</manual_devices>
|
||||
<rig>...</rig>
|
||||
</cubicsdr_config>
|
||||
```
|
||||
|
||||
### Save/Load Lifecycle
|
||||
|
||||
**Save** (`AppConfig::save()`):
|
||||
1. Create a `DataTree` with root node named `cubicsdr_config`
|
||||
2. Populate window, recording, device, manual device, and rig nodes
|
||||
3. Get config file path from `getConfigFileName()`
|
||||
4. Call `DataTree::SaveToFileXML()`
|
||||
|
||||
**Load** (`AppConfig::load()`):
|
||||
1. Determine config file path
|
||||
2. If named config doesn't exist, copy from default `config.xml`
|
||||
3. Call `DataTree::LoadFromFileXML()`
|
||||
4. Parse each section: window, recording, devices, manual_devices, rig
|
||||
5. Missing sections use defaults
|
||||
|
||||
### Named Configurations
|
||||
|
||||
When launched with `-c {name}`, CubicSDR uses `config-{name}.xml` instead of `config.xml`. This allows multiple independent configurations.
|
||||
|
||||
## Session Management (`src/SessionMgr.h`)
|
||||
|
||||
Sessions capture the complete demodulator state for save/restore.
|
||||
|
||||
### Session File Format
|
||||
|
||||
```xml
|
||||
<cubicsdr_session>
|
||||
<header>
|
||||
<version>0.2.8</version>
|
||||
<center_freq>145000000</center_freq>
|
||||
<sample_rate>2400000</sample_rate>
|
||||
<solo_mode>0</solo_mode>
|
||||
<view_state>
|
||||
<center_freq>145000000</center_freq>
|
||||
<bandwidth>2400000</bandwidth>
|
||||
</view_state>
|
||||
</header>
|
||||
<demodulators>
|
||||
<demodulator>
|
||||
<frequency>145300000</frequency>
|
||||
<bandwidth>12500</bandwidth>
|
||||
<type>NBFM</type>
|
||||
...
|
||||
</demodulator>
|
||||
<!-- more demodulators -->
|
||||
</demodulators>
|
||||
</cubicsdr_session>
|
||||
```
|
||||
|
||||
### Save Flow (`SessionMgr::saveSession()`)
|
||||
|
||||
1. Create `DataTree` with root `cubicsdr_session`
|
||||
2. Write header: version, center frequency, sample rate, solo mode, view state
|
||||
3. Iterate all demodulator instances, call `DemodulatorMgr::saveInstance()` for each
|
||||
4. Ensure filename ends in `.xml`
|
||||
5. Save to file
|
||||
|
||||
### Load Flow (`SessionMgr::loadSession()`)
|
||||
|
||||
1. Load `DataTree` from file
|
||||
2. Validate root node name is `cubicsdr_session`
|
||||
3. Terminate all existing demodulators
|
||||
4. Parse header: version, sample rate (clamped to device limits), solo mode
|
||||
5. Parse demodulators: create each via `DemodulatorMgr::loadInstance()`, call `run()`, set active
|
||||
6. Restore center frequency and view state
|
||||
7. Set active demodulator
|
||||
|
||||
## DataTree (`src/util/DataTree.h`)
|
||||
|
||||
The serialization framework underlying all configuration and session files.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
DataTree
|
||||
|
|
||||
+-- rootNode: DataNode (named, has children)
|
||||
|
|
||||
+-- DataElement (typed value: char, int, float, double, string, wstring, vectors)
|
||||
|
|
||||
+-- child DataNodes (recursive tree)
|
||||
```
|
||||
|
||||
### DataNode Operations
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `setName()` / `getName()` | Node name (maps to XML element name) |
|
||||
| `newChild(name)` | Create and append a child node |
|
||||
| `child(index)` | Get child by index |
|
||||
| `child(name)` | Get first child by name |
|
||||
| `numChildren()` | Number of children |
|
||||
| `hasAnother(name)` | Check if another child with name exists (iterator) |
|
||||
| `getNext(name)` | Get next child with name (iterator) |
|
||||
| `element()` | Get the `DataElement` value |
|
||||
|
||||
### DataElement Types
|
||||
|
||||
Supported scalar types:
|
||||
- `char`, `short`, `int`, `long`, `long long`
|
||||
- `float`, `double`
|
||||
- `std::string`, `std::wstring`
|
||||
|
||||
Supported vector types:
|
||||
- `std::vector<int>`, `std::vector<float>`, `std::vector<double>`
|
||||
|
||||
### XML Serialization
|
||||
|
||||
`DataTree::SaveToFileXML()` and `DataTree::LoadFromFileXML()` use TinyXML for XML I/O. The XML structure maps directly to the DataNode tree:
|
||||
- Element names → `DataNode::getName()`
|
||||
- Element text content → `DataElement` value
|
||||
- Child elements → child `DataNode`s
|
||||
|
||||
### Thread Safety
|
||||
|
||||
`DataTree` and `DataNode` are **not thread-safe**. Config save/load happens on the main thread during UI events. The `DeviceConfig` class uses `busy_lock` (a `std::mutex`) to protect concurrent access to individual device config data.
|
||||
|
||||
## Config Save Triggers
|
||||
|
||||
- **On exit:** `CubicSDR::OnExit()` calls `config->save()`
|
||||
- **On window move/resize:** debounced save of window geometry
|
||||
- **On setting change:** various UI actions update config immediately
|
||||
- **Session save:** explicit user action via File menu
|
||||
@@ -0,0 +1,204 @@
|
||||
# SDR Device Layer
|
||||
|
||||
This document describes CubicSDR's SDR hardware abstraction layer, device discovery, device information model, and manual device support.
|
||||
|
||||
## Overview
|
||||
|
||||
CubicSDR uses SoapySDR as its hardware abstraction layer. The device layer handles:
|
||||
|
||||
- **Device discovery:** Enumerating local and remote SDR hardware
|
||||
- **Device information:** Querying capabilities (sample rates, gains, antennas)
|
||||
- **Manual devices:** Adding devices not auto-discovered
|
||||
- **Device configuration:** Per-device settings persistence
|
||||
|
||||
```
|
||||
SDREnumerator (background thread)
|
||||
|
|
||||
+-- SoapySDR::Device::enumerate()
|
||||
|
|
||||
v
|
||||
SDRDeviceInfo (per-device capabilities)
|
||||
|
|
||||
v
|
||||
SDRDevicesDialog (UI selection)
|
||||
|
|
||||
v
|
||||
SDRThread (active device usage)
|
||||
```
|
||||
|
||||
## SDREnumerator (`src/sdr/SDREnumerator.h`)
|
||||
|
||||
Background `IOThread` that discovers available SDR devices. Results are cached in a static map.
|
||||
|
||||
### Enumeration Flow
|
||||
|
||||
1. **SoapySDR initialization** (once):
|
||||
- Load SoapySDR modules (system, bundled, or user-specified path)
|
||||
- Discover available factory functions (driver names)
|
||||
- Detect if `remote` factory is available
|
||||
|
||||
2. **Local enumeration:**
|
||||
- Call `SoapySDR::Device::enumerate()` with no arguments
|
||||
- For each result, create `SDRDeviceInfo` and populate driver/name
|
||||
- Attempt `SoapySDR::Device::make()` to query hardware info
|
||||
- Apply saved device settings from `DeviceConfig`
|
||||
- Store in `devs[""]` (empty string = local)
|
||||
|
||||
3. **Remote enumeration:**
|
||||
- For each address in `remotes` list
|
||||
- Call `SoapySDR::Device::enumerate()` with `driver=remote,remote={addr}`
|
||||
- Same device creation and info querying as local
|
||||
- Store in `devs[remoteAddr]`
|
||||
|
||||
4. **Manual device enumeration:**
|
||||
- For each entry in `manuals` list
|
||||
- Call `SoapySDR::Device::enumerate()` with `driver={factory},{params}`
|
||||
- If enumeration fails, create device entry marked as unavailable with label "Not Found ({factory})"
|
||||
- Store in `devs[""]` (same as local)
|
||||
|
||||
### Static State
|
||||
|
||||
| Map/Vector | Type | Purpose |
|
||||
|------------|------|---------|
|
||||
| `devs` | `map<string, vector<SDRDeviceInfo*>>` | Devices keyed by remote address ("" = local) |
|
||||
| `factories` | `vector<string>` | Available SoapySDR driver names |
|
||||
| `modules` | `vector<string>` | Loaded SoapySDR module paths |
|
||||
| `remotes` | `vector<string>` | Configured remote server addresses |
|
||||
| `manuals` | `vector<SDRManualDef>` | Manual device definitions |
|
||||
|
||||
### Notification
|
||||
|
||||
`SDREnumerator` sends notifications to the UI via `wxGetApp().sdrEnumThreadNotify()`:
|
||||
|
||||
| State | Meaning |
|
||||
|-------|---------|
|
||||
| `SDR_ENUM_MESSAGE` | Status message (displayed in UI) |
|
||||
| `SDR_ENUM_DEVICES_READY` | Enumeration complete, devices available |
|
||||
| `SDR_ENUM_FAILED` | No modules available |
|
||||
| `SDR_ENUM_TERMINATED` | Thread terminated |
|
||||
|
||||
## SDRDeviceInfo (`src/sdr/SDRDeviceInfo.h`)
|
||||
|
||||
Represents a discovered or manually defined SDR device.
|
||||
|
||||
### Identity
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | `string` | Display name (from SoapySDR `label` or `device` field) |
|
||||
| `serial` | `string` | Device serial number |
|
||||
| `driver` | `string` | SoapySDR driver name |
|
||||
| `hardware` | `string` | Hardware revision |
|
||||
| `tuner` | `string` | Tuner chip type |
|
||||
| `manufacturer` | `string` | Device manufacturer |
|
||||
| `product` | `string` | Product name |
|
||||
|
||||
### Device ID
|
||||
|
||||
`getDeviceId()` returns a unique identifier string. For local devices, this is typically the driver name and serial number. For remote devices, it includes the remote address. For manual devices, it includes the factory and parameters.
|
||||
|
||||
### State
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `available` | `bool` | Device was successfully queried |
|
||||
| `active` | `atomic_bool` | Device is currently in use |
|
||||
| `remote` | `bool` | Device is on a remote server |
|
||||
| `manual` | `bool` | Device was manually defined |
|
||||
| `timestamps` | `bool` | Device supports timestamped streaming |
|
||||
|
||||
### Hardware Queries
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `getSampleRates(direction, channel)` | `vector<long>` | Available sample rates |
|
||||
| `getSampleRateNear(direction, channel, rate)` | `long` | Nearest supported sample rate |
|
||||
| `getAntennaNames(direction, channel)` | `vector<string>` | Available antenna ports |
|
||||
| `getAntennaName(direction, channel)` | `string` | Currently selected antenna |
|
||||
| `getGains(direction, channel)` | `SDRRangeMap` | Per-stage gain ranges |
|
||||
| `getCurrentGain(direction, channel, name)` | `double` | Current gain value |
|
||||
| `hasCORR(direction, channel)` | `bool` | Supports DC offset correction |
|
||||
|
||||
### SoapySDR Integration
|
||||
|
||||
`SDRDeviceInfo` holds a `SoapySDR::Device*` pointer for direct hardware access:
|
||||
- Set via `setSoapyDevice()` after `SoapySDR::Device::make()`
|
||||
- Used to query capabilities (sample rates, gains, antennas)
|
||||
- Released via `SoapySDR::Device::unmake()` after enumeration
|
||||
|
||||
## Device Arguments
|
||||
|
||||
SoapySDR devices are configured via key-value argument strings:
|
||||
|
||||
| Argument | Purpose |
|
||||
|----------|---------|
|
||||
| `driver` | SoapySDR driver module name |
|
||||
| `device` | Device identifier |
|
||||
| `serial` | Device serial number |
|
||||
| `remote` | Remote server address |
|
||||
| `label` | Human-readable device label |
|
||||
|
||||
**Stream arguments** (stored in `streamArgs`):
|
||||
| Argument | Purpose |
|
||||
|----------|---------|
|
||||
| `buflen` | Buffer length |
|
||||
| `bufflen` | Alternative buffer length |
|
||||
| `remote` | Remote streaming address |
|
||||
|
||||
**Device settings** (stored in `DeviceConfig::settings`):
|
||||
- Driver-specific settings (e.g., `bias_tee` for RTL-SDR)
|
||||
- Applied during device enumeration and on device open
|
||||
|
||||
## Manual Device Definition
|
||||
|
||||
`SDRManualDef` allows users to add devices not auto-discovered:
|
||||
|
||||
```cpp
|
||||
struct SDRManualDef {
|
||||
std::string factory; // SoapySDR driver name
|
||||
std::string params; // Comma-separated key=value pairs
|
||||
};
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- Factory: `rtlsdr`
|
||||
- Params: `driver=rtlsdr,bias_tee=1`
|
||||
|
||||
Manual devices are:
|
||||
1. Stored in `AppConfig` under `manual_devices`
|
||||
2. Loaded into `SDREnumerator::manuals` at startup
|
||||
3. Enumerated alongside local devices
|
||||
4. If not found, shown as unavailable with "Not Found" label
|
||||
|
||||
## Device Selection UI
|
||||
|
||||
`SDRDevicesDialog` (`src/forms/SDRDevices/`) provides:
|
||||
|
||||
- List of all discovered devices (local, remote, manual)
|
||||
- Device properties display (sample rates, gains, antennas)
|
||||
- Manual device add/remove
|
||||
- Remote device add/remove
|
||||
- Device activation (triggers `SDRThread` creation)
|
||||
|
||||
## Device Configuration
|
||||
|
||||
Per-device settings are persisted in `DeviceConfig` (see [Configuration System](configuration-system.md)):
|
||||
|
||||
- Sample rate, frequency offset, PPM correction
|
||||
- AGC mode, antenna selection
|
||||
- Per-gain-stage values
|
||||
- Stream options
|
||||
- Driver-specific settings
|
||||
- Rig IF frequencies
|
||||
|
||||
## Module Loading
|
||||
|
||||
SoapySDR modules are loaded in this order:
|
||||
|
||||
1. **User-specified path** (`-m` command line option)
|
||||
2. **Bundled modules** (if `BUNDLE_SOAPY_MODS` is defined):
|
||||
- Check `modules/` subdirectory next to the executable
|
||||
- Optionally load system modules first (if `BUNDLED_MODS_ONLY` is not defined)
|
||||
3. **System modules** (default `SoapySDR::loadModules()`)
|
||||
|
||||
Module discovery is controlled by `SoapySDR::listModules()` and `SoapySDR::loadModule()`.
|
||||
@@ -0,0 +1,309 @@
|
||||
# Visual Architecture
|
||||
|
||||
This document describes CubicSDR's OpenGL rendering system, canvas hierarchy, font rendering, color themes, and the visual data processing pipeline.
|
||||
|
||||
## Overview
|
||||
|
||||
The visual system is built on wxWidgets' OpenGL integration (`wxGLCanvas`/`wxGLContext`) and uses a **pull-based** architecture: worker threads produce FFT/scope data into queues, and UI canvases poll those queues in their `OnIdle()` handlers. Worker threads never push data to the UI.
|
||||
|
||||
```
|
||||
SDRPostThread
|
||||
|
|
||||
+--[pipeIQVisualData]--------> SpectrumVisualProcessor --> SpectrumCanvas
|
||||
+--[pipeWaterfallIQVisualData]-> FFTVisualDataThread --> WaterfallCanvas
|
||||
+--[pipeDemodIQVisualData]----> SpectrumVisualProcessor --> Demod spectrum
|
||||
|
||||
DemodulatorThread
|
||||
|
|
||||
+--[audioVisOutputQueue]------> ScopeVisualProcessor --> ScopeCanvas
|
||||
```
|
||||
|
||||
## Canvas Class Hierarchy
|
||||
|
||||
```
|
||||
wxGLCanvas
|
||||
|
|
||||
+-- InteractiveCanvas (base: mouse tracking, frequency mapping, key state)
|
||||
|
|
||||
+-- WaterfallCanvas (waterfall display, demod creation, drag operations)
|
||||
+-- SpectrumCanvas (FFT spectrum display, linked to WaterfallCanvas)
|
||||
+-- ScopeCanvas (oscilloscope/spectrum display for demod output)
|
||||
+-- MeterCanvas (signal level meter)
|
||||
+-- TuningCanvas (fine tuning control)
|
||||
+-- ModeSelectorCanvas (modem type selection buttons)
|
||||
+-- GainCanvas (per-gain-stage slider)
|
||||
```
|
||||
|
||||
### InteractiveCanvas (`src/visual/InteractiveCanvas.h`)
|
||||
|
||||
Base class for all interactive GL canvases. Provides:
|
||||
|
||||
- **Frequency mapping:** `getFrequencyAt(x)` converts a normalized x-position [0..1] to a frequency in Hz, given the current center frequency and bandwidth
|
||||
- **View state:** `setView()` / `disableView()` for zoomed sub-band views vs. full-band views
|
||||
- **Mouse tracking:** `MouseTracker` instance with position, button state, drag deltas
|
||||
- **Key state:** `shiftDown`, `altDown`, `ctrlDown` flags updated on key/mouse events
|
||||
- **Status bar:** `setStatusText()` writes to the AppFrame status bar
|
||||
|
||||
### WaterfallCanvas (`src/visual/WaterfallCanvas.h`)
|
||||
|
||||
The primary display canvas. Handles:
|
||||
|
||||
- **FFT data consumption:** Pops `SpectrumVisualData` from `visualDataQueue` at a rate-limited pace (`linesPerSecond`)
|
||||
- **Waterfall rendering:** Delegates to `WaterfallPanel` for texture-based scrolling waterfall
|
||||
- **Demodulator markers:** Draws all active demodulators via `PrimaryGLContext::DrawDemod()`
|
||||
- **Frequency selector:** Draws hover/active frequency markers via `DrawFreqSelector()`
|
||||
- **Drag operations:**
|
||||
- `WF_DRAG_FREQUENCY` — move demodulator frequency
|
||||
- `WF_DRAG_BANDWIDTH_LEFT` / `WF_DRAG_BANDWIDTH_RIGHT` — resize demodulator bandwidth
|
||||
- `WF_DRAG_RANGE` — create new demodulator by range selection
|
||||
- **Zoom:** Mouse wheel adjusts `mouseZoom`, which smoothly animates to the target zoom level
|
||||
- **Frequency nudge:** Arrow keys shift center frequency by half/full bandwidth
|
||||
- **Scale factor:** Shift+Up/Down adjusts visual gain (FFT display scaling)
|
||||
|
||||
**Drag state machine:**
|
||||
```
|
||||
WF_DRAG_NONE ──(mouse hover over demod)──> WF_DRAG_FREQUENCY / WF_DRAG_BANDWIDTH_*
|
||||
WF_DRAG_NONE ──(Alt held)──> WF_DRAG_RANGE
|
||||
WF_DRAG_* ──(mouse released)──> WF_DRAG_NONE
|
||||
```
|
||||
|
||||
### SpectrumCanvas (`src/visual/SpectrumCanvas.h`)
|
||||
|
||||
Displays the FFT spectrum line plot. Linked to `WaterfallCanvas`:
|
||||
|
||||
- Shares the same center frequency and bandwidth
|
||||
- Receives FFT data from its own `visualDataQueue`
|
||||
- Supports dB scale display and dB offset
|
||||
- Right-drag adjusts visual gain (vertical scale factor)
|
||||
- Uses `SpectrumPanel` for rendering
|
||||
|
||||
### ScopeCanvas (`src/visual/ScopeCanvas.h`)
|
||||
|
||||
Displays demodulated audio as oscilloscope or spectrum:
|
||||
|
||||
- Pops `ScopeRenderData` from `inputData` queue
|
||||
- Toggles between scope mode (waveform) and spectrum mode (FFT)
|
||||
- Supports PPM mode for frequency calibration display
|
||||
- Uses both `ScopePanel` and `SpectrumPanel` in a composite layout
|
||||
- `GLPanel` hierarchy: `parentPanel` → `scopePanel` + `spectrumPanel` + `bgPanel`
|
||||
|
||||
## GLPanel System (`src/ui/GLPanel.h`)
|
||||
|
||||
A lightweight retained-mode UI system for OpenGL rendering:
|
||||
|
||||
**Base class `GLPanel`:**
|
||||
- Position (`pos[2]`), size (`size[2]`), rotation (`rot[3]`)
|
||||
- Fill modes: none, solid, gradient X/Y, gradient bar
|
||||
- Border and margin in pixels
|
||||
- Coordinate system options (Y-up/Y-down, zero-one or signed)
|
||||
- Transform matrix stack (`CubicVR::mat4`)
|
||||
- Child panel hierarchy (`children` vector)
|
||||
- Hit testing for mouse interaction
|
||||
|
||||
**Derived panels:**
|
||||
- `GLTextPanel` — text rendering with alignment (left/right/center, top/bottom)
|
||||
- `GLTestPanel` — debug/test rendering
|
||||
|
||||
**Panel classes:**
|
||||
| Panel | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| `WaterfallPanel` | `src/panel/WaterfallPanel.h` | Scrolling waterfall texture rendering |
|
||||
| `SpectrumPanel` | `src/panel/SpectrumPanel.h` | FFT line plot rendering |
|
||||
| `ScopePanel` | `src/panel/ScopePanel.h` | Oscilloscope waveform rendering |
|
||||
| `MeterPanel` | `src/panel/MeterPanel.h` | Signal level meter rendering |
|
||||
|
||||
## PrimaryGLContext (`src/visual/PrimaryGLContext.h`)
|
||||
|
||||
Shared OpenGL context providing drawing primitives. All canvases share this context via `wxGLContext` sharing.
|
||||
|
||||
**Drawing methods:**
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `BeginDraw(r, g, b)` | Clear screen and set up OpenGL state |
|
||||
| `EndDraw()` | Finalize frame |
|
||||
| `DrawDemod()` | Draw demodulator bandwidth indicator with label |
|
||||
| `DrawDemodInfo()` | Draw demodulator info label (frequency, type, bandwidth) |
|
||||
| `DrawFreqSelector()` | Draw frequency selection marker |
|
||||
| `DrawRangeSelector()` | Draw range selection overlay |
|
||||
| `DrawFreqBwInfo()` | Draw frequency and bandwidth text |
|
||||
|
||||
**Hover alpha:** A `hoverAlpha` float controls the transparency of hover indicators, animated smoothly over time.
|
||||
|
||||
## GLFont System (`src/util/GLFont.h`)
|
||||
|
||||
Bitmap font rendering using BMFont format. Fonts are stored in `font/` as PNG + definition files.
|
||||
|
||||
### Font Sizes
|
||||
|
||||
| Enum | Pixels | Use Case |
|
||||
|------|--------|----------|
|
||||
| `GLFONT_SIZE12` | 12px | Small labels |
|
||||
| `GLFONT_SIZE16` | 16px | Status text |
|
||||
| `GLFONT_SIZE18` | 18px | Medium labels |
|
||||
| `GLFONT_SIZE24` | 24px | Frequency display |
|
||||
| `GLFONT_SIZE27` | 27px | — |
|
||||
| `GLFONT_SIZE32` | 32px | Large labels |
|
||||
| `GLFONT_SIZE36` | 36px | — |
|
||||
| `GLFONT_SIZE48` | 48px | Header text |
|
||||
| `GLFONT_SIZE64` | 64px | — |
|
||||
| `GLFONT_SIZE72` | 72px | — |
|
||||
| `GLFONT_SIZE96` | 96px | — |
|
||||
|
||||
### Font Scaling
|
||||
|
||||
`GLFont::GLFontScale` provides user-configurable scaling:
|
||||
- `GLFONT_SCALE_NORMAL` — 1.0x
|
||||
- `GLFONT_SCALE_MEDIUM` — 1.5x
|
||||
- `GLFONT_SCALE_LARGE` — 2.0x
|
||||
|
||||
`GLFont::getFont(requestedSize, scaleFactor)` selects the best matching font from the available sizes and applies scaling.
|
||||
|
||||
### String Caching
|
||||
|
||||
Each font maintains a `stringCache` map (`wstring → GLFontStringCache`) for pre-computed vertex/UV data:
|
||||
- `cacheString()` generates OpenGL vertex and texture coordinate arrays
|
||||
- `drawCacheString()` renders from cache
|
||||
- `doCacheGC()` evicts old entries using an atomic garbage collection counter
|
||||
- Cache is per-font, invalidated on font reload
|
||||
|
||||
### Drawer Proxy
|
||||
|
||||
`GLFont::Drawer` is a lightweight proxy that selects the appropriate font size and scale factor:
|
||||
```cpp
|
||||
GLFont::Drawer drawer = GLFont::getFont(24, scaleFactor);
|
||||
drawer.drawString("Hello", x, y, GLFONT_ALIGN_LEFT, GLFONT_ALIGN_TOP);
|
||||
```
|
||||
|
||||
## ColorTheme System (`src/visual/ColorTheme.h`)
|
||||
|
||||
### Theme Structure
|
||||
|
||||
`ColorTheme` defines all colors used by the visual system:
|
||||
|
||||
| Property | Used By |
|
||||
|----------|---------|
|
||||
| `waterfallGradient` | Waterfall color mapping (256-entry LUT) |
|
||||
| `waterfallHighlight` | Active demodulator marker |
|
||||
| `waterfallNew` | New demodulator being created |
|
||||
| `waterfallHover` | Hovered demodulator / frequency selector |
|
||||
| `waterfallDestroy` | Demodulator being deleted |
|
||||
| `fftLine` | Spectrum line color |
|
||||
| `fftHighlight` | Highlighted spectrum point |
|
||||
| `scopeLine` | Oscilloscope trace color |
|
||||
| `tuningBarLight` / `tuningBarDark` | Tuning bar gradient |
|
||||
| `tuningBarUp` / `tuningBarDown` | Fine tuning direction indicators |
|
||||
| `meterLevel` / `meterValue` | Signal meter colors |
|
||||
| `text` | General text color |
|
||||
| `freqLine` | Frequency grid lines |
|
||||
| `button` / `buttonHighlight` | UI button colors |
|
||||
| `scopeBackground` | Scope canvas background |
|
||||
| `fftBackground` | Spectrum canvas background |
|
||||
| `generalBackground` | General background color |
|
||||
|
||||
### Available Themes
|
||||
|
||||
| ID | Name | Class | Description |
|
||||
|----|------|-------|-------------|
|
||||
| 0 | Default | `DefaultColorTheme` | Google Turbo colormap (rainbow) |
|
||||
| 1 | DefaultJet | `DefaultColorThemeJet` | Original CubicSDR jet colormap |
|
||||
| 2 | Black & White | `BlackAndWhiteColorTheme` | Grayscale waterfall |
|
||||
| 3 | Sharp | `SharpColorTheme` | High-contrast blue-white-yellow-red |
|
||||
| 4 | Rad | `RadColorTheme` | Blue-green-orange-red-white |
|
||||
| 5 | Touch | `TouchColorTheme` | Dark purple/cyan/green/yellow/red |
|
||||
| 6 | HD | `HDColorTheme` | Blue-green-red-yellow-white |
|
||||
| 7 | Radar | `RadarColorTheme` | Green monochrome radar style |
|
||||
|
||||
### ThemeMgr
|
||||
|
||||
Global singleton `ThemeMgr::mgr` manages theme selection:
|
||||
- `setTheme(themeId)` sets `currentTheme` pointer
|
||||
- `themes` map holds all `ColorTheme*` instances
|
||||
- Current theme is persisted in `AppConfig`
|
||||
|
||||
## Visual Data Processing Pipeline
|
||||
|
||||
### VisualProcessor Template (`src/process/VisualProcessor.h`)
|
||||
|
||||
Base template for all visual data processors:
|
||||
|
||||
```
|
||||
InputQueue → process() → distribute() → OutputQueues[]
|
||||
```
|
||||
|
||||
- `setInput()` — attach input queue
|
||||
- `attachOutput()` / `removeOutput()` — manage output queues
|
||||
- `process()` — pure virtual, implemented by subclasses
|
||||
- `distribute()` — pushes output to all attached output queues
|
||||
|
||||
### Specializations
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `VisualDataDistributor<T>` | 1:N shared-pointer dispatch (no copy) |
|
||||
| `VisualDataReDistributor<T>` | 1:N deep-copy dispatch via `ReBuffer` pool |
|
||||
|
||||
### Concrete Processors
|
||||
|
||||
**`SpectrumVisualProcessor`:**
|
||||
- Input: `DemodulatorThreadInputQueue` (IQ data)
|
||||
- Output: `SpectrumVisualDataQueue` (FFT points)
|
||||
- Computes FFT, applies windowing, generates spectrum points
|
||||
- Supports configurable FFT size, floor/ceiling, scale factor
|
||||
|
||||
**`ScopeVisualProcessor`:**
|
||||
- Input: `DemodulatorThreadOutputQueue` (audio data)
|
||||
- Output: `ScopeRenderDataQueue` (waveform/spectrum data)
|
||||
- Computes scope waveform or FFT of demodulated audio
|
||||
- Supports multiple display modes (scope, spectrum, waterfall)
|
||||
|
||||
**`FFTVisualDataThread`:**
|
||||
- Dedicated thread running `SpectrumVisualProcessor`
|
||||
- Rate-limits waterfall updates (one FFT per frame)
|
||||
- Redistributes FFT data to multiple consumers
|
||||
|
||||
**`SpectrumVisualDataThread`:**
|
||||
- Dedicated thread running `SpectrumVisualProcessor`
|
||||
- Main spectrum display computation
|
||||
|
||||
**`FFTDataDistributor`:**
|
||||
- Distributes IQ data to multiple FFT processors
|
||||
- Uses `VisualDataDistributor` for shared-pointer distribution
|
||||
|
||||
## Rendering Flow
|
||||
|
||||
### Per-Frame Rendering (WaterfallCanvas)
|
||||
|
||||
1. `OnIdle()` calls `processInputQueue()` then `Refresh()`
|
||||
2. `processInputQueue()` pops FFT data at rate-limited intervals
|
||||
3. `OnPaint()`:
|
||||
- Apply zoom and frequency nudge animations
|
||||
- Set GL context and viewport
|
||||
- Call `waterfallPanel.draw()` for waterfall texture
|
||||
- Draw demodulator markers via `PrimaryGLContext::DrawDemod()`
|
||||
- Draw frequency selector / hover indicators
|
||||
- Call `SwapBuffers()`
|
||||
|
||||
### OnIdle Processing
|
||||
|
||||
Each canvas registers its own `EVT_IDLE` handler:
|
||||
- `WaterfallCanvas::OnIdle` → `processInputQueue()` + `Refresh()`
|
||||
- `SpectrumCanvas::OnIdle` → `processInputQueue()` + `Refresh()`
|
||||
- `ScopeCanvas::OnIdle` → `processInputQueue()` + `Refresh()`
|
||||
|
||||
All use non-blocking `try_pop()` to avoid stalling the UI thread.
|
||||
|
||||
## Mouse Interaction Summary
|
||||
|
||||
| Canvas | Left Click | Left Drag | Right Drag | Wheel |
|
||||
|--------|-----------|-----------|------------|-------|
|
||||
| Waterfall | Set freq / create demod | Move demod / resize BW / range select | Adjust visual gain | Zoom |
|
||||
| Spectrum | Set freq | Move demod | Adjust visual gain | Zoom |
|
||||
| Scope | — | — | — | — |
|
||||
|
||||
## Key Constants
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
|----------|-------|---------|
|
||||
| `DEFAULT_WATERFALL_LPS` | 30 | Default waterfall lines per second |
|
||||
| `MIN_BANDWIDTH` | 30000 | Minimum demodulator bandwidth |
|
||||
| `CHANNELIZER_RATE_MAX` | varies | Maximum channelizer sample rate |
|
||||
Reference in New Issue
Block a user