Skip to content

Commit 5170508

Browse files
committed
feat: add priority-aware talkgroup policy
1 parent 49319ca commit 5170508

44 files changed

Lines changed: 5316 additions & 1235 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/cli.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ Notes
187187
- Enable trunking (NXDN/P25/EDACS/DMR): `-T`
188188
- Conventional scan mode: `-Y` (not trunking; scans for sync on enabled decoders)
189189
- Channel map CSV: `-C <file>` (e.g., `connect_plus_chan.csv`)
190-
- Group list CSV (allow/block + labels): `-G <file>`
190+
- Group list CSV (allow/block + labels, optional `priority/preempt/audio/record/stream` policy columns): `-G <file>`
191191
- CSV formats and examples: `docs/csv-formats.md` and `examples/`
192192
- Use group list as allow/whitelist: `-W`
193193
- Tune controls: `-E` disable group calls, `-p` disable private calls, `-e` enable data calls, `--enc-lockout` do not tune encrypted P25 calls, `--enc-follow` allow encrypted (default)
@@ -202,6 +202,9 @@ Notes
202202
- Env (DMR): Hangtime and grant timeout overrides:
203203
- `DSD_NEO_DMR_HANGTIME=<seconds>` — post‑voice hangtime before returning to CC
204204
- `DSD_NEO_DMR_GRANT_TIMEOUT=<seconds>` — max seconds waiting for voice after grant
205+
- Env (priority preemption):
206+
- `DSD_NEO_TG_PREEMPT_MIN_DWELL_MS=<ms>` — minimum active call dwell before displacement (default `750`)
207+
- `DSD_NEO_TG_PREEMPT_COOLDOWN_MS=<ms>` — cooldown between displacement attempts (default `1000`)
205208

206209
## RTL‑SDR details (`-i rtl` / `-i rtltcp`)
207210

docs/csv-formats.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,33 @@ Required columns:
5050

5151
Notes:
5252

53-
- Only the first 3 columns are used; extra columns are ignored.
53+
- The first line is treated as header text and is required.
54+
- Legacy/default behavior uses only the first 3 columns; extra columns are ignored.
5455
- `mode` is matched literally by features that consult it:
5556
- `A` usually means allow/normal.
56-
- `B` and `DE` are treated as locked out in the UI helpers.
57-
- Names are not CSV-escaped; avoid commas in `name`.
57+
- `B` and `DE` are treated as locked out.
58+
- Names are not CSV-escaped; avoid commas and line breaks in fields.
59+
60+
Extended policy columns are supported only when the header opts into this exact ordered prefix starting at column 4:
61+
62+
1. `priority` (0..100, default `0`)
63+
2. `preempt` (`true`/`false`, default `false`)
64+
3. `audio` (`on`/`off`, default from `mode`)
65+
4. `record` (`on`/`off`, default from `mode`)
66+
5. `stream` (`on`/`off`, default from `mode`)
67+
6. `tags` (free text metadata)
68+
69+
Important behavior:
70+
71+
- The header must contain `priority` in column 4 and continue in that order for the available policy columns.
72+
- If the header is legacy/unknown (for example `id,mode,name,metadata`), optional policy parsing is disabled and extra
73+
columns remain legacy metadata.
74+
- `id` supports exact IDs (`1201`) and ranges (`1200-1299`).
75+
- Exact rows populate both runtime alias display state and policy.
76+
- Range rows are policy-only and are not inserted as exact aliases.
77+
- Exact duplicates preserve first-match behavior.
78+
- `audio=off` forces `record=off` and `stream=off`.
79+
- `mode=B`/`DE` forces media fields off regardless of optional values.
5880

5981
Example:
6082

