Skip to content

Commit 9b4d77a

Browse files
committed
renamed LinkControl to FullLinkControl to distinguish from ShortLinkControl, added ReedSolomon129 crc field
1 parent 17acdd8 commit 9b4d77a

7 files changed

+98
-57
lines changed

okdmr/kaitai/etsi/link_control.ksy okdmr/kaitai/etsi/full_link_control.ksy

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
meta:
2-
id: link_control
2+
id: full_link_control
33
endian: le
44
doc: |
55
ETSI TS 102 361-2 V2.4.1 (2017-10), Section 7.1.1
@@ -100,4 +100,7 @@ seq:
100100
'flcos::talker_alias_header': talker_alias_header
101101
'flcos::talker_alias_block1': talker_alias_continuation
102102
'flcos::talker_alias_block2': talker_alias_continuation
103-
'flcos::talker_alias_block3': talker_alias_continuation
103+
'flcos::talker_alias_block3': talker_alias_continuation
104+
- id: crc_checksum
105+
doc: 3 bytes of Reed-Solomon CRC with applied CRC mask per B.3.12 Data Type CRC Mask
106+
size: 3

okdmr/kaitai/etsi/link_control.py okdmr/kaitai/etsi/full_link_control.py

+22-19
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414

1515

16-
class LinkControl(KaitaiStruct):
16+
class FullLinkControl(KaitaiStruct):
1717
"""ETSI TS 102 361-2 V2.4.1 (2017-10), Section 7.1.1"""
1818

