diff --git a/docs/DMR_Mini_Mode.md b/docs/DMR_Mini_Mode.md new file mode 100644 index 0000000..0f5d5c5 --- /dev/null +++ b/docs/DMR_Mini_Mode.md @@ -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. diff --git a/docs/MINIDMR_Architecture.md b/docs/MINIDMR_Architecture.md new file mode 100644 index 0000000..c947be3 --- /dev/null +++ b/docs/MINIDMR_Architecture.md @@ -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. diff --git a/reflector/Callsign.cpp b/reflector/Callsign.cpp index 893a834..30b77d3 100644 --- a/reflector/Callsign.cpp +++ b/reflector/Callsign.cpp @@ -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(); diff --git a/reflector/Client.h b/reflector/Client.h index 75dd416..673fec9 100644 --- a/reflector/Client.h +++ b/reflector/Client.h @@ -76,7 +76,7 @@ public: // reporting virtual void WriteXml(std::ofstream &); - void JsonReport(nlohmann::json &report); + virtual void JsonReport(nlohmann::json &report); protected: // data diff --git a/reflector/Configure.cpp b/reflector/Configure.cpp index ffbbde7..48a0769 100644 --- a/reflector/Configure.cpp +++ b/reflector/Configure.cpp @@ -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 &v) { @@ -104,25 +123,7 @@ static inline void split(const std::string &s, char delim, std::vector 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; } diff --git a/reflector/Configure.h b/reflector/Configure.h index 8a9e4c2..ea4c970 100644 --- a/reflector/Configure.h +++ b/reflector/Configure.h @@ -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') diff --git a/reflector/DMRMMDVMClient.cpp b/reflector/DMRMMDVMClient.cpp index 0c1cc1b..459e540 100644 --- a/reflector/DMRMMDVMClient.cpp +++ b/reflector/DMRMMDVMClient.cpp @@ -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 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); +} diff --git a/reflector/DMRMMDVMClient.h b/reflector/DMRMMDVMClient.h index 31c7fac..edc79b2 100644 --- a/reflector/DMRMMDVMClient.h +++ b/reflector/DMRMMDVMClient.h @@ -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; }; diff --git a/reflector/DMRMMDVMProtocol.cpp b/reflector/DMRMMDVMProtocol.cpp index eebc20f..a44edd3 100644 --- a/reflector/DMRMMDVMProtocol.cpp +++ b/reflector/DMRMMDVMProtocol.cpp @@ -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(Callsign, Ip)); + std::shared_ptr newClient = std::make_shared(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 &Hea std::shared_ptrclient = 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 dmrClient = std::dynamic_pointer_cast(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 &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 &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_ptrclient = 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 dmrClient = std::dynamic_pointer_cast(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 client = clients->FindClient(*callsign, Ip, EProtocol::dmrmmdvm); + if (client) { + std::shared_ptr dmrClient = std::dynamic_pointer_cast(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 client = clients->FindClient(*callsign, Ip, EProtocol::dmrmmdvm); + if (client) { + std::shared_ptr dmrClient = std::dynamic_pointer_cast(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_ptrAppend(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)); diff --git a/reflector/DMRMMDVMProtocol.h b/reflector/DMRMMDVMProtocol.h index 0d1bafa..997a030 100644 --- a/reflector/DMRMMDVMProtocol.h +++ b/reflector/DMRMMDVMProtocol.h @@ -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 &, 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); - 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; diff --git a/reflector/DMRScanner.cpp b/reflector/DMRScanner.cpp new file mode 100644 index 0000000..8fb818e --- /dev/null +++ b/reflector/DMRScanner.cpp @@ -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 +#include + +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 lock(m_Mutex); + m_SingleMode = singleMode; + m_DefaultTimeout = defaultTimeout; + m_HoldTime = holdTime; +} + +void CDMRScanner::UpdateSubscriptions(const std::string& options) +{ + std::lock_guard 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 ts1_tgs; + std::vector 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 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 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 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 lock(m_Mutex); + m_Subscriptions.clear(); + m_CurrentScanTG[0] = 0; + m_CurrentScanTG[1] = 0; +} + +bool CDMRScanner::IsSubscribed(unsigned int tgid) const +{ + std::lock_guard 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 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 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 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 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 CDMRScanner::GetSubscriptions(int slot) const +{ + std::lock_guard lock(m_Mutex); + if (m_Subscriptions.count(slot)) { + return m_Subscriptions.at(slot); + } + return {}; +} + +void CDMRScanner::GetActiveTalkgroups(std::vector& tgs) const +{ + std::lock_guard 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); + } + } +} diff --git a/reflector/DMRScanner.h b/reflector/DMRScanner.h new file mode 100644 index 0000000..070213c --- /dev/null +++ b/reflector/DMRScanner.h @@ -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 +#include +#include +#include +#include +#include + +#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 GetSubscriptions(int slot) const; + void GetActiveTalkgroups(std::vector& 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> 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); +}; diff --git a/reflector/GateKeeper.cpp b/reflector/GateKeeper.cpp index bc1e4cd..f74bfa5 100644 --- a/reflector/GateKeeper.cpp +++ b/reflector/GateKeeper.cpp @@ -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: diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index 87f9846..b90fbd0 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -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" }; }; diff --git a/reflector/Makefile b/reflector/Makefile index 3865ae0..39812a1 100644 --- a/reflector/Makefile +++ b/reflector/Makefile @@ -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) diff --git a/reflector/Protocol.cpp b/reflector/Protocol.cpp index e8680e7..236204b 100644 --- a/reflector/Protocol.cpp +++ b/reflector/Protocol.cpp @@ -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; } diff --git a/reflector/test_dmr.cpp b/reflector/test_dmr.cpp new file mode 100644 index 0000000..b9d1c77 --- /dev/null +++ b/reflector/test_dmr.cpp @@ -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 +#include +#include +#include + +// 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; +}