Skip to content

About

A codeplug generator which sources data from ssrf-lite and generates importable files for various CPS software (neonplug, qdmr, dmrconfig, etc.)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

118 Commits

Folders and files

Repository files navigation

codeplugger

🌐 Project site

codeplugger builds radio codeplugs from shared RF data and user-owned configuration. It aims to be a general-purpose codeplug compiler: SSRF data plus a profile compiles deterministically into a validated, radio-neutral codeplug, and vendor CPS tools are replaceable export or programming backends rather than the workflow. Current targets are the Baofeng DM-32, Retevis MateTalk P4, and Baofeng UV-5R Mini.

Intent, guiding principles, current state, and the roadmap live in docs/plan.md. A project overview with supported radios and features is published at chicago-offline.github.io/codeplugger (source in site/).

Quick start

The install pulls in ssrf-lite with its published RF data, so the shipped examples run without any other checkout:

git clone https://github.com/Chicago-Offline/codeplugger.git
cd codeplugger
uv sync

# Validate an example and print it as CHIRP CSV
uv run codeplugger-profile examples/profiles/baofeng_uv5r_mini/example.yml \
  --instance-registry examples/instances.yml \
  --output-format chirp-csv

# Dry-run every radio in the example registry
uv run codeplugger-fleet \
  --instance-registry examples/instances.yml \
  --profiles-root examples/profiles

When it is time to describe your own radios, copy examples/ out into its own repository and edit from there; examples/README.md walks through that. Profiles for real radios do not belong in this repository.

Workflow

flowchart LR
    public["ssrf-lite<br/>Authoritative public RF data"]
    private["SSRF overlay<br/>shared or private user data"]
    profile["Profile<br/>selection and ordering policy"]
    registry["Instance registry<br/>fleet radios, optional"]
    resolve["codeplugger<br/>Resolve and validate against radio capabilities"]
    resolved["Resolved codeplug<br/>radio-neutral, deterministic"]
    backend["Programming backend<br/>gated write: dmrconf, p64tool, chirp-writer, benlink"]
    exporter["Exporter<br/>CHIRP CSV / qdmr YAML / P4 TOML / benlink plan"]
    cps["CPS tool<br/>CHIRP / vendor CPS"]
    artifacts["Artifacts<br/>HTML reference + operation log"]
    radio["Radio"]

    public --> resolve
    private --> resolve
    profile --> resolve
    registry --> resolve
    resolve --> resolved
    resolved --> backend --> radio
    resolved --> exporter --> cps --> radio
    resolved --> artifacts
Loading

The diagram source is also available in docs/workflow.mmd.

Repository responsibilities

  • ssrf-lite is the authoritative source for shareable RF facts: channel plans, repeaters, stations, talkgroups, and their schema.
  • chioff-ssrf-test is a public reference overlay. It models the structure and merge behavior of a private SSRF repository using only public or synthetic fixture data.
  • chioff-ssrf-shared holds real Chicago Offline community-net RF facts (shared GMRS and MURS channels) that members merge on top of ssrf-lite.
  • username-ssrf-private contains private RF records and explicit overrides of ssrf-lite, such as local names, unpublished channels, or user-specific grouping metadata.
  • chioff-codeplugger-profiles-test contains public reference profiles for Chicago Offline radios and provides realistic inputs for end-to-end integration tests.
  • chioff-codeplugger-profiles-shared contains profiles selecting the shared community net for supported radios.
  • username-codeplugger-profiles contains radio and output preferences: channel selection, zones, scan lists, button assignments, display settings, and per-radio variants.
  • codeplugger resolves those inputs, validates them against radio limits, and generates files importable by a supported CPS tool.
  • dm32-info remains a research and reference repository. Stable findings needed for generation should become radio definitions or validation rules in codeplugger, rather than making generation depend directly on research notes.

Precedence

Inputs should be merged deterministically:

  1. Load RF records from ssrf-lite.
  2. Add or override RF records from username-ssrf-private using stable record identifiers, not display names.
  3. Apply a selected codeplug profile to choose and arrange records and set radio-specific preferences.
  4. Validate the result against the target radio and exporter constraints.
  5. Generate the CPS import as a disposable build artifact.

Generated codeplugs should not become an authoritative data source. Changes should flow back into SSRF data or the selected profile and then be regenerated.

Reference and user repositories

RF data describes what exists; a profile describes what a person wants programmed into a particular radio. Keeping those concerns separate makes profiles reusable across SSRF dataset updates and avoids mixing device settings into an RF schema.

The two chioff-* repositories form a public reference configuration for integration testing:

ssrf-lite + chioff-ssrf-test + chioff-codeplugger-profiles-test -> codeplugger

