Summary
Implement codeplugger's first exporter: consume an exporter-neutral ResolvedCodeplug and produce a .neonplug file accepted by NeonPlug for the Baofeng DM-32UV.
SSRF roots + profile -> ResolvedCodeplug -> .neonplug
Profile 0.1 only carries enough radio-specific information for analog channels and zones. Keep this issue to that path and reject unsupported modes rather than inventing values.
Evidence policy
Treat the existing private DM-32 generator and codec as research material, not as a specification.
The old generator currently:
- Requires a base
.neonplug file.
- Copies analog and digital channel records as templates.
- Preserves unrelated top-level state from that base.
- Contains inferred defaults and field meanings learned from exports and hardware behavior.
Its successful outputs demonstrate a working workflow, but do not prove that copied fields are required, that inferred meanings are correct, or that a codeplug can be generated safely from an empty object.
Use this evidence order when making exporter decisions:
- Current NeonPlug source and model definitions.
- Successful import into a pinned NeonPlug revision/version.
- DM-32UV hardware behavior.
- Existing private generator behavior and observed exports as hypotheses to test.
Record the source for every NeonPlug-only default. NeonPlug's DM-32 implementation itself contains TODOs and conflicting comments for some fields, so unresolved fields must not be assigned semantics by guesswork.
Verified container contract
Current NeonPlug source directly establishes that:
- A
.neonplug file is a ZIP archive containing codeplug.json.
- The current format version constant is
1.0.0.
- Import defaults many absent top-level collections to empty arrays or
null.
These facts should still be covered by exporter tests against a pinned NeonPlug revision. Do not assume every property represented by NeonPlug's TypeScript CodeplugData interface must be emitted until import behavior is tested.
Discovery gate: base-free generation
Before implementing the full writer, create the smallest synthetic analog channel/zone document and answer:
- Does current NeonPlug import it without a base export?
- Can NeonPlug write that imported data to a DM-32UV successfully?
- Which channel fields are actually required for a safe analog write?
- Which absent fields receive safe defaults, and where are those defaults defined?
- Are
radioSettings, radioInfo, or other device state required for this workflow?
If base-free generation works, implement explicit source-backed defaults. If it does not, define the minimum base-template contract and explain why each retained field is needed. Do not silently clone arbitrary personal or device-specific state.
Scope
The exporter should emit:
- Analog channels from
ResolvedCodeplug.channels, preserving order.
- Zones from
ResolvedCodeplug.zones, preserving order.
- Zone membership resolved from stable
channel_references to generated channel numbers.
- RX/TX frequencies, TX prohibition, and analog tones where verified mappings exist.
- Explicit defaults or a documented minimum base-template contract for required fields absent from
ResolvedCodeplug.
Power and bandwidth are not currently represented by ResolvedChannel; their policy must be explicit rather than inherited accidentally from a copied template.
Scan lists, contacts, DMR identities, digital channels, button settings, and profile 0.2+ features are out of scope.
Non-goals
- Decoding existing
.neonplug files into a friendly representation.
- Byte-identical decode/encode round trips.
- Preserving unknown or sparse fields from arbitrary exports.
- A general-purpose NeonPlug editing or migration library.
Reference handling
Display names are not unique. Resolve each zone member by ResolvedChannel.reference, then translate it to the generated channel number. Never resolve membership by display name.
Missing or duplicate stable references must raise an actionable error. Positional reference rules for scan lists, contacts, RX groups, and radio IDs belong in their future feature issues, not this one.
Proposed placement
src/codeplugger/exporters/neonplug/
A minimal public API:
write_neonplug(path: Path, codeplug: ResolvedCodeplug) -> None
Profile and SSRF resolution remain exporter-unaware. Add a CLI output option after the writer contract is covered by tests.
Acceptance criteria
- A minimal synthetic analog channel/zone fixture imports into a pinned NeonPlug revision without relying on private data.
- Tests document whether generation is base-free or requires a minimal base template.
- Channel and zone ordering matches
ResolvedCodeplug.
- Zone references resolve to the intended generated channel numbers.
- RX/TX frequencies, TX prohibition, and analog tones survive NeonPlug import and are confirmed in the imported model.
- Power, bandwidth, and every other NeonPlug-only field have a documented source and policy.
- Unsupported modes and invalid references fail with actionable errors.
- The archive contains
codeplug.json and declares the tested format version.
- Generated content is deterministic except for any explicitly documented metadata such as an export timestamp.
- Public tests use synthetic fixtures only.
- The generated fixture is smoke-tested on DM-32UV hardware before calling the initial milestone complete.
Source material and provenance
The private muehl-ssrf-private/radios/dm32/ implementation may be consulted for experiments and candidate mappings. Port only code whose behavior is needed and independently verified for this narrower exporter.
This repository is licensed under Apache-2.0. Preserve attribution required by any imported source and do not commit real radio exports or personal/device-specific base codeplugs.
Relationship to #3
This issue produces DM-32 output. #3 defines radio constraints. Implement currently enforceable analog channel/zone constraints alongside or immediately after this exporter; defer scan-list constraints until the profile model supports scan lists.
Summary
Implement codeplugger's first exporter: consume an exporter-neutral
ResolvedCodeplugand produce a.neonplugfile accepted by NeonPlug for the Baofeng DM-32UV.Profile
0.1only carries enough radio-specific information for analog channels and zones. Keep this issue to that path and reject unsupported modes rather than inventing values.Evidence policy
Treat the existing private DM-32 generator and codec as research material, not as a specification.
The old generator currently:
.neonplugfile.Its successful outputs demonstrate a working workflow, but do not prove that copied fields are required, that inferred meanings are correct, or that a codeplug can be generated safely from an empty object.
Use this evidence order when making exporter decisions:
Record the source for every NeonPlug-only default. NeonPlug's DM-32 implementation itself contains TODOs and conflicting comments for some fields, so unresolved fields must not be assigned semantics by guesswork.
Verified container contract
Current NeonPlug source directly establishes that:
.neonplugfile is a ZIP archive containingcodeplug.json.1.0.0.null.These facts should still be covered by exporter tests against a pinned NeonPlug revision. Do not assume every property represented by NeonPlug's TypeScript
CodeplugDatainterface must be emitted until import behavior is tested.Discovery gate: base-free generation
Before implementing the full writer, create the smallest synthetic analog channel/zone document and answer:
radioSettings,radioInfo, or other device state required for this workflow?If base-free generation works, implement explicit source-backed defaults. If it does not, define the minimum base-template contract and explain why each retained field is needed. Do not silently clone arbitrary personal or device-specific state.
Scope
The exporter should emit:
ResolvedCodeplug.channels, preserving order.ResolvedCodeplug.zones, preserving order.channel_referencesto generated channel numbers.ResolvedCodeplug.Power and bandwidth are not currently represented by
ResolvedChannel; their policy must be explicit rather than inherited accidentally from a copied template.Scan lists, contacts, DMR identities, digital channels, button settings, and profile
0.2+features are out of scope.Non-goals
.neonplugfiles into a friendly representation.Reference handling
Display names are not unique. Resolve each zone member by
ResolvedChannel.reference, then translate it to the generated channel number. Never resolve membership by display name.Missing or duplicate stable references must raise an actionable error. Positional reference rules for scan lists, contacts, RX groups, and radio IDs belong in their future feature issues, not this one.
Proposed placement
A minimal public API:
Profile and SSRF resolution remain exporter-unaware. Add a CLI output option after the writer contract is covered by tests.
Acceptance criteria
ResolvedCodeplug.codeplug.jsonand declares the tested format version.Source material and provenance
The private
muehl-ssrf-private/radios/dm32/implementation may be consulted for experiments and candidate mappings. Port only code whose behavior is needed and independently verified for this narrower exporter.This repository is licensed under Apache-2.0. Preserve attribution required by any imported source and do not commit real radio exports or personal/device-specific base codeplugs.
Relationship to #3
This issue produces DM-32 output. #3 defines radio constraints. Implement currently enforceable analog channel/zone constraints alongside or immediately after this exporter; defer scan-list constraints until the profile model supports scan lists.