diff --git a/Makefile b/Makefile index 1c3e740..d4d08cc 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index a98b27b..1d2daa9 100644 --- a/README.md +++ b/README.md @@ -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:** diff --git a/src/ipsc.c b/src/ipsc.c index 344ff1a..9636472 100644 --- a/src/ipsc.c +++ b/src/ipsc.c @@ -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; diff --git a/src/ipsc_const.h b/src/ipsc_const.h index 3ccbb0a..c0d254f 100644 --- a/src/ipsc_const.h +++ b/src/ipsc_const.h @@ -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 diff --git a/src/translate.c b/src/translate.c index 723b13c..3b6a632 100644 --- a/src/translate.c +++ b/src/translate.c @@ -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; diff --git a/tests/pvt_hbp_in.txt b/tests/pvt_hbp_in.txt new file mode 100644 index 0000000..14ed932 --- /dev/null +++ b/tests/pvt_hbp_in.txt @@ -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 diff --git a/tests/pvt_ipsc_in.txt b/tests/pvt_ipsc_in.txt new file mode 100644 index 0000000..50be4b3 --- /dev/null +++ b/tests/pvt_ipsc_in.txt @@ -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 diff --git a/tests/test_private.c b/tests/test_private.c new file mode 100644 index 0000000..c4f7105 --- /dev/null +++ b/tests/test_private.c @@ -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 +#include +#include +#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; +}