The -test repositories hold only synthetic fixture data and exist to model the overlay merge behavior. Real user repositories may follow the same layout while remaining private. Public fixtures must not contain personal identities, unpublished frequencies, credentials, location-sensitive records, or exports from real radios.

The -shared repositories are the exception: they carry real Chicago Offline community-net channels that are intended to be published and shared. They still must not contain personal identities, credentials, or radio exports.

Small synthetic fixtures should remain in this repository for fast, deterministic unit tests. The chioff-* repositories are intended for broader cross-repository contract and end-to-end tests.

Scope and status

Profile resolution and validation, the fleet dry run, P4 TOML export with a gated p64tool write backend, DM-32 qdmr YAML export with a gated, hardware-verified dmrconf write backend, CHIRP CSV export for analog radios with a gated chirp-writer write backend, Bluetooth programming of Benshi radios through a gated benlink backend, and HTML/Markdown/operation-log artifacts are working today. The chirp-writer path is hardware-verified on the FT-2800M; the benlink path has not yet been accepted against a physical radio. Prebuilt community codeplugs are published from the project site at chicago-offline.github.io/codeplugger/codeplugs/ so members can download a file without installing anything; the DM-32 shared profile is served today and the remaining shared profiles follow. The next priority is moving the benlink, qdmr, and CHIRP work the project depends on out of forks and upstream; see docs/plan.md for the sequenced plan and what is deliberately deferred.

Repository directories:

  • radios/: radio capabilities, constraints, and exporter-specific mappings.
  • examples/: runnable starter profiles and an instance registry, built only from published ssrf-lite data. examples/usecases/ holds radio-agnostic base profiles (GMRS, MURS, PMR446, marine VHF, and the combined set) that the per-radio profiles bind to hardware. Copy it to start your own profile repository; see examples/README.md.
  • tests/fixtures/: synthetic inputs for unit tests, not usable profiles.

Profile format

Profile version 0.1 selects ordered SSRF assignments by stable ID and groups them into ordered zones for one target radio:

$schema: "https://raw.githubusercontent.com/Chicago-Offline/codeplugger/main/schemas/profile-0.1.schema.json"
version: "0.1"
id: "dm32_reference"
name: "DM-32 reference"
radio: "baofeng_dm32"
radio_instance: "dm32_reference_01"
zones:
  - id: "reference"
    name: "Reference"
    assignments:
      - "asg_fixture_reference_simplex"
      - "asg_wx1"

radio identifies the radio model and radio_instance identifies the specific physical radio receiving the generated codeplug. The instance ID is arbitrary but stable, such as dm32_green_01 or d01. Profiles that omit it retain backward compatibility by using their profile id as the instance ID.

Profiles contain selection and ordering policy, not RF facts or per-user DMR identities. Display-name or RF changes belong in an SSRF overlay. Version 0.1 intentionally excludes selectors, scan lists, contacts, button mappings, and exporter settings until the explicit-ID workflow is proven end to end.

Profiles may inherit from another profile in the same directory or a relative path. A parent that names a radio constrains the child to that radio; a parent that names none is an abstract base, reusable on any radio that can hold it, and the child supplies both radio and radio_instance. The child's id and optional radio_instance are never inherited. Scalar values in the child replace the parent. Lists of objects with an id are merged by ID, with child fields patching matching objects and new objects appended. Use zones_only or omit_zones for a child variant that keeps only part of the parent's zones:

version: "0.1"
id: "dm32_lite"
name: "DM-32 lite"
radio: "baofeng_dm32"
radio_instance: "dm32_white_01"
extends: "dm32_base.yml"
zones_only: ["reference"]

examples/usecases/ uses abstract bases to describe a radio by what it is for -- a GMRS handheld, a MURS handheld, a PMR446 handheld, a marine VHF set, or all of them at once -- so the same zone layout can be programmed onto any radio with the capacity for it.

Inheritance cycles, unknown zones, mismatched radios, and excessively deep inheritance chains are rejected. Review the fully merged profile with:

uv run codeplugger-profile path/to/profile.yml --print-merged-profile

Validate a profile against authoritative data and radio limits. With no --ssrf-root, codeplugger reads the ssrf-lite data shipped inside the installed dependency, so a plain install is enough for profiles that only select published assignments:

uv run codeplugger-profile path/to/profile.yml

Pass --ssrf-root explicitly, in precedence order, to use a local ssrf-lite checkout or to layer overlays on top of it. Explicit roots replace the installed default rather than extending it:

uv run codeplugger-profile path/to/profile.yml \
  --ssrf-root ../ssrf-lite/ssrf \
  --ssrf-root ../chioff-ssrf-test/ssrf

