mirror of https://github.com/nostar/urfd.git
Feat: Flexible DMR Mode (Mini DMR) (#3)
* docs: Add Mini DMR architecture documentation * Feat: Implement Flexible DMR (Mini DMR) mode * Fix: Audio routing and Egress TG ID for Mini DMR mode * Fix: Set client reflector module in Mini DMR mode to enable OpenStream * Fix: Options parsing and TG 4000 Disconnect logic * Fix: Add Ip argument to IsValidOptionPacket to fix compilation error * Fix: Sync visual module from options and improve calling logging * Fix: Strict module/timeslot routing logic and documentation updates * Fix: Multi-module dashboard display, DMR protocol rename, and detailed subscriptions in JSON * Fix: Add missing JsonReport declaration to DMRMMDVMClient.h * Fix: Make CClient::JsonReport virtual * Fix: Enable simultaneous dual-slot operation by tracking per-slot scanner hold timers * Fix: Enable simultaneous dual-slot operation in OnDvHeaderPacketIn * Fix: Add missing slot argument to CheckAccess declaration * Fix: Define DMRScanner state members as arrays * Fix: Update GetCurrentScanTG to accept slot argument * Fix: Update DMRScanner implementation to handle m_CurrentScanTG as array * Debug: Log outgoing Talkgroup ID to console * Fix: Propagate dynamic Destination ID to all MMDVM encode functions * Feat: Implement Static vs Dynamic Subscriptions and Single Mode Logic * Fix: Clear client from module list on subscription timeout * Docs: Fix Mermaid syntax error in DMR_Mini_Mode.md * Fix: Improve MMDVM debug logging to accurately reflect sent packets * Fix: Ensure SetReflectorModule is called on every valid DMR Header to prevent Orphaned Frames * Debug: Fix callsign logging and add PTT subscription tracing * Fix: Fallback to default DMR ID for analog sources (e.g. ALLSTAR) with ID 0 * Fix: Handle slot=0 in CheckAccess to support unspecified-slot checking * Fix: Callsign fallback for unknown DMR IDs and Extended SSID support * Cleanup: Remove debug logs * Fix: Module Assignment Regression + Dashboard Backend Support * Tmp: Add debug logs for DMR Dashboard * Fix: Deduplicate DMR client reporting * Feat: Include DMRID in JSON Report * Feat: Renew dynamic subscription on PTT (and cleanup debug logs) * Docs: Update DMR Mini Mode guide with Dashboard and Renewal info * Feat: Rename MMDVM DMR to DMR in logs/dashboard * fix(dmr): preserve raw destination ID in header to support flexible TGs * fix(dmr): report Talkgroup as Target in dashboard events instead of Gateway ID * fix(dmr): revert egress TG propagation to fix timeouts, keep ingress tracking * fix(dmr): revert UR modification to prevent protocol timeouts, fix dashboard display manually * fix(dmr): resolve compiler errors - variable scope and constructor args * fix(dmr): restore working protocol state from last good commit, apply dashboard target display fix * fix(dmr): resolve uiDstId scope error in dashboard fix by recovering TG from module * fix(core): update ModuleToDmrDestId to respect configured mappings (MapA=...) with XLX fallback * fix(dmr): prevent null pointer dereference of moved Header in OpenStream flow * fix(dmr): correct 24-bit DestID encoding in Link Control (prevents TG 90 truncation)pull/23/head
parent
a74e7775a3
commit
9d47a44d91
@ -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.
|
||||
@ -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.
|
||||
@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (c) 2024 by Thomas A. Early N7TAE
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#include "DMRScanner.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
CDMRScanner::CDMRScanner() :
|
||||
m_SingleMode(false),
|
||||
m_DefaultTimeout(600),
|
||||
m_HoldTime(5)
|
||||
{
|
||||
m_CurrentScanTG[0] = 0;
|
||||
m_CurrentScanTG[1] = 0;
|
||||
}
|
||||
|
||||
CDMRScanner::~CDMRScanner()
|
||||
{
|
||||
}
|
||||
|
||||
void CDMRScanner::Configure(bool singleMode, unsigned int defaultTimeout, unsigned int holdTime)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
m_SingleMode = singleMode;
|
||||
m_DefaultTimeout = defaultTimeout;
|
||||
m_HoldTime = holdTime;
|
||||
}
|
||||
|
||||
void CDMRScanner::UpdateSubscriptions(const std::string& options)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
parseOptions(options);
|
||||
}
|
||||
|
||||
void CDMRScanner::parseOptions(const std::string& options)
|
||||
{
|
||||
// Basic parsing: Options: TS1=4001,4002;TS2=9;AUTO=600
|
||||
// Split by ';'
|
||||
if (options.empty()) return;
|
||||
|
||||
std::stringstream ss(options);
|
||||
std::string segment;
|
||||
unsigned int timeout = m_DefaultTimeout;
|
||||
|
||||
// First pass to find AUTO/Timeout if present (to apply to TGs)
|
||||
// Actually, typically AUTO applies to all in the string.
|
||||
// Let's parse into a temporary structure first.
|
||||
|
||||
std::vector<unsigned int> ts1_tgs;
|
||||
std::vector<unsigned int> ts2_tgs;
|
||||
|
||||
while(std::getline(ss, segment, ';'))
|
||||
{
|
||||
size_t eq = segment.find('=');
|
||||
if (eq != std::string::npos)
|
||||
{
|
||||
std::string key = segment.substr(0, eq);
|
||||
std::string val = segment.substr(eq + 1);
|
||||
|
||||
// trim key/val
|
||||
key.erase(0, key.find_first_not_of(" \t\r\n"));
|
||||
key.erase(key.find_last_not_of(" \t\r\n") + 1);
|
||||
|
||||
if (key == "Options") {
|
||||
// Recursive parse or just assume val contains the options?
|
||||
// Example: Options=TS1=4001,4002
|
||||
// Wait, typically MMDVMHost sends: Options=TS1=4001,4002;TS2=9
|
||||
// If the entire string is "Options=...", we need to parse 'val'.
|
||||
// If 'val' contains semicolons, std::getline logic above might have split it already?
|
||||
// No, std::getline splits on ';' first.
|
||||
// Case 1: "Options=TS1=4001,4002;TS2=9"
|
||||
// Segment 1: "Options=TS1=4001,4002". Key="Options", Val="TS1=4001,4002".
|
||||
// We should parse 'Val'.
|
||||
// But wait, 'Val' is "TS1=4001,4002". It'looks like a K=V itself?
|
||||
// Let's recursively call parseOptions(val) or just process val.
|
||||
// But verify val format.
|
||||
// Simplest: Check if val starts with TS1/TS2/AUTO ?
|
||||
parseOptions(val);
|
||||
}
|
||||
else if (key == "AUTO") {
|
||||
try {
|
||||
timeout = std::stoul(val);
|
||||
} catch(...) {}
|
||||
} else if (key == "TS1") {
|
||||
std::stringstream vs(val);
|
||||
std::string v;
|
||||
while(std::getline(vs, v, ',')) {
|
||||
try { ts1_tgs.push_back(std::stoul(v)); } catch(...) {}
|
||||
}
|
||||
} else if (key == "TS2") {
|
||||
std::stringstream vs(val);
|
||||
std::string v;
|
||||
while(std::getline(vs, v, ',')) {
|
||||
try { ts2_tgs.push_back(std::stoul(v)); } catch(...) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply (Replace existing usually? Or append? The prompt said "Options string... to configure subscriptions".
|
||||
// Usually RPTC is a full state update. Let's assume replace for provided timeslots).
|
||||
// Actually user said "clients can send options... similar to freedmr".
|
||||
// Freedmr options usually add/set.
|
||||
// Let's implement ADD logic, but if SingleMode is on, it naturally replaces.
|
||||
// Wait, typical "Options=" in password means "Set these". So we should probably existing ones if they are re-specified?
|
||||
// Let's assume for now we ADD/UPDATE.
|
||||
|
||||
// Actually, simpler implementation for now: Just Add.
|
||||
// parseOptions: call AddSubscription with isStatic=true
|
||||
for (auto tg : ts1_tgs) AddSubscription(tg, 1, timeout, true);
|
||||
for (auto tg : ts2_tgs) AddSubscription(tg, 2, timeout, true);
|
||||
}
|
||||
|
||||
void CDMRScanner::AddSubscription(unsigned int tgid, int timeslot, unsigned int timeout, bool isStatic)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
|
||||
if (tgid == 4000) {
|
||||
m_Subscriptions[timeslot].clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_SingleMode) {
|
||||
m_Subscriptions[timeslot].clear();
|
||||
isStatic = true; // Single Mode always static
|
||||
}
|
||||
|
||||
// Remove if exists to update
|
||||
RemoveSubscription(tgid, timeslot);
|
||||
|
||||
SSubscription sub;
|
||||
sub.tgid = tgid;
|
||||
sub.timeout = timeout;
|
||||
sub.expiry = (timeout == 0) ? 0 : std::time(nullptr) + timeout;
|
||||
sub.isStatic = isStatic;
|
||||
|
||||
m_Subscriptions[timeslot].push_back(sub);
|
||||
}
|
||||
|
||||
void CDMRScanner::RenewSubscription(unsigned int tgid, int timeslot, unsigned int timeout)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
|
||||
if (m_Subscriptions.count(timeslot)) {
|
||||
for (auto& s : m_Subscriptions.at(timeslot)) {
|
||||
if (s.tgid == tgid && !s.isStatic) {
|
||||
s.expiry = (timeout == 0) ? 0 : std::time(nullptr) + timeout;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CDMRScanner::RemoveSubscription(unsigned int tgid, int timeslot)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
auto& subs = m_Subscriptions[timeslot];
|
||||
subs.erase(std::remove_if(subs.begin(), subs.end(),
|
||||
[tgid](const SSubscription& s) { return s.tgid == tgid; }), subs.end());
|
||||
}
|
||||
|
||||
void CDMRScanner::ClearSubscriptions()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
m_Subscriptions.clear();
|
||||
m_CurrentScanTG[0] = 0;
|
||||
m_CurrentScanTG[1] = 0;
|
||||
}
|
||||
|
||||
bool CDMRScanner::IsSubscribed(unsigned int tgid) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
std::time_t now = std::time(nullptr);
|
||||
|
||||
for (const auto& pair : m_Subscriptions) {
|
||||
for (const auto& sub : pair.second) {
|
||||
if (sub.tgid == tgid) {
|
||||
if (!sub.isStatic && sub.timeout > 0 && now > sub.expiry) continue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CDMRScanner::IsSubscribed(unsigned int tgid, int timeslot) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
std::time_t now = std::time(nullptr);
|
||||
|
||||
if (m_Subscriptions.count(timeslot)) {
|
||||
for (const auto& sub : m_Subscriptions.at(timeslot)) {
|
||||
if (sub.tgid == tgid) {
|
||||
if (!sub.isStatic && sub.timeout > 0 && now > sub.expiry) continue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CDMRScanner::CheckAccess(unsigned int tgid, int slot)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
|
||||
if (slot == 0) {
|
||||
// Check both slots
|
||||
return CheckAccess(tgid, 1) || CheckAccess(tgid, 2);
|
||||
}
|
||||
|
||||
if (slot < 1 || slot > 2) return false;
|
||||
int idx = slot - 1;
|
||||
|
||||
cleanupExpired();
|
||||
|
||||
if (!IsSubscribed(tgid, slot)) return false;
|
||||
|
||||
// Scanner Logic for Slot
|
||||
if (m_CurrentScanTG[idx] != 0) {
|
||||
if (m_CurrentScanTG[idx] == tgid) {
|
||||
m_HoldTimer[idx].start();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_HoldTimer[idx].time() < m_HoldTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_CurrentScanTG[idx] = tgid;
|
||||
m_HoldTimer[idx].start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void CDMRScanner::cleanupExpired()
|
||||
{
|
||||
std::time_t now = std::time(nullptr);
|
||||
for (auto& pair : m_Subscriptions) {
|
||||
auto& subs = pair.second;
|
||||
subs.erase(std::remove_if(subs.begin(), subs.end(),
|
||||
[now](const SSubscription& s) { return !s.isStatic && s.timeout > 0 && now > s.expiry; }), subs.end());
|
||||
}
|
||||
|
||||
if (m_CurrentScanTG[0] != 0 && !IsSubscribed(m_CurrentScanTG[0], 1)) m_CurrentScanTG[0] = 0;
|
||||
if (m_CurrentScanTG[1] != 0 && !IsSubscribed(m_CurrentScanTG[1], 2)) m_CurrentScanTG[1] = 0;
|
||||
}
|
||||
|
||||
unsigned int CDMRScanner::GetFirstSubscription() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
|
||||
// Check TS2 first (Standard DMRReflector usually)
|
||||
if (m_Subscriptions.count(2) && !m_Subscriptions.at(2).empty()) return m_Subscriptions.at(2).front().tgid;
|
||||
if (m_Subscriptions.count(1) && !m_Subscriptions.at(1).empty()) return m_Subscriptions.at(1).front().tgid;
|
||||
|
||||
// Check any
|
||||
for(const auto& p : m_Subscriptions) {
|
||||
if (!p.second.empty()) return p.second.front().tgid;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned int CDMRScanner::GetSubscriptionSlot(unsigned int tgid) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
|
||||
// Check TS1
|
||||
if (m_Subscriptions.count(1)) {
|
||||
for(const auto& s : m_Subscriptions.at(1)) {
|
||||
if (s.tgid == tgid) return 1;
|
||||
}
|
||||
}
|
||||
// Check TS2
|
||||
if (m_Subscriptions.count(2)) {
|
||||
for(const auto& s : m_Subscriptions.at(2)) {
|
||||
if (s.tgid == tgid) return 2;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<SSubscription> CDMRScanner::GetSubscriptions(int slot) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
if (m_Subscriptions.count(slot)) {
|
||||
return m_Subscriptions.at(slot);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void CDMRScanner::GetActiveTalkgroups(std::vector<unsigned int>& tgs) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_Mutex);
|
||||
tgs.clear();
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
|
||||
// Check TS1
|
||||
if (m_Subscriptions.count(1)) {
|
||||
for(const auto& s : m_Subscriptions.at(1)) {
|
||||
if (!s.isStatic && s.timeout > 0 && now > s.expiry) continue;
|
||||
tgs.push_back(s.tgid);
|
||||
}
|
||||
}
|
||||
// Check TS2
|
||||
if (m_Subscriptions.count(2)) {
|
||||
for(const auto& s : m_Subscriptions.at(2)) {
|
||||
if (!s.isStatic && s.timeout > 0 && now > s.expiry) continue;
|
||||
tgs.push_back(s.tgid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2024 by Thomas A. Early N7TAE
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
|
||||
#include "Timer.h"
|
||||
|
||||
// Structure to hold subscription details
|
||||
struct SSubscription {
|
||||
unsigned int tgid;
|
||||
unsigned int timeout; // seconds, 0 = infinite
|
||||
std::time_t expiry; // absolute time
|
||||
bool isStatic; // true if static (no timeout)
|
||||
};
|
||||
|
||||
class CDMRScanner
|
||||
{
|
||||
public:
|
||||
CDMRScanner();
|
||||
virtual ~CDMRScanner();
|
||||
|
||||
// Configuration
|
||||
void Configure(bool singleMode, unsigned int defaultTimeout, unsigned int holdTime);
|
||||
bool IsSingleMode() const { return m_SingleMode; }
|
||||
|
||||
// Subscription Management
|
||||
void UpdateSubscriptions(const std::string& options);
|
||||
void AddSubscription(unsigned int tgid, int timeslot, unsigned int timeout, bool isStatic = false);
|
||||
void RenewSubscription(unsigned int tgid, int timeslot, unsigned int timeout);
|
||||
void RemoveSubscription(unsigned int tgid, int timeslot);
|
||||
void ClearSubscriptions();
|
||||
bool IsSubscribed(unsigned int tgid) const;
|
||||
bool IsSubscribed(unsigned int tgid, int timeslot) const;
|
||||
|
||||
// Packet Access Check (Scanner Logic)
|
||||
// Returns true if packet with this TG should be processed
|
||||
bool CheckAccess(unsigned int tgid, int slot = 0);
|
||||
|
||||
// Getters
|
||||
unsigned int GetFirstSubscription() const;
|
||||
unsigned int GetSubscriptionSlot(unsigned int tgid) const;
|
||||
std::vector<SSubscription> GetSubscriptions(int slot) const;
|
||||
void GetActiveTalkgroups(std::vector<unsigned int>& tgs) const;
|
||||
unsigned int GetCurrentScanTG(int slot) const { return (slot >= 1 && slot <= 2) ? m_CurrentScanTG[slot-1] : 0; }
|
||||
|
||||
private:
|
||||
mutable std::recursive_mutex m_Mutex;
|
||||
|
||||
// Config
|
||||
bool m_SingleMode;
|
||||
unsigned int m_DefaultTimeout;
|
||||
unsigned int m_HoldTime;
|
||||
|
||||
// State
|
||||
std::map<int, std::vector<SSubscription>> m_Subscriptions; // Map Timeslot -> List of Subscriptions
|
||||
// Scanner State per slot [0]=TS1, [1]=TS2
|
||||
unsigned int m_CurrentScanTG[2];
|
||||
CTimer m_HoldTimer[2];
|
||||
|
||||
// Helpers
|
||||
void cleanupExpired();
|
||||
void parseOptions(const std::string& options);
|
||||
};
|
||||
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2024 by Thomas A. Early N7TAE
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#include "DMRScanner.h"
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Simple test helper
|
||||
#define ASSERT(cond, msg) \
|
||||
if (!(cond)) { \
|
||||
std::cerr << "FAILED: " << msg << " (" << #cond << ")" << std::endl; \
|
||||
return 1; \
|
||||
} else { \
|
||||
std::cout << "PASS: " << msg << std::endl; \
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << "Running DMRScanner Tests..." << std::endl;
|
||||
|
||||
// Test 1: Single Mode Logic
|
||||
{
|
||||
std::cout << "\n--- Test 1: Single Mode ---" << std::endl;
|
||||
CDMRScanner scanner;
|
||||
scanner.Configure(true, 600, 5); // Single mode, 10m timeout, 5s hold
|
||||
|
||||
scanner.AddSubscription(4001, 1, 600);
|
||||
ASSERT(scanner.IsSubscribed(4001), "TG 4001 should be subscribed");
|
||||
|
||||
scanner.AddSubscription(4002, 1, 600);
|
||||
ASSERT(scanner.IsSubscribed(4002), "TG 4002 should be subscribed");
|
||||
ASSERT(!scanner.IsSubscribed(4001), "TG 4001 should be removed in single mode");
|
||||
}
|
||||
|
||||
// Test 2: Multi Mode Logic
|
||||
{
|
||||
std::cout << "\n--- Test 2: Multi Mode ---" << std::endl;
|
||||
CDMRScanner scanner;
|
||||
scanner.Configure(false, 600, 5); // Multi mode
|
||||
|
||||
scanner.AddSubscription(4001, 1, 600);
|
||||
scanner.AddSubscription(4002, 1, 600);
|
||||
ASSERT(scanner.IsSubscribed(4001), "TG 4001 should remain");
|
||||
ASSERT(scanner.IsSubscribed(4002), "TG 4002 should remain");
|
||||
}
|
||||
|
||||
// Test 3: Scanner Hold Logic
|
||||
{
|
||||
std::cout << "\n--- Test 3: Scanner Hold ---" << std::endl;
|
||||
CDMRScanner scanner;
|
||||
scanner.Configure(false, 600, 2); // 2s hold for testing
|
||||
|
||||
scanner.AddSubscription(4001, 1, 600);
|
||||
scanner.AddSubscription(4002, 1, 600);
|
||||
|
||||
// TG 4001 speaks
|
||||
ASSERT(scanner.CheckAccess(4001), "TG 4001 should be allowed");
|
||||
|
||||
// Immediately TG 4002 tries
|
||||
ASSERT(!scanner.CheckAccess(4002), "TG 4002 should be blocked by hold");
|
||||
|
||||
// Use same TG -> Should refresh hold
|
||||
ASSERT(scanner.CheckAccess(4001), "TG 4001 should still be allowed");
|
||||
|
||||
// Wait exit hold
|
||||
std::cout << "Waiting for hold timer (2s)..." << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2100)); // Sleep just over 2s due to precision
|
||||
|
||||
// Now TG 4002 should work
|
||||
ASSERT(scanner.CheckAccess(4002), "TG 4002 should be allowed after hold");
|
||||
ASSERT(!scanner.CheckAccess(4001), "TG 4001 should now be blocked by new hold");
|
||||
}
|
||||
|
||||
// Test 4: Options Parsing
|
||||
{
|
||||
std::cout << "\n--- Test 4: Options Parsing ---" << std::endl;
|
||||
CDMRScanner scanner;
|
||||
scanner.Configure(false, 600, 5);
|
||||
|
||||
std::string opts = "TS1=101,102;TS2=201;AUTO=300";
|
||||
scanner.UpdateSubscriptions(opts);
|
||||
|
||||
ASSERT(scanner.IsSubscribed(101), "Options TS1-101");
|
||||
ASSERT(scanner.IsSubscribed(102), "Options TS1-102");
|
||||
ASSERT(scanner.IsSubscribed(201), "Options TS2-201");
|
||||
|
||||
// Check timeout (inspect via logic/expiry?)
|
||||
// We can't easily inspect private member, but we can verify it expires.
|
||||
// Let's create a short timeout option test
|
||||
scanner.UpdateSubscriptions("TS1=999;AUTO=1");
|
||||
ASSERT(scanner.IsSubscribed(999), "TG 999 subscribed");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2100)); // Sleep > 2s for time_t resolution
|
||||
ASSERT(!scanner.IsSubscribed(999), "TG 999 should expire after 1s");
|
||||
}
|
||||
|
||||
// Test 5: Unsubscribe (4000)
|
||||
{
|
||||
std::cout << "\n--- Test 5: Unsubscribe 4000 ---" << std::endl;
|
||||
CDMRScanner scanner;
|
||||
scanner.Configure(false, 600, 5);
|
||||
scanner.AddSubscription(4001, 1, 600);
|
||||
|
||||
// Send 4000 on TS1
|
||||
scanner.AddSubscription(4000, 1, 0);
|
||||
ASSERT(!scanner.IsSubscribed(4001), "TG 4001 should be cleared by 4000");
|
||||
}
|
||||
|
||||
std::cout << "\nAll Tests Passed!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in new issue