Skip to content

Commit 8177560

Browse files
committed
rate34 and rate12 data pdus implemented with tests, updated CRC9 interface for easier use, test that de-serialize and serialize burst provides the same data
1 parent 867ccdf commit 8177560

File tree

8 files changed

+239
-14
lines changed

8 files changed

+239
-14
lines changed

okdmr/dmrlib/etsi/crc/crc9.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Union
2+
13
from bitarray import bitarray
24
from bitarray.util import int2ba, ba2int
35

@@ -20,17 +22,29 @@ def check(
2022
data: bytes, serial_number: int, crc9: int, mask: CrcMasks, crc32: bytes = None
2123
) -> bool:
2224
assert crc9 <= 511, "CRC-9 check value is invalid, max. for 9-bit number is 511"
25+
return (
26+
CRC9.calculate_from_parts(
27+
data=data, mask=mask, crc32=crc32, serial_number=serial_number
28+
)
29+
== crc9
30+
)
2331

32+
@staticmethod
33+
def calculate_from_parts(
34+
data: bytes, serial_number: int, mask: CrcMasks, crc32: Union[int, bytes] = None
35+
):
2436
source_data: bitarray = bytes_to_bits(data, endian="big")
2537

2638
if crc32 is not None:
39+
if isinstance(crc32, int):
40+
crc32 = crc32.to_bytes(4, byteorder="big")
2741
assert len(crc32) == 4, "32-bit CRC must be exactly 4-bytes long"
2842
source_data += bytes_to_bits(crc32, endian="big")
2943

3044
dbsnba = int2ba(serial_number, length=7, endian="big", signed=False)
3145
source_data += dbsnba
3246

33-
return CRC9.calculate(source_data, mask) == crc9
47+
return CRC9.calculate(data=source_data, mask=mask)
3448

3549
@staticmethod
3650
def calculate(data: bitarray, mask: CrcMasks) -> int:

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.rate12_data import Rate12Data
1617
from okdmr.dmrlib.etsi.layer2.pdu.rate34_data import Rate34Data
1718
from okdmr.dmrlib.etsi.layer2.pdu.slot_type import SlotType
1819
from okdmr.dmrlib.hytera.hytera_constants import IPSC_KAITAI_VOICE_SLOTS
@@ -102,6 +103,8 @@ def extract_data(self) -> Optional[BitsInterface]:
102103
return DataHeader.from_bits(self.info_bits_deinterleaved)
103104
elif self.data_type == DataTypes.Rate34Data:
104105
return Rate34Data.from_bits(self.info_bits_deinterleaved)
106+
elif self.data_type == DataTypes.Rate12Data:
107+
return Rate12Data.from_bits(self.info_bits_deinterleaved)
105108

106109
return None
107110

