A resolved codeplug leaves codeplugger one of two ways: as a file an external
CPS tool imports, or through a programming backend that drives the radio
itself. The GPL tools (chirp-writer, dmrconf, p64tool) are reached as
separate processes, never imports; benlink is Apache-2.0 and is imported.
Every write is gated: an explicit confirm, a backup taken before anything is
overwritten, and a read-back afterwards. codeplugger program wires these
gates for you; this page documents each path for when you need the pieces
directly.
| Radio family | Exporter | Backend | Write verified on hardware |
|---|---|---|---|
| Analog (CHIRP-supported) | CHIRP CSV | chirp-writer |
FT-2800M, BF-1909 (driver round trip) |
| Baofeng DM-32 | qdmr YAML | dmrconf |
yes |
| DM-1701, OpenGD77 | qdmr YAML | dmrconf |
DM-1701 OEM, RT3S OpenGD77 |
| Retevis MateTalk P4 | P4 TOML | p64tool |
yes |
| Benshi (VR-N76, UV-Pro) | benlink plan | benlink |
no |
Per-radio status, with the evidence behind each badge, is on the project site and in plan.md.
For analog radios 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.csvThen 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.
- Scan exclusion is set per assignment through
extensions.chirp; see profiles.md.
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.
The qdmr YAML exporter produces a codeplug for qdmr's dmrconf command-line
tool. Radio-wide settings and DMR APRS go through extensions.qdmr in the
profile; see profiles.md.
The dmrconf backend wraps the external binary. Its write gates, in order:
confirm=True; a headless dmrconf verify against the radio key, because
dmrconf's own write-time check does not abort; detect must report the
expected device name; and a full read into the backup path before anything
is overwritten. Neither the backup nor the read-back path may already exist,
so one session cannot destroy the record of an earlier one.
The read-back is an archived post-write image, not a verification: dmrconf takes YAML in and gives a device image back, so there is nothing to diff it against the way the CHIRP backend can. It proves the radio still reads after the write and gives a recovery and audit artifact; it does not prove the write was correct.
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.
Benshi radios (BTECH UV-Pro, Vero VR-N76) are programmed over Bluetooth
through benlink, installed with the
benlink extra. The exporter produces a region plan: these radios scope
channels to regions rather than a global pool, so an assignment repeated
across zones gets a slot in each region.
The backend's write gates, in order: confirm=True; a radio whose
capabilities are not marked hardware_verified additionally requires
allow_unverified=True; and the connected radio's self-reported region and
channel counts are checked against the plan before anything is written. No
codeplugger-generated plan has yet been written to a physical Benshi radio,
which is why no entry in that family carries hardware_verified and the site
flags their downloads.