mirror of
https://github.com/n0mjs710/ipsc2hbpc.git
synced 2026-11-08 04:18:24 +00:00
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:
1 parent
3cb47c74ac
commit
ec8bbfaedc
11 files changed
+128
-56
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user