17 KiB
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 (or ~/.config/CubicSDR on XDG-compliant builds) |
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 |
bookmarks.xml.failedload |
Created when a bookmark file fails to load |
Bookmark file loading uses a dialog-interactive recovery sequence across .backup and .lastloaded fallbacks. The exact behavior depends on the backup flag passed to BookmarkMgr::loadFromFile() and is detailed in the Bookmark System document.
AppConfig (src/AppConfig.h)
Global application configuration. Singleton accessed via wxGetApp().getConfig().
Window Settings
| Property | Type | Config Key | Default | Description |
|---|---|---|---|---|
winX, winY |
int |
window/x, window/y |
0 | Window position |
winW, winH |
int |
window/w, window/h |
0 | Window size |
winMax |
bool |
window/max |
false | Maximized state |
showTips |
bool |
window/tips |
true | Show tooltips |
modemPropsCollapsed |
bool |
window/modemprops_collapsed |
false | Modem properties panel collapsed |
perfMode |
PerfModeEnum |
window/perf_mode |
1 (normal) | 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 |
1 | Frequency snap value in Hz |
centerFreq |
long long |
window/center_freq |
100000000 | Center frequency |
waterfallLinesPerSec |
int |
window/waterfall_lps |
30 | Waterfall refresh rate |
spectrumAvgSpeed |
float |
window/spectrum_avg |
0.65 | Spectrum averaging speed |
dbOffset |
int |
window/db_offset |
0 | dB display offset |
Layout Settings
| Property | Type | Config Key | Default | Description |
|---|---|---|---|---|
mainSplit |
float |
window/main_split |
-1 | Main splitter position |
visSplit |
float |
window/vis_split |
-1 | Visualization splitter position |
bookmarkSplit |
float |
window/bookmark_split |
200 | Bookmark panel splitter position |
bookmarksVisible |
bool |
window/bookmark_visible |
true* | Bookmark panel visibility |
* Default is false when built with CUBICSDR_DEFAULT_HIDE_BOOKMARKS defined.
Recording Settings
| Property | Type | Config Key | Default | Description |
|---|---|---|---|---|
recordingPath |
string |
recording/path |
(empty) | Output directory for recordings |
recordingSquelchOption |
int |
recording/squelch |
0 | Squelch handling (0=silence, 1=skip, 2=always) |
recordingFileTimeLimitSeconds |
int |
recording/file_time_limit |
0 | Max file duration in seconds (0=unlimited) |
verifyRecordingPath() returns false (and shows a dialog) if the path is empty, does not exist, or is not writable.
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 |
The constructor explicitly initializes rigEnabled (false), rigModel (1), rigRate (57600), rigPort ("/dev/ttyUSB0"), rigControlMode (true), and rigFollowMode (true). The remaining fields (rigCenterLock, rigFollowModem) rely on implicit zero-initialization from std::atomic. On load, a zero-valued model or rate is replaced with the constructor default (1 and 57600 respectively).
Manual Devices
Stored as a list under manual_devices/device:
<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 (falls back to deviceId if empty) |
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 (defaults to true in the constructor) |
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 |
Note: DeviceConfig::load() does not read the id node — that is done by AppConfig::load(), which extracts the device ID, creates/retrieves the DeviceConfig via getDevice(deviceId), then calls DeviceConfig::load() on the resulting object.
Note: DeviceConfig::save() writes the antenna, streamOpts, settings, rig_ifs, and gains sections only when the corresponding container is non-empty. On load, gains entries whose value is NaN are skipped.
Device Config XML Structure
<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
<cubicsdr_config>
<window>...</window>
<recording>...</recording>
<devices>...</devices>
<manual_devices>...</manual_devices>
<rig>...</rig>
</cubicsdr_config>
Save/Load Lifecycle
Save (AppConfig::save()):
- Create a
DataTreewith root node namedcubicsdr_config - Populate window, recording, device, manual device, and rig nodes. The
windownode and all its children are only written when bothwinWandwinHare non-zero (a zero-sized window produces no window config). - Get config file path from
getConfigFileName() - Call
DataTree::SaveToFileXML();save()returnsfalseand logs an error if the file cannot be written.
Load (AppConfig::load()):
- Determine config file path
- If the named config doesn't exist and a default
config.xmlexists, copy from it. If neither file exists (or the copy fails),load()returnstrueearly, silently leaving defaults instead of loading. - Call
DataTree::LoadFromFileXML(); if the config file exists but is not readable,load()returnsfalse. - Parse each section: window, recording, devices, manual_devices, rig
- Missing sections use defaults
perfModeis unconditionally reset toPERF_NORMALbefore checking the XML value, so a missingperf_modenode always results inPERF_NORMALregardless of any prior value. A presentperf_modevalue is only applied if it equalsPERF_LOWorPERF_HIGH; any other (unrecognized) value leaves it asPERF_NORMAL.
AppConfig::reset() is declared but is a no-op: it returns true without applying or reloading any state, and is not currently invoked.
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
<cubicsdr_session>
<header>
<!-- version is percent-encoded, e.g. "0.2.8" ~> "%30%2e%32%2e%38" -->
<version>%30%2e%32%2e%38</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>
Note: The <version> element is stored internally as a percent-encoded wstring (not plain text), so the earlier example shows its encoded form. The <view_state> section is only written when the waterfall canvas view state is active; it is absent when no view state is saved.
Save Flow (SessionMgr::saveSession())
- Create
DataTreewith rootcubicsdr_session - Write header: version, center frequency, sample rate, solo mode, view state
- Iterate all demodulator instances, call
DemodulatorMgr::saveInstance()for each - Ensure filename ends in
.xml - Save to file
Load Flow (SessionMgr::loadSession())
- Load
DataTreefrom file - Validate root node name is
cubicsdr_session - Terminate all existing demodulators
- Parse header: version, sample rate (clamped to device limits), solo mode
- Parse demodulators: create each via
DemodulatorMgr::loadInstance(), callrun(), set active - Restore center frequency and view state
- Set active demodulator
DataTree (src/util/DataTree.h)
The serialization framework underlying all configuration and session files.
Architecture
DataTree has two constructors: a default constructor (root node unnamed, set via setName()) and DataTree(const char *name_in) which names the root node directly. SessionMgr uses the latter.
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) |
getParentNode() / setParentNode() |
Parent node navigation |
newChild(name) |
Create and append a child node |
newChild(name, otherNode) |
Append an existing child node (takes ownership, does not clone) |
newChildCloneFrom(name, cloneFrom) |
Deep-clone a child node tree from another DataNode |
child(index) |
Get child by index |
child(name, index=0) |
Get child by name (with optional index) |
numChildren() |
Number of children |
numChildren(name) |
Number of children matching a specific name |
hasAnother(name) |
Check if another child with name exists (iterator) |
hasAnother() |
Check if any next child exists (iterator) |
getNext(name) |
Get next child with name (iterator) |
getNext() |
Get next child (iterator) |
rewind(name) / rewind() |
Reset iterator to beginning (by name or generic) |
rewindAll() |
Reset all iterators |
element() |
Get the DataElement value |
findAll(name, list) |
Recursively find all descendants matching name |
DataNode also provides operator overloads that are the idiomatic access pattern throughout the codebase:
- Cast operators (
operator string,operator const char*,operator int,operator float, etc.) for reading values.operator const char*returnsnullptrif the type is notDATA_STRING. operator[]maps togetNext(name)orchild(idx)operator()maps tohasAnother(name)operator^maps tonewChild(name)
DataElement Types
Supported scalar types:
char,unsigned char,int,unsigned int,long,unsigned long,long longfloat,doublestd::string,std::wstring
Supported vector types:
std::vector<char>,std::vector<unsigned char>std::vector<int>,std::vector<unsigned int>std::vector<long>,std::vector<unsigned long>,std::vector<long long>std::vector<float>,std::vector<double>std::vector<std::string>
Supported set types:
std::set<std::string>
Additionally, DataElement supports raw byte buffers via set(const char*, long) (stored as DATA_VOID).
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 →
DataElementvalue - Child elements → child
DataNodes - Element attributes →
DataNodes whose name is prefixed with@(BadgerFish convention). ADataNodenamed e.g.@nameis written as (and read back from) the XML attributename. std::vector<std::string>values serialize as a sequence of child<str>elements, and are deserialized back into a string vector when an element's children are all namedstr.- A
DataNodewith an empty name is written using the element namenode.
LoadFromFileXML accepts a DT_FloatingPointPolicy parameter (USE_FLOAT or USE_DOUBLE) that controls whether floating-point XML text values are parsed as float or double. The default is USE_FLOAT.
DataTree also provides printXML() for debugging, which outputs the tree structure to stdout.
Thread Safety
DataTree and DataNode are not thread-safe. Config save/load happens on the main thread during UI events.
Both AppConfig and DeviceConfig use std::atomic for scalar fields to allow safe reads/writes from different threads without locking. DeviceConfig additionally uses busy_lock (a std::mutex) to protect the deviceId and deviceName strings, and the save()/load() methods. However, individual accessors for antennaName, streamOpts, gains, settings, and rigIF do not acquire the mutex — they are only safe when accessed from the same thread that calls save()/load(). AppConfig does not use a mutex for any of its non-atomic fields (recordingPath, rigPort, configName, manualDevices).
BookmarkMgr uses a std::recursive_mutex (busy_lock) to protect all bookmark data access. The saveToFile() and loadFromFile() methods acquire this lock, as do all bookmark add/remove/query operations.
Config Save Triggers
- On window close:
AppFrame::OnClose()snapshots selected UI state (window geometry, theme, font scale, snap, center frequency, spectrum/waterfall speeds, manual devices, modem properties collapsed, splitter positions, bookmarks visibility, and hamlib settings) intoAppConfig, callsconfig->save(), then saves bookmarks viaBookmarkMgr::saveToFile(). If thesaveDisabledflag is set, the handler returns early without saving. Recording settings are not re-snapshotted here; they are already current inAppConfigfrom earlier menu handler calls. - On setting change: various UI actions update the in-memory config immediately (e.g., PPM changes in
TuningCanvas→CubicSDR::setPPM()→SDRThread::setPPM()→DeviceConfig::setPPM()). The config file is only written to disk on window close, or explicitly viaCubicSDR::saveConfig()(e.g.,TuningCanvas::OnMouseLeftWindowsaves if PPM changed). - Session save: explicit user action via File menu