The stream tracking system is the core of HBlink4's DMR traffic management, providing per-slot per-repeater transmission state tracking with sophisticated contention handling and intelligent routing. This system enables HBlink4 to correctly manage simultaneous transmissions, prevent slot hijacking, and efficiently forward traffic between repeaters.
Key Capabilities:
- Fast Terminator Detection: Streams end in ~60ms (primary method) vs 2000ms timeout (fallback)
- Hang Time Protection: Prevents slot hijacking between transmissions in a conversation
- Per-Stream Routing Cache: Calculate-once, forward-many (O(1) routing decisions)
- RX/TX Contention Handling: Automatic bandwidth optimization when repeaters start receiving
- Bidirectional Routing: Symmetric inbound/outbound talkgroup filtering
Each repeater has two independent timeslots (1 and 2) that can carry different streams simultaneously. A repeater can be receiving on slot 1 while transmitting on slot 2, or handling different talkgroups on each slot.
When multiple sources attempt to use the same slot:
- Active streams: First stream wins, others are denied (contention)
- Hang time period: After stream ends, slot is reserved for the original source only
- Hang time rules:
- Same user: Can continue on same TG or switch to different TG (fast TG switching)
- Different user, same TG: Can join conversation (multi-user conversation)
- Different user, different TG: DENIED (hijacking prevention)
Primary stream ending method using DMR frame analysis:
- Terminator frame detected: Stream ends immediately (~60ms after PTT release)
- Timeout fallback: Used only when terminator packet is lost (~2000ms delay)
- Result: Near-instant slot availability for legitimate users
Traffic forwarding uses per-stream routing caches:
- Calculate once: Routing decisions made at stream start
- Forward many: All packets use cached targets (O(1) forwarding)
- Inbound/outbound filtering: Symmetric talkgroup access control
- Automatic optimization: Repeaters removed from cache when they start receiving
- Real streams (RX): Traffic received FROM a repeater (actual RF activity)
- Assumed streams (TX): Traffic sent TO a repeater (what we're forwarding)
- Priority: Real streams always win over assumed streams (repeater RX > server TX)
Tracks an active DMR transmission stream on a specific repeater slot.
@dataclass
class StreamState:
repeater_id: bytes # Repeater this stream is on (4 bytes)
rf_src: bytes # RF source DMR ID (3 bytes)
dst_id: bytes # Destination talkgroup/ID (3 bytes)
slot: int # Timeslot (1 or 2)
start_time: float # When transmission started (Unix timestamp)
last_seen: float # Last packet received (Unix timestamp)
stream_id: bytes # Unique stream identifier (4 bytes)
packet_count: int = 0 # Number of packets in this stream
ended: bool = False # True when stream ended (in hang time)
end_time: Optional[float] = None # When stream ended (for hang time calculation)
call_type: str = "unknown" # "group", "private", "data", or "unknown"
is_assumed: bool = False # True if TX stream (forwarded TO repeater)
target_repeaters: Optional[set] = None # Cached set of repeater_ids for forwarding
routing_cached: bool = False # True once routing calculated
lc_base: Optional[bytes] = None # 9-byte Link Control captured at stream start
# (decoded from VHEAD if present, else synthesized)
lc_cache: Dict[Tuple[bytes, bytes], Any] = field(default_factory=dict)
# Per-target (out_dst, out_src) → encoded
# (h_lc, t_lc, emb_lc) for payload rewrite under
# translation. See docs/dmrd_translation.md.Key Methods:
is_active(timeout: float): Returns True if stream has received a packet within timeout periodis_in_hang_time(timeout: float, hang_time: float): Returns True if stream is in hang time period
Stream Types:
- RX Stream (
is_assumed=False): Traffic received from a repeater's local users - TX Stream (
is_assumed=True): Traffic we're forwarding to a repeater (tracking what we send)
Stream tracking fields added to RepeaterState:
# Talkgroup access control (stored as sets for O(1) lookup)
# None = allow all, empty set = deny all, non-empty set = allow only those TGs
slot1_talkgroups: Optional[set] = None
slot2_talkgroups: Optional[set] = None
rpto_received: bool = False # True if repeater sent RPTO
# Active stream tracking per slot
slot1_stream: Optional[StreamState] = None
slot2_stream: Optional[StreamState] = NoneKey Methods:
get_slot_stream(slot: int): Get the active stream for a slot (1 or 2)set_slot_stream(slot: int, stream: Optional[StreamState]): Set or clear a slot's stream
When the first DMRD packet arrives on an idle slot:
def _handle_stream_start(repeater, rf_src, dst_id, slot, stream_id, call_type_bit) -> boolProcess:
-
Check for existing stream:
- If same
stream_id: Allow (continuation - should not happen, but handle gracefully) - If different
stream_idand stream active: DENY (contention) - If different
stream_idand stream in hang time: Check hang time rules
- If same
-
Hang Time Rules (if stream exists and ended):
- Same user (
rf_srcmatches): Allow (can continue or switch TG) - Different user, same TG (
dst_idmatches): Allow (join conversation) - Different user, different TG: DENY (hijacking prevention)
- Same user (
-
Inbound Routing Check (if slot available):
- Call
_check_inbound_routing(repeater_id, slot, tgid) - Verify TG is in repeater's allowed list for this slot
- If denied: Log warning and return False
- Call
-
Calculate Routing Targets (once per stream):
- Call
_calculate_stream_targets(...)to find which repeaters should receive this traffic - Checks outbound routing rules for each potential target
- Checks slot availability (no contention on target)
- Returns set of repeater_ids that will receive ALL packets in this stream
- Call
-
Create StreamState:
- Initialize with
is_assumed=False(real RX stream) - Store routing targets in
target_repeaters - Set
routing_cached=True - Set
packet_count=1
- Initialize with
-
Assign to slot:
repeater.set_slot_stream(slot, new_stream) -
Log stream start:
INFO - RX stream started on repeater X slot Y: src=..., dst=..., stream_id=..., targets=N -
Emit event:
stream_startevent to dashboard -
Update user cache: Record last known repeater for this DMR ID (for private call routing)
When a DMRD packet arrives on a slot with an active stream:
def _handle_stream_packet(repeater, rf_src, dst_id, slot, stream_id, call_type_bit) -> boolProcess:
-
Get current stream:
current_stream = repeater.get_slot_stream(slot) -
If no stream: Call
_handle_stream_start()(first packet) -
Stream ID validation:
- Same
stream_id: Update stream state - Different
stream_id: Check for fast terminator or contention
- Same
-
Fast Terminator Detection (if different
stream_id):- Check if old stream is still active (< 200ms since last packet)
- If > 200ms: Old stream terminated without terminator packet
- Log fast terminator, call
_end_stream(), allow new stream via_handle_stream_start() - If < 200ms: Real contention, deny new stream
-
Update stream state (if same stream):
stream.last_seen = current_timestream.packet_count += 1
-
Return True: Packet is valid for forwarding
When a DMR terminator frame is detected:
def _is_dmr_terminator(data: bytes, frame_type: int) -> boolTerminator Detection:
- Byte 15 (_bits) bits 4-5: frame_type must be 0x2 (HBPF_DATA_SYNC)
- Byte 15 (_bits) bits 0-3: dtype_vseq must be 0x2 (HBPF_SLT_VTERM)
- Result: Frame type 0x2 AND dtype 0x2 = terminator frame
When terminator detected:
-
Call
_end_stream(stream, repeater_id, slot, current_time, 'terminator') -
Unified ending logic:
- Set
stream.ended = True - Set
stream.end_time = current_time - Calculate duration:
duration = end_time - start_time - Log stream end:
INFO - RX stream ended: ... duration=X.XXs, packets=N, entering hang time (10.0s) - Emit
stream_endevent to dashboard - Do NOT clear slot - stream enters hang time
- Set
-
Hang time begins: Slot reserved for original
rf_src
Timing: ~60ms after PTT release (near-instant slot availability)
Checked every 1 second by _check_stream_timeouts():
Process:
- For each connected repeater, check both slots
- If stream exists and not active (> 2.0 seconds since last packet):
- If not ended: Call
_end_stream(stream, repeater_id, slot, current_time, 'timeout') - Enters hang time (same as terminator method)
- If not ended: Call
- If stream ended and hang time expired:
- Clear slot:
repeater.set_slot_stream(slot, None) - Log:
DEBUG - RX hang time completed on repeater X slot Y - Emit
hang_time_expiredevent - Slot now available for new streams from any source
- Clear slot:
Timing: ~2000ms after last packet (fallback for lost terminators only)
When a new stream arrives with different stream_id while a stream is active:
Check for stale stream:
time_since_last_packet = current_time - current_stream.last_seen- If > 200ms: Old stream effectively terminated (no terminator packet received)
- If < 200ms: Real contention (deny new stream)
If fast terminator detected:
- Log:
INFO - Fast terminator: stream on repeater X slot Y ended via inactivity (XXXms since last packet) - Call
_end_stream(stream, repeater_id, slot, current_time, 'fast_terminator') - Stream enters hang time
- Call
_handle_stream_start()to check if new stream allowed (hang time rules apply)
Benefit: Detects operator releasing PTT without transmitting terminator frame (quick key-ups)
When a DMRD packet arrives with a different stream_id than the active stream:
Scenarios:
A. Active stream (< 200ms since last packet):
- Result: DENY new stream
- Log:
WARNING - Stream contention on repeater X slot Y: existing stream (...) vs new stream (...) - Reason: First-come-first-served - active transmission has priority
B. Stale stream (> 200ms since last packet):
- Result: Fast terminator detection
- Action: End old stream, evaluate new stream against hang time rules
- Log: Fast terminator log + hang time check
C. Stream in hang time:
- Check hang time rules:
- Same
rf_src: Allow (same user continuing or switching TG) - Different
rf_src, samedst_id: Allow (different user joining same TG conversation) - Different
rf_src, differentdst_id: DENY (hijacking attempt)
- Same
- Log: Appropriate message based on rule matched
Hang time provides sophisticated protection against slot hijacking while allowing legitimate multi-user conversations and fast TG switching.
{
"global": {
"stream_hang_time": 10.0 // Seconds (10-20 recommended)
}
}Recommended values:
- 10 seconds: Fast-paced networks, experienced operators
- 15 seconds: Balanced (default)
- 20 seconds: Slower operators, roundtable conversations
| Scenario | rf_src Match | dst_id Match | Result | Use Case |
|---|---|---|---|---|
| Same user, same TG | ✓ Yes | ✓ Yes | ✅ ALLOW | User continuing conversation |
| Same user, different TG | ✓ Yes | ✗ No | ✅ ALLOW | Fast TG switching |
| Different user, same TG | ✗ No | ✓ Yes | ✅ ALLOW | Multi-user conversation |
| Different user, different TG | ✗ No | ✗ No | ❌ DENY | Hijacking prevention |
Timeline:
t=0s : User 312123 transmits on TG 3120
t=2.5s : User 312123 releases PTT (terminator detected)
t=2.5s : Hang time begins (10.0s), slot reserved for 312123
t=4.0s : User 312123 keys up again on TG 3120
Result : ✅ ALLOWED - Same user, same TG
Log: "Same user continuing conversation during hang time: src=312123, dst=3120"
Timeline:
t=0s : User 312123 transmits on TG 3120
t=2.5s : User 312123 releases PTT (terminator detected)
t=2.5s : Hang time begins (10.0s), slot reserved for 312123
t=3.0s : User 312123 keys up on TG 9 (different talkgroup!)
Result : ✅ ALLOWED - Same user can switch TGs
Log: "Same user switching talkgroup during hang time: src=312123, old_dst=3120, new_dst=9"
Note: This enables operators to quickly switch between talkgroups without waiting for hang time to expire.
Timeline:
t=0s : User 312123 transmits on TG 3120
t=2.5s : User 312123 releases PTT (terminator detected)
t=2.5s : Hang time begins (10.0s), slot reserved for 312123
t=4.0s : User 312456 keys up on TG 3120 (different user, SAME TG!)
Result : ✅ ALLOWED - Different user joining conversation
Log: "Different user joining conversation during hang time: old_src=312123, new_src=312456, dst=3120"
Benefit: Allows natural roundtable conversations without hang time interference.
Timeline:
t=0s : User 312123 transmits on TG 3120
t=2.5s : User 312123 releases PTT (terminator detected)
t=2.5s : Hang time begins (10.0s), slot reserved for 312123
t=4.0s : User 312456 tries to transmit on TG 9 (different user, different TG!)
Result : ❌ DENIED - Hijacking attempt blocked
Log: "Hang time hijacking blocked: slot reserved for TG 3120, denied src=312456 attempting TG 9"
This is the key protection: Prevents users from stealing slots mid-conversation.
Network topology:
- Repeater A: TGs [3120, 9]
- Repeater B: TGs [3120, 3121] (shares 3120 with A, shares 3121 with C)
- Repeater C: TGs [3121, 8]
Note: Repeaters A and C do NOT share any talkgroups (isolated by B).
Scenario: A transmits TG 3120, B forwards to A, then C tries TG 3121
Timeline:
---------
t=0s: User on Repeater A transmits TG 3120
→ Server receives from A (RX stream on A slot 1)
→ Server calculates targets: B has TG 3120
→ Server forwards to B (assumed TX stream on B slot 1)
Repeater A Slot 1: RX stream (src=312123, dst=3120)
Repeater B Slot 1: TX assumed stream (forwarded from A)
Repeater C Slot 1: IDLE (doesn't have TG 3120)
t=2.5s: User on A releases PTT (terminator detected)
→ A enters hang time (reserved for user 312123, TG 3120)
→ B enters hang time (assumed stream ended)
Repeater A Slot 1: HANG TIME (reserved for 312123 on TG 3120)
Repeater B Slot 1: HANG TIME (assumed, lower priority)
Repeater C Slot 1: IDLE
t=3.0s: User on Repeater C tries to transmit TG 3121
→ Server receives from C (RX stream attempt)
→ Server calculates targets: B has TG 3121 ✓
→ Server checks B slot 1 availability
→ B slot 1 is in hang time (assumed stream, TG 3120)
→ Assumed streams have LOW priority (real RX > assumed TX)
→ C's real RX wins over B's assumed TX
→ Server clears B's assumed stream
→ Server forwards C's TG 3121 to B
Repeater A Slot 1: HANG TIME (still reserved for 312123)
Repeater B Slot 1: RX from C (TG 3121) ✅ ALLOWED
Repeater C Slot 1: RX stream (src=312789, dst=3121)
Result: ✅ C successfully uses B slot 1 because:
1. A and C don't share TGs (isolated)
2. B's slot 1 only had assumed stream (low priority)
3. Real RX always wins over assumed TX
Key Points:
- Hang time on A protects A's slot from hijacking
- Hang time on B (assumed) doesn't block C (real RX wins)
- No TG overlap between A and C prevents interference
- B acts as isolated bridge between A and C networks
HBlink4 uses a "calculate-once, forward-many" approach:
At stream start (_calculate_stream_targets):
- Extract talkgroup from
dst_id - For each connected repeater (except source):
- Check outbound routing: Does target have this TG in allowed list?
- Check slot availability: Is target slot idle or only in assumed hang time?
- If both checks pass: Add to
target_repeatersset
- Store set in
StreamState.target_repeaters - Set
StreamState.routing_cached = True
For every packet (_forward_stream):
- Check if
stream.routing_cachedis True - If yes: Use
stream.target_repeatersset (O(1) lookup) - For each target in set: Send packet
- No recalculation needed - routing decisions persist for entire stream
Benefits:
- Performance: O(1) forwarding per packet (set iteration)
- Consistency: All packets in stream follow same route
- Scalability: Efficient with 100+ repeaters
- Simplicity: No per-packet routing logic
Symmetric routing: Same TG lists control both directions.
Inbound (FROM repeater):
def _check_inbound_routing(repeater_id: bytes, slot: int, tgid: int) -> bool:
allowed_tgids = repeater.slot1_talkgroups (or slot2_talkgroups)
return tgid in allowed_tgids # O(1) set membership- Called when packet ARRIVES from repeater
- Checks if repeater is ALLOWED TO SEND this TG
- If denied: Drop packet, log warning
Outbound (TO repeater):
def _check_outbound_routing(repeater_id: bytes, slot: int, tgid: int) -> bool:
allowed_tgids = repeater.slot1_talkgroups (or slot2_talkgroups)
return tgid in allowed_tgids # O(1) set membership- Called during
_calculate_stream_targets - Checks if repeater is ALLOWED TO RECEIVE this TG
- If denied: Exclude from target set
Talkgroup Filtering Modes:
| Config | Value | Behavior |
|---|---|---|
| Not configured | N/A | Allow ALL talkgroups (legacy mode) |
slot1_talkgroups: null |
None |
Allow ALL talkgroups |
slot1_talkgroups: [] |
[] (empty) |
DENY ALL (slot disabled) |
slot1_talkgroups: [1,2,3] |
Set {1,2,3} |
Allow ONLY listed TGs |
When server forwards traffic TO a repeater (TX), it creates an "assumed stream" to track what it's sending. However, repeaters have local users who can key up at any time, creating RX traffic. This creates a contention scenario.
Without proper handling:
- Server continues sending packets to busy repeater
- Repeater hardware ignores packets (can't TX and RX simultaneously)
- Wasted bandwidth (potentially significant on multi-repeater networks)
When a repeater starts receiving (RX) while we have an assumed stream (TX) to it:
Detection:
if current_stream and current_stream.is_assumed and not current_stream.ended:
# Repeater starting RX while we have active assumed TXRoute-Cache Removal:
# Remove this repeater from ALL active streams' target_repeaters
for other_repeater in self._repeaters.values():
for other_slot in [1, 2]:
other_stream = other_repeater.get_slot_stream(other_slot)
if other_stream and other_stream.routing_cached:
if repeater.repeater_id in other_stream.target_repeaters:
other_stream.target_repeaters.discard(repeater.repeater_id)Result:
- Immediate bandwidth savings (no more packets to this repeater)
- Real RX stream processed normally
- Automatic - no configuration needed
Performance: O(R×S) where R = repeaters (~10-50), S = slots (2)
- Typical: 20-100 operations when contention detected
- Frequency: Rare (only when repeater starts RX with assumed TX present)
- Method:
set.discard()is O(1) per removal
| Current Stream | New Stream | Result | Reason |
|---|---|---|---|
| Real RX | Real RX | Contention check | Normal rules apply |
| Real RX | Assumed TX | Don't create | Slot busy with real traffic |
| Assumed TX | Real RX | Real wins | Remove from route-cache |
| Assumed TX | Assumed TX | Update | Track multiple forwarding targets |
Key principle: Real (RX) always wins over Assumed (TX)
INFO - Repeater 312100 slot 1 starting RX while we have active assumed TX stream - repeater wins, removing from active route-caches
DEBUG - Removed repeater 312100 from route-cache of stream on repeater 312101 slot 1
INFO - RX stream started on repeater 312100 slot 1: src=312567, dst=3121, stream_id=abc123, targets=3
HBlink4 uses a user cache to enable efficient private call routing.
User Cache:
- Tracks last known repeater for each DMR ID
- Updated on every transmission (stream start)
- Timeout: 600 seconds (configurable, minimum 60)
- Cleanup: Every 60 seconds (removes expired entries)
Private Call Process:
- Extract
dst_idfrom packet - Check
call_typebit: 0 = private, 1 = group - If private call:
- Look up
dst_idin user cache - If found: Route to that specific repeater only
- If not found: Drop packet (user not seen recently)
- Look up
- If group call:
- Use normal talkgroup routing (all repeaters with this TG)
Configuration:
{
"global": {
"user_cache": {
"timeout": 600 // Seconds (minimum 60)
}
}
}Per Repeater:
- RepeaterState: ~2KB
- StreamState (max 2): ~400 bytes each
- Total: ~2.8KB per repeater
100 Repeaters: ~280KB (negligible)
Per-Packet Operations:
- Repeater lookup: O(1) dict lookup
- Stream validation: O(1) stream_id comparison
- Routing decision: O(1) set membership (cached targets)
- Forwarding: O(T) where T = targets in cache (typically 3-10)
Periodic Tasks:
- Stream timeout check: Every 1 second, O(R×2) where R = repeaters
- User cache cleanup: Every 60 seconds, O(U) where U = cached users
- Forwarding stats: Every 5 seconds, O(1)
Scalability: Linear O(n) with repeater count, sub-millisecond per-packet processing
Without Route-Cache Optimization:
- Worst case: Forwarding to 50 repeaters with 10 active streams
- Wasted bandwidth: ~100KB/s per busy repeater
With Route-Cache Optimization:
- Assumed streams removed immediately when repeater starts RX
- Bandwidth saved: Up to 100KB/s per optimized repeater
- Detection latency: < 60ms (first RX packet)
Stream tracking respects repeater configuration patterns and talkgroup access control.
Example Configuration:
{
"repeater_configurations": {
"patterns": [
{
"name": "KS-DMR Network",
"match": {"id_ranges": [[312000, 312099]]},
"config": {
"passphrase": "secret",
"slot1_talkgroups": [8, 9],
"slot2_talkgroups": [3120, 3121, 3122]
}
}
],
"default": {
"passphrase": "default-pass",
"slot1_talkgroups": [8],
"slot2_talkgroups": [8]
}
}
}Effect on Stream Tracking:
Repeater 312050 (matches KS-DMR pattern):
- Slot 1: Can RX and TX TGs 8, 9 only
- Slot 2: Can RX and TX TGs 3120, 3121, 3122 only
- Packet for TG 1 on slot 1: DENIED (inbound routing check fails)
- Forward TG 3120 to this repeater slot 2: ALLOWED (outbound routing check passes)
Repeater 999999 (matches default):
- Slot 1: Can RX and TX TG 8 only
- Slot 2: Can RX and TX TG 8 only
- Very restricted access (guest/untrusted repeater pattern)
HBlink4 provides comprehensive logging of stream activity:
INFO - RX stream started on repeater 312100 slot 1: src=3121234, dst=3120, stream_id=a1b2c3d4, targets=3
DEBUG - TX stream created on repeater 312100 slot 1: dst=3120 (assumed, forwarding)
INFO - RX stream ended on repeater 312100 slot 1: src=3121234, dst=3120, duration=2.46s, packets=41, entering hang time (10.0s)
INFO - RX stream ended on repeater 312100 slot 1: src=3121234, dst=3120, duration=4.52s, packets=226, entering hang time (10.0s)
INFO - Fast terminator: stream on repeater 312100 slot 1 ended via inactivity (215ms since last packet): src=3121234, dst=3120, duration=1.85s, packets=31
DEBUG - RX hang time completed on repeater 312100 slot 1: src=3121234, dst=3120, hang_duration=10.02s
WARNING - Stream contention on repeater 312100 slot 1: existing stream (src=3121234, dst=3120, active 150ms ago) vs new stream (src=3125678, dst=3121)
INFO - Same user continuing conversation on repeater 312100 slot 1 during hang time: src=3121234, dst=3120
INFO - Same user switching talkgroup on repeater 312100 slot 1 during hang time: src=3121234, old_dst=3120, new_dst=9
INFO - Different user joining conversation on repeater 312100 slot 1 during hang time: old_src=3121234, new_src=3125678, dst=3120
WARNING - Hang time hijacking blocked on repeater 312100 slot 1: slot reserved for TG 3120, denied src=3125678 attempting TG 9
WARNING - Inbound routing denied: repeater=312100 TS1/TG9 not in allowed list {8, 3120}
INFO - Repeater 312100 slot 1 starting RX while we have active assumed TX stream - repeater wins, removing from active route-caches
DEBUG - Removed repeater 312100 from route-cache of stream on repeater 312101 slot 1
Stream tracking emits real-time events to the dashboard:
{
"repeater_id": 312100,
"slot": 1,
"src_id": 3121234,
"dst_id": 3120,
"stream_id": "a1b2c3d4",
"call_type": "group"
}{
"repeater_id": 312100,
"slot": 1,
"src_id": 3121234,
"dst_id": 3120,
"duration": 2.46,
"packets": 41,
"end_reason": "terminator",
"hang_time": 10.0,
"call_type": "group",
"is_assumed": false
}{
"repeater_id": 312100,
"slot": 1
}Dashboard Usage:
- Real-time repeater slot status display
- Recent events log (filters out TX assumed streams)
- Traffic statistics and analytics
- Network health monitoring
HBlink4's stream tracking system provides capabilities that surpass other DMR server implementations:
- HBlink4: ~60ms stream end detection (primary method)
- Others: ~2000ms timeout-only detection
- Benefit: Near-instant slot availability, better user experience
- HBlink4: 4 distinct rules (same user, user switch TG, join conversation, hijacking prevention)
- Others: Simple timeout or no protection
- Benefit: Prevents hijacking while allowing natural conversations and fast TG switching
- HBlink4: Calculate-once, forward-many with O(1) decisions
- Others: Per-packet routing calculations
- Benefit: Massively improved performance on large networks
- HBlink4: Automatic route-cache removal, bandwidth optimization
- Others: Continue sending to busy repeaters
- Benefit: Significant bandwidth savings on multi-repeater networks
- HBlink4: Real RX always wins over assumed TX
- Others: May not distinguish or handle poorly
- Benefit: Correct behavior when repeaters start receiving
These capabilities enable HBlink4 to scale to large networks (100+ repeaters) while maintaining excellent performance and correct DMR behavior in complex contention scenarios.