A root can pin its own place in the merge with a _root.yml declaration, in which case --ssrf-root order no longer matters for it (higher precedence wins; duplicate values are rejected):

# ../chioff-ssrf-test/ssrf/_root.yml
ssrf_root:
  id: chioff_test_overlay
  precedence: 100

Inspect the normalized, exporter-neutral codeplug as deterministic YAML or JSON:

uv run codeplugger-profile path/to/profile.yml \
  --ssrf-root ../ssrf-lite/ssrf \
  --ssrf-root ../chioff-ssrf-test/ssrf \
  --output-format yaml

The normalized model contains ordered channels and zones, radio-facing RX/TX frequencies, mode, service, analog tones, overlay-resolved notes, and explicit TX permission. It intentionally excludes scan lists, contacts, DMR identities, button settings, and NeonPlug fields; exporters translate this stable model without participating in input resolution.

DM-32 qdmr settings

The qdmr YAML exporter accepts qdmr-native settings and DMR APRS records under the profile's extensions.qdmr namespace. Assignment-level qdmr extensions add fields to that generated channel. For example:

extensions:
  qdmr:
    settings:
      introLine1: K9ABC
      boot: {display: Text}
      audio: {fmMicGain: 2, voxDelay: 500 ms}
      dmr: {groupCallMatch: false}
      gnss: {systems: [GPS]}
    contacts:
      - dmr:
          id: aprs_contact
          name: DMR APRS
          ring: false
          type: PrivateCall
          number: 310999
    positioning:
      - dmr:
          id: aprs1
          name: DMR APRS
          period: 5 min
          contact: aprs_contact
zones:
  - id: local
    name: Local
    assignments:
      - id: local_dmr
        extensions:
          qdmr: {aprs: aprs1}

Values use qdmr's extensible YAML names and syntax. IDs supplied in contacts and positioning must be unique within the generated document. Codeplugger rejects assignment extensions that try to replace generated RF fields. qdmr currently maps DM-32 boot, audio/VOX, tone, timeout, GNSS, DMR, SMS, password, and DMR APRS settings. It knows the binary locations of the DM-32 screen colors, brightness, and backlight duration but does not expose them through its YAML configuration model, so codeplugger cannot set them yet.

P4 TOML export

The P4 exporter requires a TOML file produced by p64tool decode as its baseline. It updates resolved channel and zone records in place and preserves radio-wide settings, existing contacts, RX groups, and fields codeplugger does not yet model. It can also explicitly set analog bandwidth, clear unused channel/zone slots, and add contacts from a fleet registry:

from codeplugger.exporters.p64_toml import p64_toml_from_resolved

output = p64_toml_from_resolved(
  resolved,
  baseline_toml,
  fleet_instances=registry["instances"],
  clear_unused_channels=True,
  analog_bandwidth_khz=25.0,
  digital_contact_index=1,
  digital_rx_group_index=1,
)

When fleet_instances is supplied, the target radio's dmr_id becomes general.radio_dmr_id and each registry radio is appended as a private contact. Contact and RX-group indices must refer to records already present in the baseline; the exporter does not invent talkgroup semantics.

The external p64tool backend wrapper provides read-only info, read, and roundtrip operations plus an explicit confirm=True write operation. The write takes a mandatory raw backup first, refusing a path that already exists, and always uses p64tool's known-firmware gate and read-back verification by default.

Radio artifacts and operation logs

Resolved profiles can produce a self-contained, printable HTML reference and a JSON Lines operation log under the profiles repository. The artifact format is radio-independent, so it works for DM-32, P4, and other resolved radio models:

uv run codeplugger-profile path/to/profile.yml \
  --ssrf-root ../ssrf-lite/ssrf \
  --ssrf-root ../chioff-ssrf-test/ssrf \
  --output-format html \
  --artifact-root path/to/profiles

This creates .artifacts/<radio>/<radio-instance>/reference.html and operations.jsonl. The latter records successful profile generation and can also be passed to a backend as an ArtifactStore to audit device reads and writes. Generated artifacts are evidence of an operation, not an authoritative profile or RF data source.

The reference begins with the radio model, instance identity, UTC generation time, and channel, zone, and contact counts. It then lists channels by numbered zone followed by the DMR contact table. When --instance-registry is supplied, the heading uses the instance's DMR ID and color metadata and the contact table includes every registry instance that has a DMR ID.

Registry-driven fleet dry run

The physical fleet registry is the source of truth for which radio instance gets which profile. A registry entry's profile value names a profile file under the profile repository, and the profile's radio_instance must match the registry key. No radio is changed by this command:

