Skip to content

feat: talk group timeslot expansion via RX Group List membership #142

Description

@pskillen

Problem

Per #36 and #46, at export time we may expand a single logical channel into multiple single-mode channels, or into multiple channel×talkgroup pair channels.

The same denormalise-at-export pattern should extend to talk groups and RX group lists.

On OpenGD77 (and similar CPS layouts), repeaters carry different talk groups on different time slots (TS1 vs TS2). To support promiscuous RX correctly, operators today often:

  1. Create two copies of each talk group — one per time slot (e.g. Scotland TS1, Scotland TS2).
  2. Build a per-repeater RX Group List and assign the appropriate TG×TS variant to that list (which TGs appear varies by repeater; which slot a given TG uses also varies by repeater).

This is tedious, error-prone, and duplicates DMR IDs across wire names that differ only by slot.

Intended outcome

Model one logical talk group internally. Carry per-member time slot on RxGroupList membership. Expand into the required TS1/TS2 wire contacts at the export boundary, and wire each RGL member to the correct expanded copy.

Model changes

  • Remove TalkGroup.timeslotOverride from the internal model.
  • Add an optional time slot on each RxGroupList membership entry (alongside the existing EntityRef to a talk group or private contact). Semantics: which TS variant this member should resolve to on export (1, 2, or unset → export decides / both variants emitted only when needed).
  • Logical talk groups keep a single name, number (DMR ID), and optional abbreviation — no slot baked into the entity.

Contact.timeslotOverride is unchanged in this issue — private contacts in RGLs are a separate concern.

Export (required)

When the target format requires separate wire contacts per time slot (OpenGD77 Contacts.csv + TG_Lists.csv):

  1. For each logical talk group referenced by any RGL member (with a time slot set), emit the required expanded wire copies — typically one per distinct slot in use, with T1 or T2 appended to the contact name (e.g. ScotlandScotland T1, Scotland T2).
  2. Set the vendor TS Override column on each expanded copy from the membership slot (not from a field on the logical TG).
  3. Resolve each RxGroupList.memberRefs entry to the matching expanded wire name for that member's time slot when serialising TG_Lists.csv.
  4. Naming must be deterministic, collision-resistant, and respect profile display limits (same design constraints as #36 / #46).

Expansion is an export-boundary concern — internal CRUD, validation, and the vendor-neutral model must not assume OpenGD77 slot suffixes or TS Override wire values.

Import merge / de-dupe (best-effort, NOT a blocker for export)

Flat CPS exports arrive with separate wire contacts per slot. Import must include an automatic collapse path at the format boundary (alongside existing importMerge.ts talk-group merge):

  • Detect wire contact pairs (or larger families) that share the same DMR ID (number) and a compatible name stem after stripping T1/T2, TS1/TS2, or timeslotOverride-derived suffixes.
  • Collapse to one logical talk group (survivor name = stem; preserve abbreviation when unambiguous).
  • Rewire RGL membership: each ContactN cell that referenced an expanded wire name becomes one memberRef with the inferred per-member time slot.
  • Rewire channel contactRef and any other TG references from absorbed ids → survivor id.
  • Ambiguous groups (same ID, incompatible stems, or slot conflicts) stay as separate talk groups — no regression.

Explicitly not required to ship export-side support. Mis-grouping is acceptable.

Manual merge (post-hoc repair)

Import heuristics will miss cases. Operators need a Find merge candidates flow on the Talk Groups list — same pattern as channel merge candidates (#116):

  • Scan talk groups for groups that are TS-slot duplicates of the same logical TG (same number, compatible name stem, differing only by T1/T2/TS1/TS2 suffix or timeslotOverride).
  • Preview merge result: survivor name, absorbed members, RGL membership rewiring (member refs + inferred slots), channel contactRef impact, validation warnings.
  • Operator confirms/applies selected merges (batch or one-by-one).
  • Entry point: Talk Groups list section nav — Find merge candidates (mirror ChannelsListSectionNav + ChannelMergeCandidatesModal).

Implementation likely splits into lib (talkGroupMergeCandidates.ts or extend channelExpansion/), mutations (mergeTalkGroupsIntoOne), store wiring, and modal UI — follow #116 slice structure and progress docs.

Pattern

Mirrors #36 (channel×TG expansion) and #46 (channel×mode expansion):

Concept #36 #46 This issue
Logical entity One channel, many TGs (via RGL) One channel, many modes One talk group, many time slots (via RGL membership)
Denormalise on export channel × TG rows channel × mode rows talk group × TS wire contacts
Slot/context lives on rxGroupListId + RGL members modeProfiles RGL member time slot
Import collapse Best-effort at boundary Best-effort at boundary Best-effort at boundary + manual repair
Manual repair UI #116 (channels) #116 (channels) Talk Groups list merge candidates (this issue)

Orthogonal to multi-talkgroup expansion (DM32-style channel×TG row fan-out). This issue is primarily for formats with native RGL support (OpenGD77) where the wire still requires separate contact names per slot.

Affected

  • Internal data model (src/models/codeplug.ts) — RGL membership shape; remove TalkGroup.timeslotOverride; schema version bump + migration.
  • Export layer — OpenGD77 serialise.ts (Contacts + TG_Lists); expansion pass alongside existing channel expansion in src/lib/channelExpansion/.
  • Import layer — OpenGD77 parse.ts + importMerge.ts TS-suffix collapse and RGL member slot reconstruction.
  • Manual merge — detection/preview/apply lib; mutations; TalkGroupsList section nav + merge modal (pattern from #116).
  • CRUD — Talk group edit (remove timeslot override field); RGL member picker (per-member time slot).
  • Docs — docs/features/data-model/, docs/reference/opengd77/contacts.md, docs/features/crud/ progress logs for merge candidates.

Notes / dependencies

  • Builds on #36 and #46 (export expansion infrastructure).
  • UI pattern from #116 (channel merge candidates — detection, preview, apply, zone/RGL rewiring).
  • OpenGD77-first motivating case; other formats with TS Override or per-slot contact names may share the same expansion hook.
  • Existing codeplugs with timeslotOverride on talk groups need a one-time migration (slot → nearest RGL membership, or warn and drop).

Out of scope

  • Changing radio firmware behaviour.
  • Guaranteed loss-free import re-normalisation.
  • Contact.timeslotOverride redesign (private contacts).

Workflow note

Likely multi-commit work: branch from origin/main, atomic conventional commits per logical change, PR linking Closes #. Pair with docs/features / progress-tracking skills.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquality-of-lifefeature to improve the QOL of the person making a codeplug

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions