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
Dave Behnke 1 month ago committed by GitHub
parent a74e7775a3
commit 9d47a44d91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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.

@ -241,13 +241,40 @@ void CCallsign::SetDmrid(uint32_t dmrid, bool UpdateCallsign)
m_uiDmrid = dmrid;
if ( UpdateCallsign )
{
const UCallsign *callsign = nullptr;
g_LDid.Lock();
callsign = g_LDid.FindCallsign(dmrid);
// Attempt Extended SSID Lookup (e.g. 3xxxxxx01)
if (callsign == nullptr && dmrid > 9999999) {
uint32_t baseId = dmrid / 100;
callsign = g_LDid.FindCallsign(baseId);
if (callsign) {
// Base Found, set suffix
char suffix[3];
snprintf(suffix, 3, "%02u", dmrid % 100);
SetSuffix(suffix);
}
}
if ( callsign != nullptr )
{
auto callsign = g_LDid.FindCallsign(dmrid);
if ( callsign != nullptr )
{
m_Callsign.l = callsign->l;
m_Callsign.l = callsign->l;
}
else
{
// Fallback: Use ID as callsign string if unknown
char idBase[CALLSIGN_LEN + 1];
snprintf(idBase, CALLSIGN_LEN + 1, "%u", dmrid);
// Pad with spaces
size_t len = strlen(idBase);
if (len < CALLSIGN_LEN) {
memset(idBase + len, ' ', CALLSIGN_LEN - len);
idBase[CALLSIGN_LEN] = 0;
}
UCallsign uc;
memcpy(uc.c, idBase, CALLSIGN_LEN);
m_Callsign.l = uc.l;
}
g_LDid.Unlock();
CSIn();

@ -76,7 +76,7 @@ public:
// reporting
virtual void WriteXml(std::ofstream &);
void JsonReport(nlohmann::json &report);
virtual void JsonReport(nlohmann::json &report);
protected:
// data

@ -30,6 +30,24 @@
#include "Global.h"
#include "CurlGet.h"
// string trim helpers
static inline void ltrim(std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
}
static inline void rtrim(std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
}
static inline void trim(std::string &s) {
ltrim(s);
rtrim(s);
}
// ini file keywords
#define JAUTOLINKMODULE "AutoLinkModule"
#define JBINDINGADDRESS "BindingAddress"
@ -95,6 +113,7 @@
#define JAUDIO "Audio"
#define JYSF "YSF"
#define JYSFTXRXDB "YSF TX/RX DB"
#define JDMR "DMR"
static inline void split(const std::string &s, char delim, std::vector<std::string> &v)
{
@ -104,25 +123,7 @@ static inline void split(const std::string &s, char delim, std::vector<std::stri
v.push_back(item);
}
// trim from start (in place)
static inline void ltrim(std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) {
return !std::isspace(ch);
}));
}
// trim from end (in place)
static inline void rtrim(std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) {
return !std::isspace(ch);
}).base(), s.end());
}
// trim from both ends (in place)
static inline void trim(std::string &s) {
ltrim(s);
rtrim(s);
}
// ... (unchanged trim functions) ...
CConfigure::CConfigure()
{
@ -134,6 +135,12 @@ CConfigure::CConfigure()
data[g_Keys.dashboard.enable] = false;
data[g_Keys.dashboard.debug] = false;
data[g_Keys.ysf.ysfreflectordb.id] = 0U;
// DMR defaults
data[g_Keys.dmr.xlx] = true;
data[g_Keys.dmr.single] = false;
data[g_Keys.dmr.timeout] = 600U;
data[g_Keys.dmr.hold] = 5U;
}
bool CConfigure::ReadData(const std::string &path)
@ -237,6 +244,8 @@ bool CConfigure::ReadData(const std::string &path)
section = ESection::files;
else if (0 == hname.compare(JAUDIO))
section = ESection::audio;
else if (0 == hname.compare(JDMR))
section = ESection::dmr;
else
{
std::cerr << "WARNING: unknown ini file section: " << line << std::endl;
@ -545,6 +554,29 @@ bool CConfigure::ReadData(const std::string &path)
else
badParam(key);
break;
case ESection::dmr:
if (0 == key.compare(g_Keys.dmr.xlx))
data[g_Keys.dmr.xlx] = IS_TRUE(value[0]);
else if (0 == key.compare(g_Keys.dmr.single))
data[g_Keys.dmr.single] = IS_TRUE(value[0]);
else if (0 == key.compare(g_Keys.dmr.timeout))
data[g_Keys.dmr.timeout] = getUnsigned(value, "DMR Timeout", 30, 86400, 600);
else if (0 == key.compare(g_Keys.dmr.hold))
data[g_Keys.dmr.hold] = getUnsigned(value, "DMR Hold Time", 0, 60, 5);
else if (0 == key.compare(0, g_Keys.dmr.map_prefix.length(), g_Keys.dmr.map_prefix))
{
// Parse MapA, MapB, etc.
if (key.length() == g_Keys.dmr.map_prefix.length() + 1 && isupper(key.back()))
{
// Store custom mapping: "MapA" -> 4001
data[key] = getUnsigned(value, key, 0, 16777215, 0);
}
else
badParam(key);
}
else
badParam(key);
break;
default:
std::cout << "WARNING: parameter '" << line << "' defined before any [section]" << std::endl;
}

@ -25,7 +25,7 @@
enum class ErrorLevel { fatal, mild };
enum class ERefreshType { file, http, both };
enum class ESection { none, names, ip, modules, urf, dplus, dextra, dcs, g3, imrs, dmrplus, mmdvm, nxdn, bm, ysf, p25, m17, usrp, dmrid, nxdnid, ysffreq, files, tc, dashboard, audio };
enum class ESection { none, names, ip, modules, urf, dplus, dextra, dcs, g3, imrs, dmrplus, mmdvm, nxdn, bm, ysf, p25, m17, usrp, dmrid, nxdnid, ysffreq, files, tc, dashboard, audio, dmr };
#define IS_TRUE(a) ((a)=='t' || (a)=='T' || (a)=='1')

@ -18,6 +18,9 @@
#include "DMRMMDVMClient.h"
#include "Global.h"
#include "Configure.h"
#include "DMRMMDVMProtocol.h" // For mapping logic if accessible, or we reimplement
////////////////////////////////////////////////////////////////////////////////////////
@ -44,3 +47,97 @@ bool CDmrmmdvmClient::IsAlive(void) const
{
return (m_LastKeepaliveTime.time() < DMRMMDVM_KEEPALIVE_TIMEOUT);
}
// Multi-Module Reporting for Dashboard
void CDmrmmdvmClient::JsonReport(nlohmann::json &report)
{
// DEBUG: Check XLX Mode
// std::cout << "DEBUG: XLX Mode Comp: " << g_Configure.GetBoolean(g_Keys.dmr.xlx) << std::endl;
if (g_Configure.GetBoolean(g_Keys.dmr.xlx)) {
// Legacy behavior
CClient::JsonReport(report);
return;
}
// Mini DMR Mode
bool anySub = false;
// Collect Subscriptions Info
nlohmann::json jSubs = nlohmann::json::array();
std::vector<unsigned int> tgs;
m_Scanner.GetActiveTalkgroups(tgs);
std::time_t now = std::time(nullptr);
// Collect TS1
for(const auto& s : m_Scanner.GetSubscriptions(1)) {
nlohmann::json sub;
sub["TG"] = s.tgid;
sub["Slot"] = 1;
sub["Type"] = s.isStatic ? "Static" : "Dynamic";
if (!s.isStatic && s.timeout > 0) {
sub["TimeoutLeft"] = (s.expiry > now) ? (s.expiry - now) : 0;
} else {
sub["TimeoutLeft"] = -1; // Infinite or Static
}
jSubs.push_back(sub);
}
// Collect TS2
for(const auto& s : m_Scanner.GetSubscriptions(2)) {
nlohmann::json sub;
sub["TG"] = s.tgid;
sub["Slot"] = 2;
sub["Type"] = s.isStatic ? "Static" : "Dynamic";
if (!s.isStatic && s.timeout > 0) {
sub["TimeoutLeft"] = (s.expiry > now) ? (s.expiry - now) : 0;
} else {
sub["TimeoutLeft"] = -1;
}
jSubs.push_back(sub);
}
// Helper to add node entry
auto addNode = [&](char module) {
nlohmann::json jclient;
jclient["Callsign"] = m_Callsign.GetCS();
jclient["DMRID"] = m_Callsign.GetDmrid();
jclient["OnModule"] = std::string(1, module);
jclient["Protocol"] = GetProtocolName();
jclient["Subscriptions"] = jSubs;
char s[100];
if (std::strftime(s, sizeof(s), "%FT%TZ", std::gmtime(&m_ConnectTime)))
jclient["ConnectTime"] = s;
report["Clients"].push_back(jclient);
};
// Reimplement logic using global config.
auto dmrdstToMod = [&](uint32_t tg) -> char {
for (char c = 'A'; c <= 'Z'; c++) {
std::string key = g_Keys.dmr.map_prefix + c;
if (g_Configure.Contains(key)) {
if (g_Configure.GetUnsigned(key) == tg) return c;
} else {
if (tg == (uint32_t)(4001 + (c - 'A'))) return c;
}
}
return ' ';
};
// Process unique modules: valid, but we only want ONE entry per client for the dashboard to prevent duplicates.
// Pick the *first* mapped module as the "visual" module, or space if none.
char visualModule = ' ';
for(unsigned int tg : tgs) {
char mod = dmrdstToMod(tg);
if (mod != ' ') {
visualModule = mod;
anySub = true;
break; // Found one, good enough for display
}
}
// Always report the client once
addNode(visualModule);
}

@ -20,6 +20,7 @@
#include "Defines.h"
#include "Client.h"
#include "DMRScanner.h"
class CDmrmmdvmClient : public CClient
{
@ -32,11 +33,16 @@ public:
// destructor
virtual ~CDmrmmdvmClient() {};
// Override JsonReport for Multi-Module support
virtual void JsonReport(nlohmann::json &report) override;
// identity
EProtocol GetProtocol(void) const { return EProtocol::dmrmmdvm; }
const char *GetProtocolName(void) const { return "DMRMmdvm"; }
const char *GetProtocolName(void) const { return "DMR"; }
bool IsNode(void) const { return true; }
// status
bool IsAlive(void) const;
CDMRScanner m_Scanner;
};

