Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
f22c48f
fix(tc): prevent runaway CPU and blocking in transcoder handling
dbehnke Dec 24, 2025
2a8b30a
Implement IMRS Protocol Support
dbehnke Dec 24, 2025
3a3c8fa
Implement M17 Parrot, LSTN support, and M17P packet mode (#12)
dbehnke Dec 24, 2025
1120e64
Implement non-blocking NNG Publisher for real-time dashboard events
dbehnke Dec 24, 2025
eeae0c7
Merge remote-tracking branch 'origin/feature/issue-15-investigation' …
dbehnke Dec 24, 2025
68536f3
Merge remote-tracking branch 'origin/feature/issue-13-investigation' …
dbehnke Dec 24, 2025
783c9e0
Merge remote-tracking branch 'origin/feature/issue-12-m17-investigati…
dbehnke Dec 24, 2025
986f7c9
Merge remote-tracking branch 'origin/feature/nng' into dev
dbehnke Dec 24, 2025
6781b69
Fix: make CConfigure::GetBoolean safe against missing keys
dbehnke Dec 24, 2025
3907913
refactor: implement active talker state and always-on periodic NNG br…
dbehnke Dec 25, 2025
db01585
feat(nng): add protocol field to hearing message
dbehnke Dec 26, 2025
ba6a5df
refactor: implement active talker state and always-on periodic NNG br…
dbehnke Dec 25, 2025
d02ebe3
feat(nng): add protocol field to hearing message
dbehnke Dec 26, 2025
0f96f32
Implement closing event and fix hearing module in all protocols
dbehnke Dec 26, 2025
d33edae
Merge feature/nng into dev (resolving conflicts by choosing feature/n…
dbehnke Dec 26, 2025
465525a
Remove accidentally committed mrefd-temp embedded repository
dbehnke Dec 26, 2025
20e3c8a
Remove accidentally committed mrefd-temp embedded repository
dbehnke Dec 26, 2025
9cc9d2d
Implement NNG event system, fix deadlock, and add NNGDebug config
dbehnke Dec 26, 2025
5e93e49
Merge branch 'feature/nng' into dev
dbehnke Dec 26, 2025
3c319ec
Implement NNG support for transcoder link
dbehnke Dec 27, 2025
2f7008a
Config: add Dashboard, IMRS, and EnableDGID fields
dbehnke Dec 27, 2025
e5095eb
feat(nng): implement one-time first packet logging and periodic stati…
dbehnke Dec 27, 2025
3b3f65b
fix(m17): allow numeric callsigns/IDs (fixes Unknown M17 packet for D…
dbehnke Dec 27, 2025
18d238d
fix(nng): increase send/recv buffers to 4096 to prevent blocking/drops
dbehnke Dec 27, 2025
c71dfc9
fix(m17): allow special chars (-./) in callsign for M17 protocol comp…
dbehnke Dec 28, 2025
1230625
Impl: M17 Dual-Mode Support & Legacy Compat Flag
dbehnke Dec 28, 2025
1af4c65
Merge feature/issue-12-m17-investigation into dev
dbehnke Dec 28, 2025
564073d
Add M17 to GateKeeper ProtocolName to prevent NONE in dashboard
dbehnke Dec 28, 2025
ac77994
Merge tcd-nng into dev (resolved Callsign.cpp conflict)
dbehnke Dec 28, 2025
c4248d8
Relax M17 Callsign validation to match mvoice spec
dbehnke Dec 28, 2025
883b9a6
Implement mrefd parity: #/@ALL support and DMR ID lookup
dbehnke Dec 28, 2025
34dda43
Fix syntax error in CSIn: restore missing loop
dbehnke Dec 28, 2025
450e1d9
Add debug prints to trace M17 crash
dbehnke Dec 28, 2025
46166f7
Fix M17 OpenStream crash: cache module char before Header invalidation
dbehnke Dec 28, 2025
ada180d
Fix P25 StreamID mismatch: clear stale packets in CodecStream::ResetS…
dbehnke Dec 28, 2025
8a2a77d
Fix M17 audio drops: remove seq%2 decimation logic
dbehnke Dec 28, 2025
d8ee108
Fix M17 audio drop: uncomment header cache update
dbehnke Dec 28, 2025
7417a08
Fix M17 slow motion: implement 2:1 frame aggregation for 20ms input
dbehnke Dec 28, 2025
828dbee
Fix syntax and comparison errors in M17Protocol.cpp
dbehnke Dec 28, 2025
f1cc20f
Fix M17->P25 drop: split incoming 40ms frames into two 20ms frames
dbehnke Dec 28, 2025
8e8d85e
Fix M17 crash: capture CodecType before Header invalidation in Input
dbehnke Dec 28, 2025
462d7a2
Add debug logs for M17 frame splitting and stream closure
dbehnke Dec 28, 2025
02fd824
Enable debug logs for CodecStream packet tracking
dbehnke Dec 28, 2025
9f88014
Fix M17 payload offset for tcd and add stream keepalive
dbehnke Dec 28, 2025
8e96575
Fix M17 sped up audio: Read payload from correct offset based on sequ…
dbehnke Dec 28, 2025
4aedb18
Add debug logs to M17 HandleQueue
dbehnke Dec 28, 2025
6f2b7d6
Fix M17 garbage audio: Force usage of C2_3200 for TCD output
dbehnke Dec 28, 2025
5a146e3
Fix M17 audio speed: Assign Even/Odd sequence numbers to split packets
dbehnke Dec 28, 2025
a0dfdaf
Fix M17 audio speed: Correct Frame Number calculation to prevent dupl…
dbehnke Dec 28, 2025
034c453
Fix M17 sped-up audio: Pace input packets by 20ms
dbehnke Dec 28, 2025
03f613e
Add M17_DEBUG prints for M17 Frame Type Validation
dbehnke Dec 28, 2025
eed1881
Fix typo in M17_DEBUG print
dbehnke Dec 28, 2025
2accf49
Cleanup: Remove M17 debug logging
dbehnke Dec 28, 2025
3434c5d
Add P25_DEBUG prints for Orphaned Frame diagnosis
dbehnke Dec 28, 2025
41e58d8
Fix P25 Orphaned Frames by buffering pre-header packets
dbehnke Dec 28, 2025
f7aa467
Fix syntax error: Remove duplicate offset declaration
dbehnke Dec 28, 2025
7eaa1ac
Fix P25: Add CIp operator<, fix terminator ID, and update header fram…
dbehnke Dec 28, 2025
8bcf7d7
Fix compile error: Scope case block for local variable
dbehnke Dec 28, 2025
c500f7d
Cleanup P25 debug logs and buffering, keep Header/Terminator fixes
dbehnke Dec 28, 2025
e6af62f
Remove last leftover P25_DEBUG log
dbehnke Dec 28, 2025
ce2e902
Fix P25 and NXDN route names to use URF prefix
dbehnke Dec 28, 2025
cbfaf8c
feat: Implement audio recording with Opus/UUIDv7
dbehnke Dec 29, 2025
01da0f9
fix: Add StopRecording to CPacketStream
dbehnke Dec 29, 2025
f40c42f
fix: Exclude test_audio.cpp from build to prevent multiple main defin…
dbehnke Dec 29, 2025
5f488f3
fix: Add configuration parsing for Audio section
dbehnke Dec 29, 2025
d300a9a
fix: Allow lowercase 'path' in audio config and standardize ini
dbehnke Dec 29, 2025
28780cc
feat: Add audio recording data path and logging
dbehnke Dec 29, 2025
d8cb0f7
fix: Cache filename before stopping recorder to return correct path
dbehnke Dec 29, 2025
c826a87
feat: Add debug amplitude logging
dbehnke Dec 29, 2025
d72dc31
fix: Correct Opus granulepos calculation for 48kHz
dbehnke Dec 29, 2025
c3d78ed
chore: Remove debug audio logging
dbehnke Dec 29, 2025
94e57a7
feat: Implement Event-Driven Architecture in CodecStream
dbehnke Dec 30, 2025
26ff637
fix: Add sleep to RxThread to prevent spin loop on transcoder failure
dbehnke Dec 30, 2025
f5037d3
chore: Add debug logging to CodecStream
dbehnke Dec 30, 2025
7dd92e0
chore: Remove debug logging
dbehnke Dec 30, 2025
10991a9
fix(audio): replace rand() with mt19937 for thread-safe UUIDs to prev…
dbehnke Jan 1, 2026
a02a720
fix(audio): restore missing AudioRecorder.h include
dbehnke Jan 1, 2026
aad381c
feat(reflector): rate limit orphaned frame warnings to once per minute
dbehnke Jan 1, 2026
878ab68
feat(reflector): rate limit late entry warnings to once per minute
dbehnke Jan 1, 2026
a74e777
Merge pull request #2 from dbehnke/fix/audio-recorder-race-condition
dbehnke Jan 1, 2026
9d47a44
Feat: Flexible DMR Mode (Mini DMR) (#3)
dbehnke Jan 4, 2026
c1d6d4a
fix(dmr): generic gateway forwarding and custom TG maps
dbehnke Jan 4, 2026
6df692e
fix: compilation error using GetCallsign() instead of GetCS()
dbehnke Jan 4, 2026
22e5e49
feat(debug): add DMR burst logging (#5)
dbehnke Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ reflector/urfd.*
urfd
inicheck
dbutil
.devcontainer/
/test_urfd.ini
/staging_urfd.ini
/pr_comment_nng.md
/pr_body_fix.md
/staging/
16 changes: 16 additions & 0 deletions config/urfd.ini
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ DescriptionM = M17 Chat
DescriptionS = DStar Chat
DescriptionZ = Temp Meeting

[Dashboard]
Enable = true
NNGAddr = tcp://127.0.0.1:5555
Interval = 10
NNGDebug = false


[Audio]
Enable = false
Path = ./audio/

[Transcoder]
Port = 10100 # TCP listening port for connection(s), set to 0 if there is no transcoder, then other two values will be ignored
BindingAddress = 127.0.0.1 # or ::1, the IPv4 or IPv6 "loop-back" address for a local transcoder
Expand All @@ -66,6 +77,10 @@ Port = 20001
[G3]
Enable = true

[IMRS]
Enable = false
Port = 21110

[DMRPlus]
Port = 8880

Expand Down Expand Up @@ -101,6 +116,7 @@ Module = A # this has to be a transcoded module!
[YSF]
Port = 42000
AutoLinkModule = A # comment out if you want to disable AL
EnableDGID = false
DefaultTxFreq = 446500000
DefaultRxFreq = 446500000
# if you've registered your reflector at register.ysfreflector.de:
Expand Down
113 changes: 113 additions & 0 deletions docs/DMR_Mini_Mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Flexible DMR Mode (Mini DMR) User Guide

URFD now supports a "Flexible DMR" mode (often called "Mini DMR"), which changes how DMR clients interact with the reflector. Unlike the legacy "XLX" mode where clients link to a specific module (A-Z) and traffic is bridged, Mini DMR mode allows clients to directly subscribe to Talkgroups (TG).

## How it Works

In Mini DMR mode, the reflector acts like a **Scanner**.

1. **Subscriptions**: You "subscribe" to one or more Talkgroups on a Timeslot (TS1 or TS2).
2. **Scanning**: The reflector monitors all your subscribed Talkgroups.
3. **Hold Time**: When a Talkgroup becomes active (someone speaks), the scanner "locks" onto that Talkgroup for the duration of the transmission plus a **Hold Time** (default 5 seconds). During this hold, traffic from other Talkgroups is blocked to prevent interruption.

```mermaid
graph TD
Client[MMDVM Client] -->|Subscribe TG 3100 TS1| Reflector
Client -->|Subscribe TG 4001 TS2| Reflector

subgraph Reflector Logic
TrafficA[Traffic on TG 3100] --> Scanner{Scanner Free?}
TrafficB[Traffic on TG 4001] --> Scanner

Scanner -->|Yes| Lock[Lock onto TG 3100]
Lock --> Map["Route to Client (TS1)"]

Scanner -->|"No (Held by 3100)"| Block[Block TG 4001]
end

Map --> Client
```

### Strict Timeslot Routing

The reflector enforces strict routing based on your subscription:

* If you subscribe to **TG 3100 on TS1**, traffic for TG 3100 will **only** be sent to your radio on **Timeslot 1**.
* If you subscribe to **TG 4001 on TS2**, traffic for TG 4001 will **only** be sent to your radio on **Timeslot 2**.
* This allows a single client to monitor different Talkgroups on different Timeslots simultaneously (if the Scanner is not held by one).

## Configuration

To enable Mini DMR mode, update your `urfd.ini` (or configuration file) in the `[DMR]` section:

```ini
[DMR]
; Disable legacy XLX behavior (REQUIRED for Dashboard Subscription View)
XlxCompatibility=false

; Optional: enforce single subscription per timeslot (default false)
SingleMode=false

; Scanner Hold Time in seconds (default 5)
HoldTime=5

; Dynamic Subscription Timeout in seconds (default 600 / 10 mins)
; 0 = Infinite
DefaultTimeout=600

; Module to Talkgroup Mapping (Optional)
; Maps Module A to TG 4001, B to 4002, etc. automatically.
; You can override specific maps:
MapA=4001
MapB=4002

; IMPORTANT: Any module you map (e.g. A, B) MUST be enabled in the [Modules] section!
; If Module A is not enabled, traffic for TG 4001 will be dropped.
```

## Usage

### 1. Subscribing via PTT (Push-To-Talk)

The easiest way to subscribe to a Talkgroup is to simply **transmit** on it from your radio.

* **Action**: Key up (PTT) on `TG 1234`.
* **Result**: The reflector detects your transmission and automatically subscribes you to `TG 1234` for the configured timeout duration (e.g., 10 minutes).
* **Renewal**: If you are already subscribed, keying up again will **reset the timeout timer** back to the full duration.
* **Note**: The first transmission might be muted (Anti-Kerchunk) to prevent noise, but you will immediately be subscribed.

### 2. Subscribing via Options String

You can manage subscriptions sent from your MMDVM hotspot/repeater configuration (or Pi-Star Options field).

* **Format**: `TS1=TG_ID;TS2=TG_ID;AUTO=TIMEOUT`
* **Example**: `TS2=3100,4001;AUTO=600`
* Subscribes Timeslot 2 to TG 3100 and TG 4001.
* Sets timeout to 600 seconds.

### 3. Disconnecting / Unsubscribing

* **Disconnect All**: Transmit a Group Call to **TG 4000**. This clears all dynamic subscriptions on that timeslot.
* **Single Mode**: If `SingleMode=true` is set in config, transmitting on a *new* Talkgroup automatically unsubscribes you from the previous one.

### 4. Talkgroup 9 (Reflector)

* Traffic on **TG 9** is treated as local reflector traffic (linked functionality) if the client is essentially "linked" to a module, but in Mini DMR mode, TG 9 behavior depends on the specific map configuration or defaults. Typically, use specific Talkgroups for wide-area routing.

## Dashboard

The URFD Dashboard includes a dedicated **DMR** page (`/dmr`) to monitor Flexible DMR Mode activity.

* **Active Subscriptions**: Shows all Talkgroups a client is monitoring, along with the specific Timeslot.
* **Timers**: Displays a real-time countdown for Dynamic Subscriptions. Static subscriptions are marked as `Static`.
* **DMR ID**: Displays the client's DMR ID alongside their callsign (e.g., `CALLSIGN (3100123)`).
* **Requirements**: The dashboard requires NO additional configuration. It automatically displays data once `XlxCompatibility=false` is set in the backend config.

## Troubleshooting

### "Recordings are blank" or "No Traffic on other modes"

If clients can connect and transmit but you see no traffic on other protocols (M17, YSF) or blank recordings:

* **Check Modules**: Ensure the mapped Module (e.g. A for TG 4001) is defined and **enabled** in your `[Modules]` configuration.
* **Log Check**: Look for `Can't find module 'X' for Client ...` errors in the reflector log.
184 changes: 184 additions & 0 deletions docs/MINIDMR_Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Investigation and Fix Plan: Flexible DMR Mode

## Problem Description

User wants to support two modes of operation for DMR:

1. **XLX Mode** (Default): Legacy behaviors. MMDVM clients "link" to a module.
2. **Mini DMR Mode** (New): MMDVM clients do not "link". Modules are mapped to Talkgroups. Clients "subscribe" to TGs.

## Analysis

- **Modes**:
- `XLXCompatibility`: Legacy mode.
- `Mini DMR Mode`: Direct TG mapping.
- **Subscription Logic**:
- **Single Mode**: Only one TG allowed per timeslot. New TG replaces old.
- **Multi Mode**: Multiple subscriptions allowed per timeslot.
- **Scanner / Hold**: If >1 subscription, hold onto active TG for X seconds (default 5s) after idle before switching.
- **Timeouts**:
- Dynamic subscriptions expire after configurable time (default 10 mins).
- Configurable per connection via Options string/password.
- Static subscriptions (via config/options) do not expire.
- **Scope**:
- Only TGs defined in the Reflector's Module Map (plus 4000) are valid.
- **Anti-Kerchunk**:
- If a client Subscribes via PTT (first time), ignore/mute that transmission to prevent broadcasting unnecessary noise.

## Proposed Changes

### Configuration

- [ ] Modify `JsonKeys.h` / `Configure.h` / `Configure.cpp`:
- `Dmr.XlxCompatibility` (bool, default true).
- `Dmr.ModuleMap` (map/object).
- `Dmr.SingleMode` (bool, default false).
- `Dmr.DefaultTimeout` (int, default 600s).
- `Dmr.HoldTime` (int, default 5s).

### Client State (`DMRMMDVMClient`)

- [ ] Add `Subscription` structure:
- `TalkgroupId`
- `Timeslot`
- `Expiry` (timestamp or 0 for static)
- [ ] Add `ScannerState`:
- `CurrentSpeakingTG`
- `HoldExpiry`
- [ ] Add `Subscriptions` container (list/map).

### Reflector Logic (`DMRMMDVMProtocol.cpp`)

- [ ] **Options Parsing**:
- Parse "Options" string (e.g., `TS1=4001;AUTO=600`) from RPTC Description/Password.
- [ ] **Incoming Packet (`OnDvHeaderPacketIn`)**:
- If `!XlxCompatibility`:
- **Validate**: TG must be in `ModuleMap` or 4000.
- **Unsubscribe**: If TG 4000, remove subscription (or all depending on logic).
- **Subscribe**:
- Thread-safe update of subscriptions via `CDMRScanner`.
- **First PTT Logic**: If this is a *new* dynamic subscription, flag stream as `Muted` or don't propagate.
- [ ] **Outgoing/Queue Handling (`HandleQueue`)**:
- Filter logic:
- Thread-safe check of `CheckPacketAccess(tg)`.
- Scanner Logic handled internally in `CDMRScanner` with mutex protection.

## Architecture Diagram

```mermaid
graph TD
Client[MMDVM Client] -->|UDP Packet| Protocol[DMRMMDVMProtocol]
Protocol -->|Parse Header| CheckMode{XlxCompatibility?}

%% XLX Path
CheckMode -->|True| XLXLogic[Legacy XLX Logic]
XLXLogic -->|TG 9| Core[Reflector Core]

%% Mini DMR Path
CheckMode -->|False| MiniLogic[Mini DMR Logic]

subgraph CDMRScanner ["class CDMRScanner"]
MiniLogic -->|Check Access| ScannerState{State Check}
ScannerState -->|Blocked| Drop[Drop Packet]
ScannerState -->|Allowed| UpdateTimer[Update Hold Timer]
end

UpdateTimer -->|Mapped TG| Core

%% Configuration Flow
Config[RPTC Packet] -->|Description/Opts| Parser[Options Parser]
Parser -->|Update| Subs[Subscription List]
Subs -.-> ScannerState
```

## Cross-Protocol Traffic Flow (Outbound)

```mermaid
graph TD
Src[Source Protocol e.g. YSF] -->|Audio on Module B| Core[Reflector Core]
Core -->|Queue Packet| DMRQueue[DMRMMDVMProtocol::HandleQueue]

subgraph "Handle Queue Logic"
DMRQueue --> Encode1[Encode Buffer TS1]
DMRQueue --> Encode2[Encode Buffer TS2]

Encode1 --> ClientCheck{Client Subscribed?}
Encode2 --> ClientCheck

ClientCheck -->|TG + TS1| Send1[Send TS1 Buffer]
ClientCheck -->|TG + TS2| Send2[Send TS2 Buffer]
ClientCheck -->|No| Drop[Drop]
end

Send1 --> Client[MMDVM Client]
Send2 --> Client
``` %% Mini DMR Logic
MapLookup -->|Yes| Map[Map Module B -> TG 4002]
Map -->|TG 4002| ScannerCheck{Scanner Check}

subgraph CDMRScanner
ScannerCheck -->|Client Subscribed?| SubCheck{Subscribed?}
SubCheck -->|No| Drop[Drop]
SubCheck -->|Yes| HoldCheck{Hold Timer Active?}

HoldCheck -->|Held by other TG| Drop
HoldCheck -->|Free / Same TG| Allowed[Allow]
end

Allowed --> SendMini[Send UDP Packet TG 4002]
```

## Architecture Decision

- **Unified Protocol Class**: We will keep `DMRMMDVMProtocol` as the single class handling the UDP/DMR wire protocol.
- **Reasoning**: Both "XLX" and "Mini DMR" modes share identical packet structures, parsing, connection handshakes (RPTL/RPTK), and keepalive mechanisms. Splitting them would require either duplicating this transport logic or creating a complex inheritance hierarchy.
- **Logic Separation**: instead of polluting `DMRMMDVMProtocol.cpp` with mixed logic:
- **Legacy/XLX Logic**: Remains inline (simple routing 9->9).
- **New/Mini Logic**: Encapsulated in `CDMRScanner`. The Protocol class will call checking methods on the scanner.
- **Toggle**: A simple `if (m_XlxCompatibility)` check at the routing decision points (packet ingress/egress) will switch behavior.

## Safety & Robustness Logic

- **Concurrency**:
- `CDMRScanner` will encapsulate all state (`Subscriptions`, `HoldTimer`, `CurrentTG`) protected by an internal `std::recursive_mutex`.
- **Deadlock Prevention**: `CDMRScanner` methods will be leaf-node operations (never calling out to other complex locked systems).
- Access to `CDMRScanner` from `DMRMMDVMProtocol` will be done via thread-safe public methods only.
- **Memory Safety**:
- Avoid raw `char*` manipulation for Options parsing; use `std::string`.
- Input Description field will be clamped to `RPTC` max length (checked in `IsValidConfigPacket` before parsing).
- No fixed-size buffers for variable lists (use `std::vector` for TGs).

## Testing Strategy (TDD)

- **Objective**: Verify complex logic (Subscription management, Timeout, Scanner checks) in isolation without needing full network stack (mocking `DMRMMDVMProtocol/Client`).
- **Plan**:
- Create `reflector/DMRScanner.h/cpp` (or similar) to encapsulate the logic:
- `class CDMRScanner`:
- `AddSubscription(tg, ts, timeout)`
- `RemoveSubscription(tg, ts)`
- `IsSubscribed(tg)`
- `CheckPacketAccess(tg)` -> Validates against Hold timer & Single Mode.
- **Safety Tests**: Verify behavior under high-concurrency (if possible in unit test) or logic edge cases.
- Create `reflector/test_dmr.cpp`:
- A standalone test file similar to `test_audio.cpp`.
- **Scenarios**:
1. **Single Mode**: Add TG1, Add TG2 -> Assert TG1 removed.
2. **Scanner Hold**: Packet from TG1 accepted. Immediately Packet from TG2 -> Rejected (Hold active). Wait 5s -> Packet from TG2 Accepted.
3. **Timeout**: Add TG dynamic (timeout 1s). Wait 2s -> Assert TG removed.
4. **Options Parsing**: Feed "TS1=1,2;AUTO=300" string -> Verify Subscriptions present.
5. **Buffer Safety**: Feed malformed/oversized Option strings -> Verify no crash/leak.
- **Build**: Add `test_dmr` target to `Makefile`.

## Verification Plan

- [ ] **Run TDD Tests**: `make test_dmr && ./reflector/test_dmr`
- [ ] **Manual Verification**:
- **Test Configurations**:
- Single Mode: Verify PTT on TG A drops TG B.
- Multi Mode: Verify PTT on A adds A (keeping B).
- **Test Scanner**:
- Sub to A and B. Transmit on A. Verify B is blocked during Hold time.
- **Test Timeout**:
- Set short timeout. Verify subscription drops.
- **Test Kerchunk**:
- PTT on new TG. Verify not heard by others. Second PTT heard.
Loading