okdmr/dmrlib/etsi/layer2/pdu/data_header.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(
4747
appended_blocks: int = 0,
4848
defined_data_format: Optional[DefinedDataFormats] = None,
4949
sarq: Optional[SARQ] = None,
50+
bit_padding: Optional[bitarray] = bitarray(),
5051
# Unified Data Transport Header (UDT_HEAD) PDU
5152
is_emergency: Union[int, bool] = False,
5253
udt_option_flag: Optional[UDTOptionFlag] = None,
@@ -76,6 +77,7 @@ def __init__(
7677
self.appended_blocks: int = appended_blocks
7778
self.defined_data_format: Optional[DefinedDataFormats] = defined_data_format
7879
self.sarq: Optional[SARQ] = sarq
80+
self.bit_padding: bitarray = bit_padding
7981
# Unified Data Transport Header (UDT_HEAD) PDU
8082
self.is_emergency: bool = is_emergency in (True, 1)
8183
self.udt_option_flag: Optional[UDTOptionFlag] = udt_option_flag
@@ -209,7 +211,7 @@ def as_bits(self) -> bitarray:
209211
+ int2ba(self.llid_source, length=24)
210212
+ self.defined_data_format.as_bits()
211213
+ bitarray([self.sarq.value, self.full_message_flag.value])
212-
+ bitarray([0] * 8)
214+
+ self.bit_padding
213215
+ self.crc
214216
)
215217
elif self.data_packet_format == DataPacketFormats.UnifiedDataTransport:
@@ -282,6 +284,7 @@ def from_bits(bits: bitarray) -> "DataHeader":
282284
defined_data_format=DefinedDataFormats.from_bits(bits[64:70]),
283285
sarq=SARQ(bits[70]),
284286
full_message_flag=FullMessageFlag(bits[71]),
287+
bit_padding=bits[72:80],
285288
)
286289
elif dpf == DataPacketFormats.DataPacketUnconfirmed:
287290
return DataHeader(
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from typing import Union
2+
3+
from bitarray import bitarray
4+
from bitarray.util import int2ba
5+
6+
from okdmr.dmrlib.etsi.crc.crc9 import CRC9
7+
from okdmr.dmrlib.etsi.layer2.elements.crc_masks import CrcMasks
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 Rate12Data(BitsInterface):
13+
"""
14+
ETSI TS 102 361-1 V2.5.1 (2017-10) - 9.2.7 Rate 1/2 coded packet Data (R_1_2_DATA) PDU
15+
ETSI TS 102 361-1 V2.5.1 (2017-10) - 9.2.8 Rate 1/2 coded Last Data block (R_1_2_LDATA) PDU
16+
"""
17+
18+
def __init__(
19+
self,
20+
data: bytes,
21+
dbsn: int = 0,
22+
crc9: int = 0,
23+
crc32: Union[int, bytes] = 0,
24+
):
25+
self.data: bytes = data
26+
self.dbsn: int = dbsn
27+
self.crc32: int = (
28+
crc32 if isinstance(crc32, int) else int.from_bytes(crc32, byteorder="big")
29+
)
30+
31+
self.crc9: int = self.calculate_crc9()
32+
self.crc9_ok: bool = self.crc9 == crc9 if crc9 > 0 else True
33+
34+
def calculate_crc9(self) -> int:
35+
return CRC9.calculate_from_parts(
36+
data=self.data,
37+
serial_number=self.dbsn,
38+
crc32=self.crc32,
39+
mask=CrcMasks.Rate12DataContinuation,
40+
)
41+
42+
def __repr__(self) -> str:
43+
if len(self.data) == 12:
44+
return f"[RATE 1/2 DATA] [DATA(12) {self.data.hex()}]"
45+
elif len(self.data) == 10:
46+
return (
47+
f"[RATE 1/2 DATA CONFIRMED] [DATA(10) {self.data.hex()}]"
48+
+ f" [CRC9: {self.crc9}]"
49+
+ (" [CRC9 INVALID]" if not self.crc9_ok else "")
50+
)
51+
elif len(self.data) == 8:
52+
return (
53+
f"[RATE 1/2 DATA - LAST BLOCK UNCONFIRMED] [DATA(8) {self.data.hex()}]"
54+
+ f" [CRC32 int({self.crc32}) hex({self.crc32.to_bytes(4, byteorder='big').hex()})]"
55+
)
56+
elif len(self.data) == 6:
57+
return (
58+
f"[RATE 1/2 DATA - LAST BLOCK CONFIRMED] [DATA(6) {self.data.hex()}]"
59+
+ f" [CRC9: {self.crc9}]"
60+
+ (" [CRC9 INVALID]" if not self.crc9_ok else "")
61+
+ f" [CRC32 int({self.crc32}) hex({self.crc32.to_bytes(4, byteorder='big').hex()})]"
62+
)
63+
raise ValueError(f"__repr__ not implemented for data len {len(self.data)}")
64+
65+
@staticmethod
66+
def from_bits(bits: bitarray) -> "Rate12Data":
67+
assert (
68+
len(bits) == 96
69+
), f"Rate 1/2 Data packet must be 96 bits (12 bytes) long, got {len(bits)} bits"
70+
return Rate12Data(data=bits_to_bytes(bits))
71+
72+
def as_bits(self):
73+
if len(self.data) == 12:
74+
# R_1_2_DATA PDU content for unconfirmed data
75+
return bytes_to_bits(self.data)
76+
elif len(self.data) == 10:
77+
# R_1_2_DATA PDU content for confirmed data
78+
return (
79+
int2ba(self.dbsn, length=7)
80+
+ int2ba(self.crc9, length=9)
81+
+ bytes_to_bits(self.data)
82+
)
83+
elif len(self.data) == 8:
84+
# R_1_2_LDATA PDU content for confirmed data
85+
return bytes_to_bits(self.data) + int2ba(self.crc32, length=32)
86+
elif len(self.data) == 6:
87+
# R_3_4_LDATA PDU content for confirmed data
88+
return (
89+
int2ba(self.dbsn, length=7)
90+
+ int2ba(self.calculate_crc9(), length=9)
91+
+ bytes_to_bits(self.data)
92+
+ int2ba(self.crc32, length=32)
93+
)

okdmr/dmrlib/etsi/layer2/pdu/rate34_data.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,66 @@
1+
from typing import Union
2+
13
from bitarray import bitarray
24
from bitarray.util import int2ba
35

6+
from okdmr.dmrlib.etsi.crc.crc9 import CRC9
7+
from okdmr.dmrlib.etsi.layer2.elements.crc_masks import CrcMasks
48
from okdmr.dmrlib.utils.bits_bytes import bits_to_bytes, bytes_to_bits
59
from okdmr.dmrlib.utils.bits_interface import BitsInterface
610

711

812
class Rate34Data(BitsInterface):
13+
"""
14+
ETSI TS 102 361-1 V2.5.1 (2017-10) - 9.2.2 Rate ¾ coded packet Data (R_3_4_DATA) PDU
15+
ETSI TS 102 361-1 V2.5.1 (2017-10) - 9.2.3 Rate ¾ coded Last Data block (R_3_4_LDATA) PDU
16+
"""
17+
918
def __init__(
1019
self,
1120
data: bytes,
1221
dbsn: int = 0,
1322
crc9: int = 0,
14-
crc32: int = 0,
23+
crc32: Union[int, bytes] = 0,
1524
):
1625
self.data: bytes = data
1726
self.dbsn: int = dbsn
18-
self.crc9: int = crc9
19-
self.crc32: int = crc32
20-
21-
if len(data) in (14, 12):
22-
# verify crc32
23-
pass
24-
if len(data) in (16, 12):
25-
# verify crc9
26-
pass
27+
self.crc32: int = (
28+
crc32 if isinstance(crc32, int) else int.from_bytes(crc32, byteorder="big")
29+
)
30+
31+
self.crc9: int = self.calculate_crc9()
32+
self.crc9_ok: bool = self.crc9 == crc9 if crc9 > 0 else True
33+
34+
def calculate_crc9(self) -> int:
35+
return CRC9.calculate_from_parts(
36+
data=self.data,
37+
serial_number=self.dbsn,
38+
crc32=self.crc32,
39+
mask=CrcMasks.Rate34DataContinuation,
40+
)
41+
42+
def __repr__(self) -> str:
43+
if len(self.data) == 18:
44+
return f"[RATE 3/4 DATA] [DATA(18) {self.data.hex()}]"
45+
elif len(self.data) == 16:
46+
return (
47+
f"[RATE 3/4 DATA CONFIRMED] [DATA(16) {self.data.hex()}]"
48+
+ f" [CRC9: {self.crc9}]"
49+
+ (" [CRC9 INVALID]" if not self.crc9_ok else "")
50+
)
51+
elif len(self.data) == 14:
52+
return (
53+
f"[RATE 3/4 DATA - LAST BLOCK UNCONFIRMED] [DATA(14) {self.data.hex()}]"
54+
+ f" [CRC32 int({self.crc32}) hex({self.crc32.to_bytes(4, byteorder='big').hex()})]"
55+
)
56+
elif len(self.data) == 12:
57+
return (
58+
f"[RATE 3/4 DATA - LAST BLOCK CONFIRMED] [DATA(12) {self.data.hex()}]"
59+
+ f" [CRC9: {self.crc9}]"
60+
+ (" [CRC9 INVALID]" if not self.crc9_ok else "")
61+
+ f" [CRC32 int({self.crc32}) hex({self.crc32.to_bytes(4, byteorder='big').hex()})]"
62+
)
63+
raise ValueError(f"__repr__ not implemented for data len {len(self.data)}")
2764

2865
@staticmethod
2966
def from_bits(bits: bitarray) -> "Rate34Data":
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from okdmr.dmrlib.etsi.layer2.pdu.rate12_data import Rate12Data
2+
3+
4+
def test_rate12_data():
5+
r12u: Rate12Data = Rate12Data(data=b"\xc0K;\xb7NN\x83\xb5\xc9\x01\x03\xcb")
6+
r12u_bits = r12u.as_bits()
7+
assert len(repr(r12u))
8+
assert Rate12Data.from_bits(r12u_bits).as_bits() == r12u_bits
9+
10+
r12c: Rate12Data = Rate12Data(data=b"t~\xa3B5\xcc\xc4JwK", dbsn=1)
11+
r12c_bits = r12c.as_bits()
12+
assert r12c.crc9_ok
13+
assert Rate12Data.from_bits(r12c_bits).as_bits() == r12c_bits
14+
assert len(repr(r12c))
15+
16+
r12ul: Rate12Data = Rate12Data(
17+
data=b"y\x02\xe8\xf3c\xc3\x82\x88", crc32=b"\n\xfc\xb3\xe1"
18+
)
19+
r12ul_bits = r12ul.as_bits()
20+
assert Rate12Data.from_bits(r12ul_bits).as_bits() == r12ul_bits
21+
assert len(repr(r12ul))
22+
23+
r12cl: Rate12Data = Rate12Data(
24+
data=b"\xaf\x12\xf4O\xdeK", crc32=b"\x82j\xfdX", dbsn=1
25+
)
26+
r12cl_bits = r12cl.as_bits()
27+
assert r12cl.crc9_ok
28+
assert len(repr(r12cl))
29+
assert Rate12Data.from_bits(r12cl_bits).as_bits() == r12cl_bits
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from okdmr.dmrlib.etsi.layer2.pdu.rate34_data import Rate34Data
2+
3+
4+
def test_rate34_data():
5+
# R_3_4_DATA PDU content for unconfirmed data
6+
r34u: Rate34Data = Rate34Data(
7+
data=b"\xdd\xda\xe4\xa1\xaeT'oe\xc9\xf6\xe4\x97\x0e#\xed\x17s"
8+
)
9+
r34u_bits = r34u.as_bits()
10+
assert Rate34Data.from_bits(r34u_bits).as_bits() == r34u_bits
11+
assert len(repr(r34u))
12+
13+
r34c: Rate34Data = Rate34Data(
14+
data=b"\xc1\x1bb\x1a\xd4\x8b\x97\x91\x17A\x9e\xb1r\xeb\xe0\xb8",
15+
dbsn=1,
16+
crc9=306,
17+
)
18+
r34c_bits = r34c.as_bits()
19+
assert r34c.crc9_ok
20+
assert Rate34Data.from_bits(r34c_bits).as_bits() == r34c_bits
21+
assert len(repr(r34c))
22+
23+
r34ul: Rate34Data = Rate34Data(
24+
data=b"\x90\xbc\x85\x00\x89\xc1[K\xe4l\x83P\x88\xe0", crc32=b"\xdb\xb2p\xe5"
25+
)
26+
r34ul_bits = r34ul.as_bits()
27+
assert Rate34Data.from_bits(r34ul_bits).as_bits() == r34ul_bits
28+
assert len(repr(r34ul))
29+
30+
r34cl: Rate34Data = Rate34Data(
31+
data=b"\xfe\x1ey\x9cR\xb5\xe4&T\xc2#\x83", dbsn=1, crc32=b'\x12"\x18N'
32+
)
33+
r34cl_bits = r34cl.as_bits()
34+
assert Rate34Data.from_bits(r34cl_bits).as_bits() == r34cl_bits
35+
assert r34cl.crc9_ok
36+
assert len(repr(r34cl))

okdmr/tests/dmrlib/etsi/layer2/test_burst.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,22 @@ def test_burst_as_bits():
106106
"2522222222a632b222222222560dff57d75df5dce2822222222791522222222c11",
107107
BurstTypes.DataAndControl,
108108
),
109+
(
110+
"3a1f36af232d7afda01bd78255bdff57d75df5d55c045c2e3361260e501f863363",
111+
BurstTypes.DataAndControl,
112+
),
113+
(
114+
"00e527076f2a4b0f03ed010115ddff57d75df5d6f145422817d6234b6e08802018",
115+
BurstTypes.DataAndControl,
116+
),
109117
]
110118
for (hexstr, burst_type) in bursts:
111119
_bytes: bytes = bytes.fromhex(hexstr)
112120
b: Burst = Burst.from_bytes(data=_bytes, burst_type=burst_type)
113-
assert b.data.as_bits() == b.info_bits_deinterleaved
114-
assert b.as_bits().tobytes() == _bytes
121+
assert (
122+
b.data.as_bits() == b.info_bits_deinterleaved
123+
), f"Mismatch in {repr(b)} {b.as_bits().tobytes().hex()}"
124+
assert b.as_bits().tobytes() == _bytes, f"Mismatch in {repr(b)}"
115125

116126

117127
if __name__ == "__main__":

0 commit comments

Comments
 (0)