uv run codeplugger-fleet \
  --instance-registry ../muehlstein-codeplugger-profiles/instances.yml \
  --profiles-root ../muehlstein-codeplugger-profiles/profiles \
  --ssrf-root ../ssrf-lite/ssrf \
  --ssrf-root ../muehlstein-ssrf-private/ssrf

The command resolves every registered radio, validates its profile against the radio capabilities, and reports a deterministic SHA-256 of the resolved codeplug. A nonzero exit status means at least one instance is not ready; this is the planning gate before a programming adapter is invoked.

CHIRP workflow for analog radios

For analog radios such as the UV-5R Mini, codeplugger emits CHIRP-compatible CSV. That CSV can either be imported in the CHIRP GUI and uploaded from there, or handed to the gated chirp-writer backend, which programs the radio without a GUI step.

Generate CHIRP CSV from a validated profile:

uv run codeplugger-profile path/to/profile.yml \
  --ssrf-root ../ssrf-lite/ssrf \
  --ssrf-root ../chioff-ssrf-test/ssrf \
  --output-format chirp-csv > output.csv

Then import output.csv in CHIRP and upload from CHIRP to the radio.

Notes:

  • CHIRP CSV export currently supports FM channels only.
  • Any non-FM channel selected by the profile fails export with an explicit error.
  • Channel order in the CSV matches resolved profile order.

Excluding channels from scan

Analog radios have a single global scan and express membership negatively, per memory, so there is no scan list to join. Set CHIRP's Skip value through an assignment's extensions.chirp namespace:

zones:
  - id: main
    name: Main
    assignments:
      - {id: asg_wx1, display_name: "WX 1", extensions: {chirp: {skip: "S"}}}
      - {id: us_ham_vhf_call, display_name: "2MCALL", extensions: {chirp: {skip: "P"}}}

skip accepts only "" (scan normally), "S" (skip) and "P" (priority) — chirp_common's three values. Individual drivers narrow that set further in valid_skips, which the CSV exporter cannot see, so a value a radio does not support is caught at import rather than at export. extensions.chirp is ignored by every other exporter.

Headless CHIRP writes

CHIRP is GPL-3 and codeplugger is Apache-2.0, so CHIRP is reached the same way dmrconf and p64tool are: as a separate process, never an import. The chirp-writer helper does the CHIRP-linked work and must be installed separately, in an environment where chirp is importable. CHIRP's own chirpc cannot substitute for it: chirpc never applies the driver's baud rate when opening the port, and it has no CSV-import path at all.

Which driver addresses a radio lives in that radio's capabilities.json:

"chirp": {
  "driver": "Yaesu_VX-177",
  "model": "Yaesu VX-177",
  "clone_mode": "manual"
}

model is the identity the connected radio must report, which is often not the name on the case: an FT-277R reports as a Yaesu VX-177. clone_mode is manual when an operator must put the radio into clone mode for every transfer, and on_demand when the radio answers whenever the cable is connected.

from pathlib import Path
import json

from codeplugger.artifacts import ArtifactStore
from codeplugger.backends.chirp import ChirpTarget, ChirpWriter

capabilities = json.loads(Path("radios/retevis_c64/capabilities.json").read_text())
target = ChirpTarget.from_capabilities(capabilities)
store = ArtifactStore(Path("path/to/profiles"), "retevis_c64", "jhm_c64_01")

ChirpWriter(artifact_store=store).write(
    Path("output.csv"),
    "/dev/cu.usbserial-110",
    target=target,
    backup=Path("before.img"),
    read_back=Path("after.img"),
    confirm=True,
)

The gates, in order: confirm=True; a read_back path unless verify=False is passed explicitly; and, inside the helper, a mandatory pre-write backup whose reported model must match target.model. Because the backup download happens before the upload, a wrong radio is rejected before anything is written. Afterwards the helper re-reads the radio and diffs it per channel against what was sent; any mismatch raises rather than returning quietly. Unlisted memories are erased, since the generated codeplug is the whole truth for that radio.

A radio whose capabilities carry no chirp block is refused rather than addressed with a guessed driver.

Optional instance registry

Profiles may declare radio_instance, and codeplugger can optionally validate that identifier against a separate instance registry document:

version: "0.1"
instances:
  dm32_green_01:
    radio: baofeng_dm32
    label: "Green DM-32"
    firmware: "DM32.01.L01.048"

Use --instance-registry to enable this join check:

uv run codeplugger-profile path/to/profile.yml \
  --instance-registry path/to/instances.yml

When enabled, codeplugger verifies:

  • the profile instance ID exists in the registry
  • the registry entry's radio matches the profile's radio

If no registry is provided, behavior stays backward compatible with existing profile-only workflows.

About

A codeplug generator which sources data from ssrf-lite and generates importable files for various CPS software (neonplug, qdmr, dmrconfig, etc.)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages