Problem
Several channel fields are stored in the internal model in a vendor-neutral form, but each CPS format (and often each radio within a format) represents them differently on the wire. There is no single deterministic translation without knowing which radio the file came from or is destined for.
Examples today:
Internal (Channel) |
OpenGD77 wire |
CHIRP wire |
power: 25 (percent) |
P2 |
1.0W |
power: null (radio default / high) |
Master / P1 |
5.0W / 10W / 25W (profile-dependent) |
squelch: 75 (percent) |
75% |
(not applicable on CHIRP) |
squelch: 0 (open) |
Disabled |
(not applicable) |
The internal model correctly uses 0–100 percent (or null = radio default) for power and squelch — see Channel. Import/export adapters already contain ad-hoc mappers (parseOpenGd77PowerWire, parseChirpPowerWire, formatChirpPowerWireForProfile, …), but:
- Import does not ask for a radio profile — CHIRP import uses a generic power ladder that cannot distinguish, e.g., whether
5.0W on a UV-5R Mini means 100% or “default high”.
- Export has a CHIRP profile picker (#103) used mainly for memory limits and export-time power serialisation, but mapping tables are not centralised or symmetric with import.
- OpenGD77 export still hard-codes the Baofeng 1701 profile (#72) with no import-side profile selection at all.
- There is no shared, documented per-profile mapping table that enables a meaningful full path: import → edit in app → export across formats and radios.
Without profile context, operators lose fidelity when crossing format boundaries (e.g. CHIRP → internal → CHIRP on a different radio, or CHIRP → OpenGD77).
Intended outcome
At import and export time, let the operator pick a radio profile (format-specific) that supplies mapping hints for fields that cannot be translated generically.
Scope (v1 fields)
Channel.power — wire ↔ percent
Channel.squelch — wire ↔ percent (OpenGD77 and other formats that carry squelch; CHIRP N/A)
Future fields (tones, bandwidth steps, etc.) can follow the same pattern — out of scope for this ticket unless trivial to generalise the registry shape.
Behaviour
- Import UI: when the detected format supports profiles, show a profile picker (or sensible default with override) before/during import. Pass
profileId into the import adapter.
- Export UI: extend existing profile pickers (CHIRP shipped; OpenGD77 #72) to use the same mapping tables, not just cardinality limits.
- Mapping tables: per format + profile, keyed lookup for wire → percent on import and percent → wire on export. Document in
docs/reference/<format>/radios/<profile>.md (tier 3 wire reference).
- Example: importing a CHIRP file for Baofeng UV-5R Mini maps
Power=5.0W → power: 100; exporting back with the same profile maps power: 100 → 5.0W. Retevis RT95 uses a different ladder (10W / 25W) — already partially handled on export; import should use the same profile tables.
- Round-trip: system tests should pass using model serialisation alone (no wire stash — see no-wire-stash-roundtrip). Profile-aware mappers are the approved boundary approach.
Architecture notes
- Mapping tables and profile constants stay at the import/export boundary only — not in
src/models/, mutations, validation, or CRUD UI (vendor boundaries).
- Extend or unify profile registries (
src/lib/export/chirp/profiles.ts, future OpenGD77 profiles) to include field mapping config, not just maxMemorySlots.
- Add
ImportOptions (mirror ExportOptions.profileId in types.ts) and thread through importFiles / adapters.
- CHIRP and OpenGD77 can ship first; DM32 and others adopt the same contract when they gain profiles.
Affected
src/lib/import-export/types.ts — ImportOptions.profileId
src/lib/import/<format>/ — profile-aware parse/serialise for power, squelch
src/lib/export/<format>/ — centralise mapping tables shared with import
- Import UI (
ImportDropzone or successor) — profile picker when format supports it
- Export UI — wire existing pickers to mapping tables (#72, CHIRP)
docs/reference/<format>/radios/ — per-profile power/squelch ladders
src/test/system/*RoundTrip.system.test.ts — profile-scoped assertions
References
Suggested phasing
- Registry shape — extend profile types with
powerLadder / squelchLadder (or equivalent); add ImportOptions.
- CHIRP — symmetric import/export tables for existing three profiles; import UI picker; round-trip tests per profile.
- OpenGD77 — Baofeng 1701 profile tables for power + squelch; tie into #72 export picker; import picker on multi-file drop.
- Docs — per-radio mapping tables under
docs/reference/<format>/radios/.
Out of scope
- Changing internal percent semantics or CRUD UI.
- Profile inference from filename or CHIRP driver metadata (manual picker is fine for v1).
- Power/squelch mapping for formats without profile registries yet.
- Wire-stash round-trip (
wireColumns, replaying imported cells on export).
Manual verify
- Import CHIRP UV-5R Mini sample with profile selected →
5.0W channels show 100% power in channel list.
- Export same project with same profile →
Power column reads 5.0W for those channels.
- Switch profile to Retevis RT95 on export → high power serialises to
25W, not 5.0W.
- Import OpenGD77 export with 1701 profile →
P2 → 25%, P100 → 100%, Disabled squelch → 0%.
- Full import → edit power in CRUD → export → re-import with same profile preserves intended wire values.
Problem
Several channel fields are stored in the internal model in a vendor-neutral form, but each CPS format (and often each radio within a format) represents them differently on the wire. There is no single deterministic translation without knowing which radio the file came from or is destined for.
Examples today:
Channel)power: 25(percent)P21.0Wpower: null(radio default / high)Master/P15.0W/10W/25W(profile-dependent)squelch: 75(percent)75%squelch: 0(open)DisabledThe internal model correctly uses 0–100 percent (or
null= radio default) forpowerandsquelch— seeChannel. Import/export adapters already contain ad-hoc mappers (parseOpenGd77PowerWire,parseChirpPowerWire,formatChirpPowerWireForProfile, …), but:5.0Won a UV-5R Mini means 100% or “default high”.Without profile context, operators lose fidelity when crossing format boundaries (e.g. CHIRP → internal → CHIRP on a different radio, or CHIRP → OpenGD77).
Intended outcome
At import and export time, let the operator pick a radio profile (format-specific) that supplies mapping hints for fields that cannot be translated generically.
Scope (v1 fields)
Channel.power— wire ↔ percentChannel.squelch— wire ↔ percent (OpenGD77 and other formats that carry squelch; CHIRP N/A)Future fields (tones, bandwidth steps, etc.) can follow the same pattern — out of scope for this ticket unless trivial to generalise the registry shape.
Behaviour
profileIdinto the import adapter.docs/reference/<format>/radios/<profile>.md(tier 3 wire reference).Power=5.0W→power: 100; exporting back with the same profile mapspower: 100→5.0W. Retevis RT95 uses a different ladder (10W/25W) — already partially handled on export; import should use the same profile tables.Architecture notes
src/models/, mutations, validation, or CRUD UI (vendor boundaries).src/lib/export/chirp/profiles.ts, future OpenGD77 profiles) to include field mapping config, not justmaxMemorySlots.ImportOptions(mirrorExportOptions.profileIdintypes.ts) and thread throughimportFiles/ adapters.Affected
src/lib/import-export/types.ts—ImportOptions.profileIdsrc/lib/import/<format>/— profile-aware parse/serialise for power, squelchsrc/lib/export/<format>/— centralise mapping tables shared with importImportDropzoneor successor) — profile picker when format supports itdocs/reference/<format>/radios/— per-profile power/squelch ladderssrc/test/system/*RoundTrip.system.test.ts— profile-scoped assertionsReferences
Channel.power/Channel.squelchsrc/lib/import/chirp/channelWire.ts,src/lib/export/chirp/channelWire.tssrc/lib/import/opengd77/channelWire.tsSuggested phasing
powerLadder/squelchLadder(or equivalent); addImportOptions.docs/reference/<format>/radios/.Out of scope
wireColumns, replaying imported cells on export).Manual verify
5.0Wchannels show100%power in channel list.Powercolumn reads5.0Wfor those channels.25W, not5.0W.P2→ 25%,P100→ 100%,Disabledsquelch → 0%.