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>
This commit is contained in:
Cort BuffingtonandClaude Opus 5.5 committed 2026-10-13 10:24:16 -05:00
1 parent ec83237268
commit 7d9ce4dc8e
8 files changed
+287 -40

No files matched your search

+4 -1
View File
@@ -90,11 +90,14 @@ TEST_SUPPORT := $(SRC_DIR)/config.c $(SRC_DIR)/toml.c $(SRC_DIR)/log.c \
# Tests:
# - test_dsp: dmr module vs golden vectors generated from dmr_utils3
# - test_parity: C translator output vs Python translator output, byte-for-byte
test: tests/test_dsp.c tests/test_parity.c $(TEST_SUPPORT)
# - test_private: private calls both directions, from captured repeater/HBlink3 traffic
test: tests/test_dsp.c tests/test_parity.c tests/test_private.c $(TEST_SUPPORT)
$(CC) $(CFLAGS) -I. -o /tmp/ipsc2hbpc_test_dsp tests/test_dsp.c $(DMR_SOURCES)
/tmp/ipsc2hbpc_test_dsp tests/dsp_vectors.txt
$(CC) $(CFLAGS) -I. -o /tmp/ipsc2hbpc_test_parity tests/test_parity.c $(TEST_SUPPORT)
/tmp/ipsc2hbpc_test_parity
$(CC) $(CFLAGS) -I. -o /tmp/ipsc2hbpc_test_private tests/test_private.c $(TEST_SUPPORT)
/tmp/ipsc2hbpc_test_private
clean:
rm -f $(OBJECTS) $(BIN)
+3 -2
View File
@@ -1,6 +1,6 @@
## PROJECT: IPSC to HomeBrew Protocol Translator (C) ##
**NOTICE:** This software only supports Group Voice traffic at this time. IPSC is not an open standard. Supporting it involves painstaking reverse engineering of IPSC packets, and much is unknown. IPSC is owned by and heavily protected by Motorola Solutions, Inc. Please do not ask for features that require further deciphering of IPSC without providing verified correct, legally obtained information about the protocol necessary to support a feature.
**NOTICE:** This software only supports Group and Private (unit-to-unit) Voice traffic at this time; confirmed private calls are not supported. IPSC is not an open standard. Supporting it involves painstaking reverse engineering of IPSC packets, and much is unknown. IPSC is owned by and heavily protected by Motorola Solutions, Inc. Please do not ask for features that require further deciphering of IPSC without providing verified correct, legally obtained information about the protocol necessary to support a feature.
**PURPOSE:** A single, small native daemon that connects a Motorola MOTOTRBO IPSC system to one upstream DMR network server running the open HomeBrew Repeater Protocol (BrandMeister, DMR+, FreeDMR, HBlink4, etc.).
@@ -33,9 +33,10 @@ The Python `ipsc2hbp` is a single asyncio process — already simple and effecti
**VERIFICATION:**
`make test` runs two suites:
`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 private-call test replays private calls captured from a MOTOTRBO repeater and HBlink3, in both directions, full and late entry.
**WHAT IT IS NOT:**
+11 -10
View File
@@ -252,24 +252,24 @@ static void master_de_reg_req(ipsc *ip, const uint8_t *d, int len, const char *h
remove_peer(ip, find_peer(ip, pid));
}
static void on_group_voice(ipsc *ip, const uint8_t *d, int len, const char *host, int port,
int from_master)
static void on_voice(ipsc *ip, const uint8_t *d, int len, const char *host, int port,
int from_master)
{
if (!from_master) { /* master mode: require registered peer */
if (peer_count(ip) == 0) return;
if (len < 5 || find_peer(ip, d + 1) < 0) {
LOGD(LOGN, "GROUP_VOICE from unregistered peer at %s:%d — dropped", host, port);
LOGD(LOGN, "voice 0x%02x from unregistered peer at %s:%d — dropped", d[0], host, port);
return;
}
} else {
if (!ip->connected) return;
}
if (len < GV_MIN_LEN) { LOGW(LOGN, "GROUP_VOICE too short (%d bytes) from %s:%d", len, host, port); return; }
if (len < GV_MIN_LEN) { LOGW(LOGN, "voice 0x%02x too short (%d bytes) from %s:%d", d[0], len, host, port); return; }
int burst_type = d[GV_BURST_TYPE_OFF];
int call_info = d[GV_CALL_INFO_OFF];
LOGD(LOGN, "GROUP_VOICE len=%d burst=0x%02x raw[0:32]=%s from %s:%d",
len, burst_type, log_hex(d, len < 32 ? len : 32), host, port);
LOGD(LOGN, "%s len=%d burst=0x%02x raw[0:32]=%s from %s:%d",
d[0] == PVT_VOICE ? "PVT_VOICE" : "GROUP_VOICE", len, burst_type, log_hex(d, len < 32 ? len : 32), host, port);
int ts;
if (burst_type == VOICE_HEAD || burst_type == VOICE_TERM)
@@ -553,8 +553,8 @@ static void recv_cb(ev_loop *loop, int fd, void *ud)
case MASTER_ALIVE_REQ: master_alive_req(ip, buf, n, host, port); break;
case PEER_LIST_REQ: master_peer_list_req(ip, host, port); break;
case DE_REG_REQ: master_de_reg_req(ip, buf, n, host, port); break;
case GROUP_VOICE: on_group_voice(ip, buf, n, host, port, 0); break;
case PVT_VOICE: LOGD(LOGN, "PVT_VOICE from %s:%d — ignored", host, port); break;
case GROUP_VOICE: on_voice(ip, buf, n, host, port, 0); break;
case PVT_VOICE: on_voice(ip, buf, n, host, port, 0); break;
case GROUP_DATA: case PVT_DATA:
LOGD(LOGN, "Data packet 0x%02x from %s:%d — ignored", opcode, host, port); break;
case REPEATER_BLOCKED: LOGD(LOGN, "REPEATER_BLOCKED from %s:%d", host, port); break;
@@ -581,7 +581,8 @@ static void recv_cb(ev_loop *loop, int fd, void *ud)
return;
}
switch (opcode) {
case GROUP_VOICE: on_group_voice(ip, buf, n, host, port, 1); break;
case GROUP_VOICE: case PVT_VOICE:
on_voice(ip, buf, n, host, port, 1); break;
case MASTER_REG_REPLY: peer_reg_reply(ip, buf, n, host, port); break;
case PEER_LIST_REPLY: ip->missed = 0; ip->state = ST_ACTIVE;
peer_process_list(ip, buf, n); break;
@@ -597,7 +598,7 @@ static void recv_cb(ev_loop *loop, int fd, void *ud)
case GROUP_DATA: case PVT_DATA:
LOGD(LOGN, "Data packet 0x%02x from %s:%d — ignored", opcode, host, port); break;
case CALL_CONFIRMATION: case TXT_MESSAGE_ACK: case CALL_MON_STATUS: case CALL_MON_RPT:
case REPEATER_BLOCKED: case PVT_VOICE: case RPT_WAKE_UP: case CALL_INTERRUPT_REQ:
case REPEATER_BLOCKED: case RPT_WAKE_UP: case CALL_INTERRUPT_REQ:
case DE_REG_REPLY: case UNKNOWN_9E: case WIRELINE: case REMOTE_PROG_REQ:
case REMOTE_PROG_REPLY: case OPCODE_0xF0:
LOGD(LOGN, "opcode 0x%02x from %s:%d — received, not handled", opcode, host, port); break;
+1
View File
@@ -67,6 +67,7 @@
#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 */
#define FLCO_PRIVATE 0x03 /* Unit-to-Unit Voice Channel User (private call) */
#define AUTH_DIGEST_LEN 10
+39 -27
View File
@@ -102,6 +102,9 @@ 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(); }
}
/* LC options for a private (unit-to-unit) call: FLCO, FID, service options. */
static const uint8_t LC_OPT_PVT[3] = {FLCO_PRIVATE, 0x00, 0x00};
/* 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};
@@ -170,13 +173,13 @@ static void build_embed(translator *tr, int ts, int pos, dmr_bit embed[48])
memcpy(embed + 40, DMR_EMB[idx] + 8, 8);
}
/* assemble a complete GROUP_VOICE packet; returns length */
/* assemble a complete GROUP_VOICE / PVT_VOICE packet; returns length */
static int build_gv(translator *tr, const uint8_t src[3], const uint8_t dst[3],
int call_info, const uint8_t rtp_hdr[12],
const uint8_t *gv_payload, int gv_len, int stream_id, uint8_t *out)
const uint8_t *gv_payload, int gv_len, int stream_id, int pvt, uint8_t *out)
{
int p = 0;
out[p++] = GROUP_VOICE;
out[p++] = pvt ? PVT_VOICE : GROUP_VOICE;
memcpy(out + p, tr->master_id_b, 4); p += 4;
out[p++] = (uint8_t)stream_id;
memcpy(out + p, src, 3); p += 3;
@@ -335,6 +338,10 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
const uint8_t *src_sub = data + GV_SRC_SUB_OFF;
const uint8_t *dst_group = data + GV_DST_GROUP_OFF;
int flags = (ts == 2) ? HBPF_TGID_TS2 : 0x00;
int is_pvt = (data[0] == PVT_VOICE);
if (is_pvt) flags |= HBPF_TGID_CALL_P;
const uint8_t *lc_opt = is_pvt ? LC_OPT_PVT : DMR_LC_OPT;
const char *dst_kind = is_pvt ? "unit" : "tg";
if (len >= 17) {
tr->peer_call_type = data[12];
@@ -356,15 +363,15 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
rand4(tr->out_stream_id[ts]);
tr->out_has_stream[ts] = 1;
tr->out_ipsc_stream_id[ts] = ipsc_stream_id;
LOGI(LOGN, "IPSC call start: src=%u tg=%u ts=%d stream=%s int_seq_id=0x%02x",
(unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]),
LOGI(LOGN, "IPSC call start: src=%u %s=%u ts=%d stream=%s int_seq_id=0x%02x",
(unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]), dst_kind,
(unsigned)(dst_group[0]<<16|dst_group[1]<<8|dst_group[2]),
ts, log_hex(tr->out_stream_id[ts], 4), ipsc_stream_id);
} else {
LOGD(LOGN, "Duplicate VOICE_HEAD ts=%d — keeping stream=%s", ts, log_hex(tr->out_stream_id[ts], 4));
}
tr->out_frame_pos[ts] = 0;
uint8_t lc[9]; memcpy(lc, DMR_LC_OPT, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
uint8_t lc[9]; memcpy(lc, lc_opt, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
memcpy(tr->out_lc[ts], lc, 9); tr->out_has_lc[ts]=1;
dmr_encode_emblc(lc, tr->out_emb[ts]); tr->out_has_emb[ts]=1;
dmr_bit full_lc[196]; dmr_bptc_encode_lc(lc, 0, full_lc);
@@ -380,7 +387,7 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
if (!tr->out_has_stream[ts]) return;
uint8_t lc[9];
if (tr->out_has_lc[ts]) memcpy(lc, tr->out_lc[ts], 9);
else { memcpy(lc, DMR_LC_OPT, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3); }
else { memcpy(lc, lc_opt, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3); }
dmr_bit full_lc[196]; dmr_bptc_encode_lc(lc, 1, full_lc);
dmr_bit fb[264]; int at=0;
memcpy(fb+at, full_lc, 98); at+=98;
@@ -403,24 +410,25 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
out_reset_call(tr, ts);
}
if (!tr->out_has_stream[ts]) {
/* Late entry: anchor identity ONLY to a GVCU burst E. A burst E carrying
* Talker Alias / GPS (FLCO != GROUP at byte 56) puts alias bytes in the header
* src/dst, so we must not adopt them — wait for the next GVCU superframe. */
/* Late entry: anchor identity ONLY to a burst E carrying the voice LC (GVCU for
* group, UU_V_Ch_Usr for private). A burst E carrying Talker Alias / GPS puts
* alias bytes in the header src/dst, so we must not adopt them — wait for the
* next voice-LC superframe. */
if (len <= GV_BE_LC_FLCO_OFF || data[32] != GV_BE_FLAG) return;
if (data[GV_BE_LC_FLCO_OFF] != FLCO_GROUP) {
LOGD(LOGN, "IPSC late entry deferred ts=%d — burst E embedded LC is non-GVCU "
"(flco=0x%02x, Talker Alias/GPS); waiting for GVCU superframe",
if (data[GV_BE_LC_FLCO_OFF] != (is_pvt ? FLCO_PRIVATE : FLCO_GROUP)) {
LOGD(LOGN, "IPSC late entry deferred ts=%d — burst E embedded LC is not the voice LC "
"(flco=0x%02x, Talker Alias/GPS); waiting for voice-LC superframe",
ts, data[GV_BE_LC_FLCO_OFF]);
return;
}
uint8_t lc[9]; memcpy(lc, DMR_LC_OPT, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
uint8_t lc[9]; memcpy(lc, lc_opt, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
rand4(tr->out_stream_id[ts]); tr->out_has_stream[ts]=1;
tr->out_ipsc_stream_id[ts]=ipsc_stream_id;
memcpy(tr->out_lc[ts], lc, 9); tr->out_has_lc[ts]=1;
dmr_encode_emblc(lc, tr->out_emb[ts]); tr->out_has_emb[ts]=1;
tr->out_frame_pos[ts]=4;
LOGI(LOGN, "IPSC late entry: ts=%d src=%u tg=%u — LC from GVCU Burst E, stream=%s int_seq_id=0x%02x",
ts, (unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]),
LOGI(LOGN, "IPSC late entry: ts=%d src=%u %s=%u — LC from Burst E, stream=%s int_seq_id=0x%02x",
ts, (unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]), dst_kind,
(unsigned)(dst_group[0]<<16|dst_group[1]<<8|dst_group[2]),
log_hex(tr->out_stream_id[ts], 4), ipsc_stream_id);
}
@@ -456,7 +464,7 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
uint8_t fb_lc[9];
const uint8_t *locked_lc;
if (tr->out_has_lc[ts]) { locked_lc = tr->out_lc[ts]; }
else { memcpy(fb_lc, DMR_LC_OPT, 3); memcpy(fb_lc+3, dst_group, 3); memcpy(fb_lc+6, src_sub, 3); locked_lc = fb_lc; }
else { memcpy(fb_lc, lc_opt, 3); memcpy(fb_lc+3, dst_group, 3); memcpy(fb_lc+6, src_sub, 3); locked_lc = fb_lc; }
const uint8_t *out_dst = locked_lc + 3;
const uint8_t *out_src = locked_lc + 6;
@@ -475,8 +483,8 @@ void translator_ipsc_voice_received(translator *tr, const uint8_t *data, int len
hbp_send_dmrd(tr->hb, dmrd, p);
if (burst_type == VOICE_TERM) {
LOGI(LOGN, "IPSC call end: src=%u tg=%u ts=%d stream=%s int_seq_id=0x%02x",
(unsigned)(out_src[0]<<16|out_src[1]<<8|out_src[2]),
LOGI(LOGN, "IPSC call end: src=%u %s=%u ts=%d stream=%s int_seq_id=0x%02x",
(unsigned)(out_src[0]<<16|out_src[1]<<8|out_src[2]), dst_kind,
(unsigned)(out_dst[0]<<16|out_dst[1]<<8|out_dst[2]),
ts, log_hex(tr->out_stream_id[ts], 4), tr->out_ipsc_stream_id[ts]);
out_reset_call(tr, ts);
@@ -561,7 +569,7 @@ static void clear_in_call(translator *tr, int ts)
tr->in_has_emb[ts] = 0;
}
/* Build and send one inbound GROUP_VOICE frame; advance RTP counters. */
/* Build and send one inbound GROUP_VOICE / PVT_VOICE frame; advance RTP counters. */
static void emit_in(translator *tr, int ts, const uint8_t *lc, int call_info,
int rtp_pt, const uint8_t *gv_payload, int gv_len, int advance_ts)
{
@@ -572,8 +580,9 @@ static void emit_in(translator *tr, int ts, const uint8_t *lc, int call_info,
rtp_hdr[6]=(uint8_t)(tr->in_rtp_ts[ts]>>8); rtp_hdr[7]=(uint8_t)(tr->in_rtp_ts[ts]);
tr->in_rtp_seq[ts]++;
uint8_t gv[128];
int pvt = (lc[0] & 0x3F) == FLCO_PRIVATE; /* opcode follows the LC's FLCO */
int n = build_gv(tr, lc + 6, lc + 3, call_info, rtp_hdr, gv_payload, gv_len,
tr->in_stream_id[ts], gv);
tr->in_stream_id[ts], pvt, gv);
ipsc_send_voice(tr->ip, gv, n);
}
@@ -584,8 +593,8 @@ static void emit_term(translator *tr, int ts)
uint8_t gv_payload[24]; gv_payload[0]=VOICE_TERM;
build_ipsc_voice_payload(lc, VOICE_TERM, gv_payload+1);
emit_in(tr, ts, lc, call_info, 0x5e, gv_payload, 24, 0);
LOGI(LOGN, "HBP call end: src=%u tg=%u ts=%d ipsc_id=0x%02x",
(unsigned)(lc[6]<<16|lc[7]<<8|lc[8]),
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",
(unsigned)(lc[3]<<16|lc[4]<<8|lc[5]), ts, tr->in_stream_id[ts]);
clear_in_call(tr, ts);
}
@@ -648,6 +657,7 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
const uint8_t *payload_33 = dmrd + DMRD_PAYLOAD_OFF;
int ts = (flags & HBPF_TGID_TS2) ? 2 : 1;
const uint8_t *lc_opt = (flags & HBPF_TGID_CALL_P) ? LC_OPT_PVT : DMR_LC_OPT;
tr->in_last_pkt[ts] = ev_now(tr->loop);
int frame_type = flags & HBPF_FRAMETYPE_MASK;
int dtype = flags & HBPF_DTYPE_MASK;
@@ -667,8 +677,9 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
memcpy(tr->in_hbp_stream[ts], hbp_stream, 4); tr->in_has_hbp_stream[ts]=1;
tr->in_stream_ctr = (tr->in_stream_ctr + 1) & 0xFF;
tr->in_stream_id[ts] = tr->in_stream_ctr;
LOGI(LOGN, "HBP call start: src=%u tg=%u ts=%d stream=%s ipsc_id=0x%02x",
LOGI(LOGN, "HBP call start: src=%u %s=%u ts=%d stream=%s ipsc_id=0x%02x",
(unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]),
(lc[0] & 0x3F) == FLCO_PRIVATE ? "unit" : "tg",
(unsigned)(dst_group[0]<<16|dst_group[1]<<8|dst_group[2]),
ts, log_hex(hbp_stream, 4), tr->in_stream_id[ts]);
} else {
@@ -694,7 +705,7 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
if (!tr->in_has_lc[ts]) {
/* Terminator with no active call — build a fallback LC so we can
* still relay a clean call end. */
uint8_t lc[9]; memcpy(lc, DMR_LC_OPT, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
uint8_t lc[9]; memcpy(lc, lc_opt, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
memcpy(tr->in_lc[ts], lc, 9); tr->in_has_lc[ts]=1;
dmr_encode_emblc(lc, tr->in_emb[ts]); tr->in_has_emb[ts]=1;
if (!tr->in_has_hbp_stream[ts]) {
@@ -720,15 +731,16 @@ void translator_hbp_voice_received(translator *tr, const uint8_t *dmrd, int len)
clear_in_call(tr, ts);
}
if (!tr->in_has_lc[ts]) {
uint8_t lc[9]; memcpy(lc, DMR_LC_OPT, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
uint8_t lc[9]; memcpy(lc, lc_opt, 3); memcpy(lc+3, dst_group, 3); memcpy(lc+6, src_sub, 3);
memcpy(tr->in_lc[ts], lc, 9); tr->in_has_lc[ts]=1;
dmr_encode_emblc(lc, tr->in_emb[ts]); tr->in_has_emb[ts]=1;
memcpy(tr->in_hbp_stream[ts], hbp_stream, 4); tr->in_has_hbp_stream[ts]=1;
tr->in_stream_ctr = (tr->in_stream_ctr + 1) & 0xFF;
tr->in_stream_id[ts] = tr->in_stream_ctr;
tr->in_rtp_seq[ts] = 0; tr->in_rtp_ts[ts] = 0;
LOGI(LOGN, "HBP late entry: ts=%d src=%u tg=%u — LC from stream, hbp_stream=%s",
LOGI(LOGN, "HBP late entry: ts=%d src=%u %s=%u — LC from stream, hbp_stream=%s",
ts, (unsigned)(src_sub[0]<<16|src_sub[1]<<8|src_sub[2]),
(flags & HBPF_TGID_CALL_P) ? "unit" : "tg",
(unsigned)(dst_group[0]<<16|dst_group[1]<<8|dst_group[2]), log_hex(hbp_stream, 4));
}
int cur_pos;
+50
View File
@@ -0,0 +1,50 @@
444d5244002f9be52f9f67002f9e1a61bba7928a4f6535d70f5229e8e0c1b642846dff57d75df5de31c82cd0b46105633557ca0fb9003c
444d5244012f9be52f9f67002f9e1a50bba7928ab9e881526173002a6bb9e881526755fd7df75f7173002a6bb9e881526173002a68001e
444d5244022f9be52f9f67002f9e1a41bba7928ab9e881526173002a6bb9e881526131e06061e91173002a6bb9e881526173002a68001e
444d5244032f9be52f9f67002f9e1a42bba7928ab9e881526173002a6bb9e881526173f18b1b774173002a6b39c084120170b1e3e4001e
444d5244042f9be52f9f67002f9e1a43bba7928ad8f3a8b31166dc4af4c43882416172d333c827477b2100bf8f1681274677e40dd0011e
444d5244052f9be52f9f67002f9e1a44bba7928aeec5a15731177e92c90e3ea7147158e871b930734150f0591f2ca770747155a44c001e
444d5244062f9be52f9f67002f9e1a45bba7928ae6abe66030176e8d19ea1a831401100000000e2407f89336cce5a277154268c3a8011e
444d5244072f9be52f9f67002f9e1a50bba7928adde7a11733111aa59d6c6fc0621755fd7df75f712fb5d2a50c1ea564170254e77c0048
444d5244082f9be52f9f67002f9e1a41bba7928a0d2fa560341250926fd2d5aff24131e06061e916e57697940e20d8ae2547e9489c0048
444d5244092f9be52f9f67002f9e1a42bba7928ad7aac74127530bce69d69be7036173f18b1b774577089c3faed3a3d46ae366e8c40048
444d52440a2f9be52f9f67002f9e1a43bba7928a4e04afd75151bf858111daea712172d333c82740fa3ede5cf92a8233526799d2240148
444d52440b2f9be52f9f67002f9e1a44bba7928abfa7e4043649a8c705fde481463158e871b93072177ce5bfecd6816667764ac2c80148
444d52440c2f9be52f9f67002f9e1a45bba7928af82a82333435f88225e59bc75701100000000e24730ce92fc78be71304535ecb3c0148
444d52440d2f9be52f9f67002f9e1a50bba7928a5b00857116755b6bf87a12a5515755fd7df75f73131d6ec9eb18a1031665eee604001e
444d52440e2f9be52f9f67002f9e1a41bba7928ac808a2271023dcc354c719a2107131e06061e9140c7026abc13cc572594fd2b34c011e
444d52440f2f9be52f9f67002f9e1a42bba7928a95853b9a8e5e6f47e2b3d56374c173f18b1b7747edeece63d7a16235ace5aa180c021e
444d5244102f9be52f9f67002f9e1a43bba7928ac4ea01608b5316cefbb08c0326b172d333c827432b421fb7809e2226b25a470ee4001e
444d5244112f9be52f9f67002f9e1a44bba7928af0f8474729cdee9fcee4ce20127158e871b93073fe08695dc2eb2d3d32e7729f08011e
444d5244122f9be52f9f67002f9e1a45bba7928a80b8683960edb4dbc097992d7aa1100000000e21771acd91e28e0e3661a9a421fc001e
444d5244132f9be52f9f67002f9e1a50bba7928a86ac12f86b51e28db0b3a9281e1755fd7df75f78164205986c7e87cf7edcac57f20348
444d5244142f9be52f9f67002f9e1a41bba7928a86979ce2c56628903fd0a98056b131e06061e91299b08b6fe2525e9dc519731f0a0348
444d5244152f9be52f9f67002f9e1a42bba7928af36027b6cc22e92d9e95422232d173f18b1b774208e3195ea24027610d1deeb2460448
444d5244162f9be52f9f67002f9e1a43bba7928a689c27132dbb0a28d508cf42303172d333c8274af1f85f385bad24321e98286d860448
444d5244172f9be52f9f67002f9e1a44bba7928a2a8c606cf6412475582e897c50b158e871b930744d6f84691ba858705c2e535b120248
444d5244182f9be52f9f67002f9e1a45bba7928a5ffe09353a8cf06047a3510d0801100000000e2becf44ffbd3356b48b46828b8c20248
444d5244192f9be52f9f67002f9e1a50bba7928a86414a4a9c254a0faba4040963e755fd7df75f73870e5871e2142307ad1090cd30001e
444d52441a2f9be52f9f67002f9e1a41bba7928ad1060023fb47d6ec04c2346076e131e06061e9179b062519a44623114bf68af99c001e
444d52441b2f9be52f9f67002f9e1a42bba7928ac6413c1111bb612536803b29a52173f18b1b7742bf471a49e75e08811ac360ec54001e
444d52441c2f9be52f9f67002f9e1a43bba7928ad68b86116fde8e1517c4ad5fe89172d333c827447ef72ed4d42c02c4e186d33ab8001e
444d52441d2f9be52f9f67002f9e1a44bba7928ac10444fda4b2f96e3783462634e158e871b93075d0967197c6106712c234567ba4001e
444d52441e2f9be52f9f67002f9e1a45bba7928a84402336f70a96785ac04e2042e1100000000e2578ccb4dab01e4410e5701ac410001e
444d52441f2f9be52f9f67002f9e1a50bba7928af65c23fb4d101e61bbc2fa08a00755fd7df75f709dfcf343b902e4216e5342ce140246
444d5244202f9be52f9f67002f9e1a41bba7928ab06e5bb94f4e530bd44a8f6c3c4131e06061e910a6974861a5520848f6dcdfd4900046
444d5244212f9be52f9f67002f9e1a42bba7928ac269094b0942dbc893c47866478173f18b1b774d5d7f5518b4386354c39c0e88dc0246
444d5244222f9be52f9f67002f9e1a43bba7928af25c0071f728dee1ba86933c221172d333c82744cbe8b908b8b0a3000a3b5e00c00446
444d5244232f9be52f9f67002f9e1a44bba7928a9fa28604c60e768fac8a068c29b158e871b9307897f05b42c6c0a60662d4865cbc0446
444d5244242f9be52f9f67002f9e1a45bba7928af4407abd0f5f39904693646636d1100000000e2a1902cfbc94384242f3bc1bc9fc0846
444d5244252f9be52f9f67002f9e1a50bba7928a940d06be07973ceb6ab00b4bb15755fd7df75f7b41f4d73796c5088165faa46cc00023
444d5244262f9be52f9f67002f9e1a41bba7928aa192a603c474fa7e40904e7bdd0131e06061e91a48253be4837a7f10d5deb7c11c0023
444d5244272f9be52f9f67002f9e1a42bba7928ac26f2116f23adab2aee44a0453e173f18b1b7742b7acfa45b4d6460659fb8310580023
444d5244282f9be52f9f67002f9e1a43bba7928a80c327ef11b6ccba909780c0453172d333c82742be760d73d69da416e331a1f0900123
444d5244292f9be52f9f67002f9e1a44bba7928ae5d2c5122b4c1383e1e48c5eaaf158e871b9307259a349a392ca06e5e7192f8d380023
444d52442a2f9be52f9f67002f9e1a45bba7928af854446c94512a03cf8f171c3001100000000e2cccbf53b3ff455b323c907a502c0023
444d52442b2f9be52f9f67002f9e1a50bba7928aef780d841b16ac3273c96228ae3755fd7df75f77ac43cc9446be930e048aa60de40140
444d52442c2f9be52f9f67002f9e1a41bba7928af28ce164739d5f90b7b812c6713131e06061e91c3361ed07c872e33156936232f40040
444d52442d2f9be52f9f67002f9e1a42bba7928ab692c02601f9274c40b4021ef96173f18b1b774b65cd94cf925030ff3d7703893c0040
444d52442e2f9be52f9f67002f9e1a43bba7928ac10c4cea21bb12e715b25fe2224172d333c82744bbb35cbadac1a65537cf5cca740140
444d52442f2f9be52f9f67002f9e1a44bba7928a9b90e0353290aabcbd9aa0c2353158e871b93075b5b98daba34c1e7434aa3bbe900140
444d5244302f9be52f9f67002f9e1a45bba7928aa47835d525dfde27de864b35d741100000000e25bcfc128ed0b261e846bf5fd8380140
444d5244312f9be52f9f67002f9e1a62bba7928a4f0a35030fe22990e0b1b6e284adff57d75df5d964dc2fe8b30109233d57d30f8a002a
+47
View File
@@ -0,0 +1,47 @@
1 1 810004c313142f9f672f9be502000046d20080dd46791b244858000000000140000a800a00600300002f9be52f9f67783e23001140a7
1 1 810004c313142f9f672f9be502000046d200805d467a1b244a38000000000140000a800a00600300002f9be52f9f67783e23001140b8
1 1 810004c313142f9f672f9be502000046d200805d467b1b244c18000000000140000a800a00600300002f9be52f9f67783e23001140bd
1 1 810004c313142f9f672f9be502000046d200805d467c1b244df8000000000140000a800a00600300002f9be52f9f67783e23001140c2
1 10 810004c313142f9f672f9be502000046d200805d467d1b244fd8000000000a1440a80fa886a48caa001adb71206a802619724df0
1 10 810004c313142f9f672f9be502000046d200805d467e1b2451b8000000000a1906a801bd14e4923600f484615bc6aec99ca267381e06051e12
1 10 810004c313142f9f672f9be502000046d200805d467f1b245398000000000a1906ee0a85038b5e3a058b60f1c66e816b49c672c83609b1b716
1 10 810004c313142f9f672f9be502000046d200805d46801b245578000000000a1906e804a54e8fdc2a8dfee4dbde49e162781e7f983f363c8216
1 10 810004c313142f9f672f9be502000046d200805d46811b245758000000000a2216cf0948418325aa0168d082d12ca3eed4e4db48939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d46821b245938000000000a19069e08118b4b9b3a009b212be64b80b8f9988f580000000010
1 10 810004c313142f9f672f9be502000046d200805d46831b245b18000000000a1440b81379b3e003a7c370d3722acb806d99e2c7a8
1 10 810004c313142f9f672f9be502000046d200805d46841b245cf8000000000a1906a8022a004a5caa03583882d74a808eb478bdb81e06051e12
1 10 810004c313142f9f672f9be502000046d200805d46851b245ed8000000000a1906673e33fedd1c19e3ff4cb3578a58b17da7f0183609b1b716
1 10 810004c313142f9f672f9be502000046d200805d46861b2460b8000000000a1906b0fbbc3fe0282c329f47c0696b0af582f44df03f363c8216
1 10 810004c313142f9f672f9be502000046d200805d46871b246298000000000a2216b08876ae67cdb00b1caed3110c62a1b476d540939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d46881b246478000000000a1906c6363c124be531938b84e0688b64cd36547ea00000000010
1 10 810004c313142f9f672f9be502000046d200805d46891b246658000000000a1440b35f0847b602b19309afe022ec6bbaf8fa52d8
1 10 810004c313142f9f672f9be502000046d200805d468a1b246838000000000a1906d73dfa4e685da9b23e6bd10eea49d1f7f99bc01e06051e12
1 10 810004c313142f9f672f9be502000046d200805d468b1b246a18000000000a190690cb3ebefd02a427cbf4e301a90851edf021583609b1b716
1 10 810004c313142f9f672f9be502000046d200805d468c1b246bf8000000000a190680845e5227f4201d1698117a6807498bd4bcb83f363c8216
1 10 810004c313142f9f672f9be502000046d200805d468d1b246dd8000000000a221680851651aa64a025258d12f06808098318cc78939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d468e1b246fb8000000000a190680a80f3b0506a021874e71846805e5952ef2180000000010
1 10 810004c313142f9f672f9be502000046d200805d468f1b247198000000000a1440908cd75e8804a02e204801e1680c983044fe38
1 10 810004c313142f9f672f9be502000046d200805d46901b247378000000000a190680d5d64367d0a0316dc413e8e90b88fb208d601e06051e12
1 10 810004c313142f9f672f9be502000046d200805d46911b247558000000000a1906902a1a4c4907259227b18066a963832296bb283609b1b716
1 10 810004c313142f9f672f9be502000046d200805d46921b247738000000000a1906964e78b92769a9b1fc47a8d7294afafb7077d83f363c8216
1 10 810004c313142f9f672f9be502000046d200805d46931b247918000000000a221694bd1fde181e2074971e2bd1c90d46f6e515f8939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d46941b247af8000000000a190690bd5d2e8bffa4294f4b8172090bdbf6fcd1800000000010
1 10 810004c313142f9f672f9be502000046d200805d46951b247cd8000000000a144090ba7f5f8d34a42f4b4bbbb6e90c99d2f47d50
1 10 810004c313142f9f672f9be502000046d200805d46961b247eb8000000000a190690c4d61207d5a42c25c463bee90ce473446e201e06051e12
1 10 810004c313142f9f672f9be502000046d200805d46971b248098000000000a1906a096573122a1a70fd60619336e9ac90ef360103609b1b716
1 10 810004c313142f9f672f9be502000046d200805d46981b248278000000000a1906ef4bf6227f40aa1ba700f9c32cc5bcd2e95f003f363c8216
1 10 810004c313142f9f672f9be502000046d200805d46991b248458000000000a2216b889285f0d1da9e605ec85792b2e4e6318bdb8939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d469a1b248638000000000a1906c099149263f6b429184498b1ae0865626605280000000010
1 10 810004c313142f9f672f9be502000046d200805d469b1b248818000000000a14401e3d0703cece3a1b99ba82e30d85f9c17abbc8
1 10 810004c313142f9f672f9be502000046d200805d469c1b2489f8000000000a1906d84df22d2bd2ae25ef06f17aab87456726cc781e06051e12
1 10 810004c313142f9f672f9be502000046d200805d469d1b248bd8000000000a1906ee393d1b4fc53a0ecef2b3c2ada2e0ab2475683609b1b716
1 10 810004c313142f9f672f9be502000046d200805d469e1b248db8000000000a1906e8737794eff0b306052f05290d8285f10c2bb83f363c8216
1 10 810004c313142f9f672f9be502000046d200805d469f1b248f98000000000a2216e82524ba25d3ba075db9a3524a819f663830e0939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d46a01b249178000000000a19069802c3086f2d26017d05a3d7a98027fa98f3380000000010
1 10 810004c313142f9f672f9be502000046d200805d46a11b249358000000000a1440b805a7896b60b604ec2e6141ad805ff8648dc8
1 10 810004c313142f9f672f9be502000046d200805d46a21b249538000000000a1906c800f3b9ae7bba003f91e26f8e81ba9a8699f01e06051e12
1 10 810004c313142f9f672f9be502000046d200805d46a31b249718000000000a19069c1ea2837c1cba004917926ace808e73d49b183609b1b716
1 10 810004c313142f9f672f9be502000046d200805d46a41b2498f8000000000a1906d80a60820f52ae0690c4795eeb83bf47e096e83f363c8216
1 10 810004c313142f9f672f9be502000046d200805d46a51b249ad8000000000a2216b728fb8f0901ae047edf987aed817a90325610939911960300002f9be52f9f6714
1 10 810004c313142f9f672f9be502000046d200805d46a61b249cb8000000000a1906e811d51e47ed36478b38fab12de3e5200379680000000010
1 2 810004c313142f9f672f9be502000046d240805e46a71b249e98000000000240000a800a00600300002f9be52f9f6777312c00123f4e
+132
View File
@@ -0,0 +1,132 @@
/* test_private.c — private (unit-to-unit) voice in both directions, using calls
* captured from a MOTOTRBO repeater and an HBlink3 server.
*
* pvt_ipsc_in.txt : PVT_VOICE call 3120999 -> 3120101, TS1, from the repeater
* pvt_hbp_in.txt : HBP private-call DMRD 3120101 -> 3120999, TS1, from HBlink3
*
* Each direction is run with the full call and as a late entry (headers dropped).
* Stubs the ipsc and hbp layers so only translate.c + the dmr module run. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../src/translate.h"
#include "../src/config.h"
#include "../src/eventloop.h"
#include "../src/dmr/dmr.h"
#define MAXCAP 256
static uint8_t cap[MAXCAP][128]; static int caplen[MAXCAP]; static int ncap;
static uint8_t gv[MAXCAP][128]; static int gvlen[MAXCAP]; static int ngv;
int ipsc_has_peers(struct ipsc *ip) { (void)ip; return 1; }
void ipsc_send_voice(struct ipsc *ip, const uint8_t *p, int n) {
(void)ip; if (ngv < MAXCAP) { memcpy(gv[ngv], p, (size_t)n); gvlen[ngv++] = n; }
}
int hbp_is_connected(struct hbp *hb) { (void)hb; return 1; }
void hbp_activate(struct hbp *hb) { (void)hb; }
void hbp_deactivate(struct hbp *hb) { (void)hb; }
void hbp_send_dmrd(struct hbp *hb, const uint8_t *d, int n) {
(void)hb; if (ncap < MAXCAP) { memcpy(cap[ncap], d, (size_t)n); caplen[ncap++] = n; }
}
static void stop_cb(ev_loop *loop, void *ud){ (void)ud; ev_stop(loop); }
static int hexv(char c){ if(c>='0'&&c<='9')return c-'0'; if(c>='a'&&c<='f')return c-'a'+10; return -1; }
static int unhex(const char *s, uint8_t *out){ int n=0; for(;s[0]&&s[1]&&hexv(s[0])>=0;s+=2) out[n++]=(uint8_t)((hexv(s[0])<<4)|hexv(s[1])); return n; }
static unsigned id3(const uint8_t *p) { return (unsigned)(p[0]<<16 | p[1]<<8 | p[2]); }
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); fails++; } } while (0)
/* LC carried in a DMRD voice header / terminator (BPTC full LC). */
static void dmrd_lc(const uint8_t *dmrd, uint8_t lc[9])
{
dmr_bit fb[264]; dmr_bytes_to_bits(dmrd + 20, 33, fb);
dmr_bit b[196]; memcpy(b, fb, 98); memcpy(b + 98, fb + 166, 98);
dmr_bptc_decode_full_lc(b, lc);
}
static void ipsc_to_hbp(const Config *cfg, ev_loop *loop, int late)
{
const char *name = late ? "IPSC->HBP late entry" : "IPSC->HBP";
translator *tr = translator_new(cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ncap = 0;
FILE *f = fopen("tests/pvt_ipsc_in.txt", "r");
if (!f) { perror("tests/pvt_ipsc_in.txt"); exit(2); }
char line[512]; int fed = 0;
while (fgets(line, sizeof line, f)) {
int ts, bt; char hex[400];
if (sscanf(line, "%d %d %399s", &ts, &bt, hex) != 3) continue;
if (late && bt == 1) continue; /* drop VOICE_HEADs */
uint8_t raw[400]; int n = unhex(hex, raw);
translator_ipsc_voice_received(tr, raw, n, ts, bt);
fed++;
}
fclose(f);
CHECK(ncap > 0, "%s: no DMRD produced", name);
int hdr = 0, term = 0;
for (int i = 0; i < ncap; i++) {
const uint8_t *d = cap[i]; int fl = d[15];
CHECK(fl & 0x40, "%s: DMRD[%d] flags 0x%02x missing private-call bit", name, i, fl);
CHECK(!(fl & 0x80), "%s: DMRD[%d] not TS1", name, i);
CHECK(id3(d + 5) == 3120999 && id3(d + 8) == 3120101, "%s: DMRD[%d] src/dst %u->%u", name, i, id3(d + 5), id3(d + 8));
if ((fl & 0x30) == 0x20) {
uint8_t lc[9]; dmrd_lc(d, lc);
CHECK((lc[0] & 0x3F) == 3, "%s: DMRD[%d] LC FLCO 0x%02x, want 3", name, i, lc[0] & 0x3F);
if ((fl & 0x0F) == 1) hdr++; else if ((fl & 0x0F) == 2) term++;
}
}
CHECK(term == 1, "%s: %d terminators, want 1", name, term);
if (!late) CHECK(hdr == 4 && ncap == fed, "%s: %d DMRD (%d headers) from %d IPSC packets", name, ncap, hdr, fed);
else CHECK(hdr == 0, "%s: %d headers, want 0", name, hdr);
printf("%s: %d DMRD\n", name, ncap);
translator_free(tr);
}
static void hbp_to_ipsc(const Config *cfg, ev_loop *loop, int late)
{
const char *name = late ? "HBP->IPSC late entry" : "HBP->IPSC";
translator *tr = translator_new(cfg, loop);
translator_set_protocols(tr, (struct ipsc *)1, (struct hbp *)1);
ngv = 0;
FILE *f = fopen("tests/pvt_hbp_in.txt", "r");
if (!f) { perror("tests/pvt_hbp_in.txt"); exit(2); }
char line[512];
while (fgets(line, sizeof line, f)) {
uint8_t raw[128]; int n = unhex(line, raw);
if (n < 55) continue;
if (late && (raw[15] & 0x3F) == 0x21) continue; /* drop voice headers */
translator_hbp_voice_received(tr, raw, n);
}
fclose(f);
ev_timer_after(loop, 4.0, stop_cb, loop);
ev_run(loop);
CHECK(ngv > 0, "%s: no IPSC voice produced", name);
int term = 0;
for (int i = 0; i < ngv; i++) {
const uint8_t *g = gv[i];
CHECK(g[0] == 0x81, "%s: GV[%d] opcode 0x%02x, want PVT_VOICE 0x81", name, i, g[0]);
CHECK(id3(g + 6) == 3120101 && id3(g + 9) == 3120999, "%s: GV[%d] src/dst %u->%u", name, i, id3(g + 6), id3(g + 9));
if (g[30] == 1 || g[30] == 2) { /* VOICE_HEAD / VOICE_TERM: LC at 38 */
CHECK((g[38] & 0x3F) == 3, "%s: GV[%d] LC FLCO 0x%02x, want 3", name, i, g[38] & 0x3F);
if (g[30] == 2) term++;
}
}
CHECK(term == 1, "%s: %d terminators, want 1", name, term);
printf("%s: %d PVT_VOICE\n", name, ngv);
translator_free(tr);
}
int main(void)
{
Config cfg; char err[4096];
if (config_load("tests/parity.toml", &cfg, err, sizeof err)) { fprintf(stderr, "config: %s\n", err); return 2; }
ev_loop *loop = ev_new();
ipsc_to_hbp(&cfg, loop, 0);
ipsc_to_hbp(&cfg, loop, 1);
hbp_to_ipsc(&cfg, loop, 0);
hbp_to_ipsc(&cfg, loop, 1);
ev_free(loop);
printf("Private calls: %d failures\n", fails);
return fails ? 1 : 0;
}