The connection flow described a handshake the code never performed: MSTCL
as the login challenge (it is a shutdown notice), RPTA as the accept (the
constant is never sent), no RPTK acknowledgment, and state names that did
not exist. The keepalive section contradicted itself, describing an "MSTP"
command answered with RPTACK.
Rewrite it from the code. The challenge is RPTACK + salt, the accept is
RPTACK + repeater_id, and every handshake step is shown with its reply.
Fix the ID width claim: repeater IDs are 4 bytes, but the radio and
talkgroup IDs inside DMRD are 3. Document DMRD as a 53-byte structure with
a 55-byte wire length, which is what the parser actually requires.
Add what was missing: MSTCL, MSTNAK, and DMRA as commands, the RPTK hash
construction, the ID-takeover challenge, RPTO's trust gating and UNIT=,
voice-vs-data stream classification, and the outbound client role.
Soften two claims the code does not enforce. RPTO does not require the
connected state, and any packet from the registered address refreshes
liveness, so disconnect lands at max_missed times the sweep interval
rather than timeout_duration.
The outbound path logged "waiting for MSTCL..." while correctly waiting
for RPTACK + salt — the same confusion the document had. Group the
four-byte command prefixes in constants.py and record what they are for,
since HBlink4 only uses RPTP and the rest read as dead code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documentation cleanup pass across the tree, driven by the upcoming merge
of the dmrd-translation branch to main.
Deletions (obsolete / superseded):
- docs/user_csv_automation_proposal.md — implemented as dashboard/user_db.py
- docs/QUICK_TEST_REFERENCE.md — stale test counts, referenced a
TEST_COVERAGE.md that no longer exists
- user.old.csv — development backup artifact (also added user.old* and
user.bak* to .gitignore)
Release notes renumbered to the 4.x series (file kept 1.x-labelled from
a prior numbering scheme that predated the 4.1.0 release):
- docs/RELEASE_NOTES_v1.5.md -> docs/RELEASE_NOTES_v4.5.md
- docs/RELEASE_NOTES_v1.6.1.md -> docs/RELEASE_NOTES_v4.6.1.md
- Also fixed a stray "v1.5+" -> "v4.5+" in stream_tracking_diagrams.md
Content fixes where docs contradicted code or were stale:
- docs/dmrd_translation.md: replaced the "Payload blanking" section with
"Link Control rewriting" describing VHEAD/VTERM full-LC and voice-burst
EMB_LC re-encoding via dmr_utils3.bptc
- docs/connecting_to_hblink4.md: cross-reference updated to match
- docs/protocol.md: corrected call-type bit direction (0=Group,
1=Private/Unit — previously reversed), replaced "Bits 0-3: Reserved"
with actual dtype_vseq semantics, updated translation-doc link
- readme.md: "Built on Twisted" -> asyncio (Twisted was removed in the
outbound-connection release); expanded docs index to include routing,
DMRD translation, diagrams, TODO, and OpenBridge analysis; added DMRD
translation to the features list
- docs/stream_tracking.md: StreamState snippet now shows lc_base and
lc_cache fields added for LC rewriting
- scripts/README.md: noted that dashboard/user_db.py is the primary
refresh path; script remains as manual fallback
- dashboard/user_db.py: dropped the doctring reference to the deleted
proposal file
Note: dmrd-translation branch remains in active development but is
fully functional as of this commit. Tests: 75/75 pass (pre-existing
test_access_control.py failures are unrelated and predate this branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trusted repeaters can now declare per-entry slot/TGID translation rules in
their RPTO (OPTIONS) string, plus an optional single-ID rf_src override for
outbound group voice.
Grammar (operator-facing, goes into MMDVM.ini / DMRGateway.ini):
TS1 = entry[,entry...]
TS2 = entry[,entry...]
SRC = radio_id
entry = net_tgid_spec[:local_slot[:local_tgid]]
net_tgid_spec: N | N-M (range, specificity 2) | N (exact, specificity 3)
local_slot: 1 | 2 | * (preserve net slot)
local_tgid: N | * (preserve matched net tgid)
Wildcards on the network side (*, N*) are rejected at parse time to keep
the hot path O(1). Ranges expand to concrete map entries at RPTO parse
time (capped at 10,000 tgids). Most-specific rule wins on collision;
losers are dropped with a warning.
Runtime:
- RepeaterState grows inbound_map / outbound_map (inverses) and a single
tx_src_override.
- _check_inbound_routing translates local -> network before the ACL check
so subscriptions stay in one vocabulary.
- _calculate_stream_targets uses each target's outbound_map to run the
slot-busy / hang-time check against the slot the packet will actually
occupy on air.
- _forward_stream (and the outbound->local forwarding path) rewrite
bytes 5-7 (rf_src via override), 8-10 (tgid), 11-14 (repeater id for
outbound targets), and the slot bit in byte 15. Data-sync frames
(frame_type==2) get bytes 20-52 zeroed so MMDVMHost's BPTC decode
fails and it falls back to the rewritten DMRD header; voice frames
keep AMBE vocoder bits intact.
- Untranslated repeaters pay zero overhead (empty-dict short-circuits).
- Unit/private calls are still rejected upstream; SRC= has a defensive
group-only gate regardless.
- Mid-stream RPTO logs a warning and applies new rules immediately;
in-flight streams finish on their cached routing.
Validated via parser unit tests (exact/range/collision/error cases, NAT
grammar, specificity precedence) and module imports. NOT yet validated
on live DMR traffic — that's what this branch is for.
Docs:
- docs/dmrd_translation.md: full feature reference (new)
- docs/configuration.md: trust + RPTO extensions section
- docs/connecting_to_hblink4.md: operator Options= examples for
MMDVMHost / DMRGateway (Pi-Star/WPSD references stripped — out of scope)
- docs/protocol.md: extended RPTO grammar note
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clarify the HomeBrew DMR protocol implementation details and correct errors in the keepalive mechanism. Update the table formatting for configuration data fields and DMRD packet structure.
- Document RPTO message format and purpose
- Explain talkgroup negotiation and intersection logic
- Note that server config is master allow list
- Include examples of RPTO usage and filtering
- Completes protocol documentation for all implemented commands
- Created docs/TODO.md with 11 prioritized features
- Clarified LC extraction rationale: needed for stream forwarding with ID modification, not for current read-only ops
- Updated all docs to reflect completed terminator detection (60ms via packet header flags)
- Removed outdated TODO comments about sync pattern extraction
- Updated IMPLEMENTATION_SUMMARY.md with completion status
- Updated hang_time.md, protocol.md, lc_extraction.md with current state
- Updated readme.md feature list and documentation links
- Updated hblink.py comments to reference docs/TODO.md
- Add fast terminator detection: when new stream tries to start, check if current stream inactive >200ms
- If inactive >200ms, end old stream immediately and allow new one (provides ~200ms turnaround)
- Add end_reason field to stream_end events ('fast_terminator' or 'timeout')
- Update dashboard to display end_reason in event log
- Mark sync pattern terminator detection as TODO (not yet working, needs investigation)
- Mark LC extraction from sync frames as TODO (may need FEC decoding)
- Update all documentation to reflect new approach
- Remove negative language about 'limitations' - frame as TODO items instead
This provides acceptable fast turnaround (~200ms) without needing to decode ETSI sync
patterns, which don't appear to match in Homebrew protocol packets (may need FEC or
other decoding that requires investigation).
1. Fixed call_type logic inversion:
- Was: call_type_bit == 1 → private (WRONG)
- Now: call_type_bit == 1 → group (CORRECT per Homebrew protocol)
- Bit 6 of _bits field: 0=private, 1=group
2. Added data sync terminator detection:
- Was only detecting voice terminators (0xD5DD7DF75D55)
- Now also detects data terminators (0x7DFFD5F55D5F)
- Fixes issue where frame_type=2 streams showed 'timeout' instead of 'terminator'
3. Improved dashboard event log formatting:
- Added proper display for hang_time_expired events
- Shows 'Hang time expired: Repeater X slot Y'
Testing revealed: Group voice calls were using Data Sync terminators, not
Voice Sync terminators. Both patterns now supported.
Critical fixes:
- Fixed call_type showing 'unknown' by using Homebrew packet metadata (byte 15, bit 6)
instead of waiting for LC extraction
- Fixed hang time never ending by emitting hang_time_expired event when hang time
actually expires in HBlink4
- Dashboard now handles hang_time_expired event and clears slot display
Improvements:
- Removed color_code from dashboard events (RF air interface only, not needed by server)
- Updated protocol.md to clarify color code is for RF only
- Removed unused remove_stream_delayed() method from dashboard
- Logo increased 20% (60px -> 72px)
Design principle documented: Prefer Homebrew protocol metadata over LC extraction
for information available in packet headers (call type, timeslot, IDs, etc.)