1919
class Flcos(Enum):
@@ -61,39 +61,42 @@ def _read(self):
6161
self.protect_flag = self._io.read_bits_int_be(1) != 0
6262
self.reserved = self._io.read_bits_int_be(1) != 0
6363
self.full_link_control_opcode = KaitaiStream.resolve_enum(
64-
LinkControl.Flcos, self._io.read_bits_int_be(6)
64+
FullLinkControl.Flcos, self._io.read_bits_int_be(6)
6565
)
6666
self.feature_set_id = KaitaiStream.resolve_enum(
67-
LinkControl.FeatureSetIds, self._io.read_bits_int_be(8)
67+
FullLinkControl.FeatureSetIds, self._io.read_bits_int_be(8)
6868
)
6969
self._io.align_to_byte()
7070
_on = self.full_link_control_opcode
71-
if _on == LinkControl.Flcos.talker_alias_header:
72-
self.specific_data = LinkControl.TalkerAliasHeader(
71+
if _on == FullLinkControl.Flcos.talker_alias_header:
72+
self.specific_data = FullLinkControl.TalkerAliasHeader(
7373
self._io, self, self._root
7474
)
75-
elif _on == LinkControl.Flcos.unit_to_unit_voice:
76-
self.specific_data = LinkControl.UnitToUnitVoiceChannelUser(
75+
elif _on == FullLinkControl.Flcos.unit_to_unit_voice:
76+
self.specific_data = FullLinkControl.UnitToUnitVoiceChannelUser(
7777
self._io, self, self._root
7878
)
79-
elif _on == LinkControl.Flcos.group_voice:
80-
self.specific_data = LinkControl.GroupVoiceChannelUser(
79+
elif _on == FullLinkControl.Flcos.group_voice:
80+
self.specific_data = FullLinkControl.GroupVoiceChannelUser(
8181
self._io, self, self._root
8282
)
83-
elif _on == LinkControl.Flcos.talker_alias_block3:
84-
self.specific_data = LinkControl.TalkerAliasContinuation(
83+
elif _on == FullLinkControl.Flcos.talker_alias_block3:
84+
self.specific_data = FullLinkControl.TalkerAliasContinuation(
8585
self._io, self, self._root
8686
)
87-
elif _on == LinkControl.Flcos.talker_alias_block1:
88-
self.specific_data = LinkControl.TalkerAliasContinuation(
87+
elif _on == FullLinkControl.Flcos.talker_alias_block1:
88+
self.specific_data = FullLinkControl.TalkerAliasContinuation(
8989
self._io, self, self._root
9090
)
91-
elif _on == LinkControl.Flcos.talker_alias_block2:
92-
self.specific_data = LinkControl.TalkerAliasContinuation(
91+
elif _on == FullLinkControl.Flcos.talker_alias_block2:
92+
self.specific_data = FullLinkControl.TalkerAliasContinuation(
9393
self._io, self, self._root
9494
)
95-
elif _on == LinkControl.Flcos.gps_info:
96-
self.specific_data = LinkControl.GpsInfoLcPdu(self._io, self, self._root)
95+
elif _on == FullLinkControl.Flcos.gps_info:
96+
self.specific_data = FullLinkControl.GpsInfoLcPdu(
97+
self._io, self, self._root
98+
)
99+
self.crc_checksum = self._io.read_bytes(3)
97100

98101
class GpsInfoLcPdu(KaitaiStruct):
99102
def __init__(self, _io, _parent=None, _root=None):
@@ -105,7 +108,7 @@ def __init__(self, _io, _parent=None, _root=None):
105108
def _read(self):
106109
self.reserved = self._io.read_bits_int_be(4)
107110
self.position_error = KaitaiStream.resolve_enum(
108-
LinkControl.PositionErrors, self._io.read_bits_int_be(3)
111+
FullLinkControl.PositionErrors, self._io.read_bits_int_be(3)
109112
)
110113
self.longitude = self._io.read_bits_int_be(25)
111114
self.latitude = self._io.read_bits_int_be(24)
@@ -143,7 +146,7 @@ def __init__(self, _io, _parent=None, _root=None):
143146

144147
def _read(self):
145148
self.talker_alias_data_format = KaitaiStream.resolve_enum(
146-
LinkControl.TalkerDataFormats, self._io.read_bits_int_be(2)
149+
FullLinkControl.TalkerDataFormats, self._io.read_bits_int_be(2)
147150
)
148151
self.talker_alias_data_length = self._io.read_bits_int_be(5)
149152
self.talker_alias_data = self._io.read_bits_int_be(49)

okdmr/kaitai/hytera/gpsdata.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
66

77

8-
if parse_version(kaitaistruct.__version__) < parse_version('0.9'):
9-
raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__))
8+
if parse_version(kaitaistruct.__version__) < parse_version("0.9"):
9+
raise Exception(
10+
"Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s"
11+
% (kaitaistruct.__version__)
12+
)
13+
1014

1115
class Gpsdata(KaitaiStruct):
1216
def __init__(self, _io, _parent=None, _root=None):
@@ -28,10 +32,8 @@ def _read(self):
2832

2933
@property
3034
def gps_available(self):
31-
if hasattr(self, '_m_gps_available'):
32-
return self._m_gps_available if hasattr(self, '_m_gps_available') else None
35+
if hasattr(self, "_m_gps_available"):
36+
return self._m_gps_available if hasattr(self, "_m_gps_available") else None
3337

3438
self._m_gps_available = (self.gps_status).decode(u"ASCII") == u"A"
35-
return self._m_gps_available if hasattr(self, '_m_gps_available') else None
36-
37-
39+
return self._m_gps_available if hasattr(self, "_m_gps_available") else None

okdmr/kaitai/hytera/hytera_dmr_application_protocol.py

+33-16
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@
66
from enum import Enum
77

88

9-
if parse_version(kaitaistruct.__version__) < parse_version('0.9'):
10-
raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__))
9+
if parse_version(kaitaistruct.__version__) < parse_version("0.9"):
10+
raise Exception(
11+
"Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s"
12+
% (kaitaistruct.__version__)
13+
)
1114

12-
from okdmr.kaitai.hytera import radio_registration_service
1315
from okdmr.kaitai.hytera import radio_control_protocol
14-
from okdmr.kaitai.hytera import location_protocol
15-
from okdmr.kaitai.hytera import text_message_protocol
16-
from okdmr.kaitai.hytera import data_transmit_protocol
1716
from okdmr.kaitai.hytera import data_delivery_states
1817
from okdmr.kaitai.hytera import telemetry_protocol
19-
class HyteraDmrApplicationProtocol(KaitaiStruct):
18+
from okdmr.kaitai.hytera import text_message_protocol
19+
from okdmr.kaitai.hytera import location_protocol
20+
from okdmr.kaitai.hytera import radio_registration_service
21+
from okdmr.kaitai.hytera import data_transmit_protocol
22+
2023

24+
class HyteraDmrApplicationProtocol(KaitaiStruct):
2125
class MessageHeaderTypes(Enum):
2226
radio_control_protocol = 2
2327
location_protocol = 8
@@ -26,6 +30,7 @@ class MessageHeaderTypes(Enum):
2630
telemetry_protocol = 18
2731
data_transmit_protocol = 19
2832
data_delivery_states = 20
33+
2934
def __init__(self, _io, _parent=None, _root=None):
3035
self._io = _io
3136
self._parent = _parent
@@ -34,27 +39,42 @@ def __init__(self, _io, _parent=None, _root=None):
3439

3540
def _read(self):
3641
self.is_reliable_message = self._io.read_bits_int_be(1) != 0
37-
self.message_header = KaitaiStream.resolve_enum(HyteraDmrApplicationProtocol.MessageHeaderTypes, self._io.read_bits_int_be(7))
42+
self.message_header = KaitaiStream.resolve_enum(
43+
HyteraDmrApplicationProtocol.MessageHeaderTypes,
44+
self._io.read_bits_int_be(7),
45+
)
3846
self._io.align_to_byte()
3947
_on = self.message_header
4048
if _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.radio_registration:
4149
self.data = radio_registration_service.RadioRegistrationService(self._io)
4250
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.telemetry_protocol:
4351
self.data = telemetry_protocol.TelemetryProtocol(self._io)
44-
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.radio_control_protocol:
52+
elif (
53+
_on
54+
== HyteraDmrApplicationProtocol.MessageHeaderTypes.radio_control_protocol
55+
):
4556
self.data = radio_control_protocol.RadioControlProtocol(self._io)
46-
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.text_message_protocol:
57+
elif (
58+
_on == HyteraDmrApplicationProtocol.MessageHeaderTypes.text_message_protocol
59+
):
4760
self.data = text_message_protocol.TextMessageProtocol(self._io)
48-
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.data_delivery_states:
61+
elif (
62+
_on == HyteraDmrApplicationProtocol.MessageHeaderTypes.data_delivery_states
63+
):
4964
self.data = data_delivery_states.DataDeliveryStates(self._io)
5065
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.location_protocol:
5166
self.data = location_protocol.LocationProtocol(self._io)
52-
elif _on == HyteraDmrApplicationProtocol.MessageHeaderTypes.data_transmit_protocol:
67+
elif (
68+
_on
69+
== HyteraDmrApplicationProtocol.MessageHeaderTypes.data_transmit_protocol
70+
):
5371
self.data = data_transmit_protocol.DataTransmitProtocol(self._io)
5472
self.checksum = self._io.read_u1()
5573
self.message_footer = self._io.read_bytes(1)
5674
if not self.message_footer == b"\x03":
57-
raise kaitaistruct.ValidationNotEqualError(b"\x03", self.message_footer, self._io, u"/seq/4")
75+
raise kaitaistruct.ValidationNotEqualError(
76+
b"\x03", self.message_footer, self._io, u"/seq/4"
77+
)
5878

5979
class UndefinedProtocol(KaitaiStruct):
6080
def __init__(self, _io, _parent=None, _root=None):
@@ -65,6 +85,3 @@ def __init__(self, _io, _parent=None, _root=None):
6585

6686
def _read(self):
6787
self.data = self._io.read_bytes_full()
68-
69-
70-

okdmr/kaitai/hytera/location_protocol.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
)
1414

1515
from okdmr.kaitai.hytera import radio_ip
16-
from okdmr.kaitai.hytera import datetimestring
1716
from okdmr.kaitai.hytera import gpsdata
17+
from okdmr.kaitai.hytera import datetimestring
1818
from okdmr.kaitai.hytera import intervalstring
1919

2020

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from okdmr.kaitai.etsi.full_link_control import FullLinkControl
2+
from okdmr.tests.kaitai.tests_utils import prettyprint
3+
4+
5+
def test_group_full_link_control():
6+
voice_lc_header_hex: str = "000000000009280722504778"
7+
lc: FullLinkControl = FullLinkControl.from_bytes(bytes.fromhex(voice_lc_header_hex))
8+
assert isinstance(lc.specific_data, FullLinkControl.GroupVoiceChannelUser)
9+
assert lc.specific_data.group_address == 9
10+
assert lc.specific_data.source_address == 2623266
11+
assert lc.crc_checksum == b"PGx"

okdmr/tests/kaitai/hytera/test_location_protocol.py

+17-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class TestLocationProtocl(unittest.TestCase):
10-
1110
def test_lp(self):
1211
hexmessages: List[str] = [
1312
# location protocol
@@ -30,8 +29,13 @@ def test_parse_standard_answer(self):
3029
message = LocationProtocol.from_bytes(bytes.fromhex(hex_location_germany))
3130

3231
# Assert properties from LocationProtocol
33-
self.assertEqual(message.opcode_header, LocationProtocol.LpSpecificTypes.standard_answer)
34-
self.assertEqual(message.opcode, LocationProtocol.LpGeneralTypes.standard_location_immediate_service)
32+
self.assertEqual(
33+
message.opcode_header, LocationProtocol.LpSpecificTypes.standard_answer
34+
)
35+
self.assertEqual(
36+
message.opcode,
37+
LocationProtocol.LpGeneralTypes.standard_location_immediate_service,
38+
)
3539

3640
# Assert properties in Standard Answer
3741
self.assertEqual(message.data.result, LocationProtocol.ResultCodes.ok)
@@ -40,18 +44,19 @@ def test_parse_standard_answer(self):
4044

4145
# Assert properties in gps payload
4246
gps_msg = message.data.gpsdata
43-
self.assertEqual(gps_msg.gps_status.decode('ascii'), 'A')
44-
self.assertEqual(gps_msg.gps_date.decode('ascii'), '311018')
45-
self.assertEqual(gps_msg.gps_time.decode('ascii'), '162223')
46-
self.assertEqual(gps_msg.east_west.decode('ascii'), 'E')
47-
self.assertEqual(gps_msg.longitude.decode('ascii'), '00739.2680')
48-
self.assertEqual(gps_msg.north_south.decode('ascii'), 'N')
49-
self.assertEqual(gps_msg.latitude.decode('ascii'), '4736.6977')
50-
self.assertEqual(gps_msg.direction.decode('ascii'), '005')
51-
self.assertEqual(gps_msg.speed.decode('ascii'), '0.0')
47+
self.assertEqual(gps_msg.gps_status.decode("ascii"), "A")
48+
self.assertEqual(gps_msg.gps_date.decode("ascii"), "311018")
49+
self.assertEqual(gps_msg.gps_time.decode("ascii"), "162223")
50+
self.assertEqual(gps_msg.east_west.decode("ascii"), "E")
51+
self.assertEqual(gps_msg.longitude.decode("ascii"), "00739.2680")
52+
self.assertEqual(gps_msg.north_south.decode("ascii"), "N")
53+
self.assertEqual(gps_msg.latitude.decode("ascii"), "4736.6977")
54+
self.assertEqual(gps_msg.direction.decode("ascii"), "005")
55+
self.assertEqual(gps_msg.speed.decode("ascii"), "0.0")
5256

5357
# Payload instances
5458
self.assertTrue(gps_msg.gps_available)
5559

60+
5661
if __name__ == "__main__":
5762
unittest.main()

0 commit comments

Comments
 (0)