@@ -64,6 +86,15 @@ DEC,Mode(A=Allow; B=Block; DE=Enc),Name,Tag
6486
22033,DE,Law Dispatch,Law
6587
```
6688

89+
Extended policy example:
90+
91+
```csv
92+
id,mode,name,priority,preempt,audio,record,stream,tags
93+
1201,A,Dispatch 1,80,true,on,on,on,primary
94+
1202,A,Dispatch 2,40,false,on,off,on,secondary
95+
1300-1399,A,Ops Range,10,false,on,on,on,wide
96+
```
97+
6798
## Decimal Key List CSV (`-k <file>`)
6899

69100
Purpose: Import decimal key IDs and values for basic privacy/scrambler helpers.

docs/ui-terminal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ Common controls:
2525
- Back / close: `Esc`, `q`, `Left`
2626
- Item help: `h`
2727

28+
Group policy reload:
29+
30+
- In the Trunking/Import menu path, importing a group list (`-G` CSV) performs a full policy reload. On parse failure,
31+
the currently loaded list remains active.
32+
2833
## Hotkeys (Main Screen)
2934

3035
Keys are case-sensitive. Some commands only make sense in specific modes (for example, trunking controls require

include/dsd-neo/core/csv_import.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extern "C" {
2020
#endif
2121

2222
int csvGroupImport(dsd_opts* opts, dsd_state* state);
23+
int csvGroupImportPath(const char* group_file_path, dsd_state* state);
2324
int csvLCNImport(dsd_opts* opts, dsd_state* state);
2425
int csvChanImport(dsd_opts* opts, dsd_state* state);
2526
int csvKeyImportDec(dsd_opts* opts, dsd_state* state);

include/dsd-neo/core/state_ext.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ enum { DSD_STATE_EXT_MAX = 32 };
4545
typedef enum dsd_state_ext_id {
4646
DSD_STATE_EXT_ENGINE_START_MS = 0,
4747
DSD_STATE_EXT_ENGINE_TRUNK_CC_CANDIDATES = 1,
48+
/*
49+
* DSD_STATE_EXT_CORE_TG_POLICY lives in the engine range (0-7) because it
50+
* is a cross-cutting core facility, not module-private state. Engine owns
51+
* 0-1; this slot is a documented exception, not a precedent for arbitrary
52+
* core use.
53+
*/
54+
DSD_STATE_EXT_CORE_TG_POLICY = 2,
4855
DSD_STATE_EXT_PROTO_NXDN_TRUNK_DIAG = 24,
4956
} dsd_state_ext_id;
5057

@@ -54,6 +61,8 @@ typedef void (*dsd_state_ext_cleanup_fn)(void*);
5461

5562
void* dsd_state_ext_get(dsd_state* state, dsd_state_ext_id id);
5663

64+
const void* dsd_state_ext_get_const(const dsd_state* state, dsd_state_ext_id id);
65+
5766
int dsd_state_ext_set(dsd_state* state, dsd_state_ext_id id, void* ptr, dsd_state_ext_cleanup_fn cleanup);
5867

5968
void dsd_state_ext_free_all(dsd_state* state);
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
/*
3+
* Copyright (C) 2026 by arancormonk <180709949+arancormonk@users.noreply.github.com>
4+
*/
5+
6+
/**
7+
* @file
8+
* @brief Talkgroup/private policy evaluation and shared mutation helpers.
9+
*/
10+
11+
#pragma once
12+
13+
#include <dsd-neo/core/opts_fwd.h>
14+
#include <dsd-neo/core/state_fwd.h>
15+
16+
#include <stdint.h>
17+
18+
#ifdef __cplusplus
19+
extern "C" {
20+
#endif
21+
22+
typedef enum {
23+
DSD_TG_POLICY_MATCH_NONE = 0,
24+
DSD_TG_POLICY_MATCH_RANGE = 1,
25+
DSD_TG_POLICY_MATCH_EXACT = 2,
26+
} dsd_tg_policy_match_type;
27+
28+
typedef enum {
29+
DSD_TG_POLICY_PRIVATE_ALLOWLIST_UNKNOWN_ALLOW = 0,
30+
DSD_TG_POLICY_PRIVATE_ALLOWLIST_UNKNOWN_BLOCK = 1,
31+
} dsd_tg_policy_private_allowlist_mode;
32+
33+
typedef enum {
34+
DSD_TG_POLICY_HOLD_COMPAT_GRANT = 0,
35+
DSD_TG_POLICY_HOLD_FORCE_MEDIA_ONLY = 1,
36+
DSD_TG_POLICY_HOLD_FORCE_TUNE_AND_MEDIA = 2,
37+
} dsd_tg_policy_hold_behavior;
38+
39+
typedef enum {
40+
DSD_TG_POLICY_SOURCE_IMPORTED = 0,
41+
DSD_TG_POLICY_SOURCE_RUNTIME_ALIAS = 1,
42+
DSD_TG_POLICY_SOURCE_USER_LOCKOUT = 2,
43+
DSD_TG_POLICY_SOURCE_ENC_LOCKOUT = 3,
44+
DSD_TG_POLICY_SOURCE_LEGACY_UNKNOWN = 4,
45+
} dsd_tg_policy_entry_source;
46+
47+
typedef enum {
48+
DSD_TG_POLICY_UPSERT_ADD_IF_MISSING = 0,
49+
DSD_TG_POLICY_UPSERT_REPLACE_FIRST = 1,
50+
DSD_TG_POLICY_UPSERT_REPLACE_LEARNED_ONLY = 2,
51+
} dsd_tg_policy_upsert_mode;
52+
53+
typedef enum {
54+
DSD_TG_POLICY_BLOCK_NONE = 0u,
55+
DSD_TG_POLICY_BLOCK_GROUP_DISABLED = 1u << 0,
56+
DSD_TG_POLICY_BLOCK_PRIVATE_DISABLED = 1u << 1,
57+
DSD_TG_POLICY_BLOCK_DATA_DISABLED = 1u << 2,
58+
DSD_TG_POLICY_BLOCK_ENCRYPTED_DISABLED = 1u << 3,
59+
DSD_TG_POLICY_BLOCK_ALLOWLIST = 1u << 4,
60+
DSD_TG_POLICY_BLOCK_MODE = 1u << 5,
61+
DSD_TG_POLICY_BLOCK_HOLD = 1u << 6,
62+
DSD_TG_POLICY_BLOCK_AUDIO = 1u << 7,
63+
DSD_TG_POLICY_BLOCK_RECORD = 1u << 8,
64+
DSD_TG_POLICY_BLOCK_STREAM = 1u << 9,
65+
} dsd_tg_policy_block_reason;
66+
67+
typedef struct {
68+
uint32_t id_start;
69+
uint32_t id_end;
70+
char mode[8];
71+
char name[50];
72+
int priority;
73+
uint8_t preempt;
74+
uint8_t audio;
75+
uint8_t record;
76+
uint8_t stream;
77+
uint8_t is_range;
78+
uint8_t source;
79+
unsigned int row;
80+
} dsd_tg_policy_entry;
81+
82+
typedef struct {
83+
dsd_tg_policy_match_type match;
84+
dsd_tg_policy_entry entry;
85+
} dsd_tg_policy_lookup;
86+
87+
typedef struct {
88+
uint32_t target_id;
89+
uint32_t source_id;
90+
int encrypted;
91+
int data_call;
92+
int tune_allowed;
93+
int audio_allowed;
94+
int record_allowed;
95+
int stream_allowed;
96+
int priority;
97+
int preempt_requested;
98+
uint32_t block_reasons;
99+
int tg_hold_active;
100+
int tg_hold_match;
101+
char mode[8];
102+
char name[50];
103+
dsd_tg_policy_match_type match;
104+
} dsd_tg_policy_decision;
105+
106+
typedef struct {
107+
uint32_t target_id;
108+
uint32_t source_id;
109+
long freq_hz;
110+
int channel;
111+
int slot;
112+
int requires_tuner_retune;
113+
} dsd_tg_policy_call_route;
114+
115+
int dsd_tg_policy_make_legacy_exact_entry(uint32_t id, const char* mode, const char* name,
116+
dsd_tg_policy_entry_source source, dsd_tg_policy_entry* out);
117+
int dsd_tg_policy_add_range_entry(dsd_state* state, const dsd_tg_policy_entry* entry);
118+
int dsd_tg_policy_lookup_id(const dsd_state* state, uint32_t id, dsd_tg_policy_lookup* out);
119+
int dsd_tg_policy_evaluate_group_call(const dsd_opts* opts, const dsd_state* state, uint32_t tg, uint32_t src,
120+
int encrypted, int data_call, dsd_tg_policy_hold_behavior hold_behavior,
121+
dsd_tg_policy_decision* out);
122+
int dsd_tg_policy_evaluate_private_call(const dsd_opts* opts, const dsd_state* state, uint32_t src, uint32_t dst,
123+
int encrypted, int data_call,
124+
dsd_tg_policy_private_allowlist_mode allowlist_mode,
125+
dsd_tg_policy_hold_behavior hold_behavior, dsd_tg_policy_decision* out);
126+
int dsd_tg_policy_append_legacy_exact(dsd_state* state, const dsd_tg_policy_entry* entry);
127+
int dsd_tg_policy_upsert_legacy_exact(dsd_state* state, const dsd_tg_policy_entry* entry,
128+
dsd_tg_policy_upsert_mode mode);
129+
int dsd_tg_policy_append_group_file_row(const dsd_opts* opts, const dsd_tg_policy_entry* entry,
130+
const char* legacy_metadata);
131+
132+
int dsd_tg_policy_should_preempt(const dsd_opts* opts, const dsd_state* state,
133+
const dsd_tg_policy_call_route* candidate_route,
134+
const dsd_tg_policy_decision* candidate, double now_mono_s);
135+
int dsd_tg_policy_note_active_call(dsd_state* state, const dsd_tg_policy_call_route* route,
136+
const dsd_tg_policy_decision* decision, double now_mono_s);
137+
int dsd_tg_policy_clear_active_call(dsd_state* state, int slot);
138+
int dsd_tg_policy_clear_active_call_route(dsd_state* state, const dsd_tg_policy_call_route* route);
139+
140+
int dsd_tg_policy_reload_group_file(dsd_opts* opts, dsd_state* state);
141+
142+
#ifdef __cplusplus
143+
}
144+
#endif

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ target_sources(dsd-neo_core PRIVATE
2020
util/dsd_events.c
2121
util/dsd_alias.c
2222
util/dsd_reset.c
23+
util/talkgroup_policy.c
2324
util/synctype.c
2425
file/dsd_file.c
2526
file/dsd_import.c

src/core/audio/dsd_audio2.c

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
#include <mbelib.h>
2626
#include <sndfile.h>
2727
#include <stdint.h>
28-
#include <stdio.h>
2928
#include <string.h>
3029
#include <sys/types.h>
3130

@@ -1327,57 +1326,9 @@ playSynthesizedVoiceSS4(dsd_opts* opts, dsd_state* state) {
13271326
encR = 1;
13281327
}
13291328

1330-
//WIP: Mute if on B list (or not W list)
1331-
char modeL[8];
1332-
sprintf(modeL, "%s", "");
1333-
char modeR[8];
1334-
sprintf(modeR, "%s", "");
1335-
13361329
unsigned long TGL = (unsigned long)state->lasttg;
13371330
unsigned long TGR = (unsigned long)state->lasttgR;
1338-
1339-
//if we are using allow/whitelist mode, then write 'B' to mode for block
1340-
//comparison below will look for an 'A' to write to mode if it is allowed
1341-
if (opts->trunk_use_allow_list == 1) {
1342-
sprintf(modeL, "%s", "B");
1343-
sprintf(modeR, "%s", "B");
1344-
}
1345-
1346-
for (unsigned int gi = 0; gi < state->group_tally; gi++) {
1347-
if (state->group_array[gi].groupNumber == TGL) {
1348-
strncpy(modeL, state->group_array[gi].groupMode, sizeof(modeL) - 1);
1349-
modeL[sizeof(modeL) - 1] = '\0';
1350-
// break; //need to keep going to check other potential slot group
1351-
}
1352-
if (state->group_array[gi].groupNumber == TGR) {
1353-
strncpy(modeR, state->group_array[gi].groupMode, sizeof(modeR) - 1);
1354-
modeR[sizeof(modeR) - 1] = '\0';
1355-
// break; //need to keep going to check other potential slot group
1356-
}
1357-
}
1358-
1359-
//flag either left or right as 'enc' to mute if B
1360-
if (strcmp(modeL, "B") == 0) {
1361-
encL = 1;
1362-
}
1363-
if (strcmp(modeR, "B") == 0) {
1364-
encR = 1;
1365-
}
1366-
1367-
//if TG Hold in place, mute anything but that TG #132
1368-
if (state->tg_hold != 0 && state->tg_hold != (uint32_t)TGL) {
1369-
encL = 1;
1370-
}
1371-
if (state->tg_hold != 0 && state->tg_hold != (uint32_t)TGR) {
1372-
encR = 1;
1373-
}
1374-
//likewise, override and unmute if TG hold matches TG
1375-
if (state->tg_hold != 0 && state->tg_hold == (uint32_t)TGL) {
1376-
encL = 0;
1377-
}
1378-
if (state->tg_hold != 0 && state->tg_hold == (uint32_t)TGR) {
1379-
encR = 0;
1380-
}
1331+
(void)dsd_audio_group_gate_dual(opts, state, TGL, TGR, encL, encR, &encL, &encR);
13811332

13821333
//test hpf
13831334
if (opts->use_hpf_d == 1) {

0 commit comments

Comments
 (0)