Skip to content

feat: CHIRP CSV import and export (analogue FM/AM) #103

Description

@pskillen

Problem

OpenGD77 is the only supported CPS interchange format today (src/lib/import/opengd77/, src/lib/export/opengd77/). Operators using analogue-only radios (Baofeng UV-5R, Retevis RT95, etc.) typically manage channel lists in CHIRP — a single CSV per radio with FM/AM channels, CTCSS/DCS tones, duplex/offset, and no DMR concepts.

There is no path to import a CHIRP export into our internal models, edit/visualise it in the app (especially the channel map), or export back to CHIRP for flashing via CHIRP.

CHIRP is deliberately not DMR — no zones file, no contacts, no talk groups, no colour code or timeslot. It is the cleanest litmus test of whether the internal model is truly mode-neutral: vendor specifics must live at the import/export boundary, and analogue channels must round-trip without leaking OpenGD77/DMR assumptions into CRUD, validation, or the store. See format-taxonomy — CHIRP as a litmus test.

Intended outcome

Add CHIRP CSV as an import/export adapter pair, mirroring the OpenGD77/DM32 pattern: wire specifics at the boundary; feature code sees only internal Codeplug models.

Import

  • Register chirp adapter in src/lib/import/registry.ts.
  • Classify files by CHIRP header row (parse by header name, not column index):
    • Required columns: Location, Name, Frequency, Duplex, Offset, Tone, rToneFreq, cToneFreq, DtcsCode, DtcsPolarity, RxDtcsCode, CrossMode, Mode, TStep, Skip, Power, Comment (plus optional DMR-ish columns URCALL, RPT1CALL, RPT2CALL, DVCODE — present in wire format but empty on analogue exports).
  • Single-file import: one .csvChannel[] only (no zones, contacts, talk groups, or RX group lists).
  • Map CHIRP wire values → internal model:
CHIRP column Internal target
Name Channel.name (+ preserve as import provenance in meta if needed)
Frequency rxFrequencyHz / txFrequencyHz (derive TX from duplex + offset)
Duplex / Offset split/offset semantics → internal frequency fields
Mode mode (NFMfm, AMam, …)
Tone, rToneFreq, cToneFreq, DtcsCode, DtcsPolarity, RxDtcsCode, CrossMode RX/TX tone fields (typed per #52)
TStep channel step / bandwidth (kHz)
Skip scanSkip (and/or CHIRP-specific skip semantics — document)
Power power (radio-specific wire strings like 5.0W / 10W / 25W → percent or null = high)
Comment description or note
Location not stored — assign at export (per #53 pattern; CHIRP memory slot)
Empty DMR columns drop on import; do not create contacts/talk groups
  • Unmapped CHIRP-only columns → Channel.vendorExtras (or chirpExtras if we split vendor escape hatches per format).
  • Document skipped/lossy fields and classification rules in docs/features/import-export/chirp/README.md + docs/reference/chirp/.

Export

  • Register chirp adapter in src/lib/export/registry.ts.
  • Serialise internal analogue (and optionally mixed) channels → single CHIRP CSV download.
  • Map internal values → CHIRP wire format (fmNFM, tone enum → Tone/rToneFreq/…, MHz with six decimal places, etc.).
  • Assign Location memory indices at export time (stable ordering policy — document; may follow import order or sort by name).
  • Target radio profile at export — CHIRP column availability, power ladder, and memory size vary by radio (filename in sample exports encodes model). Likely needs a radio picker similar in spirit to OpenGD77 #72 but CHIRP-specific (UV-5R vs RT95 vs UV-21ProV2). v1: export using a chosen profile with warnings when channels exceed radio memory or use unsupported power levels.
  • Omit or blank DMR columns for analogue-only export; do not synthesise contacts/talk groups.
  • Surface warnings when exporting DMR channels to CHIRP (skip, strip digital fields, or block — decide during design).

UI

  • Import: extend ImportDropzone / importFiles to recognise CHIRP CSV alongside OpenGD77 (adapter selection by detected format; error if mixed vendors in one drop).
  • Export: add CHIRP section on /export with CSV download (+ optional target-radio selector).
  • Channel map should render imported analogue channels (frequencies, names, tones) without requiring zones.

CHIRP ↔ internal model mapping notes

Key differences from OpenGD77/DM32 (design during implementation):

Concept CHIRP Internal model today
File shape Single CSV per radio Multi-entity codeplug (channels + zones + …)
Mode NFM, AM (analogue) fm, am — mode enum shipped (#45)
Zones / scan No zone file; Skip column only Zone[] optional — CHIRP import leaves zones empty
Contacts / TG / RX lists Wire columns exist but unused on analogue DMR entities — leave empty on import; must not be required for analogue CRUD
Tones Primary analogue feature (CTCSS/DCS/Tone/TSQL/CrossMode) Typed tone fields (#52)
Power 5.0W, 10W, 25W, 1.0W strings Percent 0–100 or null
Memory slot Location column Not stored — assign at export (#53)
Duplex ``, +, `-`, `off` + `Offset` MHz Derive TX frequency from RX + offset rules

Model / UX gaps (likely sub-tasks)

  • Channels-only projects: import creates a valid project with channels but no zones — ensure home/map/CRUD do not assume DMR entities exist.
  • Mixed-mode export: if the active project contains DMR channels (from OpenGD77) and the operator exports CHIRP, define behaviour (filter analogue only vs error).
  • Target radio profile: CHIRP export is radio-specific — design profile registry under docs/reference/chirp/radios/ (start with radios represented in sample fixtures).

Reference material

  • Sample fixtures (in repo): sample-exports/Chirp 2026-06-29/ — added in #101:
    • Baofeng_UV-21ProV2_20251129.csv
    • Baofeng_UV-5R Mini_20251129.csv
    • Retevis_RT95 VOX_20251106.csv
  • Analogue FM/AM channels: repeaters, PMR446, SAR, FRS, calling channels, airband (AM row in UV-21ProV2 sample).
  • CHIRP project docs: https://chirp.danplanet.com/ (column semantics vary slightly by radio driver — document per profile).
  • Mental model: format-taxonomy.md — CHIRP is a sibling format, unrelated to OpenGD77.

Affected

  • src/lib/import/chirp/ — adapter, parse, columns, tests
  • src/lib/export/chirp/ — serialise, download, round-trip tests
  • src/lib/import/registry.ts, src/lib/export/registry.ts
  • src/lib/import/index.ts — format detection / adapter routing
  • src/routes/Export.tsx — CHIRP export UI
  • ImportDropzone hint text + classification
  • docs/features/import-export/chirp/, docs/reference/chirp/ — mapping tables
  • Possibly map/CRUD empty-state copy when project has channels but no zones

Notes / dependencies

  • Mirrors #67 (DM32) and #38 (OpenGD77) architecture — read existing adapters as template.
  • Distinct from #67 — DM32 is DMR dual-mode stock CPS; CHIRP is analogue-only, single CSV, no zones.
  • Builds on #93 pristine model epic — especially typed tones/power (#52), mode enum (#45), and export-time slot assignment (#53).
  • Related: #58 (import into active project) — CHIRP partial imports (channels only) should merge cleanly.
  • Vendor boundaries: no CHIRP wire strings or radio memory caps in src/models/, mutations, validation, or CRUD UI.
  • All processing client-side; privacy unchanged.

Suggested phasing

  1. Reference + detect: docs/reference/chirp/ column tables; header-based classification; parse unit tests against sample fixtures.
  2. Import: channels-only → internal model; map modes/tones/duplex/power; verify channel map renders.
  3. Export: serialise back to CHIRP CSV; round-trip tests against committed samples; assign Location at export.
  4. Radio profiles: target-radio picker + per-radio limits/warnings; document lossy fields.
  5. Polish: export page UI, mixed-format drop guards, progress/outstanding docs.

Out of scope

  • CHIRP live radio programming (USB) or .img binary images.
  • Modelling CHIRP bank/group abstractions beyond Location slot assignment.
  • DMR digital modes via CHIRP (some radios support DMR in CHIRP — defer until analogue path is solid).
  • Guaranteed loss-free round-trip for every CHIRP column on every radio driver — document lossy fields; vendorExtras for the rest.

Manual verify

  1. Import sample-exports/Chirp 2026-06-29/Baofeng_UV-5R Mini_20251129.csv → channels visible on map and in CRUD; no phantom zones/contacts.
  2. Edit a channel (frequency, tone, name) → export CHIRP CSV → re-import in CHIRP app (or diff against source).
  3. Mixed OpenGD77 + CHIRP drop → clear error, no partial corrupt state.
  4. Export project with DMR channels to CHIRP → documented behaviour (filter/warn/error).
  5. Round-trip all three committed sample files through import → export → byte-compare or field-compare (allowing Location reorder if policy differs).

Workflow note

Large multi-commit feature: branch from origin/main, atomic conventional commits per layer (detect → parse → import tests → export → UI → docs). Use docs/features/import-export/chirp/chirp-progress.md + outstanding log. PR linking Closes #.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestradio-format-supportsupport for 3rd party radio formats - import/export, etc

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions