Programming a radio should be a deterministic build, not a manual CPS session. Codeplugger compiles shared RF facts (SSRF) and user-owned profiles into validated codeplugs and, where possible, programs radios directly — so a fleet of radios can be kept consistent, reproducible, and auditable from version-controlled inputs.
Codeplugger is a general-purpose codeplug compiler. Vendor CPS tools and their file formats are replaceable backends, not the workflow. The stable core is:
SSRF data + overlays + profile
-> resolved, radio-neutral codeplug (validated against capabilities)
-> exporter or programming backend for the target radio
Any radio should be supportable by adding a capabilities definition plus an exporter or backend, without touching input resolution. Long term, the preferred backend for each radio is headless programming: no interactive CPS step between the resolved codeplug and the hardware.
- Generated codeplugs are disposable build artifacts, never a data source. Changes flow back into SSRF data or profiles and get regenerated.
- Profiles hold selection and ordering policy; RF facts live in SSRF.
- Radio writes are gated: explicit confirmation, known-firmware checks, read-back verification, and an operation log.
- Exporter defaults are named and reviewed, not copied wholesale from a CPS implementation.
- Fixtures in public repositories stay synthetic; no personal identities, unpublished frequencies, or real radio exports.
Working today:
- One command from a boxed radio to a programmed one (
codeplugger):radios,use-casesandprofileslist what is supported,buildcompiles a use case or a community profile onto a radio, andprogramwrites it through whichever of the four backends that radio's capabilities declare. No profile, no instance registry and no format knowledge required, which is the gap between "codeplugger supports your radio" and "you can use codeplugger". Nothing here is a second code path: the synthesized profile faces the same schema, limit and radio-support checks as a hand-written one, and writes go through the backends' existing confirm / backup / read-back gates. Which exporter and backend a radio uses is derived from itscapabilities.jsonblocks (chirp,benlink,dmrconf,p64tool), so adding a radio stays a capabilities file. - Fitting a use case to a radio does what the committed per-radio example
profiles do by hand: drop the assignments the radio physically cannot
use, then whole zones in the order the use case ranks them, until the
result fits. It reaches the same answers those profiles do (15 channels
in one zone for a BF-888S GMRS handheld, 26 for a UV-5R Mini marine set),
but every drop is printed and has to be accepted before a radio is
touched, and
--zonestakes the decision back. - Profile sources:
--profiletakes a file, a directory, a git URL, or the name of a registered collection (--radio baofeng_bf888s --profile chioff), resolved from the samesite/codeplug-sources.jsonthe downloads are built from, so anything the site publishes can also be programmed directly;--radiopicks the profile inside the collection, and two candidates for one radio is a question rather than a guess. A git URL is cloned once into a cache and its profiles are found by content (schema 0.1 with aradio). Only git's ordinary transports are accepted, becauseext::would let a URL run commands. - The registry (
site/codeplug-sources.json) stays a reviewed allowlist -- being listed is what gets a collection published as downloads -- but it is not a gate on use: a group can hand out a URL before anyone reviews anything. A repository the registry already knows by URL is treated as the registered entry, which is what supplies the SSRF overlay its profiles select from; a bare directory with the same name borrows only the overlay. - Profile resolution and validation (
codeplugger-profile): SSRF roots plus overlays, profile schema 0.1, radio capability limits including firmware-dependent limits, optional instance-registry join checks, severity-graded validation (hint/warning/critical) that reports every critical issue in one run. Checks stay service-specific where a general rule would misfire: a reversed GMRS repeater pair warns because 47 CFR 95.1763 fixes those eight pairs, but no offset-direction rule is applied to ham bands, where negative offsets and reverse splits are normal. - Profile policy beyond plain channel lists: RX group lists, scan lists
(membership only), per-assignment TX contact, profile inheritance
(
extends, withzones_only/omit_zonesvariants, radio-agnostic abstract base profiles, and--print-merged-profilefor review), and namespacedextensionsat the profile and assignment level for exporter-native settings. - Multi-identity DMR IDs: a registry instance may carry several
dmr_ids(for example a licensed ID and an unregistered simplex ID) with adefault_dmr_id; profiles bind one per zone or per assignment. Both the qdmr and P4 exporters emit one radio ID per identity actually used. - Channel facts derived from SSRF: bandwidth from the ITU emission designator, AM mode for airband receive-only assignments.
- Registry-driven fleet dry run (
codeplugger-fleet): resolves every registered radio, validates, and reports deterministic codeplug hashes. - Retevis MateTalk P4: TOML export against a
p64tool decodebaseline, fleet-registry contacts, and a gatedp64toolwrite backend with a mandatory pre-write backup and read-back verification. - Baofeng DM-32: qdmr YAML export (
exporters/qdmr_yaml.py, every default named and reviewed; DM-32UV structural minimums satisfied by explicit unused placeholders; profileextensions.qdmrfor qdmr-native settings, contacts, and DMR APRS) and a gateddmrconfwrite backend (backends/dmrconf.py) mirroring thep64toolsafety pattern: read-onlydetect/read/verify, explicit confirmation, pre-write verify gate, detected-radio identity gate, a mandatory pre-write backup and post-write read-back (neither path may already exist; the read-back is an archived image, not a diff -- dmrconf takes YAML in and returns a device image, so there is nothing to compare it against), and operation-log auditing.dmrconf verify --radio=dm32uvpasses on generated output andencode/decoderound-trips it (skip-if-uninstalled tests keep this checked). Hardware acceptance passed (2026-09): a real DM-32UV write plusdmrconf readread-back matched the intended codeplug, so the path is considered safe and operational. Transport decision record: qdmr'sdmrconfCLI (2026-09-08); the earlier NeonPlug import experiment (neonplug-base-free-experiment.md) is retained as evidence but that exporter path is dropped. - DM-32UV button mappings are implemented in a local qdmr fork and hardware-validated on the green radio: SK1/SK2/P1/P2 short and long presses, long-press duration, and side-key lock survived encode/write/ read-back, and the physical buttons behaved as configured. The qdmr fork's full 27-test suite passes; upstream PR preparation remains.
- DM-32 operational profiles: the shared community profile
(
chioff_dm32_shared, GMRS + MURS) and theco_dm32_ericregistry entry live inchioff-codeplugger-profiles-shared; the profile resolves, validates, and passesdmrconf verify --radio=dm32uv. The operator's private repository carries the full personal DM-32 profile (97 channels / 12 zones) and is fleet-ready. The fleet dry run is intentionally out of scope for the shared repository: community profiles are instance-agnostic (one profile, many radios), which the 1:1 profile-instance registry join does not model; shared profiles are validated per-profile withcodeplugger-profileinstead. - Baofeng DM-1701 (
baofeng_dm1701, OEM firmware; the Retevis RT84 is the same radio under qdmr's alias keyrt84): the seconddmrconftarget, and the first one that was not just a capabilities file. Limits come from qdmr'sDM1701Limits(3000 channels, 250 zones, 31-channel scan lists — not the DM-32UV's 15 — and a single DMR ID). Hardware acceptance passed 2026-10-08 oneam_dm1701_01: detect, binary read, a 219-channel / 22-zone write, read-back, decode and diff, with the read-back matching exactly and only the targeted bytes differing from the factory image. That run also found the exporter bug the DM-32 could not: TyT OEM firmware renders the second display line from zone bank B, so every channel showed "Unprogram." until the exporter mirrored the zone into both banks. That is driven by a newzone_bankscapability (2 on this radio, absent everywhere else) so OpenGD77 targets stay byte-identical. The write progress bar lies on this family as on the DM-32; the read-back diff is the only judge. - OpenGD77 via
dmrconf:retevis_rt3s_opengd77(OpenGD77 on MD-UV380 hardware, dmrconf keyopenuv380) andbaofeng_dm1701_opengd77(the same STM32 build on the DM-1701, so identical limits). Capabilities come from qdmr's sharedOpenGD77Limits: 1024 channels, 68 zones of 80, a 470 MHz UHF ceiling where the OEM firmware allows 480, andmax_scan_lists: 0because OpenGD77 scans by zone and qdmr silently ignores scan lists, so a profile that carries them is rejected loudly rather than dropped. The backend carries aVERIFY_RADIO_KEYSstand-in becauseverify --radio=openuv380segfaults in qdmr 0.15.1. The RT3S was hardware-verified end to end on 2026-09-23 (eam_rt3s_01: a 16-channel GMRS/MURS profile written with--init-codeplug, since a fresh flash has an undecodable empty zone table, and read back with names, zones and DCS tones intact). The DM-1701 OpenGD77 entry is not badged: that firmware has never been flashed on the radio here. - Analog radios (UV-5R Mini, Ailunce HA2, Retevis C64, Baofeng BF-888S, Baofeng
W31E, Yaesu FT-270R / FT-277R / FT-2800M): CHIRP CSV export (FM only), either imported in
the CHIRP GUI or written headlessly by the
chirp-writerbackend described below. The HA2 has capabilities (1024 channels / 16 zones, AM airband RX) but no HA2-specific exporter yet; it uses the generic CHIRP path. The C64 (64 channels, 136-174 / 400-480 MHz, no zone concept) likewise uses the generic CHIRP path; its capabilities are measured from a physical radio inretevis-c64-infoand match the CHIRP C64 driver, which is a Retevis C2 protocol variant. The FT-270R / FT-277R are the same submersible chassis in 2 m and 70 cm; their capabilities (200 channels, 6-character names, ham-band TX inside a wider receive span, no reachable banks) come from CHIRP's VX-170 / VX-177 drivers and are bench-verified against both physical radios inft270-277-info. The radios report the stock model IDsAH022$/AH022U, so stock CHIRP programs them with no patch, and a CHIRP upload to a physical FT-277R is confirmed. A MARS/CAP-modded radio gets its own radio id (yaesu_ft277_mars) rather than a flag or an override: transmit permission is the one capability that must never be inferred, and the instance registry already binds one physical radio to one radio id, so the mod is recorded where the radio is. The FT-2800M is a 2 m mobile on the same generic CHIRP path, with the same 200 channels and 6-character names but its own driver (chirp/drivers/ft2800.py), which reports the name on the case instead of a VX alias. It ships as a stock/modded pair too,yaesu_ft2800mandyaesu_ft2800m_mars, and here the distinction is machine-readable: the clone ID block ends02 00 b8on a stock US radio and03 00 b9on one with the extended-TX mod, so the right radio id can be confirmed from a read rather than taken on trust. A bench radio read on 2026-09-14 sent the stock block and a 7680-byte codeplug, which is whatyaesu_ft2800mrecords; the modded id has no physical instance yet. That read also made the point therx_onlyspan exists for: 47 of its 177 memories were public safety and marine VHF up to 161 MHz, which a stock radio stores and receives perfectly well and still cannot transmit on. - Baofeng BF-C50 (
baofeng_bf_c50): validation and CSV / markdown reference output only, with no programming path. CHIRP has no BF-C50 driver (chirpmyradio.com issue #10176 is still open, and the fork opened to build one,emuehlstein/chirpnewfangs, never got a driver committed), and the Retevis RB618 workaround circulating there transfers frequencies but not CTCSS/DCS, so itscapabilities.jsondeliberately omits thechirpblock and the backend refuses the radio rather than swapping in a model id that would silently write wrong tones. The entry gets the FT-270R band treatment: the advertised 400-470 MHz span is receive-only either side of a 430-440 MHz transmit block, which is what the unit's CMIIT type approval (2020FP6711, FM handheld, amateur service) actually covers. That keeps the uncertified GMRS/FRS 462-467 MHz block rejected. 16 channels, operator- confirmed; with no screen a channel is only ever identified by its knob position, so channel names are operator documentation rather than data the radio stores. Other BF-C50 builds ship with different transmit permissions — the EU PMR446 one is programmed as an RB618 — and each would be a separate radio id. - Baofeng BF-888S (
baofeng_bf888s): 16 channels, no display and no channel-name field, generic CHIRP path plus headlesschirp-writerprogramming. The case and CHIRP disagree on the name, and the write gate has to use CHIRP's: the driver isH777Radioinchirp/drivers/h777.py, nominally the Heng Shun Tong H-777 but registered asBaofeng BF-888and written around this radio — its own comments cite timing measured on "the Baofeng BF-888S model", and the rest of the clone family (Arcshell, Greaval, Ansoko, Tenway) rides on it as aliases. So the radio id keeps the name on the case andchirp.modelrecords what the driver reports, the same split asyaesu_ft277reporting as a VX-177. That gate earns its keep here: the H-777 family has a dozen registered variants on one protocol, and the BF-1901 siblings tune to 520 MHz. The band is the W31E treatment — a single 400-470 MHz transmit span, which is the published figure and is tighter than the driver's 400-490 MHz catch-all — because the FCC grant that applies has not been read off this unit; the BF-888S has shipped under more than one FCC ID by batch, and reading the label can only narrow this band, never widen it. Bench-confirmed on 2026-09-14 by a read-only detect and read: the radio reportsBaofeng BF-888and clones 16 nameless slots. It came back holding seven GMRS channels, which is not the factory codeplug it first looked like — slots 1-6 matchchioff_bf888s_sharedfield for field, down to CTCSS 141.3 on ChiO ROAD alone — so the radio had already been programmed with an earlier revision of the shared profile, and what it holds is operator choice rather than evidence about the band. - Baofeng W31E (
baofeng_w31e): 16 channels, no display and no channel-name field, generic CHIRP path plus headlesschirp-writerprogramming. The id comes from achirp-writer detectclone rather than from the case, which says onlyW31: the radio reportsBaofeng W31E, and CHIRP carries two unrelated W31 drivers — this analog Retevis RT22 variant, and a 30-channelW31Dwith a proprietary digital mode and a different codeplug. The reported-model gate is what keeps one from being written as the other. Unlike the BF-C50 and the Yaesus, its band is a single 400-470 MHz transmit span with nothing receive-only: those radios are narrowed by an equipment approval record (a CMIIT amateur-service approval, type acceptance), and this unit's label carries no FCC ID, no CMIIT ID and no rated power, so there is no equipment-side fact to narrow it with. Which slice of that span an operator may key up on is licence and jurisdiction policy, and policy belongs in profiles — codeplugger is not a ham-only tool, and acapabilities.jsonshould not encode one radio service's band plan. - Baofeng BF-1909 (
baofeng_bf1909): the third H-777 family radio, 16 nameless channels on the BF-888S clone protocol, and the one where id, case and CHIRP'sVendor Modelstring finally agree. Bench-confirmed 2026-10-09 againsteam_1909_01: a read-only clone returned the driver's full 1008-byte image, a generated 16-channel plan chosen to differ from the factory content wrote and re-read byte-identical, and the factory image was restored and re-read to its original hash. The band is the weak claim and is flagged as such: 400-470 MHz is from retail listings, not a grant, and the unit shipped on the PMR446 grid but accepted 462 MHz, so firmware bounds nothing. It carries the published PMR446 use case. On 2026-10-16 it became the first radio programmed end to end bycodeplugger programoverchirp-writer: the GMRS and then the PMR446 use case, each with a clean read-back diff and an operation-log entry, which also surfaced a post-upload grace period the GUI never sees (the radio refuses a new handshake for about 2.5 s while it commits the upload; the helper's read-back now retries). - Quansheng UV-K5 running F4HWN (
quansheng_uvk5_f4hwn): a custom-firmware radio id, deliberately separate from a stock UV-K5 because custom builds change the TX span, channel count and settings surface, and a stock image will not load in the custom driver. 200 channels, 10-character names, no zones; TX only in 137-174 and 400-470 MHz, every other span receive-only. The driver isarmel/uv-k5-chirp-driver, out of tree and pinned to F4HWN v4.3 — CHIRP's bundleduvk5.pydispatches on firmware prefix and refuses this one — and it imports wx at module level, so headless use needs a wx stub. Limits were probed fromget_features()with the radio on the cable on 2026-10-16. - Paper-derived CHIRP radios: TYT TH-9800 (
tyt_th9800, 800 channels, quad-band 10 m / 6 m / 2 m / 70 cm FM mobile, with atyt_th9800_marsextended-TX twin) and Yaesu VX-6R (yaesu_vx6r, 900 channels, tri-band submersible handheld). Both have 6-character names and both are limits read out of CHIRP driver source rather than off a radio; the TH-9800 driver is anExperimentalRadiowhose own prompt tells the operator to keep an OEM backup, and anything codeplugger emits for it inherits that warning. Neither has been read or written here. The FT-270R also has itsyaesu_ft270_marstwin now, on the same reasoning as the FT-277R. - Headless analog writes: a gated
chirp-writerbackend (backends/chirp.py) mirroring thedmrconf/p64toolsafety pattern — explicit confirmation, a mandatory pre-write backup that doubles as the detected-radio identity gate, a required read-back diff, and operation-log auditing. CHIRP is GPL-3 and codeplugger is Apache-2.0, so CHIRP is reached as a subprocess, never an import: the helper lives in the separate GPL-3chirp-writerrepository and writes a JSON result the backend gates on. CHIRP's ownchirpccannot do this job — it opens the port without applying the driver'sBAUD_RATE(only the GUI does, which is why a 115200-baud C64 never answers it) and has no CSV-import path at all. Each radio'scapabilities.jsoncarries achirpblock binding it to a driver, theVendor Modelstring that driver reports, and whether the radio needs an operator to put it into clone mode for every transfer;clone_mode: manual(the Yaesu ft7800 family) makes the backend run the helper with the terminal attached instead of capturing it. First hardware verification landed 2026-09-14 on the FT-2800M, aclone_mode: manualradio: a headless write was confirmed in both directions with a deliberately distinguishable payload each way, checked on the radio's own display and corroborated by each run's pre-write backup capturing exactly what the previous run left. A headless read separately reproduced an intended codeplug with zero mismatches. The on-demand path landed 2026-10-16 on the BF-1909, driven bycodeplugger programrather than the helper (see item 1). Remaining acceptance work is re-running the two C64 writes through the backend rather than the one-off script. - Benshi Bluetooth radios (
vero_vrn76,btech_uv_pro): a region-plan JSON export (exporters/benlink_plan.py) and a gated write backend (backends/benlink.py) built onbenlink. These radios have no programming cable and no CHIRP driver, and there is no prospect of one: CHIRP has no Bluetooth transport at all, so the vendor phone app and benlink's reverse-engineered BLE/RFCOMM protocol are the only ways in. benlink is Apache-2.0, the same licence as codeplugger, so unlike the GPL-3 CHIRP path it is imported rather than subprocessed — as an optional extra, so codeplugger stays installable without a Bluetooth stack. Two things about these radios do not fit the shape every other target has. Channels are region-scoped: each region (a "group" on the VR-N76, a "bank" on the UV-Pro) owns its own channel table instead of indexing one global pool, so a resolved zone becomes a region and a channel wanted in two zones occupies a slot in both. And the channel record carries atx_disablebit, so a receive-only assignment is finally exported as receive-only rather than as the plain simplex channel the CHIRP CSV path has to settle for. The safety model differs to match: there is no codeplug image to diff, each channel is its own round trip, so the mandatory backup is a JSON snapshot of every region the plan will touch and slots the plan does not fill are erased rather than left behind (a region may opt out withblank_unlisted: false, which is how the N76's firmware-owned APRS region stays untouched). There is no model string to gate on either. Where aproduct_idhas been read off a physical radio the backend refuses to write to anything else; where it has not, the fallback is the connected radio's self-reported region and channel counts — which differ across the family, 6x32 on the VR-N76 against the UV-Pro's advertised 180 channels in six banks — and any model not markedhardware_verifiedneeds an explicit opt-in regardless. Region support (read/write region names and per-region channels) is not in an upstream benlink release yet; the dependency is pinned to the Chicago Offline fork while khusmann/benlink#28 is in draft. The VR-N76's bands get the W31E treatment — 136-174 and 400-470 MHz transmit, per the case marking and the vendor, with the 108-136 MHz aviation band receive-only — because FCC Part 97 is not an equipment-certification regime and no grant exists to narrow them with. The UV-Pro entry omitsbandsentirely rather than guessing one: BTECH publishes no transmit span, and an invented one is exactly the inference this project refuses. Neither radio has had a codeplugger-generated plan written to it yet. - DM-32 analog channels/zones: NeonPlug
.neonplugexport (exporters/neonplug/, profile 0.1, FM only) as an interchange format for NeonPlug's own GUI, verified base-free against a pinned NeonPlug revision's real import path and DM-32UV byte codec. This is a manual-review interchange path, like CHIRP for the UV-5R Mini — separate from, and not a replacement for, thedmrconfheadless write backend below. - Artifacts: self-contained HTML and Markdown radio references (tones, color code, timeslot, bandwidth, power, scan lists, RX groups) plus a JSON Lines operation log for generation and device operations.
- Synthetic DM-32 profile coverage: the public
chioff-codeplugger-profiles-testrepository owns the canonical synthetic DM-32 profile; its integration workflow resolves assignments acrossssrf-liteandchioff-ssrf-test, validates against this repository's DM-32 capabilities, and generates qdmr YAML. Small self-contained fixtures remain here for fast unit tests.
Every stage of the pipeline works and four write backends exist, but the only operator who has ever run it is the maintainer, on the bench. The priority now is getting the output into other people's hands with nothing to install, and making the ecosystem the project depends on outlast a single maintainer's forks:
-
Run
codeplugger programagainst a physical radio, once per backend. The README leads with this command, and it wires the same four backends the bench work verified, but until 2026-10-16 the wiring itself (port handling, backup and read-back paths, the operation log it opens) had only been exercised against stubbed subprocesses; every hardware result recorded above was produced by the backend or helper directly.chirp-writerdone 2026-10-16 on the BF-1909: two consecutive--confirmruns (GMRS, then PMR446), each with a 0-byte read-back diff and awrite successoperation-log entry, each run's backup matching what the previous run left. The first attempts failed at the read-back gate with the write already on the radio, which is exactly the class of bug this item exists to find (see item 4). Remaining:dmrconfon a DM-32 or DM-1701,p64toolon the P4,benlinkonce item 4 has a radio -- each one write with a clean read-back diff and the operation-log entry it produced. -
Serve prebuilt community codeplugs from the site. Landed 2026-09-21 for the DM-32; every shared profile with a source-buildable export is published as of 2026-10-14. The Pages workflow checks out
ssrf-liteat the revisionpyproject.tomlpins the package to, pluschioff-ssrf-sharedandchioff-codeplugger-profiles-shared, andtools/build_site_artifacts.pyresolves every profile listed insite/codeplug-sources.jsonand publishes per profile: the export files the radio's support-table row promises, the HTML reference, the resolved radio-neutral codeplug, and aSHA256SUMS. Downloads are linked fromsite/codeplugs/, generated fresh on every build and never committed. Which formats a profile produces is derived from the radio's row insite/radios.json, so the table cannot advertise a CHIRP CSV column and then fail to offer the file; a radio whose only export needs a device read (the P4'sp64tool decodebaseline) fails the build rather than publishing nothing. The same job is the end-to-end integration test the project has lacked: every SSRF or profile change rebuilds on CI, a change like the September GMRS wide/narrow flip shows up as an artifact diff rather than a bench surprise, and the published hashes make the deterministic-build claim something anyone can check locally. Shared profiles are instance-agnostic, so no personal DMR IDs are involved (qdmr output carries theUNUSEDplaceholder ID). A source may also omitcheckoutand ship in this repository, resolving against the pinnedssrf-litepackage with nothing cloned; theuse-casessource publishes theexamples/usecases/codeplugs (GMRS, PMR446, MURS, marine VHF, and the combined set) that way, which is what lets someone buy a supported radio and download a file for it. The support table links each radio to the codeplugs built for it:gen_radio_table.py --codeplugsreads the manifestbuild_site_artifacts.pywrites, so the deploy runs the builder first and a link only exists for a file that exists. The links are a deploy-time artifact and never committed, which is why--checkrefuses a manifest. The published set is now theuse-casessource plus the DM-32, UV-5R Mini and BF-888S shared profiles; theuse-casessource also publishes for the BF-1909 and the UV-Pro. The UV-Pro download is a benlink region plan, derived from the radio'sbenlinkcapabilities block rather than a table column, for a radio whose write path has no hardware acceptance yet (item 4): the file is reviewable and deterministic, but nobody has yet written one to a radio, and the download card says so for anybenlinkradio not markedhardware_verified. Remaining: the P4, which stays unpublishable until its export can be built without a device read; and turning on the rebuild trigger. The receiving side is done --pages.ymlaccepts asources-changedrepository_dispatch, runs nightly, and since 2026-10-16 also rebuilds whenpyproject.tomlmoves thessrf-litepin, which is the only way anssrf-litechange reaches the site (the build checks out the pinned revision, so a push there needs no dispatch).chioff-ssrf-sharedandchioff-codeplugger-profiles-sharedeach carry anotify-codepluggerworkflow that sends the dispatch on push to their data paths. What it waits on is aCODEPLUGGER_DISPATCH_TOKENorganization secret -- a fine-grained PAT with contents write on this repository, sinceGITHUB_TOKENcannot dispatch across repositories -- and the workflow fails loudly rather than silently until it exists. Until then, a profile or SSRF change waits for the nightly run. -
Upstream the forks the project is pinned to. Three write paths currently depend on code that lives only in a personal fork or a local checkout, each a single-maintainer bus factor:
- benlink: region (channel bank) read/write is pinned to the
emuehlstein/benlink@n76-supportbranch while khusmann/benlink#28 is in draft. Land it, then move thebenlinkextra to a release. - qdmr: the DM-32UV button mappings and display colors are
hardware-validated in a local fork but not upstream. The work is a
DM32UVExtensionin the pattern of qdmr's AnyTone/Radioddity/TYT extensions, wired intoGeneralSettingsElement::decode/encode, plusdmrconf verifylimits and docs. One PR each, smallest first: display colors, then programmable key functions (SK1/SK2/P1/P2 short and long press, long-press duration, side-key lock), then the remaining general settings NeonPlug exposes (backlight, date format, roger tone, APO), then per-channel extras (key index). Each PR is validated against a real radio via read/write round-trip. On the codeplugger side, expose each landed setting through the existingextensions.qdmrpassthrough first; promote something to first-class profile schema only if it turns out to be radio-neutral policy (button mappings are the likely candidate). NeonPlug's byte codec and the base-free experiment are the reference for field meanings, not a delivery path. Until a PR is released, pin the exporter'sQDMR_CONFIG_VERSIONand tests to the qdmr revision that carries it and document the required build. - CHIRP: the BF-C50 driver exists on the
bf-c50branch ofemuehlstein/chirpnewfangsbut cannot go upstream until an image read off a physical radio exists, because CHIRP's driver tests are image-driven. Read one, add the test image, open the PR; only then doesbaofeng_bf_c50get achirpblock.
- benlink: region (channel bank) read/write is pinned to the
-
Finish
chirp-writeracceptance on an on-demand radio, and make the badges match the evidence. On-demand half landed 2026-10-16. The backend was hardware-verified on the FT-2800M, aclone_mode: manualradio, which was the risky half; the BF-1909 now covers the other half, throughcodeplugger programrather than the helper alone (item 1), and carries a headlesshwbadge. The run found a real defect: a radio commits an upload to flash after acknowledging the last block and refuses a new programming-mode handshake until it has (the BF-1909 answersVinstead of ACK at once, nothing for the next second, and normally from about 2.5 s), so the helper's read-back, started 30 ms after the upload, reported "Radio refused to enter programming mode" on three writes that had landed. The GUI never sees this because an operator sits between the two transfers.chirp-writernow retries only the read-back download, once a second for up to eight attempts, and recordsread_back_attemptsin its result; the pre-upload backup does not retry, because a silent radio there is a real fault. The UV-K5 F4HWN's headless badge stays dropped: its notes record only aget_features()probe, no write and no read-back. Still open: re-run the two C64 writes through the backend and retire the one-off script (it lives inretevis-c64-info, not here). Thebenlinkbackend is in the position this item started in, with no radio at all: nothing codeplugger generated has been written to a VR-N76 or UV-Pro, andvero_vrn76stayshardware_verified: falseuntil it has. -
Allow the same assignment in more than one zone. Landed 2026-10-16. A repeat is the same channel shown in another zone: it takes no new channel number and counts once against
max_channels(and in use-case fitting), but against each zone'smax_channels_per_zone. Every occurrence must ask for the same channel -- identical per-assignment settings and effective DMR ID -- or the profile is rejected; listing one assignment twice in one zone still is. Global-pool exporters (CHIRP, qdmr, P4) emit the channel once; the benlink plan gives it a slot in each region. -
Model instance-agnostic profiles in the fleet join. Landed 2026-10-16. A profile that names no
radio_instanceis shared, andcodeplugger-fleetbinds it to every registered radio whoseprofilenames it, so each gets its own DMR ID and firmware limits from the registry;codeplugger build --instancedoes the same for one radio. A profile that names a different instance is rejected rather than rebound. The shared community profiles still are not run through a fleet in this repository, since the registry that would bind them is each operator's own. -
Keep the DM-32 fleet running. Program the real fleet from the private profile repository, keep the operation log and read-back archives as the audit trail, and feed any on-radio surprises back into SSRF data, profiles, or exporter defaults (never into the generated codeplug).
-
More qdmr-supported radios via
capabilities.jsononly. The qdmr YAML exporter anddmrconfbackend are radio-neutral, and the DM-1701 is the test of the claim: it needed a capabilities file, admrconf verifyskip-if-uninstalled test, aRADIO_NAMESentry captured from a real detect -- and one exporter change (zone_banks), because TyT OEM firmware's two-bank zones were invisible from source and only showed up on the radio's display. Expect each new family to cost about that much. Candidates in rough order of community demand: AnyTone AT-D868UV/878UV/578UV, TYT MD-UV380/UV390 on OEM firmware and Retevis RT3S likewise, Radioddity GD-77, BTECH DMR-6X2. Each needs a named, reviewed set of exporter defaults where qdmr's limits differ from the DM-32UV (placeholders, name lengths, list minimums), and none gets anhwbadge off admrconf verifypass alone. -
OpenGD77 via
dmrconf. Landed and hardware-verified on the RT3S (see Current state). Remaining: flasheam_dm1701_01to OpenGD77 and write throughbaofeng_dm1701_opengd77once, so the second entry stops being a copy of the first's limits with no radio behind it; and keep theVERIFY_RADIO_KEYSworkaround until a qdmr release fixes the no-deviceverifycrash. An OpenGD77 CPS CSV exporter is still only warranted if thedmrconfpath leaves gaps. -
codeplugger import: onboard from a radio the user already likes. The on-ramp still assumes someone starts from SSRF assignment IDs; most people start from a radio that is already set up the way they want it.importreads a radio of a known model ----radiois required and the backend'sdetectis an identity gate, never a guess -- decodes it to the neutral channel shape, and matches every channel against SSRF. The matching is the feature, not the read; it sorts into three buckets. An exact match (frequency, offset, tone, mode agree) becomes an assignment ID in a draft profile, with the radio's zone and channel order carried over as ordering policy and any differing label as the per-assignmentdisplay_namethat already exists. A near miss (frequency agrees, tone/color code/bandwidth/name differs) becomes a reconcile report plus a ready-to-PR SSRF overlay fragment -- that is the other half of this item, the path for field edits noticed in use ("this repeater's color code is not what the directory says") to get back into shared data instead of dying in one operator's radio. No match becomes a draft overlay assignment carrying its provenance and a TODO, referenced by the draft profile so the result resolves.This does not make a codeplug a data source. The output is draft source for human review -- a profile and an overlay fragment -- never a round-trip, and the match report states plainly what did not survive the read. Guardrails follow the existing patterns: write only into a user-named output directory, never into the installed
ssrf-litedata, the registry, or this repository; refuse to overwrite an existing path the way the backup and read-back gates do. Privacy matters here in a way it does not elsewhere in the pipeline, because a real read carries DMR IDs, contact names, and possibly unpublished frequencies: warn on it, and keep every committed fixture synthetic.Phase it so no hardware risk arrives before the logic is proven. Phase 0 is
--imageonly, an offline file-in/drafts-out command tested against synthetic fixtures, with one backend's decode (CHIRP first -- widest radio coverage and the simplest neutral form). Phase 1 adds--portover the reads the backends already expose (chirp.read,p64tool.read,dmrconf.read,benlink.backup). Phase 2 adds areconcilemode that compares a radio against an existing profile and emits only the SSRF-shaped differences. The decoders belong in animporters/package mirroringexporters/, since each one is the inverse of an exporter that already exists.
Known gaps not tracked by an item above:
- Scan lists are membership-only (no priority channels) across exporters; listed under Deferred.
- Several real capabilities (call-sign database size, APRS positioning
systems, encryption key slots, SSB receive on the UV-K5) have no schema
field and live only in
notes. Fine until a check needs one.
- Publish to PyPI so
uvx codeplugger-profileworks without a clone. Lower priority once the site serves artifacts, since most community members will never need the CLI; blocked anyway on thessrf-liteandbenlinkdependencies being releases rather than git pins. - AnyTone CPS CSV export-only path, if the qdmr route proves insufficient for AnyTone owners.
dmrconfig/editcpbackends are skipped unless qdmr coverage gaps appear.
- Capturing Retevis P4 CPS v1.5 reference data under
radios/retevis_matetalk_p4/reference/(user manual captured; CPS field-mapping notes still to come).
Until the explicit-ID workflow is proven end to end on more radios:
- Profile selectors (anything beyond explicit ordered assignment IDs)
- First-class button-mapping and display settings in the profile schema;
they arrive first as
extensions.qdmrpassthrough once the upstream qdmr work in the Now section lands - Scan-list priority channels
- Radios outside the qdmr / CHIRP / p64tool / benlink backends already in the tree
- Becoming a CPS GUI or codeplug editor.
- Treating any generated file or radio read as authoritative data.
- Supporting codeplug round-trip editing; the flow is one-way from sources.