How CPS export files enter the app, become internal codeplug models, and leave again as vendor formats.
Tracking: codeplug-tool#7 (import foundation), #38 (OpenGD77 full I/O), #58 (active import), #84 (doc collate), #103 (CHIRP)
Operator workflow (create → edit → persist → export to multiple formats): operator lifecycle.
Import was originally hard-wired to OpenGD77 CSV inside the channel map. The app now has a format registry, OpenGD77 as the first adapter pair, and a central store that resolves vendor names to internal ids. Export serialises the internal models back to a format the vendor CPS accepts.
The internal model is format- and radio-agnostic. Format specifics — column mapping, cardinality caps, skipped files — apply at the import/export boundary only.
Formats vs variants. OpenGD77 CSV is one import/export format, sibling to Baofeng DM32 CSV, qDMR YAML, native YAML, and analogue-only formats like CHIRP (DM32 and CHIRP are unrelated to OpenGD77). Within the OpenGD77 format there are per-radio variants (1701, MD9600, GD-77, …). Variant-specific limits are documented in OpenGD77 radio profiles and are intended to be applied when the operator picks a target OpenGD77 radio at export time (#72 — OpenGD77-only; not cross-format work).
| Area | Status | Notes |
|---|---|---|
| Internal models | Shipped | src/models/codeplug.ts — schema v18 |
| Adapter interface contracts | Shipped | src/lib/import-export/ — ImportAdapter, ExportAdapter |
| Export format registry | Shipped | src/lib/export/ |
| OpenGD77 import | Shipped | Channels, Zones, Contacts, TG_Lists (#38) |
| OpenGD77 export | Shipped | Per-file + ZIP; DTMF/APRS header-only |
| Multi-file + directory import UI | Shipped | ImportDropzone on home |
| Active project import | Shipped | ImportIntoActivePanel on Import & export (#58) |
| Merge / overwrite modes | Shipped | importMerge.ts — idempotent merge by vendor name |
| Name → id resolution | Shipped | Store + src/lib/codeplug.ts |
Export page (/export) |
Shipped | Nav link when a project is active |
| Delivery-aware export UI | Shipped | Registry dispatch + CHIRP profile picker (#103) |
| LocalStorage persistence | Shipped | #9 — persistence/ |
| Multi-project import | Shipped | Home creates project; Import & export merges into active — codeplug-project/ |
| OpenGD77 radio-variant picker | Planned | Apply per-radio (1701, MD9600, …) limits within OpenGD77 export — #72; OpenGD77-only, not cross-format |
| qDMR YAML | Deferred | #37 — UI placeholder |
| Native YAML | Shipped | #10 — native-yaml/ |
| Baofeng DM32 CPS | Shipped | #67 — dm32/; expandModes: false, RX list fan-out |
| Multi-talkgroup expansion (shared lib) | Shipped | #36 — channelExpansion/; OpenGD77 export unchanged |
| CHIRP CSV (analogue FM/AM) | Shipped | #103 — chirp/ |
| Channel wire name split + export composition | Shipped | #54 — channel-name-parsing.md |
| Export name shortening | Shipped | #130, #150 — name-shortening.md |
| Zone-derived scan lists + scratch (DM32) | Shipped | #164, #163 — zone-derived-scan-lists-progress.md |
| Doc | Contents |
|---|---|
| import-export-fidelity-contract.md | Authoritative — what fidelity adapters and tests guarantee, and what we deliberately let slip |
| channel-name-parsing.md | CPS wire name → callsign + name split (#54) |
| name-shortening.md | Export-time channel/zone name shortening (#130, #150) |
| name-shortening-progress.md | Execution log for #130 and #150 |
| name-shortening-outstanding.md | Debt discovered during name-shortening work |
| data-model/README.md | Entity definitions (canonical, vendor-neutral) |
| opengd77/README.md | OpenGD77 adapter behaviour; columns in reference/opengd77/ |
| adding-a-new-vendor.md | Contributor checklist for new formats |
| format-taxonomy.md | Formats vs variants mental model + data-model findings (planning input) |
| outstanding.md | Collated open debt |
| opengd77/progress.md | OpenGD77 execution log |
| dm32/README.md | DM32 adapter behaviour (#67) |
| zone-derived-scan-lists-progress.md | Zone-derived scan + scratch execution log (#164, #163) |
| zone-derived-scan-lists-outstanding.md | Debt from zone-derived scan work |
| ../../reference/zone-derived-scan-lists.md | Tier-2 policy — gating, format matrix, scratch/carrier semantics |
| chirp/README.md | CHIRP adapter behaviour (#103) |
| operator-lifecycle.md | Multi-format operator workflow |
| Testing strategy | Format fidelity, layers, CI |
| persistence/README.md | LocalStorage envelope |
| codeplug-project/README.md | Project wrapper + CRUD |
flowchart TD
HomeUI["ImportDropzone (home)"] --> importFiles
ExportUI["ImportIntoActivePanel (import & export page)"] --> importFiles
importFiles --> detect["detectImportAdapter / getImportAdapter"]
detect --> Adapter["format adapter (OpenGD77, CHIRP, …)"]
Adapter --> Raw["ImportResult"]
Raw --> Merge["importMerge — merge or overwrite"]
Merge --> Store["codeplugStore — active project"]
Store --> Resolve["resolveZoneMembers"]
Resolve --> Codeplug["Codeplug"]
Codeplug --> Serialise["getExportAdapter — multi-file or single-file"]
Serialise --> Download["per-file CSV, ZIP, or single CHIRP CSV"]
All vendor formats convert through the radio-agnostic internal model. Import adapters parse vendor files into entities; export adapters serialise entities back to vendor columns. Feature code (map, CRUD, store) works on the internal model only.
Shared logic in src/lib/channelExpansion/ — adapters call expandAllChannelsForExport before serialising wire rows:
| Axis | When to enable | OpenGD77 |
|---|---|---|
| Multi-mode | Format has no native dual-mode row | Always (separate Analogue/Digital rows) |
| Multi-talkgroup | Format has no native RX group lists | Never — lean export with TG List |
Pass ExportOptions.expandRxGroupLists and expandRxGroupListMembers through expandOptionsFromExport() (exportOptions.ts). Zone export uses expandZoneMemberWireNames with the same flags.
Domain rules: multi-talkgroup-expansion.md. DM32 (#67) will enable TG expansion on export.
Import modes (#58)
Only entity types present in the import batch are touched.
| Mode | Behaviour |
|---|---|
| Merge (default) | Match by vendor name (case-sensitive). Update rows only when imported fields differ; append new names; preserve internal ids and app-only fields (hideFromMap). Re-importing an unchanged file is a no-op. |
| Overwrite | Replace the entire array for each imported file type (e.g. all channels when Channels.csv is included). |
| Entity | Match key |
|---|---|
| Channel | Channel Name |
| Zone | Zone Name |
| Contact / talk group | Contact Name |
| RX group list | TG List Name |
After apply, all zones' memberChannelIds are re-resolved from meta.imported.memberWireNames. Unresolved member names appear in the confirm modal and import report.
- Per-file download:
Channels.csv,Zones.csv,Contacts.csv,TG_Lists.csv - ZIP: all six CPS files;
DTMF.csvandAPRS.csvare header-only (not modelled) - Route:
/#/exportwhen a codeplug project is active
OpenGD77 CPS CSV is one interchange format shared by many radios. Import/export adapters are format-level; radio-specific limits are profile-level at export time.
Code: src/lib/export/opengd77/, UI src/routes/ImportExport.tsx
| CPS file | Reference |
|---|---|
| All files — cross-cutting rules | file-format.md |
Channels.csv |
channels.md |
Zones.csv |
zones.md |
Contacts.csv |
contacts.md |
TG_Lists.csv |
tg-lists.md |
DTMF.csv / APRS.csv |
dtmf-aprs.md |
Authoritative column and conversion reference: reference/opengd77/. Per-radio limits: radio profiles.
| Symbol | File | Role |
|---|---|---|
importFiles |
src/lib/import/index.ts |
Read files, route by adapter, classify, parse |
getImportAdapter / getExportAdapter |
src/lib/import-export/registry.ts |
Format registry |
previewImportMerge / applyImportToCodeplug |
src/lib/importMerge.ts |
Merge/overwrite + stats |
channelsImportEqual |
src/lib/importEntityCompare.ts |
Idempotent field compare |
opengd77Adapter |
src/lib/import/opengd77/adapter.ts |
detectKind, delegates to parse |
parseChannels / parseZones |
src/lib/import/opengd77/parse.ts |
CSV → models / raw zones |
exportCodeplug |
src/lib/export/index.ts |
Serialise to vendor format |
CodeplugProvider |
src/state/codeplugStore.tsx |
Central state + applyImportToActive |
runActiveImportWorkflow |
src/test/system/importWorkflow.ts |
System test harness |
- Home:
ImportDropzonecreates a new codeplug project (importNewProject). - Import & export (
/export):ImportIntoActivePanelmerges into the active project with confirm modal (applyImportToActive). - Drop target: multiple
.csvfiles or a whole folder. - New project naming (Home import only): folder selection → leaf directory name; loose files (one or many) →
{adapter projectNameLabel} YYYY-MM-DD(ISO date). Each import adapter setsprojectNameLabel— see per-format docs (e.g. OpenGD77). See also codeplug-project. - Recognised (OpenGD77):
Channels.csv,Zones.csv,Contacts.csv,TG_Lists.csv - Skipped (OpenGD77):
DTMF.csv,APRS.csv, other unknown CSVs when OpenGD77 files are present - CHIRP: single memory CSV with standard 21-column header fingerprint
npm run test # unit tests including importMerge, round-trip
npm run test:system # workflow harness + ImportIntoActivePanel UI flowSynthetic CSV bundles: src/test/opengd77/bundles.ts.
Format fidelity strategy: format-fidelity.md. Authoritative tier promises: import-export-fidelity-contract.md.
npm run dev→ Home → import a supported CPS export (folder or loose files) → Summary opens with new project named from the folder leaf or{adapter projectNameLabel} YYYY-MM-DD.- Import & export → Merge → import
Zones.csv→ confirm shows zones added → zones resolve on/zones. - Re-import identical
Channels.csv→ confirm shows all unchanged. - Re-import modified
Channels.csv→ only changed rows updated; zone links intact. - Import
Contacts.csv/TG_Lists.csvalone → other entities unchanged.
- With a populated codeplug, Import & export → Overwrite → import smaller
Channels.csv→ confirm warns removed count. - Overwrite
Zones.csvonly → channels/contacts unchanged.
- With a populated codeplug,
/#/export→ download per-file CSVs or ZIP. - Re-import exported files → merge shows unchanged (same-format round-trip).
- Home → import second codeplug → still creates new project.
- Hard refresh → data persists from LocalStorage.
- #7 — Genericise import: internal models, format registry, central store, home import UI.
- #38 — OpenGD77 full import/export: extended channel model, Contacts/TG lists, export serialisers, round-trip test.
- #58 — Active project import: merge/overwrite modes,
ImportIntoActivePanel, system test harness.