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>
This commit is contained in:
Cort BuffingtonandClaude Opus 5.5 committed 2026-10-21 07:41:11 -05:00
1 parent 3cb47c74ac
commit ec8bbfaedc
11 files changed
+128 -56

No files matched your search

+4 -4
View File
@@ -24,7 +24,7 @@ The Python `ipsc2hbp` is a single asyncio process — already simple and effecti
- **TRACKING mode by default.** The HBP connection follows the repeater: it comes up when the repeater registers and drops when the repeater goes away. PERSISTENT mode is available if you'd rather keep the upstream connection up regardless.
- **Native call timing on the IPSC side.** Inbound HBP→IPSC voice is clocked out through a jitter buffer at the exact 60 ms DMR cadence — anchored to the first voice burst so every burst gets the full buffer lead — so the repeater sees a continuous grid identical to real MOTOTRBO equipment. The terminator is held until the buffered tail drains, so the call tail is never clipped. Depth is tunable per link via `[hbp] jitter_buffer_depth` (default 2 = 120 ms; raise it for marginal/high-latency RF backhaul).
- **Rebuild for IPSC what HBP leaves implicit.** The two sides expect different things: an HBP endpoint (MMDVMHost/hotspot) reassembles the audio itself and tolerates jitter, lost bursts, and a stream that isn't fully framed; a MOTOTRBO repeater does not — it plays what it's handed and expects a clean, self-framed DMR stream. So the HBP→IPSC path reconstructs the structure HBP's endpoint would otherwise supply: the embedded LC in every superframe (DMR late entry — rebuilt from the voice stream's `src`/`dst` even when no `VOICE_HEAD` was seen), AMBE silence for any missing burst so the 60 ms grid never stalls, and a `VOICE_TERM` to frame every call end (held until the tail drains, or synthesized by the dead-man if the stream just stops). The one thing never fabricated is the `VOICE_HEAD` packet itself — genuine headers are forwarded, late entry rides the embedded LC instead. See section 7.9 of the Python `ipsc2hbp` theory of operation for the full rationale.
- **Carry what the repeater knows about the RF.** On the IPSC→HBP path, AMBE frames the repeater flags as uncorrectable are sent as MMDVMHost's silence frame (what an MMDVMHost repeater does with its own unrepairable frames), the repeater's RSSI from each `VOICE_HEAD` / `VOICE_TERM` goes out in DMRD byte 54 (0, HBP's "no reading", on voice bursts), the superframe position re-syncs on every burst A, and the repeater's "sync not detected" placeholders for lost bursts are logged and dropped.
- **Carry what the repeater knows about the RF.** On the IPSC→HBP path, AMBE frames the repeater flags as uncorrectable are sent as MMDVMHost's silence frame (what an MMDVMHost repeater does with its own unrepairable frames), the repeater's RSSI from each `VOICE_HEAD` / `VOICE_TERM` goes out in DMRD byte 54 (0, HBP's "no reading", on voice bursts), the superframe position re-syncs on every burst A, and the repeater's "sync not detected" placeholders for lost bursts are logged and dropped. On the HBP→IPSC path, the RSSI in DMRD byte 54 of a voice header or terminator goes into the `VOICE_HEAD` / `VOICE_TERM` built from it, and 53-byte DMRD (no BER or RSSI bytes) is accepted.
**ARCHITECTURE:**
@@ -36,10 +36,10 @@ The Python `ipsc2hbp` is a single asyncio process — already simple and effecti
`make test` runs three suites:
- The DSP self-test confirms every FEC/DSP primitive matches `dmr_utils3` bit-for-bit.
- The parity test confirms the C translator emits byte-identical DMRD (IPSC→HBP) and GROUP_VOICE (HBP→IPSC) frames versus the Python `ipsc2hbp`. The same program also replays weak-signal calls captured from a MOTOTRBO repeater to check bad-voice silence, burst-A re-sync, late entry on a degraded burst E, and RSSI decoding.
- The parity test confirms the C translator emits byte-identical DMRD (IPSC→HBP) and GROUP_VOICE (HBP→IPSC) frames versus the Python `ipsc2hbp`. The same program also replays weak-signal calls captured from a MOTOTRBO repeater to check bad-voice silence, burst-A re-sync, late entry on a degraded burst E, and RSSI decoding, and checks HBP→IPSC RSSI encoding, the timeslot bit, and 53-byte DMRD.
- The private-call test replays private calls captured from a MOTOTRBO repeater and HBlink3, in both directions, full and late entry.
Private calls, RSSI forwarding, bad-voice silence and burst-A re-sync are in the C version only; the Python `ipsc2hbp` does not have them. The configuration file is still shared: the one new key, `[ipsc] rssi_encoding_new`, is optional.
The Python `ipsc2hbp` has the same features and runs the same captured fixtures in its own tests. The configuration file is shared.
**WHAT IT IS NOT:**
@@ -64,7 +64,7 @@ See `INSTALL.md` for the systemd service and a note on coexisting with the Pytho
**PROPERTY:**
This work represents the author's interpretation of the Motorola MOTOTRBO IPSC protocol and the HomeBrew Repeater Protocol. IPSC protocol behavior is derived from reverse-engineering work originally done in DMRlink. HBP behavior is derived from HBlink, HBlink3, and HBlink4. The DMR DSP/FEC code is a C port of `dmr_utils3` (N0MJS, with original AMBE work by Mike Zingman N4IRR and FEC routines after Jonathan Naylor G4KLX). Private (unit-to-unit) call support is based on a patch contributed by Marco Brunozzi IW0RED, who also located the RSSI field. IPSC RSSI decoding, both firmware encodings, and the voice-burst control and bad-voice flags follow `node-dmr-lib` by rick51231 (MIT). Motorola and MOTOTRBO are registered trademarks of Motorola Solutions, Inc. This project is not affiliated with Motorola Solutions in any way.
This work represents the author's interpretation of the Motorola MOTOTRBO IPSC protocol and the HomeBrew Repeater Protocol. IPSC protocol behavior is derived from reverse-engineering work originally done in DMRlink. HBP behavior is derived from HBlink, HBlink3, and HBlink4. The DMR DSP/FEC code is a C port of `dmr_utils3` (N0MJS, with original AMBE work by Mike Zingman N4IRR and FEC routines after Jonathan Naylor G4KLX). Private (unit-to-unit) call support is based on a patch contributed by Marco Brunozzi IW0RED, who also located the RSSI field. The IPSC RSSI encoding and the voice-burst control and bad-voice flags follow `node-dmr-lib` by rick51231 (MIT). Motorola and MOTOTRBO are registered trademarks of Motorola Solutions, Inc. This project is not affiliated with Motorola Solutions in any way.
Additional IPSC packet structure knowledge was derived from study of **node-dmr-lib** by rick51231: https://github.com/rick51231/node-dmr-lib
-19
View File
@@ -107,25 +107,6 @@ keepalive_watchdog = 60
# ---------------------------------------------------------------------------
ts_prefer_call_info = false
# ---------------------------------------------------------------------------
# RSSI encoding
#
# The repeater reports the received signal strength of each call in its
# VOICE_HEAD and VOICE_TERM packets (voice bursts carry none). ipsc2hbp sends
# it to the HBP server in DMRD byte 54 of those packets as -dBm (80 = -80 dBm);
# every other DMRD carries 0, HBP's "no reading" value.
#
# MOTOTRBO firmware has used two encodings of this value:
# rssi_encoding_new = true : 8.8 fixed-point -dBm (newer firmware)
# rssi_encoding_new = false : the same, offset by 56 dB (raw 0 = -56 dBm)
# The change is believed to be around repeater firmware 2.30.10.0, but that
# version is not confirmed. Only true has been tested on a repeater. If the
# levels reported are about 56 dB too strong, set it to false.
#
# Decoding and both encodings from node-dmr-lib by rick51231 (MIT licence).
# ---------------------------------------------------------------------------
rssi_encoding_new = true
# ---------------------------------------------------------------------------
# IPSC capability advertisement
#
-1
View File
@@ -151,7 +151,6 @@ int config_load(const char *path, Config *cfg, char *err, size_t errlen)
{ static const char *M[] = {"MASTER","PEER"};
get_choice(t, &e, "ipsc", "mode", 0, "MASTER", M, 2, cfg->ipsc_mode, sizeof cfg->ipsc_mode); }
cfg->ipsc_rssi_old = !get_bool(t, &e, "ipsc", "rssi_encoding_new", 0, 1);
if (!strcmp(cfg->ipsc_mode, "PEER")) {
get_str(t, &e, "ipsc", "master_ip", 1, "", cfg->ipsc_master_ip, sizeof cfg->ipsc_master_ip);
-1
View File
@@ -30,7 +30,6 @@ typedef struct {
uint8_t ipsc_version[4]; /* 4 bytes */
int ipsc_ts_prefer_call_info;
int ipsc_rssi_old; /* 1 = [ipsc] rssi_encoding_new = false: old RSSI encoding (-56 dB offset) */
/* connection mode */
char ipsc_mode[8]; /* "MASTER" | "PEER" */
+2 -1
View File
@@ -14,7 +14,8 @@
#define HBPF_DTYPE_MASK 0x0F
/* DMRD packet layout */
#define DMRD_LEN 55
#define DMRD_LEN 55 /* as sent: core + BER + RSSI */
#define DMRD_CORE_LEN 53 /* minimum accepted: BER and RSSI are absent below 55 */
#define DMRD_SEQ_OFF 4
#define DMRD_SRC_OFF 5
#define DMRD_DST_OFF 8
+1
View File
@@ -68,6 +68,7 @@
#define GV_BAD2_BIT 49 /* AMBE field bit: frame 2 uncorrectable */
#define GV_BAD3_BIT 99 /* AMBE field bit: frame 3 uncorrectable */
/* VOICE_HEAD / VOICE_TERM payload (after node-dmr-lib DMRDataPayload) */
#define GV_PDU_SLOT_MSK 0x80 /* byte 31 bit 7: timeslot 2 */
#define GV_RSSI_FLAG_OFF 34 /* bit 7: RSSI present */
#define GV_RSSI_FLAG_MSK 0x80
#define GV_SLOTTYPE_OFF 35 /* bit 3: 2-byte slot type follows the data */
+26 -13
View File
@@ -63,6 +63,7 @@ struct translator {
int in_buf_present[3][6];
uint8_t in_buf[3][6][19];
int in_term_pending[3]; /* terminator received; emit after tail drains */
uint8_t in_term_rssi[3]; /* DMRD byte 54 of that terminator; 0 = no reading */
double in_next_slot_time[3];
int in_burst_pos[3];
int in_consec_synth[3];
@@ -142,21 +143,24 @@ static void extract_ambe_from_dmrd(const uint8_t payload33[33], uint8_t out19[19
dmr_bits_to_bytes(bits, 152, out19);
}
/* build the 23-byte VOICE_HEAD/VOICE_TERM payload (after burst-type byte) */
static void build_ipsc_voice_payload(const uint8_t lc[9], int burst_type, uint8_t out[23])
/* build the 23-byte VOICE_HEAD/VOICE_TERM payload (after burst-type byte); rssi_raw is
* the 16-bit RSSI field, 0 = no reading */
static void build_ipsc_voice_payload(const uint8_t lc[9], int burst_type, int ts, uint16_t rssi_raw,
uint8_t out[23])
{
uint8_t fec[3];
dmr_rs129_lc_encode(lc, burst_type == VOICE_TERM ? 1 : 0, fec);
uint8_t tag = (burst_type == VOICE_HEAD) ? 0x11 : 0x12;
int p = 0;
out[p++] = 0x80;
out[p++] = (ts == 2) ? GV_PDU_SLOT_MSK : 0x00;
out[p++] = 0x00; out[p++] = 0x0a; /* length_to_follow = 10 */
out[p++] = 0x80;
out[p++] = 0x0a;
out[p++] = 0x00; out[p++] = 0x60; /* data size = 96 bits */
memcpy(out + p, lc, 9); p += 9;
memcpy(out + p, fec, 3); p += 3;
out[p++] = 0x00; out[p++] = tag; out[p++] = 0x00; out[p++] = 0x00;
out[p++] = 0x00; out[p++] = tag;
out[p++] = (uint8_t)(rssi_raw >> 8); out[p++] = (uint8_t)rssi_raw;
}
/* 48-bit EMBED for superframe position 0..5 */
@@ -250,6 +254,7 @@ static void init_call_state(translator *tr)
tr->in_last_pkt[1]=tr->in_last_pkt[2]=0.0;
memset(tr->in_buf_present, 0, sizeof tr->in_buf_present);
tr->in_term_pending[1]=tr->in_term_pending[2]=0;
tr->in_term_rssi[1]=tr->in_term_rssi[2]=0;
tr->in_next_slot_time[1]=tr->in_next_slot_time[2]=0.0;
tr->in_burst_pos[1]=tr->in_burst_pos[2]=0;
tr->in_consec_synth[1]=tr->in_consec_synth[2]=0;
@@ -302,9 +307,9 @@ static int out_call_continues(uint32_t rtp_now, int has_rtp, uint32_t prev_rtp,
}
/* RSSI carried in a VOICE_HEAD / VOICE_TERM, as the DMRD byte-54 value (-dBm magnitude);
* 0 = no reading. The raw value is 8.8 fixed-point -dBm; the old firmware encoding adds a
* 56 dB offset (node-dmr-lib, rick51231). Voice bursts carry no RSSI. */
static uint8_t ipsc_rssi(const translator *tr, const uint8_t *d, int len)
* 0 = no reading. The raw value is 8.8 fixed-point -dBm (node-dmr-lib, rick51231). Voice
* bursts carry no RSSI. */
static uint8_t ipsc_rssi(const uint8_t *d, int len)
{
if (len < GV_DATA_OFF || !(d[GV_RSSI_FLAG_OFF] & GV_RSSI_FLAG_MSK)) return 0;
int bits = (d[GV_DATA_BITS_OFF] << 8) | d[GV_DATA_BITS_OFF + 1];
@@ -313,10 +318,15 @@ static uint8_t ipsc_rssi(const translator *tr, const uint8_t *d, int len)
int raw = (d[off] << 8) | d[off + 1];
if (raw == 0) return 0;
int v = (raw + 128) >> 8;
if (tr->cfg->ipsc_rssi_old) v += 56;
return (uint8_t)(v > 255 ? 255 : v);
}
/* Inverse of ipsc_rssi(): DMRD byte-54 magnitude -> 16-bit IPSC RSSI field; 0 = no reading. */
static uint16_t ipsc_rssi_raw(uint8_t mag)
{
return (uint16_t)(mag << 8);
}
void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len, int ts, int burst_type)
{
if (!hbp_is_connected(tr->hb)) return;
@@ -400,7 +410,7 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
memcpy(fb+at, full_lc+98, 98); at+=98;
dmr_bits_to_bytes(fb, 264, payload_33);
flags |= HBPF_FRAMETYPE_DATASYNC | HBPF_SLT_VHEAD;
rssi = ipsc_rssi(tr, data, len);
rssi = ipsc_rssi(data, len);
} else if (burst_type == VOICE_TERM) {
if (!tr->out_has_stream[ts]) return;
uint8_t lc[9];
@@ -415,7 +425,7 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
memcpy(fb+at, full_lc+98, 98); at+=98;
dmr_bits_to_bytes(fb, 264, payload_33);
flags |= HBPF_FRAMETYPE_DATASYNC | HBPF_SLT_VTERM;
rssi = ipsc_rssi(tr, data, len);
rssi = ipsc_rssi(data, len);
} else {
/* SLOT1_VOICE / SLOT2_VOICE. Boundary detection: the IPSC call-seq (byte 5) is
* deliberately NOT consulted — current XPR8400 firmware mints a new call-seq every
@@ -587,6 +597,7 @@ static void clear_in_call(translator *tr, int ts)
cancel_delivery_timer(tr, ts);
memset(tr->in_buf_present[ts], 0, sizeof tr->in_buf_present[ts]);
tr->in_term_pending[ts] = 0;
tr->in_term_rssi[ts] = 0;
tr->in_next_slot_time[ts] = 0.0;
tr->in_burst_pos[ts] = 0;
tr->in_consec_synth[ts] = 0;
@@ -617,7 +628,7 @@ static void emit_term(translator *tr, int ts)
const uint8_t *lc = tr->in_lc[ts];
int call_info = ((ts == 2) ? TS_CALL_MSK : 0x00) | END_MSK;
uint8_t gv_payload[24]; gv_payload[0]=VOICE_TERM;
build_ipsc_voice_payload(lc, VOICE_TERM, gv_payload+1);
build_ipsc_voice_payload(lc, VOICE_TERM, ts, ipsc_rssi_raw(tr->in_term_rssi[ts]), gv_payload+1);
emit_in(tr, ts, lc, call_info, 0x5e, gv_payload, 24, 0);
LOGI(LOGN, "HBP call end: src=%u %s=%u ts=%d ipsc_id=0x%02x",
(unsigned)(lc[6]<<16|lc[7]<<8|lc[8]), (lc[0] & 0x3F) == FLCO_PRIVATE ? "unit" : "tg",
@@ -674,7 +685,8 @@ static void on_stream_timeout(translator *tr, int ts)
void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
{
if (!ipsc_has_peers(tr->ip)) return;
if (len < DMRD_LEN) return;
if (len < DMRD_CORE_LEN) return;
uint8_t rssi = (len > DMRD_RSSI_OFF) ? dmrd[DMRD_RSSI_OFF] : 0;
const uint8_t *src_sub = dmrd + DMRD_SRC_OFF;
const uint8_t *dst_group = dmrd + DMRD_DST_OFF;
@@ -721,7 +733,7 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
{
int call_info = (ts == 2) ? TS_CALL_MSK : 0x00;
uint8_t gv_payload[24]; gv_payload[0]=VOICE_HEAD;
build_ipsc_voice_payload(lc, VOICE_HEAD, gv_payload+1);
build_ipsc_voice_payload(lc, VOICE_HEAD, ts, ipsc_rssi_raw(rssi), gv_payload+1);
emit_in(tr, ts, lc, call_info, 0xdd, gv_payload, 24, 0);
}
return;
@@ -740,6 +752,7 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
}
/* Mark pending so the call tail isn't clipped. */
tr->in_term_pending[ts] = 1;
tr->in_term_rssi[ts] = rssi;
if (clock_idle(tr, ts)) {
/* No buffered voice tail to drain — emit the terminator now. */
emit_term(tr, ts);
-1
View File
@@ -8,7 +8,6 @@ bind_ip = "127.0.0.1"
bind_port = 50000
ipsc_master_id = 9999999
auth_enabled = false
rssi_encoding_new = true
keepalive_watchdog = 60
[ipsc.capabilities]
use_safe_defaults = true
+2 -2
View File
@@ -1,2 +1,2 @@
800098967f012f9b8100005b02000043e20080dd000000000000000000000180000a800a006000000000005b2f9b81bbdb4b00110000
800098967f012f9b8100005b02000043e240805e000100000000000000000280000a800a006000000000005b2f9b81b4d44400120000
800098967f012f9b8100005b02000043e20080dd000000000000000000000100000a800a006000000000005b2f9b81bbdb4b00110000
800098967f012f9b8100005b02000043e240805e000100000000000000000200000a800a006000000000005b2f9b81b4d44400120000
@@ -1,5 +1,5 @@
# Config fixture for the byte-for-byte parity test (mirrors the Python tests/test.toml
# fields that affect translator output: hbp_repeater_id and ipsc_master_id).
# parity.toml plus the retired [ipsc] rssi_encoding_new key: an existing config that still
# carries it must load, and RSSI must decode the same as without it.
[global]
log_level = "WARNING"
[ipsc]
+91 -12
View File
@@ -9,6 +9,8 @@
#include "../src/config.h"
#include "../src/eventloop.h"
#include "../src/dmr/dmr.h"
#include "../src/hbp_const.h"
#include "../src/ipsc_const.h"
/* ---- captured DMRD output ---- */
#define MAXCAP 64
@@ -199,10 +201,11 @@ int main(void)
/* ---- late entry on weak signal (IPSC->HBP): weak-signal call 3120101 -> TG 2.
* Joining at packet 4, the first burst E (packet 8, ctrl 0x96) has a corrupt LC and
* must be skipped; anchor on packet 14 (0x16). Joining at packet 33, the first
* burst E (packet 38, ctrl 0x17: voice frame bad, LC good) is usable. ---- */
static const int le_join[2] = {4, 33}, le_anchor[2] = {14, 38};
* burst E (packet 38, ctrl 0x17: voice frame bad, LC good) is usable. The third case
* gives packet 8's corrupt LC a group FLCO, so only the LC-error bit can reject it. ---- */
static const int le_join[3] = {4, 33, 4}, le_anchor[3] = {14, 38, 14};
int le_fails = 0;
for (int c = 0; c < 2; c++) {
for (int c = 0; c < 3; c++) {
tr = translator_new(&cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ncap = 0;
@@ -214,6 +217,7 @@ int main(void)
idx++;
if (idx < le_join[c] || bt == 1 || (bt & 0x7F) == 0x13) continue;
uint8_t raw[400]; int n = unhex(hex, raw);
if (c == 2 && idx == 8) raw[GV_BE_LC_FLCO_OFF] = 0x00;
int before = ncap;
translator_ipsc_voice_received(tr, raw, n, ts, bt);
if (anchor < 0 && ncap > before) anchor = idx;
@@ -241,13 +245,11 @@ int main(void)
static const char *rs_file[2] = {"tests/pvt_ipsc_in.txt", "tests/lateentry_ipsc_in.txt"};
static const int rs_new[2][5] = {{65, 65, 65, 65, 63}, {112, 112, 113, 113, -1}};
int rs_fails = 0;
for (int old = 0; old < 2; old++) {
/* Parse the encoding from a real config file, so the accepted values are tested too. */
for (int legacy = 0; legacy < 2; legacy++) {
/* A config still carrying the retired rssi_encoding_new key must load and decode the same. */
Config c2;
if (config_load(old ? "tests/rssi_old.toml" : "tests/parity.toml", &c2, err, sizeof err) ||
c2.ipsc_rssi_old != old) {
fprintf(stderr, "FAIL rssi: rssi_encoding_new = %s not loaded as the %s encoding: %s\n",
old ? "false" : "true", old ? "old" : "new", err);
if (config_load(legacy ? "tests/rssi_legacy_key.toml" : "tests/parity.toml", &c2, err, sizeof err)) {
fprintf(stderr, "FAIL rssi: %s config rejected: %s\n", legacy ? "legacy-key" : "plain", err);
rs_fails++; continue;
}
for (int fidx = 0; fidx < 2; fidx++) {
@@ -267,10 +269,10 @@ int main(void)
for (int i = 0; i < ncap; i++) {
int fl = cap[i][15], got = cap[i][54];
int want = 0;
if ((fl & 0x30) == 0x20) { want = rs_new[fidx][k++]; if (want >= 0) want += old ? 56 : 0; }
if ((fl & 0x30) == 0x20) want = rs_new[fidx][k++];
if (got != want) {
fprintf(stderr, "FAIL rssi: %s %s DMRD[%d] flags 0x%02x byte54=%d, want %d\n",
rs_file[fidx], old ? "old" : "new", i, fl, got, want);
rs_file[fidx], legacy ? "legacy-key" : "plain", i, fl, got, want);
rs_fails++;
}
}
@@ -279,6 +281,83 @@ int main(void)
}
printf("RSSI (IPSC->HBP): %d failures\n", rs_fails);
/* ---- HBP->IPSC: 53/55-byte DMRD, RSSI packing, slot bit. parity_in_dmrd.txt is one
* TS1 VOICE_HEAD + VOICE_TERM (55 bytes, byte 54 = 0). Each emitted HEAD/TERM is fed
* back through the IPSC->HBP decoder, which is verified against repeater captures. ---- */
int hr_fails = 0;
uint8_t dm[2][128]; int dmn[2] = {0, 0};
fi = fopen("tests/parity_in_dmrd.txt", "r");
for (int k = 0; k < 2 && fi && fgets(line, sizeof line, fi); k++) dmn[k] = unhex(line, dm[k]);
if (fi) fclose(fi);
if (dmn[0] != 55 || dmn[1] != 55) { fprintf(stderr, "FAIL hbp rssi: parity_in_dmrd.txt not 2x55 bytes\n"); hr_fails++; }
static const int hr_mag[][2] = {{0, 0}, {60, 63}, {85, 112}, {113, 57}, {255, 1}};
if (!hr_fails) {
for (int m = 0; m < (int)(sizeof hr_mag / sizeof hr_mag[0]); m++) {
for (int ts = 1; ts <= 2; ts++) {
for (int len = 53; len <= 55; len += 2) {
tr = translator_new(&cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ngv = 0; ncap = 0;
for (int k = 0; k < 2; k++) {
uint8_t d[128]; memcpy(d, dm[k], 55);
if (ts == 2) d[15] |= 0x80;
d[54] = (uint8_t)hr_mag[m][k];
translator_hbp_voice_received(tr, d, len);
}
ev_timer_after(loop, 0.05, stop_cb, loop);
ev_run(loop);
if (ngv != 2) {
fprintf(stderr, "FAIL hbp rssi: len %d ts %d: %d GROUP_VOICE, want 2\n", len, ts, ngv);
hr_fails++; translator_free(tr); continue;
}
for (int k = 0; k < 2; k++) {
const uint8_t *g = gvcap[k];
int mag = (len == 55) ? hr_mag[m][k] : 0;
int raw = g[52] << 8 | g[53];
int want_raw = mag << 8, want = mag;
if (g[31] != (ts == 2 ? 0x80 : 0x00) || g[34] != 0x80 || raw != want_raw) {
fprintf(stderr, "FAIL hbp rssi: len %d ts %d %s: byte31=%02x byte34=%02x raw=%04x, want %02x 80 %04x\n",
len, ts, k ? "TERM" : "HEAD", g[31], g[34], raw,
ts == 2 ? 0x80 : 0x00, want_raw);
hr_fails++;
}
translator_ipsc_voice_received(tr, g, gvlen[k], ts, g[30]);
if (ncap != k + 1 || cap[k][54] != want || ((cap[k][15] & 0x80) != 0) != (ts == 2)) {
fprintf(stderr, "FAIL hbp rssi: len %d ts %d %s round trip: byte54=%d, want %d\n",
len, ts, k ? "TERM" : "HEAD",
ncap > k ? cap[k][54] : -1, want);
hr_fails++;
}
}
translator_free(tr);
}
}
}
}
/* A terminator reading must not carry into the next call's TERM, whether that TERM is
* relayed or synthesized after a stream timeout. */
if (!hr_fails) {
tr = translator_new(&cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ngv = 0;
uint8_t d[128];
/* call 1: HEAD, TERM with a reading */
memcpy(d, dm[0], 55); translator_hbp_voice_received(tr, d, 55);
memcpy(d, dm[1], 55); d[54] = 90; translator_hbp_voice_received(tr, d, 55);
/* call 2: HEAD, one voice burst, then the stream stops -> synthesized TERM */
memcpy(d, dm[0], 55); d[16] ^= 0x0F; translator_hbp_voice_received(tr, d, 55);
d[15] = (d[15] & 0xC0) | HBPF_FRAMETYPE_VOICESYNC; translator_hbp_voice_received(tr, d, 55);
ev_timer_after(loop, 0.8, stop_cb, loop);
ev_run(loop);
if (ngv < 4 || gvcap[1][30] != 0x02 || (gvcap[1][52] << 8 | gvcap[1][53]) != 90 << 8 ||
gvcap[ngv - 1][30] != 0x02 || (gvcap[ngv - 1][52] | gvcap[ngv - 1][53]) != 0) {
fprintf(stderr, "FAIL hbp rssi: TERM reading carried into the next call (%d GROUP_VOICE)\n", ngv);
hr_fails++;
}
translator_free(tr);
}
printf("53/55-byte DMRD, RSSI packing, slot bit (HBP->IPSC): %d failures\n", hr_fails);
ev_free(loop);
return (fails || in_fails || bad_fails || sync_fails || le_fails || rs_fails) ? 1 : 0;
return (fails || in_fails || bad_fails || sync_fails || le_fails || rs_fails || hr_fails) ? 1 : 0;
}