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>
This commit is contained in:
Cort BuffingtonandClaude Opus 5.5 committed 2026-10-13 09:22:11 -05:00
1 parent 667c1a559b
commit ec83237268
4 files changed
+70 -13

No files matched your search

+7
View File
@@ -57,6 +57,13 @@
#define GV_BURST_TYPE_OFF 30
#define GV_PAYLOAD_OFF 31
#define GV_MIN_LEN 31
#define GV_AMBE_OFF 33 /* 19-byte AMBE field: three 49-bit frames at bits 0, 50, 100 */
/* Repeater-set "bad voice" flags, one per AMBE frame (uncorrectable on RF receive):
* frame 1 = data[32] bit 0; frames 2/3 = bits 49/99 of the AMBE field. */
#define GV_BAD1_OFF 32
#define GV_BAD1_MSK 0x01
#define GV_BAD2_BIT 49
#define GV_BAD3_BIT 99
#define GV_BE_FLAG 0x16 /* data[32] value identifying burst E (carries reassembled LC repeat) */
#define GV_BE_LC_FLCO_OFF 56 /* FLCO of the reassembled LC repeat carried only on burst E */
#define FLCO_GROUP 0x00 /* Group Voice Channel User (real call identity); 0x04-0x08 = TA/GPS */
+13 -4
View File
@@ -102,6 +102,10 @@ static void rand4(uint8_t out[4])
if (getrandom(out, 4, 0) != 4) { out[0]=rand(); out[1]=rand(); out[2]=rand(); out[3]=rand(); }
}
/* 72-bit AMBE silence frame from MMDVMHost DMR_SILENCE_DATA; replaces frames the
* repeater flagged bad (IPSC->HBP). */
static const uint8_t AMBE_SILENCE_MMDVM[9] = {0xB9,0xE8,0x81,0x52,0x61,0x73,0x00,0x2A,0x6B};
/* precompute 19-byte IPSC AMBE payload of three silence frames */
static void make_ambe_silence(uint8_t out[19])
{
@@ -424,11 +428,16 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
* bytes on a TA superframe); all AMBE is forwarded under the locked identity. */
if (len < 52) { LOGW(LOGN, "SLOT_VOICE too short for AMBE: %d bytes", len); return; }
dmr_bit raw[152]; dmr_bytes_to_bits(data + 33, 19, raw);
dmr_bit raw[152]; dmr_bytes_to_bits(data + GV_AMBE_OFF, 19, raw);
dmr_bit a1_72[72], a2_72[72], a3_72[72];
dmr_ambe_49_to_72(raw + 0, a1_72);
dmr_ambe_49_to_72(raw + 50, a2_72);
dmr_ambe_49_to_72(raw + 100, a3_72);
dmr_bit *a72[3] = {a1_72, a2_72, a3_72};
int bad[3] = {data[GV_BAD1_OFF] & GV_BAD1_MSK, raw[GV_BAD2_BIT], raw[GV_BAD3_BIT]};
for (int i = 0; i < 3; i++) {
if (bad[i]) dmr_bytes_to_bits(AMBE_SILENCE_MMDVM, 9, a72[i]);
else dmr_ambe_49_to_72(raw + 50 * i, a72[i]);
}
if (bad[0] || bad[1] || bad[2])
LOGD(LOGN, "IPSC bad AMBE ts=%d flags=%d%d%d — sent as silence", ts, bad[0], bad[1], bad[2]);
int pos = tr->out_frame_pos[ts] % 6;
dmr_bit embed[48]; build_embed(tr, ts, pos, embed);
dmr_bit fb[264]; int at=0;
+8 -8
View File
@@ -1,16 +1,16 @@
1 1 800004c317052f9b8100005b02000043e200000000000000100000000000010000000000000000000000000000000000000000000000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a000000070e151c232a31383f464d545b626970777e0000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a0000858c939aa1a8afb6bdc4cbd2d9e0e7eef5fc030000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a00000a11181f262d343b424950575e656c737a81880000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a000000070e151c232a31383f464d445b626970777e0000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a0000858c939aa1a8afb6bdc4cbd2c9e0e7eef5fc030000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a00000a11181f262d343b424950574e656c737a81880000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a00008f969da4abb2b9c0c7ced5dce3eaf1f8ff060d0000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a0016141b222930373e454c535a61686f767d848b920000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a000099a0a7aeb5bcc3cad1d8dfe6edf4fb020910170000
1 10 800004c317052f9b8100005b02000043e2000000000000001000000000000a000099a0a7aeb5bc83cad1d8dfe6edf4fb020910170000
1 2 800004c317052f9b8100005b02000043e200000000000000100000000000020000000000000000000000000000000000000000000000
1 1 800004c317102f9be500000902000043e200000000000000100000000000010000000000000000000000000000000000000000000000
1 10 800004c317112f9be500000902000043e2000000000000001000000000000a000000050a0f14191e23282d32373c41464b50555a0000
1 10 800004c317112f9be500000902000043e2000000000000001000000000000a000000050a0f14191e23282d32372c41464b50555a0000
1 10 800004c317112f9be500000902000043e2000000000000001000000000000a000073787d82878c91969ba0a5aaafb4b9bec3c8cd0000
1 10 800004c317122f9be500000902000043e2000000000000001000000000000a0000e6ebf0f5faff04090e13181d22272c31363b400000
1 10 800004c317122f9be500000902000043e2000000000000001000000000000a0000595e63686d72777c81868b90959a9fa4a9aeb30000
1 10 800004c317132f9be500000902000043e2000000000000001000000000000a0016ccd1d6dbe0e5eaeff4f9fe03080d12171c21260000
1 10 800004c317132f9be500000902000043e2000000000000001000000000000a00003f44494e53585d62676c71767b80858a8f94990000
1 10 800004c317122f9be500000902000043e2000000000000001000000000000a0000595e63686d72377c81868b90859a9fa4a9aeb30000
1 10 800004c317132f9be500000902000043e2000000000000001000000000000a0016ccd1d6dbe0e5aaeff4f9fe03080d12171c21260000
1 10 800004c317132f9be500000902000043e2000000000000001000000000000a00003f44494e53581d62676c71766b80858a8f94990000
1 2 800004c317132f9be500000902000043e200000000000000100000000000020000000000000000000000000000000000000000000000
+42 -1
View File
@@ -8,6 +8,7 @@
#include "../src/translate.h"
#include "../src/config.h"
#include "../src/eventloop.h"
#include "../src/dmr/dmr.h"
/* ---- captured DMRD output ---- */
#define MAXCAP 64
@@ -114,7 +115,47 @@ int main(void)
}
if (fi) fclose(fi);
if (fr) fclose(fr);
translator_free(tr);
/* ---- bad-voice flags (IPSC->HBP): flagged AMBE frames become MMDVMHost silence ---- */
static const uint8_t SIL[9] = {0xB9,0xE8,0x81,0x52,0x61,0x73,0x00,0x2A,0x6B};
static const int want[2][3] = {{1,1,1}, {0,1,0}}; /* per voice frame: flagged? */
int bad_fails = 0;
tr = translator_new(&cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ncap = 0;
fi = fopen("tests/parity_in.txt", "r");
for (int k = 0; k < 3 && fi && fgets(line, sizeof line, fi); k++) {
int ts, bt; char hex[400];
if (sscanf(line, "%d %d %399s", &ts, &bt, hex) != 3) continue;
uint8_t raw[400]; int n = unhex(hex, raw);
if (k >= 1) {
const int *w = want[k - 1];
if (w[0]) raw[32] |= 0x01;
if (w[1]) raw[33 + 49 / 8] |= (uint8_t)(0x80 >> (49 % 8));
if (w[2]) raw[33 + 99 / 8] |= (uint8_t)(0x80 >> (99 % 8));
}
translator_ipsc_voice_received(tr, raw, n, ts, bt);
}
if (fi) fclose(fi);
if (ncap != 3) { fprintf(stderr, "FAIL bad-voice: expected 3 DMRD, got %d\n", ncap); bad_fails++; }
for (int v = 0; v < 2 && v + 1 < ncap; v++) {
dmr_bit b[264]; dmr_bytes_to_bits(cap[v + 1] + 20, 33, b);
dmr_bit f[3][72];
memcpy(f[0], b, 72);
memcpy(f[1], b + 72, 36); memcpy(f[1] + 36, b + 156, 36);
memcpy(f[2], b + 192, 72);
dmr_bit s[72]; dmr_bytes_to_bits(SIL, 9, s);
for (int i = 0; i < 3; i++) {
int is_sil = memcmp(f[i], s, sizeof s) == 0;
if (is_sil != want[v][i]) {
fprintf(stderr, "FAIL bad-voice: voice %d frame %d silence=%d, want %d\n", v, i + 1, is_sil, want[v][i]);
bad_fails++;
}
}
}
printf("Bad-voice flags (IPSC->HBP): %d failures\n", bad_fails);
translator_free(tr); ev_free(loop);
return (fails || in_fails) ? 1 : 0;
return (fails || in_fails || bad_fails) ? 1 : 0;
}