@ -158,7 +158,16 @@ void CDmrmmdvmProtocol::Task(void)
std::cout << "DMRmmdvm login from " << Callsign << " at " << Ip << std::endl;
// create the client and append
clients->AddClient(std::make_shared<CDmrmmdvmClient>(Callsign, Ip));
std::shared_ptr<CDmrmmdvmClient> newClient = std::make_shared<CDmrmmdvmClient>(Callsign, Ip);
// Configure Scanner
newClient->m_Scanner.Configure(
g_Configure.GetBoolean(g_Keys.dmr.single),
g_Configure.GetUnsigned(g_Keys.dmr.timeout),
g_Configure.GetUnsigned(g_Keys.dmr.hold)
);
clients->AddClient(newClient);
}
else
{
@ -221,7 +230,7 @@ void CDmrmmdvmProtocol::Task(void)
// ignore...
}
else if ( IsValidOptionPacket(Buffer, &Callsign) )
else if ( IsValidOptionPacket(Buffer, &Callsign, Ip) )
{
std::cout << "DMRmmdvm options packet from " << Callsign << " at " << Ip << std::endl;
@ -280,6 +289,95 @@ void CDmrmmdvmProtocol::OnDvHeaderPacketIn(std::unique_ptr<CDvHeaderPacket> &Hea
std::shared_ptr<CClient>client = g_Reflector.GetClients()->FindClient(Ip, EProtocol::dmrmmdvm);
if ( client )
{
// Mini DMR / Flexible Mode Logic
if (!g_Configure.GetBoolean(g_Keys.dmr.xlx))
{
std::shared_ptr<CDmrmmdvmClient> dmrClient = std::dynamic_pointer_cast<CDmrmmdvmClient>(client);
if (dmrClient)
{
// Map Destination ID (TG) to Module (if applicable, but we mostly care about TG)
// Actually, DmrDstIdToModule handles dynamic mapping now.
// But we want to use the RAW TG for subscription?
// DmrDstIdToModule uses the map to find 'A' from TG.
// If we are in XlxMode=false, DmrDstIdToModule uses the map.
// rpt2 checks GetCSModule().
// We need to know the Talkgroup.
// Helper: module 'A' -> TG X.
// Header->GetRpt2Callsign() call has Module set by DmrDstIdToModule.
char mod = rpt2.GetCSModule();
uint32_t tg = ModuleToDmrDestId(mod);
// Mini DMR: Explicit Disconnect (TG 4000 or specific unlink cmd)
if (tg == 4000 || cmd == CMD_UNLINK)
{
std::cout << "DMRmmdvm client " << client->GetCallsign() << " Mini DMR Disconnect (TG 4000)" << std::endl;
dmrClient->m_Scanner.ClearSubscriptions();
client->SetReflectorModule(' '); // Clear module attachment
g_Reflector.ReleaseClients();
return;
}
// Anti-Kerchunk / Hold Check
// If this is a new transmission (Header), we check access.
if (!dmrClient->m_Scanner.CheckAccess(tg)) {
// Blocked by Scanner Hold or not subscribed?
// Wait, strict logic: "Clients subscribe... traffic is routed".
// If I PTT on a TG, should I auto-subscribe?
// Plan says: "Subscribe: Thread-safe update... First PTT Logic".
// So we SHOULD subscribe.
// But if we subscribe, CheckAccess(tg) will return true (unless held by OTHER).
// So we add subscription first.
// Add Subscription (Dynamic)
unsigned int timeout = g_Configure.GetUnsigned(g_Keys.dmr.timeout);
// Slot? Usually assume Slot 2 or from Header? Header has slot info?
// CDvHeaderPacket doesn't easily expose slot in args here, passed in?
// Header->GetBitField?
// Actually buffer parsing did it.
// We don't have slot easily available here except from previous context?
// Buffer parsing sets 'header' and 'cmd'.
// Mini DMR Mode: Scanner Check
// We need to know which slot the user is transmitting on.
// The packet doesn't explicitly tell us (it's embedded in obscure bits or implicit).
// However, if the user is transmitting on TG X, they MUST be subscribed to TG X.
// So we can look up the slot from the scanner!
int slot = dmrClient->m_Scanner.GetSubscriptionSlot(tg);
if (slot == 0) slot = 2; // Default to TS2 if not found (e.g. initial PTT)
// Auto-subscribe if not subscribed?
// If slot was 0, it means not subscribed. We should probably auto-subscribe.
// But which slot? Usually TS2 is safe default for Hotspots.
if (slot == 2 && dmrClient->m_Scanner.GetSubscriptionSlot(tg) == 0) {
// PTT -> Dynamic Subscription (isStatic=false)
dmrClient->m_Scanner.AddSubscription(tg, 2, timeout, false);
}
// Check Access on the specific slot
if (!dmrClient->m_Scanner.CheckAccess(tg, slot)) {
// Blocked (Held by another TG on this slot)
g_Reflector.ReleaseClients();
return;
}
// FIX: Ensure OpenStream sees the client attached to this module
client->SetReflectorModule(rpt2.GetCSModule());
} else {
// Access Granted (Already Subscribed) - Renew Timer if Dynamic
unsigned int timeout = g_Configure.GetUnsigned(g_Keys.dmr.timeout);
int slot = dmrClient->m_Scanner.GetSubscriptionSlot(tg);
if (slot != 0) {
dmrClient->m_Scanner.RenewSubscription(tg, slot, timeout);
}
}
// Always ensure module is set if we are processing this packet (Access Granted)
// DEBUG: Trace Module Assignment
// std::cout << "DEBUG: " << client->GetCallsign().GetCS() << " assigned to module " << rpt2.GetCSModule() << std::endl;
client->SetReflectorModule(rpt2.GetCSModule());
}
}
// process cmd if any
if ( !client->HasReflectorModule() )
{
@ -288,9 +386,14 @@ void CDmrmmdvmProtocol::OnDvHeaderPacketIn(std::unique_ptr<CDvHeaderPacket> &Hea
{
if ( g_Reflector.IsValidModule(rpt2.GetCSModule()) )
{
std::cout << "DMRmmdvm client " << client->GetCallsign() << " linking on module " << rpt2.GetCSModule() << std::endl;
// link
client->SetReflectorModule(rpt2.GetCSModule());
// In Mini DMR, we don't necessarily "Link" the client object,
// but existing logic uses SetReflectorModule for routing.
// WE should ONLY do this in XLX mode.
if (g_Configure.GetBoolean(g_Keys.dmr.xlx)) {
std::cout << "DMRmmdvm client " << client->GetCallsign() << " linking on module " << rpt2.GetCSModule() << std::endl;
// link
client->SetReflectorModule(rpt2.GetCSModule());
}
}
else
{
@ -339,7 +442,18 @@ void CDmrmmdvmProtocol::OnDvHeaderPacketIn(std::unique_ptr<CDvHeaderPacket> &Hea
// update last heard
if ( lastheard )
{
g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::dmrmmdvm);
// Fix Dashboard Target Display
// Construct target with explicit stringID to show "7002" instead of "CQCQCQ"
// CRITICAL FIX: Header is unique_ptr and was MOVED in OpenStream above if stream opened.
// We cannot access Header here if stream != nullptr.
// Reconstruct a clean target object.
CCallsign target("CQCQCQ"); // Default safe initialization
// uiDstId is not in scope, recover it from the module (which relies on urfd.ini mapping)
uint32_t tg = ModuleToDmrDestId(rpt2.GetCSModule());
target.SetCallsign(std::to_string(tg));
g_Reflector.GetUsers()->Hearing(my, target, rpt1, rpt2, EProtocol::dmrmmdvm);
g_Reflector.ReleaseUsers();
}
}
@ -358,8 +472,9 @@ void CDmrmmdvmProtocol::HandleQueue(void)
// get our sender's id
const auto mod = packet->GetPacketModule();
// encode
CBuffer buffer;
// encode buffers for both slots
CBuffer bufferTS1;
CBuffer bufferTS2;
// check if it's header
if ( packet->IsDvHeader() )
@ -369,20 +484,32 @@ void CDmrmmdvmProtocol::HandleQueue(void)
m_StreamsCache[mod].m_dvHeader = CDvHeaderPacket((const CDvHeaderPacket &)*packet.get());
m_StreamsCache[mod].m_uiSeqId = 0;
// Calculate Destination ID based on Module (XLX or Mini DMR logic)
uint32_t tg = ModuleToDmrDestId(packet->GetPacketModule());
// encode it
EncodeMMDVMHeaderPacket((CDvHeaderPacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, &buffer);
EncodeMMDVMHeaderPacket((CDvHeaderPacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, tg, 1, &bufferTS1);
EncodeMMDVMHeaderPacket((CDvHeaderPacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, tg, 2, &bufferTS2);
m_StreamsCache[mod].m_uiSeqId = 1;
// Store TG in cache for subsequent frames? Or recalculate?
// ModuleToDmrDestId is fast enough.
}
// check if it's a last frame
else if ( packet->IsLastPacket() )
{
uint32_t tg = ModuleToDmrDestId(packet->GetPacketModule());
// encode it
EncodeLastMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_uiSeqId, &buffer);
EncodeLastMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_uiSeqId, tg, 1, &bufferTS1);
EncodeLastMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_uiSeqId, tg, 2, &bufferTS2);
m_StreamsCache[mod].m_uiSeqId = (m_StreamsCache[mod].m_uiSeqId + 1) & 0xFF;
}
// otherwise, just a regular DV frame
else
{
uint32_t tg = ModuleToDmrDestId(packet->GetPacketModule());
// update local stream cache or send triplet when needed
switch ( packet->GetDmrPacketSubid() )
{
@ -393,7 +520,8 @@ void CDmrmmdvmProtocol::HandleQueue(void)
m_StreamsCache[mod].m_dvFrame1 = CDvFramePacket((const CDvFramePacket &)*packet.get());
break;
case 3:
EncodeMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_dvFrame0, m_StreamsCache[mod].m_dvFrame1, (const CDvFramePacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, &buffer);
EncodeMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_dvFrame0, m_StreamsCache[mod].m_dvFrame1, (const CDvFramePacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, tg, 1, &bufferTS1);
EncodeMMDVMPacket(m_StreamsCache[mod].m_dvHeader, m_StreamsCache[mod].m_dvFrame0, m_StreamsCache[mod].m_dvFrame1, (const CDvFramePacket &)*packet.get(), m_StreamsCache[mod].m_uiSeqId, tg, 2, &bufferTS2);
m_StreamsCache[mod].m_uiSeqId = (m_StreamsCache[mod].m_uiSeqId + 1) & 0xFF;
break;
default:
@ -402,20 +530,52 @@ void CDmrmmdvmProtocol::HandleQueue(void)
}
// send it
if ( buffer.size() > 0 )
if ( bufferTS1.size() > 0 || bufferTS2.size() > 0 )
{
// and push it to all our clients linked to the module and who are not streaming in
CClients *clients = g_Reflector.GetClients();
auto it = clients->begin();
std::shared_ptr<CClient>client = nullptr;
// Calculate TG again for convenience or use from above scope?
// The logic above is inside if/else blocks.
// Recalculate is safest and clean.
uint32_t tg = ModuleToDmrDestId(packet->GetPacketModule());
while ( (client = clients->FindNextClient(EProtocol::dmrmmdvm, it)) != nullptr )
{
// is this client busy ?
if ( !client->IsAMaster() && (client->GetReflectorModule() == packet->GetPacketModule()) )
if ( !client->IsAMaster() )
{
// no, send the packet
Send(buffer, client->GetIp());
if (g_Configure.GetBoolean(g_Keys.dmr.xlx))
{
// Legacy XLX Mode: Link Check
// Default to TS2 buffer for XLX
// Or should we support both slots in XLX? Usually Reflector runs on TS2.
if (client->GetReflectorModule() == packet->GetPacketModule())
Send(bufferTS2, client->GetIp());
}
else
{
// Mini DMR Mode: Scanner Check
std::shared_ptr<CDmrmmdvmClient> dmrClient = std::dynamic_pointer_cast<CDmrmmdvmClient>(client);
if (dmrClient)
{
// uint32_t tg = ModuleToDmrDestId(packet->GetPacketModule()); // Already calculated
// Check Access for each slot independently
bool ts1 = bufferTS1.size() > 0 && dmrClient->m_Scanner.CheckAccess(tg, 1);
bool ts2 = bufferTS2.size() > 0 && dmrClient->m_Scanner.CheckAccess(tg, 2);
if (ts1) {
Send(bufferTS1, client->GetIp());
}
if (ts2) {
Send(bufferTS2, client->GetIp());
}
}
}
}
}
g_Reflector.ReleaseClients();
@ -547,12 +707,41 @@ bool CDmrmmdvmProtocol::IsValidConfigPacket(const CBuffer &Buffer, CCallsign *ca
{
std::cout << "Invalid callsign in DMRmmdvm RPTC packet from IP: " << Ip << " CS:" << *callsign << " DMRID:" << callsign->GetDmrid() << std::endl;
}
else
{
// Update Options from Description
// Description starts at offset 67, length 40 (approx). Buffer size 302.
if (Buffer.size() >= 107) {
std::string desc((const char*)(Buffer.data() + 67), 40);
// Trim nulls or grab until null
size_t nullpos = desc.find('\0');
if (nullpos != std::string::npos) desc.resize(nullpos);
// Find client
CClients *clients = g_Reflector.GetClients();
std::shared_ptr<CClient> client = clients->FindClient(*callsign, Ip, EProtocol::dmrmmdvm);
if (client) {
std::shared_ptr<CDmrmmdvmClient> dmrClient = std::dynamic_pointer_cast<CDmrmmdvmClient>(client);
if (dmrClient) {
std::cout << "DMRmmdvm Options Update for " << client->GetCallsign() << ": " << desc << std::endl;
dmrClient->m_Scanner.UpdateSubscriptions(desc);
// FIX: Update Visual Module based on First Subscription
uint32_t firstTG = dmrClient->m_Scanner.GetFirstSubscription();
if (firstTG > 0) {
char mod = DmrDstIdToModule(firstTG);
if (mod != ' ') dmrClient->SetReflectorModule(mod);
}
}
}
g_Reflector.ReleaseClients();
}
}
}
return valid;
}
bool CDmrmmdvmProtocol::IsValidOptionPacket(const CBuffer &Buffer, CCallsign *callsign)
bool CDmrmmdvmProtocol::IsValidOptionPacket(const CBuffer &Buffer, CCallsign *callsign, const CIp &Ip)
{
uint8_t tag[] = { 'R','P','T','O' };
@ -563,6 +752,32 @@ bool CDmrmmdvmProtocol::IsValidOptionPacket(const CBuffer &Buffer, CCallsign *ca
callsign->SetDmrid(uiRptrId, true);
callsign->SetCSModule(MMDVM_MODULE_ID);
valid = callsign->IsValid();
if (valid && Buffer.size() > 8) {
// Extract Options String
std::string options((const char*)(Buffer.data() + 8), Buffer.size() - 8);
// Trim potential nulls
size_t nullpos = options.find('\0');
if (nullpos != std::string::npos) options.resize(nullpos);
// Find client and update
CClients *clients = g_Reflector.GetClients();
std::shared_ptr<CClient> client = clients->FindClient(*callsign, Ip, EProtocol::dmrmmdvm);
if (client) {
std::shared_ptr<CDmrmmdvmClient> dmrClient = std::dynamic_pointer_cast<CDmrmmdvmClient>(client);
if (dmrClient) {
std::cout << "DMRmmdvm RPTO Options for " << client->GetCallsign() << ": " << options << std::endl;
dmrClient->m_Scanner.UpdateSubscriptions(options);
// FIX: Update Visual Module based on First Subscription
uint32_t firstTG = dmrClient->m_Scanner.GetFirstSubscription();
if (firstTG > 0) {
char mod = DmrDstIdToModule(firstTG);
if (mod != ' ') dmrClient->SetReflectorModule(mod);
}
}
}
g_Reflector.ReleaseClients();
}
}
return valid;
}
@ -863,8 +1078,11 @@ void CDmrmmdvmProtocol::EncodeClosePacket(CBuffer *Buffer, std::shared_ptr<CClie
}
bool CDmrmmdvmProtocol::EncodeMMDVMHeaderPacket(const CDvHeaderPacket &Packet, uint8_t seqid, CBuffer *Buffer) const
bool CDmrmmdvmProtocol::EncodeMMDVMHeaderPacket(const CDvHeaderPacket &Packet, uint8_t seqid, uint32_t dstId, uint8_t slot, CBuffer *Buffer) const
{
// Debug Encode
// std::cout << "DEBUG: EncodeHeader dstId=" << dstId << " Slot=" << (int)slot << std::endl;
uint8_t tag[] = { 'D','M','R','D' };
Buffer->Set(tag, sizeof(tag));
@ -874,17 +1092,19 @@ bool CDmrmmdvmProtocol::EncodeMMDVMHeaderPacket(const CDvHeaderPacket &Packet, u
Buffer->Append((uint8_t)seqid);
// uiSrcId
uint32_t uiSrcId = Packet.GetMyCallsign().GetDmrid();
// Fallback to default ID if source has none (e.g. Analog bridge)
if (uiSrcId == 0) uiSrcId = m_DefaultId;
AppendDmrIdToBuffer(Buffer, uiSrcId);
// uiDstId = TG9
uint32_t uiDstId = 9; // ModuleToDmrDestId(Packet.GetRpt2Module());
AppendDmrIdToBuffer(Buffer, uiDstId);
// uiDstId
AppendDmrIdToBuffer(Buffer, dstId);
// uiRptrId
uint32_t uiRptrId = Packet.GetRpt1Callsign().GetDmrid();
AppendDmrRptrIdToBuffer(Buffer, uiRptrId);
// uiBitField
uint8_t uiBitField =
(DMRMMDVM_FRAMETYPE_DATASYNC << 4) |
((DMRMMDVM_REFLECTOR_SLOT == DMR_SLOT2) ? 0x80 : 0x00) |
((slot == 2) ? 0x80 : 0x00) |
MMDVM_SLOTTYPE_HEADER;
Buffer->Append((uint8_t)uiBitField);
// uiStreamId
@ -892,7 +1112,7 @@ bool CDmrmmdvmProtocol::EncodeMMDVMHeaderPacket(const CDvHeaderPacket &Packet, u
Buffer->Append((uint32_t)uiStreamId);
// Payload
AppendVoiceLCToBuffer(Buffer, uiSrcId);
AppendVoiceLCToBuffer(Buffer, uiSrcId, dstId);
// BER
Buffer->Append((uint8_t)0);
@ -904,7 +1124,7 @@ bool CDmrmmdvmProtocol::EncodeMMDVMHeaderPacket(const CDvHeaderPacket &Packet, u
return true;
}
void CDmrmmdvmProtocol::EncodeMMDVMPacket(const CDvHeaderPacket &Header, const CDvFramePacket &DvFrame0, const CDvFramePacket &DvFrame1, const CDvFramePacket &DvFrame2, uint8_t seqid, CBuffer *Buffer) const
void CDmrmmdvmProtocol::EncodeMMDVMPacket(const CDvHeaderPacket &Header, const CDvFramePacket &DvFrame0, const CDvFramePacket &DvFrame1, const CDvFramePacket &DvFrame2, uint8_t seqid, uint32_t dstId, uint8_t slot, CBuffer *Buffer) const
{
uint8_t tag[] = { 'D','M','R','D' };
Buffer->Set(tag, sizeof(tag));
@ -931,15 +1151,14 @@ void CDmrmmdvmProtocol::EncodeMMDVMPacket(const CDvHeaderPacket &Header, const C
}
AppendDmrIdToBuffer(Buffer, uiSrcId);
// uiDstId = TG9
uint32_t uiDstId = 9; // ModuleToDmrDestId(Header.GetRpt2Module());
AppendDmrIdToBuffer(Buffer, uiDstId);
// uiDstId
AppendDmrIdToBuffer(Buffer, dstId);
// uiRptrId
uint32_t uiRptrId = Header.GetRpt1Callsign().GetDmrid();
AppendDmrRptrIdToBuffer(Buffer, uiRptrId);
// uiBitField
uint8_t uiBitField =
((DMRMMDVM_REFLECTOR_SLOT == DMR_SLOT2) ? 0x80 : 0x00);
((slot == 2) ? 0x80 : 0x00);
if ( DvFrame0.GetDmrPacketId() == 0 )
{
uiBitField |= (DMRMMDVM_FRAMETYPE_VOICESYNC << 4);
@ -983,7 +1202,7 @@ void CDmrmmdvmProtocol::EncodeMMDVMPacket(const CDvHeaderPacket &Header, const C
}
void CDmrmmdvmProtocol::EncodeLastMMDVMPacket(const CDvHeaderPacket &Packet, uint8_t seqid, CBuffer *Buffer) const
void CDmrmmdvmProtocol::EncodeLastMMDVMPacket(const CDvHeaderPacket &Packet, uint8_t seqid, uint32_t dstId, uint8_t slot, CBuffer *Buffer) const
{
uint8_t tag[] = { 'D','M','R','D' };
@ -994,17 +1213,19 @@ void CDmrmmdvmProtocol::EncodeLastMMDVMPacket(const CDvHeaderPacket &Packet, uin
Buffer->Append((uint8_t)seqid);
// uiSrcId
uint32_t uiSrcId = Packet.GetMyCallsign().GetDmrid();
// Fallback to default ID if source has none
if (uiSrcId == 0) uiSrcId = m_DefaultId;
AppendDmrIdToBuffer(Buffer, uiSrcId);
// uiDstId
uint32_t uiDstId = 9; //ModuleToDmrDestId(Packet.GetRpt2Module());
AppendDmrIdToBuffer(Buffer, uiDstId);
AppendDmrIdToBuffer(Buffer, dstId);
// uiRptrId
uint32_t uiRptrId = Packet.GetRpt1Callsign().GetDmrid();
AppendDmrRptrIdToBuffer(Buffer, uiRptrId);
// uiBitField
uint8_t uiBitField =
(DMRMMDVM_FRAMETYPE_DATASYNC << 4) |
((DMRMMDVM_REFLECTOR_SLOT == DMR_SLOT2) ? 0x80 : 0x00) |
((slot == 2) ? 0x80 : 0x00) |
MMDVM_SLOTTYPE_TERMINATOR;
Buffer->Append((uint8_t)uiBitField);
// uiStreamId
@ -1012,7 +1233,7 @@ void CDmrmmdvmProtocol::EncodeLastMMDVMPacket(const CDvHeaderPacket &Packet, uin
Buffer->Append((uint32_t)uiStreamId);
// Payload
AppendTerminatorLCToBuffer(Buffer, uiSrcId);
AppendTerminatorLCToBuffer(Buffer, uiSrcId, dstId);
// BER
Buffer->Append((uint8_t)0);
@ -1027,13 +1248,39 @@ void CDmrmmdvmProtocol::EncodeLastMMDVMPacket(const CDvHeaderPacket &Packet, uin
char CDmrmmdvmProtocol::DmrDstIdToModule(uint32_t tg) const
{
// is it a 4xxx ?
if (tg > 4000 && tg < 4027)
if (g_Configure.GetBoolean(g_Keys.dmr.xlx))
{
const char mod = 'A' + (tg - 4001U);
if (g_Reflector.IsValidModule(mod))
// Legacy XLX Mode
if (tg > 4000 && tg < 4027)
{
return mod;
const char mod = 'A' + (tg - 4001U);
if (g_Reflector.IsValidModule(mod))
{
return mod;
}
}
}
else
{
// Mini DMR Mode - Reverse Lookup
// Iterate A-Z and check map
for (char c = 'A'; c <= 'Z'; c++)
{
std::string key = g_Keys.dmr.map_prefix + c;
if (g_Configure.Contains(key))
{
if (g_Configure.GetUnsigned(key) == tg)
return c;
}
else
{
// Default Mapping check
// If no map entry, assume default 4001-4026?
// User said "allows custom mapping... default mapping A=4001... should be supported".
// So if key missing, fallback to default?
if (tg == (uint32_t)(4001 + (c - 'A')))
return c;
}
}
}
return ' ';
@ -1041,13 +1288,27 @@ char CDmrmmdvmProtocol::DmrDstIdToModule(uint32_t tg) const
uint32_t CDmrmmdvmProtocol::ModuleToDmrDestId(char m) const
{
return (uint32_t)(m - 'A')+4001;
if (g_Configure.GetBoolean(g_Keys.dmr.xlx))
{
return (uint32_t)(m - 'A')+4001;
}
else
{
// Mini DMR Mode - Forward Lookup
std::string key = g_Keys.dmr.map_prefix + m;
if (g_Configure.Contains(key))
{
return g_Configure.GetUnsigned(key);
}
// Default fallback
return (uint32_t)(m - 'A')+4001;
}
}
////////////////////////////////////////////////////////////////////////////////////////
// Buffer & LC helpers
void CDmrmmdvmProtocol::AppendVoiceLCToBuffer(CBuffer *buffer, uint32_t uiSrcId) const
void CDmrmmdvmProtocol::AppendVoiceLCToBuffer(CBuffer *buffer, uint32_t uiSrcId, uint32_t uiDstId) const
{
uint8_t payload[33];
@ -1058,8 +1319,10 @@ void CDmrmmdvmProtocol::AppendVoiceLCToBuffer(CBuffer *buffer, uint32_t uiSrcId)
uint8_t lc[12];
{
memset(lc, 0, sizeof(lc));
// uiDstId = TG9
lc[5] = 9;
// uiDstId
lc[3] = (uint8_t)LOBYTE(HIWORD(uiDstId));
lc[4] = (uint8_t)HIBYTE(LOWORD(uiDstId));
lc[5] = (uint8_t)LOBYTE(LOWORD(uiDstId));
// uiSrcId
lc[6] = (uint8_t)LOBYTE(HIWORD(uiSrcId));
lc[7] = (uint8_t)HIBYTE(LOWORD(uiSrcId));
@ -1094,7 +1357,7 @@ void CDmrmmdvmProtocol::AppendVoiceLCToBuffer(CBuffer *buffer, uint32_t uiSrcId)
buffer->Append(payload, sizeof(payload));
}
void CDmrmmdvmProtocol::AppendTerminatorLCToBuffer(CBuffer *buffer, uint32_t uiSrcId) const
void CDmrmmdvmProtocol::AppendTerminatorLCToBuffer(CBuffer *buffer, uint32_t uiSrcId, uint32_t uiDstId) const
{
uint8_t payload[33];
@ -1105,8 +1368,10 @@ void CDmrmmdvmProtocol::AppendTerminatorLCToBuffer(CBuffer *buffer, uint32_t uiS
uint8_t lc[12];
{
memset(lc, 0, sizeof(lc));
// uiDstId = TG9
lc[5] = 9;
// uiDstId
lc[3] = (uint8_t)LOBYTE(HIWORD(uiDstId));
lc[4] = (uint8_t)HIBYTE(LOWORD(uiDstId));
lc[5] = (uint8_t)LOBYTE(LOWORD(uiDstId));
// uiSrcId
lc[6] = (uint8_t)LOBYTE(HIWORD(uiSrcId));
lc[7] = (uint8_t)HIBYTE(LOWORD(uiSrcId));

@ -77,7 +77,7 @@ protected:
bool IsValidAuthenticationPacket(const CBuffer &, CCallsign *, const CIp &);
bool IsValidDisconnectPacket(const CBuffer &, CCallsign *);
bool IsValidConfigPacket(const CBuffer &, CCallsign *, const CIp &);
bool IsValidOptionPacket(const CBuffer &, CCallsign *);
bool IsValidOptionPacket(const CBuffer &, CCallsign *, const CIp &);
bool IsValidKeepAlivePacket(const CBuffer &, CCallsign *);
bool IsValidRssiPacket(const CBuffer &, CCallsign *, int *);
bool IsValidDvHeaderPacket(const CBuffer &, std::unique_ptr<CDvHeaderPacket> &, uint8_t *, uint8_t *);
@ -90,17 +90,17 @@ protected:
void EncodeConnectAckPacket(CBuffer *, const CCallsign &, uint32_t);
void EncodeNackPacket(CBuffer *, const CCallsign &);
void EncodeClosePacket(CBuffer *, std::shared_ptr<CClient>);
bool EncodeMMDVMHeaderPacket(const CDvHeaderPacket &, uint8_t, CBuffer *) const;
void EncodeMMDVMPacket(const CDvHeaderPacket &, const CDvFramePacket &, const CDvFramePacket &, const CDvFramePacket &, uint8_t, CBuffer *) const;
void EncodeLastMMDVMPacket(const CDvHeaderPacket &, uint8_t, CBuffer *) const;
bool EncodeMMDVMHeaderPacket(const CDvHeaderPacket &, uint8_t, uint32_t, uint8_t, CBuffer *) const;
void EncodeMMDVMPacket(const CDvHeaderPacket &, const CDvFramePacket &, const CDvFramePacket &, const CDvFramePacket &, uint8_t, uint32_t, uint8_t, CBuffer *) const;
void EncodeLastMMDVMPacket(const CDvHeaderPacket &, uint8_t, uint32_t, uint8_t, CBuffer *) const;
// dmr DstId to Module helper
char DmrDstIdToModule(uint32_t) const;
uint32_t ModuleToDmrDestId(char) const;
// Buffer & LC helpers
void AppendVoiceLCToBuffer(CBuffer *, uint32_t) const;
void AppendTerminatorLCToBuffer(CBuffer *, uint32_t) const;
void AppendVoiceLCToBuffer(CBuffer *, uint32_t, uint32_t) const;
void AppendTerminatorLCToBuffer(CBuffer *, uint32_t, uint32_t) const;
void ReplaceEMBInBuffer(CBuffer *, uint8_t) const;
void AppendDmrIdToBuffer(CBuffer *, uint32_t) const;
void AppendDmrRptrIdToBuffer(CBuffer *, uint32_t) const;

@ -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);
};

@ -278,7 +278,7 @@ const std::string CGateKeeper::ProtocolName(const EProtocol p) const
case EProtocol::dextra:
return "DExtra";
case EProtocol::dmrmmdvm:
return "MMDVM DMR";
return "DMR";
case EProtocol::dmrplus:
return "DMR+";
case EProtocol::urf:

@ -83,4 +83,7 @@ struct SJsonKeys {
struct DASHBOARD { const std::string enable, nngaddr, interval, debug; }
dashboard { "DashboardEnable", "DashboardNNGAddr", "DashboardInterval", "NNGDebug" };
struct DMR { const std::string xlx, single, timeout, hold, map_prefix; }
dmr { "XlxCompatibility", "SingleMode", "DefaultTimeout", "HoldTime", "Map" };
};

@ -27,9 +27,9 @@ DBUTIL = dbutil
include urfd.mk
ifeq ($(debug), true)
CFLAGS = -ggdb3 -DDEBUG -W -Werror -std=c++17 -MMD -MD
CFLAGS = -ggdb3 -DDEBUG -W -Werror -std=c++17 -MMD
else
CFLAGS = -W -Werror -std=c++17 -MMD -MD
CFLAGS = -W -Werror -std=c++17 -MMD
endif
LDFLAGS=-pthread -lcurl -lnng -lopus -logg
@ -40,7 +40,7 @@ else
CFLAGS += -DNO_DHT
endif
SRCS = $(filter-out test_audio.cpp, $(wildcard *.cpp))
SRCS = $(filter-out test_audio.cpp test_dmr.cpp, $(wildcard *.cpp))
OBJS = $(SRCS:.cpp=.o)
DEPS = $(SRCS:.cpp=.d)
DBUTILOBJS = Configure.o CurlGet.o Lookup.o LookupDmr.o LookupNxdn.o LookupYsf.o YSFNode.o Callsign.o
@ -56,11 +56,14 @@ $(INICHECK) : Configure.cpp CurlGet.o
$(DBUTIL) : Main.cpp $(DBUTILOBJS)
$(CXX) -DUTILITY $(CFLAGS) $< $(DBUTILOBJS) -o $@ -pthread -lcurl
test_dmr: test_dmr.cpp DMRScanner.o
$(CXX) $(CFLAGS) $^ -o $@ -pthread
%.o : %.cpp
$(CXX) $(CFLAGS) -c $< -o $@
clean :
$(RM) *.o *.d $(EXE) $(INICHECK) $(DBUTIL)
$(RM) *.o *.d $(EXE) $(INICHECK) $(DBUTIL) test_dmr
-include $(DEPS)

@ -229,6 +229,13 @@ char CProtocol::DmrDstIdToModule(uint32_t tg) const
uint32_t CProtocol::ModuleToDmrDestId(char m) const
{
// Check for custom mapping first (Mini DMR Mode)
std::string key = g_Keys.dmr.map_prefix + std::string(1, m);
if (g_Configure.Contains(key)) {
return g_Configure.GetUnsigned(key);
}
// Fallback to legacy XLX logic (A=1, B=2...)
return (uint32_t)(m - 'A')+1;
}

@ -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…
Cancel
Save

Powered by TurnKey Linux.