Skip to content

Rig Loader

TOML rig file loading, validation, and runtime object construction.

Module: rigplane.rig_loader

from rigplane.rig_loader import load_rig, discover_rigs, RigConfig, RigLoadError

Functions

load_rig()

def load_rig(path: Path) -> RigConfig

Load and validate a single TOML rig file.

Args:

  • path — Path to the .toml file.

Returns: Parsed and validated RigConfig.

Raises: RigLoadError if the file is missing, cannot be parsed as TOML, or fails schema validation (missing sections, unknown capability, invalid VFO scheme, etc.).

Example:

from pathlib import Path
from rigplane.rig_loader import load_rig

cfg = load_rig(Path("rigs/ic7610.toml"))
print(cfg.model)          # "IC-7610"
print(cfg.civ_addr)       # 0x98
print(cfg.receiver_count) # 2
print(cfg.vfo_scheme)     # "main_sub"

discover_rigs()

def discover_rigs(directory: Path) -> dict[str, RigConfig]

Discover and load all rig TOML files in a directory.

Files whose names start with _ are skipped (e.g. _schema.md, _template.toml).

Returns: Dict mapping model name → RigConfig, sorted by filename.

Example:

from pathlib import Path
from rigplane.rig_loader import discover_rigs

rigs = discover_rigs(Path("rigs/"))
for model, cfg in rigs.items():
    print(f"{model}: protocol={cfg.protocol_type}, receivers={cfg.receiver_count}")
# FTX-1: protocol=yaesu_cat, receivers=2
# IC-705: protocol=civ, receivers=1
# IC-7300: protocol=civ, receivers=1
# IC-7610: protocol=civ, receivers=2
# IC-9700: protocol=civ, receivers=2
# TX-500: protocol=kenwood_cat, receivers=1
# X6100: protocol=civ, receivers=1
# X6200: protocol=civ, receivers=1

Classes

RigConfig

profiles/rig_loader.py: RigConfig holds every field parsed from a rig file. Frozen dataclass. All values are immutable after construction.

.to_profile() → RadioProfile

Build a RadioProfile from this config for use in command routing and capability guards.

cfg = load_rig(Path("rigs/ic7300.toml"))
profile = cfg.to_profile()

profile.receiver_count          # 1
profile.vfo_scheme              # "ab"
profile.supports_capability("digisel")  # False — IC-7300 lacks DIGI-SEL

.to_command_map() → CommandMap

Build a CommandMap from this config's commands (base commands + overrides merged). CI-V entries may also carry value-specific complete wire tuples while retaining one command name:

[commands]
set_data_mode = { bytes = [0x1A, 0x06], value_variants = { "0" = [0x1A, 0x06, 0x00, 0x00], "1" = [0x1A, 0x06, 0x01, 0x01] } }

The loader requires the exact two-key descriptor, canonical decimal integer variant keys, unique valid byte arrays, and variants that strictly extend the base prefix. Existing CI-V arrays, CAT specifications, declared-absent entries, and command overrides remain compatible.

cfg = load_rig(Path("rigs/ic7610.toml"))
cmd_map = cfg.to_command_map()

cmd_map.get("get_af_level")  # (0x14, 0x01)
cmd_map.get("scope_on")      # (0x27, 0x10)

CommandMap

class CommandMap:
    def __init__(
        self,
        commands: dict[str, tuple[int, ...]],
        *,
        value_variants: dict[str, dict[int, tuple[int, ...]]] | None = None,
    ) -> None: ...

Immutable lookup mapping command names to CI-V wire byte tuples. Value variants are immutable private per-name data: .get(), .has(), iteration, length, and representation continue to describe only base entries. Equality and hashing include both base entries and value variants. See docs/api/commands.md for usage with command builder functions.

.get(name) → tuple[int, ...]

Return wire bytes for name. Raises KeyError with a helpful message listing available commands if name is not found.

cm.get("get_af_level")   # (0x14, 0x01)
cm.get("nonexistent")    # KeyError: Unknown command 'nonexistent'. Available: ...

.has(name) → bool

Check whether name is a known command.

cm.has("digisel")  # True (IC-7610), False (IC-7300)

len(cm) and iter(cm)

len(cm)         # number of commands
list(cm)        # sorted list of command names

RigLoadError

class RigLoadError(Exception): ...

Raised by load_rig() when the rig file is invalid. The message includes the filename and the specific field or section that failed validation.

from rigplane.rig_loader import load_rig, RigLoadError

try:
    cfg = load_rig(Path("rigs/bad.toml"))
except RigLoadError as e:
    print(e)
# bad.toml: missing required section [capabilities]

Validation Rules

load_rig() enforces the following:

Check Error
File exists file not found: <path>
Valid TOML syntax failed to parse TOML: <detail>
Required sections present missing required section [<section>]
All [radio] fields present missing required field [radio].<field>
civ_addr in 0x00–0xFF (if present) [radio].civ_addr = X out of range
[capabilities].features non-empty [capabilities].features must not be empty
All capability strings known unknown capability 'xyz'. Known: [...]
[vfo].scheme valid [vfo].scheme must be one of {'ab', 'main_sub', 'ab_shared', 'single'}
[protocol].type valid (if present) [protocol].type must be one of {'civ', 'kenwood_cat', 'yaesu_cat'}
[controls.X].style valid (if present) [controls.X].style must be one of {'toggle', 'stepped', ...}
[[rules]].kind valid (if present) rule kind must be one of {'mutex', 'disables', 'requires', 'value_limit'}
[capabilities].rf_sql_control_model valid (if present) [capabilities].rf_sql_control_model must be one of ['combined', 'separate']
[modes].list a list of strings [modes].list must be a list of strings
[modes].list non-empty [modes].list must not be empty
[modes].families names only cw, each a non-empty list of strings (if present) [modes].families unknown family 'X'; known: ['cw'], [modes].families.cw must be a non-empty list of strings
[modes].families members are in [modes].list, each at most once [modes].families.cw names mode 'X' that is not in [modes].list, [modes].families.cw repeats mode 'X' (already in family 'cw')
[filters].list present (may be empty: a radio without a selectable IF filter) missing required [filters].list
[cw_text] keys known and typed (if present): supported bool, max_length and memory_channel positive integers, memory_type "text" or "message" [cw_text].<key> must be ... / [cw_text] unknown key '<key>'
[cw_text].supported = true only with a send_cw command the protocol can write (CI-V bytes on civ, a CAT write template on yaesu_cat) [cw_text].supported = true needs a send_cw command <protocol> can write
Without [cw_text].supported, CW text is supported only on CI-V profiles with a send_cw command; Yaesu CAT and Kenwood CAT default to false —
Kenwood CAT: [cw_text].supported = true is refused (no backend sends CW text) [cw_text].supported = true is not available for kenwood_cat profiles: ...
Yaesu CAT: [cw_text].max_length is at most 50 (one KM write) and required when supported = true [cw_text].max_length must be at most 50 (one KM write), got <n> / [cw_text].max_length is required when [cw_text].supported = true ...
Yaesu CAT: [cw_text].memory_channel is a keyer memory 1-5 [cw_text].memory_channel must be a keyer memory channel 1-5, got <n>
Yaesu CAT: [cw_text].supported = true needs memory_channel and a write_keyer_memory CAT write template [cw_text].supported = true needs [cw_text].memory_channel (keyer memory 1-5) / ... needs a writable write_keyer_memory command

Unknown sections or extra fields are silently ignored (forward-compatible).


See Also