Skip to content

Commit 8b9f5ec

Browse files
committed
added pi-header placeholder for consuming and producing crc-ccit and bptc196,96 protected payload per-etsi-specs, add hytera MFID, add VBPTC32,11 with tests to extract and (de)interleave single-burst-variable-bptc data
1 parent 8177560 commit 8b9f5ec

File tree

8 files changed

+334
-4
lines changed

8 files changed

+334
-4
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
from typing import Dict, Tuple
2+
3+
import numpy
4+
from bitarray import bitarray
5+
from okdmr.dmrlib.etsi.fec.hamming_16_11_4 import Hamming16114
6+
7+
8+
class VBPTC3211:
9+
"""
10+
ETSI TS 102 361-1 V2.5.1 (2017-10) - B.2.2 Single Burst Variable length BPTC
11+
"""
12+
13+
# disable formatter for this whole table, as manual formatting is applied
14+
# fmt: off
15+
# @formatter:off
16+
INTERLEAVING_INDICES: Dict[int, Tuple[int, int, int, bool, bool]] = {
17+
# (key) index => (value) interleave index, row, column, is row hamming, is parity check bit
18+
# rows are numbered from 1 to match the documentation/specification
19+
20+
# Row 1 of table, starts with SB(10)/RC(10)
21+
0: (0, 1, 0, False, False),
22+
1: (2, 1, 1, False, False),
23+
2: (4, 1, 2, False, False),
24+
3: (6, 1, 3, False, False),
25+
4: (8, 1, 4, False, False),
26+
5: (10, 1, 5, False, False),
27+
6: (12, 1, 6, False, False),
28+
7: (14, 1, 7, False, False),
29+
8: (16, 1, 8, False, False),
30+
9: (18, 1, 9, False, False),
31+
10: (20, 1, 10, False, False),
32+
# Row 1 hamming bits, starts with H1(4)
33+
11: (22, 1, 11, True, False),
34+
12: (24, 1, 12, True, False),
35+
13: (26, 1, 13, True, False),
36+
14: (28, 1, 14, True, False),
37+
15: (30, 1, 15, True, False),
38+
39+
# Row 4 of table, starts with PC(15)
40+
16: (17, 2, 0, False, True),
41+
17: (19, 2, 1, False, True),
42+
18: (21, 2, 2, False, True),
43+
19: (23, 2, 3, False, True),
44+
20: (25, 2, 4, False, True),
45+
21: (27, 2, 5, False, True),
46+
22: (29, 2, 6, False, True),
47+
23: (31, 2, 7, False, True),
48+
24: (1, 2, 8, False, True),
49+
25: (3, 2, 9, False, True),
50+
26: (5, 2, 10, False, True),
51+
27: (7, 2, 11, False, True),
52+
28: (9, 2, 12, False, True),
53+
29: (11, 2, 13, False, True),
54+
30: (13, 2, 14, False, True),
55+
31: (15, 2, 15, False, True),
56+
}
57+
"""Interleave table as key(index) => value(interleave index, row, column, is reserved, is hamming)"""
58+
# @formatter:on
59+
# fmt: on
60+
61+
FULL_INTERLEAVING_MAP: Dict[int, int] = dict(
62+
(k, v[0]) for k, v in INTERLEAVING_INDICES.items()
63+
)
64+
"""Extract only (table index -> interleave index)"""
65+
FULL_DEINTERLEAVING_MAP: Dict[int, int] = dict(
66+
(v[0], k) for k, v in INTERLEAVING_INDICES.items()
67+
)
68+
"""Extract only (interleave index -> index)"""
69+
DEINTERLEAVE_INFO_BITS_ONLY_MAP: Dict[int, int] = dict(
70+
(i, l)
71+
for i, l in enumerate(
72+
dict(
73+
(idx, interleave_idx)
74+
for idx, (
75+
interleave_idx,
76+
row,
77+
col,
78+
is_hamming,
79+
is_parity,
80+
) in INTERLEAVING_INDICES.items()
81+
if not is_hamming and not is_parity # not parity bits or hamming
82+
).values()
83+
)
84+
)
85+
"""Extract only (interleave index -> index) where it's not reserved or hamming bit"""
86+
INTERLEAVE_INFO_BITS_ONLY_MAP: Dict[int, int] = dict(
87+
(i, l)
88+
for i, l in enumerate(
89+
dict(
90+
(interleave_idx, idx)
91+
for idx, (
92+
interleave_idx,
93+
row,
94+
col,
95+
is_hamming,
96+
is_parity,
97+
) in INTERLEAVING_INDICES.items()
98+
if not is_hamming and not is_parity
99+
).values()
100+
)
101+
)
102+
"""Extract only (index -> interleave index) where it's not reserved or hamming bit"""
103+
104+
@staticmethod
105+
def deinterleave_all_bits(bits: bitarray) -> bitarray:
106+
"""
107+
Will take BPTC interleaved (and FEC protected) bits and return 11 bits of deinterleaved bits
108+
:param bits: 32 bits of on-air payload
109+
:return:
110+
"""
111+
assert (
112+
len(bits) == 32
113+
), f"VBPTC 31,11 deinterleave_all_bits requires 32 bits, got {len(bits)}"
114+
mapping = VBPTC3211.FULL_DEINTERLEAVING_MAP
115+
116+
out = bitarray([0] * len(mapping), endian="big")
117+
for i, n in mapping.items():
118+
out[i] = bits[n]
119+
120+
return out
121+
122+
@staticmethod
123+
def deinterleave_data_bits(bits: bitarray) -> bitarray:
124+
"""
125+
Will take BPTC interleaved (and FEC protected) bits and return 11bits of data
126+
:param bits: 32 bits of on-air payload
127+
:return: bitarray with 11 (data bits)
128+
"""
129+
assert len(bits) == 32, f"VBPTC 32,11 decode requires 32 bits, got {len(bits)}"
130+
mapping = VBPTC3211.DEINTERLEAVE_INFO_BITS_ONLY_MAP
131+
132+
out = bitarray([0] * len(mapping.keys()), endian="big")
133+
for i, n in mapping.items():
134+
out[i] = bits[n]
135+
136+
return out
137+
138+
@staticmethod
139+
def make_encoding_table() -> numpy.ndarray:
140+
# create table 4 rows, 17 columns, for FEC encoding
141+
table: numpy.ndarray = numpy.ndarray(shape=(2, 16), dtype=int)
142+
table.fill(0)
143+
144+
return table
145+
146+
@staticmethod
147+
def fill_encoding_table(
148+
table: numpy.ndarray, bits_deinterleaved: bitarray
149+
) -> numpy.ndarray:
150+
assert (
151+
len(bits_deinterleaved) == 11 or len(bits_deinterleaved) == 32
152+
), f"Can fill encoding table only with data bits (len 11) or full bits (len 32), got {len(bits_deinterleaved)}"
153+
154+
# make bitarray of size 32, fill with provided bits
155+
mapping = (
156+
VBPTC3211.DEINTERLEAVE_INFO_BITS_ONLY_MAP
157+
if len(bits_deinterleaved) == 11
158+
else VBPTC3211.FULL_DEINTERLEAVING_MAP
159+
)
160+
bits_interleaved: bitarray = bitarray([0] * 32, endian="big")
161+
162+
for index, interleave_index in mapping.items():
163+
bits_interleaved[interleave_index] = bits_deinterleaved[index]
164+
165+
for data_index, (
166+
interleave_idx,
167+
row_no,
168+
col_no,
169+
is_hamming,
170+
is_parity,
171+
) in VBPTC3211.INTERLEAVING_INDICES.items():
172+
table[row_no - 1][col_no] = bits_interleaved[interleave_idx]
173+
174+
return table
175+
176+
@staticmethod
177+
def encode(bits_deinterleaved: bitarray) -> bitarray:
178+
"""
179+
Takes 11 bits of data (info bits) and return interleaved and FEC protected 32 bits
180+
:param bits_deinterleaved:
181+
:return:
182+
"""
183+
if len(bits_deinterleaved) == 32:
184+
# full deinterleaved data including hamming and parity
185+
# interleave again and deinterleave only data bits
186+
interleaved: bitarray = bitarray([0] * 32)
187+
for data_index, (
188+
interleave_index,
189+
_,
190+
_,
191+
_,
192+
_,
193+
) in VBPTC3211.INTERLEAVING_INDICES.items():
194+
interleaved[data_index] = bits_deinterleaved[interleave_index]
195+
bits_deinterleaved = VBPTC3211.deinterleave_data_bits(interleaved)
196+
197+
assert (
198+
len(bits_deinterleaved) == 11
199+
), f"Unexpected number of bits fed to VBPTC3211.encode, expected 11 or 32, got {len(bits_deinterleaved)}"
200+
201+
table: numpy.ndarray = VBPTC3211.make_encoding_table()
202+
table = VBPTC3211.fill_encoding_table(
203+
table=table, bits_deinterleaved=bits_deinterleaved
204+
)
205+
206+
# fill row 0 with hamming
207+
for row in range(0, 1):
208+
table[row] = Hamming16114.generate(table[row][:11])
209+
210+
# fill columns with parity bit
211+
for column in range(0, 16):
212+
table[:, column] = VBPTC3211.set_parity(table[:, column])
213+
214+
out: bitarray = bitarray([0] * 32)
215+
for index, (
216+
interleave_index,
217+
row,
218+
col,
219+
is_hamming,
220+
is_parity,
221+
) in VBPTC3211.INTERLEAVING_INDICES.items():
222+
out[interleave_index] = table[row - 1][col]
223+
224+
return out
225+
226+
@staticmethod
227+
def set_parity(column: numpy.ndarray) -> numpy.ndarray:
228+
assert len(column) in (1, 2)
229+
if len(column) == 1:
230+
column = numpy.append(column, [0])
231+
column[1] = column[0]
232+
return column

okdmr/dmrlib/etsi/layer2/burst.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from okdmr.dmrlib.etsi.layer2.pdu.data_header import DataHeader
1414
from okdmr.dmrlib.etsi.layer2.pdu.embedded_signalling import EmbeddedSignalling
1515
from okdmr.dmrlib.etsi.layer2.pdu.full_link_control import FullLinkControl
16+
from okdmr.dmrlib.etsi.layer2.pdu.pi_header import PIHeader
1617
from okdmr.dmrlib.etsi.layer2.pdu.rate12_data import Rate12Data
1718
from okdmr.dmrlib.etsi.layer2.pdu.rate34_data import Rate34Data
1819
from okdmr.dmrlib.etsi.layer2.pdu.slot_type import SlotType
@@ -97,6 +98,8 @@ def extract_data(self) -> Optional[BitsInterface]:
9798
return CSBK.from_bits(self.info_bits_deinterleaved)
9899
elif self.data_type == DataTypes.VoiceLCHeader:
99100
return FullLinkControl.from_bits(self.info_bits_deinterleaved)
101+
elif self.data_type == DataTypes.PIHeader:
102+
return PIHeader.from_bits(self.info_bits_deinterleaved)
100103
elif self.data_type == DataTypes.TerminatorWithLC:
101104
return FullLinkControl.from_bits(self.info_bits_deinterleaved)
102105
elif self.data_type == DataTypes.DataHeader:

okdmr/dmrlib/etsi/layer2/elements/feature_set_ids.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class FeatureSetIDs(BitsInterface, enum.Enum):
1616
StandardizedFID = 0b00000000
1717
ReservedForFutureStandardization = 0b00000001
1818
ManufacturerFID = 0b00000100
19+
HyteraFID = 0b00010000
1920
ReservedForFutureMFID = 0b10000000
2021

2122
@classmethod
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Union
2+
3+
from bitarray import bitarray
4+
from bitarray.util import ba2int, int2ba
5+
from okdmr.dmrlib.etsi.crc.crc16 import CRC16
6+
from okdmr.dmrlib.etsi.layer2.elements.crc_masks import CrcMasks
7+
8+
from okdmr.dmrlib.utils.bits_bytes import bits_to_bytes, bytes_to_bits
9+
from okdmr.dmrlib.utils.bits_interface import BitsInterface
10+
11+
12+
class PIHeader(BitsInterface):
13+
def __init__(self, data: bytes, crc: Union[int, bytes] = 0):
14+
self.data: bytes = data
15+
self.crc: int = self.calculate_crc()
16+
self.crc_ok: bool = self.crc == (
17+
crc if isinstance(crc, int) else int.from_bytes(crc, byteorder="big")
18+
)
19+
20+
def calculate_crc(self) -> int:
21+
return CRC16.calculate(data=self.data, mask=CrcMasks.PiHeader)
22+
23+
def __repr__(self):
24+
return (
25+
f"[PI Header] [Data({len(self.data)}) {self.data.hex()} {bytes_to_bits(self.data)}]"
26+
+ ("" if self.crc_ok else " [CRC16-CCIT INVALID]")
27+
)
28+
29+
@staticmethod
30+
def from_bits(bits: bitarray) -> "PIHeader":
31+
return PIHeader(data=bits_to_bytes(bits[:-16]), crc=ba2int(bits[-16:]))
32+
33+
def as_bits(self) -> bitarray:
34+
return bytes_to_bits(self.data) + int2ba(self.crc, length=16)

