10 Commits
Author SHA1 Message Date
Cort BuffingtonandClaude Opus 5.5 ec8bbfaedc HBP->IPSC: accept 53-byte DMRD, carry RSSI, set the slot bit; drop old RSSI encoding
DMRD bytes 53-54 (BER, RSSI) are optional on input: HBlink3 passes a
53-byte DMRD through unchanged. Accept anything from 53 bytes and read
byte 54 only when present; output is still 55 bytes.

DMRD byte 54 of a voice header or terminator now goes into the RSSI
field (bytes 52-53) of the VOICE_HEAD / VOICE_TERM built from it, as
8.8 fixed-point -dBm. No reading (53 bytes, 0, or a synthesized
HEAD/TERM) sends 0 as before. The terminator reading is cleared at the
end of each call so it cannot reach a later synthesized TERM.

Byte 31 bit 7 of VOICE_HEAD / VOICE_TERM is the timeslot (0x40 / 0xC0
in repeater captures); it was hardcoded 0x80 (TS2). The RSSI status
bit (bit 6) is left 0.

The old firmware RSSI encoding (-56 dB offset) is dropped: no repeater
using it is known. [ipsc] rssi_encoding_new is no longer read; configs
that still carry it load unchanged.

Tests: 53/55-byte input, RSSI round trip through the IPSC->HBP decoder,
slot bit, terminator reading not carried into the next call, legacy
key fixture, and a late-entry case where only the LC-error bit can
reject a corrupt burst E LC. parity_ref_gv.txt byte 31 corrected to
TS1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:41:11 -05:00
Cort BuffingtonandClaude Opus 5.5 3cb47c74ac Config: make the RSSI encoding a boolean, rssi_encoding_new
[ipsc] rssi_encoding = "new" | "old" could not be set at all: get_choice()
upper-cases the configured value before comparing, and the choices were
lowercase, so both values were rejected as invalid. Only a missing key
(the default) worked, and the tests set the config field directly.

Replace it with [ipsc] rssi_encoding_new = true | false (default true).
The RSSI tests now load both values from config files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 05:58:38 -05:00
Cort BuffingtonandClaude Opus 5.5 06b02e6ea3 IPSC->HBP: forward the repeater's RSSI in DMRD byte 54
VOICE_HEAD and VOICE_TERM carry a 16-bit RSSI when bit 7 of byte 34 is
set; it follows the data and slot type. It is 8.8 fixed-point -dBm, and
goes out as the HBP byte-54 magnitude on the DMRD built from that packet.
Voice bursts carry no RSSI, so their DMRD send 0, HBP's "no reading";
holding a value would report a measurement that was not made.

[ipsc] rssi_encoding = "new" | "old" selects the firmware encoding (old
adds 56 dB). Only "new" is verified OTA (-60 to -115 dBm on a walk test);
the firmware version where it changed is uncertain.

Decoding and both encodings from node-dmr-lib by rick51231 (MIT). The
field was first located by Marco Brunozzi IW0RED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:14:59 -05:00
Cort BuffingtonandClaude Opus 5.5 640a952df5 IPSC->HBP late entry: test burst E control bits, not the whole byte
Late entry required data[32] == 0x16 exactly. On a weak signal the
repeater also sets 0x01 (AMBE frame 1 bad) and 0x80 (embedded LC failed
its check). In the OTA capture every 0x80 burst E carried a corrupt LC,
while 0x17 carried a good one, so the exact match was right to reject
0x9x but also rejected 0x17 and delayed late entry a superframe.

Identify burst E by the embedded-LC-present bit (0x10) and reject it
only when the LC-error bit is set. The data[32] flags now share one set
of named constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:51:04 -05:00
Cort BuffingtonandClaude Opus 5.5 603501e55c IPSC->HBP: re-sync the superframe position on every burst A
The burst position was set only at VOICE_HEAD / late entry and then
counted. When the repeater loses a burst it sends a 0x13 placeholder and
restarts at a fresh burst A, so every later burst went to HBP one
position late for the rest of the call (sync and embedded LC on the
wrong bursts). IPSC marks burst A with the voice-sync flag (data[32]
bit 6); reset the position to 0 on it.

Test replays a weak-signal call captured OTA in which this happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:46:56 -05:00
Cort BuffingtonandClaude Opus 5.5 7d9ce4dc8e Translate private (unit-to-unit) voice calls in both directions
IPSC->HBP: PVT_VOICE (0x81) was dropped. It uses the GROUP_VOICE layout,
so it now goes through the same path: DMRD gets the private-call flag
and the LC is FLCO 3. Late entry anchors on a burst E carrying FLCO 3.

HBP->IPSC: the HBP header LC was already copied verbatim, so radios saw
private calls, but the packets went out as GROUP_VOICE. The opcode now
follows the LC's FLCO, and the fallback LCs (late entry, orphan
terminator) honour the HBP private-call flag.

No on/off setting: the translator carries private calls; the network
server decides who may make them. Confirmed private calls are not
supported.

Tests replay a PVT_VOICE call captured from a MOTOTRBO repeater and an
HBP private call captured from HBlink3, full and late entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:24:16 -05:00
Cort BuffingtonandClaude Opus 5.5 ec83237268 IPSC->HBP: send MMDVMHost silence for AMBE frames the repeater flags bad
The repeater sets a per-AMBE-frame "bad voice" flag when it could not
correct the frame on RF receive (confirmed OTA on a weak signal). We
re-encoded those frames with fresh, valid FEC, so they reached HBP
looking good. MMDVMHost replaces uncorrectable frames with silence, so
substitute its DMR_SILENCE_DATA frame for each flagged frame.

No FEC repair is added in the HBP->IPSC direction: MMDVMHost already
corrects every RF frame or replaces it with silence before it reaches
the network, so HBP never carries damaged frames.

Tests: new bad-voice check; synthetic parity inputs had the flag bits set
at random, cleared them (the reference ignores those bits, output
unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:22:11 -05:00
Cort BuffingtonandClaude Opus 4.8 eb73b5b462 translate: clock entire HBP→IPSC call through the jitter buffer
Port the unified delivery-clock model from ipsc2hbp. Headers (pre-roll FIFO),
voice (position-indexed buffer), and the terminator (held pending until the
voice tail drains) all emit on one per-timeslot 60 ms-cadence timer, preserving
the source's header→voice spacing so the repeater sees a continuous native grid
— no header→voice gap and no clipped tail. Headers are relayed faithfully, never
fabricated.

Add [hbp] jitter_buffer_depth config (default 2 = 120 ms, range 1-8) to tune
the delay per link. Drive the event loop in the inbound parity test and
regenerate the inbound parity fixtures for the clocked model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:51:01 -05:00
Cort BuffingtonandClaude Opus 4.8 ae6a7b2ca1 translate: coalesce Talker-Alias per-superframe IPSC streams (IPSC->HBP)
Port of the Python fix (ipsc2hbp fix/ta-ghost-streams). Current XPR8400 firmware,
when a radio transmits with Talker Alias enabled, emits a new IPSC stream every
superframe for one OTA call: the call-seq (byte 5) increments per superframe and
the TA superframes carry the alias text in the common-header src/dst (non-GVCU
FLCO at burst-E byte 56). RTP timestamp stays continuous — it is one call. Read
on the wire: 1 PTT -> 30 IPSC streams (half a phantom 'N0MJS' identity) = the
ghost traffic.

Re-anchor the IPSC->HBP path in translator_ipsc_voice_received:
  - Stop treating an IPSC call-seq (byte 5) change as a call boundary; detect a
    new call by RTP-timestamp discontinuity (out_call_continues, OUT_RTP_MAX_FWD).
  - Late entry anchors identity only to a GVCU (FLCO 0x00) burst E; a Talker
    Alias / GPS burst E is skipped (its header src/dst are alias bytes).
  - Forward every frame under the LOCKED identity (out_lc) and one stable HBP
    stream ID, ignoring per-frame header relabels.
  - Relabel the start/end pairing token ipsc_id -> int_seq_id (no longer a stable
    IPSC id; it is just a token to pair the log lines).

VOICE_HEAD stays byte-5 anchored (HEAD/TERM carry no usable RTP; a new byte 5 at
a header = new stream). New ipsc_const.h: GV_RTP_TS_OFF, GV_BE_FLAG,
GV_BE_LC_FLCO_OFF, FLCO_GROUP. Parity goldens regenerated from the fixed Python:
make test passes byte-for-byte (DSP 83/83, parity IPSC->HBP 16/16, HBP->IPSC 2/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:07:06 -05:00
Cort BuffingtonandClaude Opus 4.8 a8b4eca5c0 Initial commit: ipsc2hbpc — C reimplementation of ipsc2hbp
Feature-for-feature, config-for-config port of the Python ipsc2hbp IPSC↔HBP
translator to dependency-free C11 (links only libc).

- src/dmr/: self-contained DMR DSP/FEC library (BPTC(196,96), embedded LC,
  AMBE 49<->72, RS(12,9), Hamming, Golay), validated bit-for-bit against
  dmr_utils3 via golden vectors.
- src/: config (in-tree TOML parser), logging, poll() event loop, UDP,
  SHA-1/SHA-256/HMAC, IPSC master/peer protocol, HBP client, call translator.
- Single-threaded event loop mirrors the original asyncio design.
- tests/: DSP self-test + byte-for-byte translator parity vs the Python
  reference (IPSC->HBP DMRD and HBP->IPSC GROUP_VOICE).

make && make test both pass; integration-tested against the fake peer/master.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:37:41 -05:00