okdmr/dmrlib/tools/pcap_tool.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from typing import Callable, List, Dict, Optional, Tuple
77

88
from bitarray import bitarray
9+
from kaitaistruct import KaitaiStruct
910
from okdmr.kaitai.homebrew.mmdvm2020 import Mmdvm2020
1011
from okdmr.kaitai.hytera.ip_site_connect_heartbeat import IpSiteConnectHeartbeat
1112
from okdmr.kaitai.hytera.ip_site_connect_protocol import IpSiteConnectProtocol
13+
from okdmr.tests.dmrlib.tests_utils import prettyprint
1214
from scapy.data import UDP_SERVICES
1315
from scapy.layers.inet import UDP, IP
1416
from scapy.layers.l2 import Ether
@@ -20,7 +22,6 @@
2022
from okdmr.dmrlib.etsi.layer2.elements.preemption_power_indicator import (
2123
PreemptionPowerIndicator,
2224
)
23-
from okdmr.dmrlib.etsi.layer2.elements.sync_patterns import SyncPatterns
2425
from okdmr.dmrlib.etsi.layer2.pdu.full_link_control import FullLinkControl
2526
from okdmr.dmrlib.utils.bits_bytes import bytes_to_bits, byteswap_bytes
2627
from okdmr.dmrlib.utils.parsing import try_parse_packet
@@ -38,6 +39,15 @@ def process_packet(self, data: bytes, packet: IP) -> Optional[FullLinkControl]:
3839
burst: Optional[Burst] = PcapTool.debug_packet(
3940
data=data, packet=packet, hide_unknown=True, silent=True
4041
)
42+
if (
43+
burst
44+
and burst.has_emb
45+
and burst.emb.link_control_start_stop == LCSS.SingleFragmentLCorCSBK
46+
):
47+
print(
48+
f"Single burst data for VBPTC 32,11 [{burst.emb.preemption_and_power_control_indicator}] {burst.embedded_signalling_bits}"
49+
)
50+
4151
if (
4252
not burst
4353
or not burst.has_emb
@@ -127,7 +137,6 @@ def debug_packet(
127137
dmr_bytes = byteswap_bytes(pkt.ipsc_payload)[:-1]
128138
if burst.as_bits() != bytes_to_bits(dmr_bytes):
129139
print(f"as_bits no match {dmr_bytes.hex()}")
130-
exit()
131140
elif isinstance(pkt, Mmdvm2020):
132141
if isinstance(pkt.command_data, Mmdvm2020.TypeDmrData):
133142
burst: Burst = Burst.from_mmdvm(pkt.command_data)
@@ -138,7 +147,6 @@ def debug_packet(
138147
)
139148
if burst.as_bits() != bytes_to_bits(pkt.command_data.dmr_data):
140149
print(f"as_bits no match {pkt.command_data.dmr_data.hex()}")
141-
exit()
142150
elif isinstance(pkt, IpSiteConnectHeartbeat):
143151
pass
144152
elif not hide_unknown and not silent:
@@ -151,6 +159,8 @@ def debug_packet(
151159
else f" type {str(type(pkt)).rsplit('.')[-1]}"
152160
)
153161
)
162+
if isinstance(pkt, KaitaiStruct):
163+
prettyprint(pkt)
154164

155165
return burst
156166

okdmr/tests/dmrlib/etsi/crc/test_crc16.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def test_crc16():
1313
("4da323383b23383b0560", "8040", CrcMasks.DataHeader),
1414
# csbk
1515
("bd0080180008fd23383b", "b2ed", CrcMasks.CSBK),
16+
# hytera pi header contents
17+
("211002177afc73000009", "0dda", CrcMasks.PiHeader)
1618
]
1719
# fmt:on
1820
# @formatter:on

0 commit comments

Comments
 (0)