Coverage for test/d7a/alp/test_alp_parser.py: 97%
172 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-24 08:03 +0200
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-24 08:03 +0200
1#
2# Copyright (c) 2015-2021 University of Antwerp, Aloxy NV.
3#
4# This file is part of pyd7a.
5# See https://github.com/Sub-IoT/pyd7a for further info.
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18#
20# author: Christophe VG <contact@christophe.vg>
21# unit tests for the D7 ALP parser
23import unittest
25from bitstring import ConstBitStream
27from d7a.alp.interface import InterfaceType
28from d7a.alp.operands.file_header import FileHeaderOperand
29from d7a.alp.operands.indirect_interface_operand import IndirectInterfaceOperand
30from d7a.alp.operands.interface_configuration import InterfaceConfiguration
31from d7a.alp.operands.interface_status import InterfaceStatusOperand
32from d7a.alp.operands.lorawan_interface_configuration_abp import LoRaWANInterfaceConfigurationABP
33from d7a.alp.operands.lorawan_interface_configuration_otaa import LoRaWANInterfaceConfigurationOTAA
34from d7a.alp.operands.query import QueryOperand
35from d7a.alp.operations.forward import Forward
37from d7a.alp.parser import Parser
38from d7a.d7anp.addressee import Addressee, IdType
39from d7a.parse_error import ParseError
40from d7a.phy.channel_header import ChannelBand, ChannelCoding, ChannelClass
43class TestParser(unittest.TestCase):
44 def setUp(self):
45 self.interface_status_action = [
46 0x62, # Interface Status action
47 0xD7, # D7ASP interface
48 12, # interface status length
49 32, # channel_header
50 0, 0, # channel_id
51 0, # rxlevel (- dBm)
52 0, # link budget
53 80, # target rx level
54 0, # status
55 0, # fifo token
56 0, # seq
57 0, # response timeout
58 1 << 4, # addressee ctrl (NOID, nls_method=NONE)
59 0 # access class
60 ]
62 def test_basic_valid_message(self):
63 cmd_data = [
64 0x20, # action=32/ReturnFileData
65 0x40, # File ID
66 0x00, # offset
67 0x04, # length
68 0x00, 0xf3, 0x00, 0x00 # data
69 ] + self.interface_status_action
71 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
72 self.assertEqual(len(cmd.actions), 1)
73 self.assertEqual(cmd.actions[0].operation.op, 32)
74 self.assertEqual(cmd.actions[0].operation.operand.length.value, 4)
76 def test_command_without_interface_status(self):
77 cmd_data = [
78 0x20, # action=32/ReturnFileData
79 0x40, # File ID
80 0x00, # offset
81 0x04, # length
82 0x00, 0xfF, 0x00, 0x00 # data
83 # missing interface status action!
84 ]
85 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
86 self.assertEqual(cmd.interface_status, None)
88 def test_empty_data(self):
89 alp_action_bytes = [
90 0x20,
91 0x40,
92 0x00,
93 0x00
94 ]
96 parser = Parser()
97 cmd = parser.parse(ConstBitStream(bytes=alp_action_bytes), len(alp_action_bytes))
98 self.assertEqual(cmd.actions[0].operation.op, 32)
99 self.assertEqual(len(cmd.actions[0].operation.operand.data), 0)
101 def test_unsupported_action(self):
102 alp_action_bytes = [
103 0x25,
104 0x40,
105 0x00,
106 0x00
107 ]
108 with self.assertRaises(ParseError):
109 cmd = Parser().parse(ConstBitStream(bytes=alp_action_bytes), len(alp_action_bytes))
111 def test_multiple_actions(self):
112 alp_action_bytes = [
113 0x20, # action=32/ReturnFileData
114 0x40, # File ID
115 0x00, # offset
116 0x04, # length
117 0x00, 0xf3, 0x00, 0x00, # data
118 ]
120 cmd_bytes = alp_action_bytes + alp_action_bytes + self.interface_status_action
121 cmd = Parser().parse(ConstBitStream(bytes=cmd_bytes), len(cmd_bytes))
122 self.assertEqual(cmd.actions[0].operation.op, 32)
123 self.assertEqual(cmd.actions[0].operation.operand.length.value, 4)
124 self.assertEqual(cmd.actions[1].operation.op, 32)
125 self.assertEqual(cmd.actions[1].operation.operand.length.value, 4)
127 def test_multiple_non_grouped_actions_in_command(self):
128 alp_action_bytes = [
129 0x20, # action=32/ReturnFileData
130 0x40, # File ID
131 0x00, # offset
132 0x04, # length
133 0x00, 0xf3, 0x00, 0x00 # data
134 ]
136 cmd_bytes = alp_action_bytes + alp_action_bytes + self.interface_status_action
137 cmd = Parser().parse(ConstBitStream(bytes=cmd_bytes), len(cmd_bytes))
139 self.assertEqual(len(cmd.actions), 2)
140 self.assertEqual(cmd.actions[0].operation.op, 32)
141 self.assertEqual(cmd.actions[0].operation.operand.length.value, 4)
142 self.assertEqual(cmd.actions[1].operation.op, 32)
143 self.assertEqual(cmd.actions[1].operation.operand.length.value, 4)
145 # TODO not implemented yet
146 # def test_multiple_grouped_actions_in_command(self):
147 # alp_action_first_in_group_bytes = [
148 # 0xa0, # action=32/ReturnFileData + grouped flag
149 # 0x40, # File ID
150 # 0x00, # offset
151 # 0x04, # length
152 # 0x00, 0xf3, 0x00, 0x00 # data
153 # ]
154 # alp_action_second_in_group_bytes = [
155 # 0x20, # action=32/ReturnFileData
156 # 0x40, # File ID
157 # 0x00, # offset
158 # 0x04, # length
159 # 0x00, 0xf3, 0x00, 0x00 # data
160 # ]
161 # (cmds, info) = self.parser.parse([
162 # 0xc0, 0, len(alp_action_first_in_group_bytes) + len(alp_action_second_in_group_bytes)
163 # ] + alp_action_first_in_group_bytes + alp_action_second_in_group_bytes)
164 #
165 # self.assertEqual(len(cmds), 1)
166 # self.assertEqual(len(cmds[0].actions), 2)
167 # self.assertEqual(cmds[0].actions[0].operation.op, 32)
168 # self.assertEqual(cmds[0].actions[0].operation.operand.length, 4)
169 # self.assertEqual(cmds[0].actions[0].group, True)
170 # self.assertEqual(cmds[0].actions[1].operation.op, 32)
171 # self.assertEqual(cmds[0].actions[1].operation.operand.length, 4)
172 # self.assertEqual(cmds[0].actions[0].group, False)
174 def test_interface_status_action_d7asp(self):
175 alp_action_bytes = [
176 34 + 0b01000000, # action=34 + inf status
177 0xd7, # interface ID
178 12, # interface status length
179 32, # channel_header
180 0, 16, # channel_index
181 70, # rx level
182 80, # link budget
183 80, # target rx level
184 0, # status
185 0xa5, # fifo token
186 0x00, # request ID
187 20, # response timeout
188 0b00100010, # addr control
189 5, # access class
190 0x24, 0x8a, 0xb6, 0x01, 0x51, 0xc7, 0x96, 0x6d, # addr
191 ]
193 cmd = Parser().parse(ConstBitStream(bytes=alp_action_bytes), len(alp_action_bytes))
194 self.assertIsNotNone(cmd.interface_status)
195 self.assertEqual(cmd.interface_status.op, 34)
196 self.assertEqual(type(cmd.interface_status.operand), InterfaceStatusOperand)
197 self.assertEqual(cmd.interface_status.operand.interface_id, 0xD7)
198 self.assertEqual(cmd.interface_status.operand.interface_status.channel_id.channel_header.channel_band, ChannelBand.BAND_433)
199 self.assertEqual(cmd.interface_status.operand.interface_status.channel_id.channel_header.channel_coding, ChannelCoding.PN9)
200 self.assertEqual(cmd.interface_status.operand.interface_status.channel_id.channel_header.channel_class, ChannelClass.LO_RATE)
201 self.assertEqual(cmd.interface_status.operand.interface_status.channel_id.channel_index, 16)
202 self.assertEqual(cmd.interface_status.operand.interface_status.rx_level, 70)
203 self.assertEqual(cmd.interface_status.operand.interface_status.link_budget, 80)
204 self.assertEqual(cmd.interface_status.operand.interface_status.missed, False)
205 self.assertEqual(cmd.interface_status.operand.interface_status.nls, False)
206 self.assertEqual(cmd.interface_status.operand.interface_status.seq_nr, 0)
207 self.assertEqual(cmd.interface_status.operand.interface_status.response_to.exp, 0)
208 self.assertEqual(cmd.interface_status.operand.interface_status.response_to.mant, 20)
209 self.assertEqual(cmd.interface_status.operand.interface_status.retry, False)
211 #def test_interface_status_action_unknown_interface(self):
212 # TODO
215 def test_without_tag_request(self):
216 cmd_data = [
217 0x20, # action=32/ReturnFileData
218 0x40, # File ID
219 0x00, # offset
220 0x04, # length
221 0x00, 0xf3, 0x00, 0x00 # data
222 ]
224 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
225 self.assertEqual(len(cmd.actions), 1)
226 self.assertEqual(cmd.tag_id, None) # a random ID will not be generated
228 def test_with_tag_request(self):
229 cmd_data = [
230 52, # action=TagRequest, without EOP bit set
231 14, # tag ID
232 0x20, # action=32/ReturnFileData
233 0x40, # File ID
234 0x00, # offset
235 0x04, # length
236 0x00, 0xf3, 0x00, 0x00 # data
237 ]
239 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
240 self.assertEqual(len(cmd.actions), 1)
241 self.assertEqual(cmd.tag_id, 14)
242 self.assertEqual(cmd.send_tag_response_when_completed, False)
245 def test_with_tag_request_EOP_bit_set(self):
246 action = 52
247 action |= 1 << 7
248 cmd_data = [
249 action, # action=TagRequest, withEOP bit set
250 14, # tag ID
251 0x20, # action=32/ReturnFileData
252 0x40, # File ID
253 0x00, # offset
254 0x04, # length
255 0x00, 0xf3, 0x00, 0x00 # data
256 ]
258 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
259 self.assertEqual(len(cmd.actions), 1)
260 self.assertEqual(cmd.tag_id, 14)
261 self.assertEqual(cmd.send_tag_response_when_completed, True)
263 def test_with_multiple_tag_requests_with_different_tag_id(self):
264 cmd_data = [
265 52, # action=TagRequest, without EOP bit set
266 14, # tag ID
267 52, # another TagRequest, without EOP bit set
268 15, # tag ID
269 0x20, # action=32/ReturnFileData
270 0x40, # File ID
271 0x00, # offset
272 0x04, # length
273 0x00, 0xf3, 0x00, 0x00 # data
274 ]
276 with self.assertRaises(ParseError):
277 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
279 def test_with_tag_response(self):
280 cmd_data = [
281 35, # action=TagResponse, without EOP bit set
282 14, # tag ID
283 0x20, # action=32/ReturnFileData
284 0x40, # File ID
285 0x00, # offset
286 0x04, # length
287 0x00, 0xf3, 0x00, 0x00 # data
288 ]
290 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
291 self.assertEqual(len(cmd.actions), 1)
292 self.assertEqual(cmd.tag_id, 14)
293 self.assertEqual(cmd.execution_completed, False)
295 def test_with_tag_response_EOP_bit_set(self):
296 action = 35
297 action |= 1 << 7
298 cmd_data = [
299 action, # action=TagResponse, with EOP bit set
300 14, # tag ID
301 0x20, # action=32/ReturnFileData
302 0x40, # File ID
303 0x00, # offset
304 0x04, # length
305 0x00, 0xf3, 0x00, 0x00 # data
306 ]
308 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
309 self.assertEqual(len(cmd.actions), 1)
310 self.assertEqual(cmd.tag_id, 14)
311 self.assertEqual(cmd.execution_completed, True)
313 def test_with_multiple_tag_response_with_different_tag_id(self):
314 cmd_data = [
315 35, # action=TagResponse, without EOP bit set
316 14, # tag ID
317 35, # another TagResponse, without EOP bit set
318 15, # tag ID
319 0x20, # action=32/ReturnFileData
320 0x40, # File ID
321 0x00, # offset
322 0x04, # length
323 0x00, 0xf3, 0x00, 0x00 # data
324 ]
326 with self.assertRaises(ParseError):
327 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
330 def test_return_file_header(self):
331 cmd_data = [ 0x21, 0x02, 0x00, 0x03, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00 ]
332 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
333 self.assertEqual(len(cmd.actions), 1)
334 self.assertEqual(type(cmd.actions[0].operand), FileHeaderOperand)
335 self.assertEqual(cmd.actions[0].operand.file_id, 2)
336 self.assertEqual(cmd.actions[0].operand.file_header.properties.act_enabled, False)
338 def test_indirect_fwd(self):
339 cmd_data = [
340 51, # indirect fwd, no overload
341 64 # interface file id
342 ]
344 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
345 self.assertEqual(len(cmd.actions), 1)
346 self.assertEqual(type(cmd.actions[0].operand), IndirectInterfaceOperand)
347 self.assertEqual(cmd.actions[0].overload, False)
348 self.assertEqual(cmd.actions[0].operand.interface_file_id, 64)
349 self.assertEqual(cmd.actions[0].operand.interface_configuration_overload, None)
351 def test_indirect_fwd_with_overload(self):
352 cmd_data = [
353 (1 << 7) + 51, # indirect fwd, no overload
354 64, # interface file id
355 1 << 4, # addressee ctrl (NOID, nls_method=NONE)
356 0 # access class
357 ]
359 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
360 self.assertEqual(len(cmd.actions), 1)
361 self.assertEqual(type(cmd.actions[0].operand), IndirectInterfaceOperand)
362 self.assertEqual(cmd.actions[0].overload, True)
363 self.assertEqual(cmd.actions[0].operand.interface_file_id, 64)
364 self.assertEqual(type(cmd.actions[0].operand.interface_configuration_overload), Addressee)
365 self.assertEqual(cmd.actions[0].operand.interface_configuration_overload.id_type, IdType.NOID)
367 def test_break_query(self):
368 cmd_data = [
369 9, # break query
370 0x44, # arith comp with value, no mask, unsigned, >
371 0x01, # compare length
372 25, # compare value
373 0x20, 0x01 # file offset
374 ]
376 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
377 self.assertEqual(len(cmd.actions), 1)
378 self.assertEqual(type(cmd.actions[0].operand), QueryOperand)
380 def test_break_query(self):
381 cmd_data = [
382 9, # break query
383 0x44, # arith comp with value, no mask, unsigned, >
384 0x01, # compare length
385 25, # compare value
386 0x20, 0x01 # file offset
387 ]
389 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data))
390 self.assertEqual(len(cmd.actions), 1)
391 self.assertEqual(type(cmd.actions[0].operand), QueryOperand)
393 def test_parse_forward_LoRaWAN_iface_ABP(self):
394 lorawan_config = LoRaWANInterfaceConfigurationABP(
395 adr_enabled=True,
396 request_ack=True,
397 app_port=0x01,
398 data_rate=0,
399 dev_addr=1,
400 netw_id=2,
401 )
403 #bytes = bytearray(lorawan_config)
405 bytes = [
406 50, # forward
407 0x02, # LoRaWAN iface id
408 ]
410 bytes.extend(bytearray(lorawan_config))
412 cmd = Parser().parse(ConstBitStream(bytes=bytes), len(bytes))
413 self.assertEqual(len(cmd.actions), 1)
414 self.assertEqual(type(cmd.actions[0].operation), Forward)
415 self.assertEqual(type(cmd.actions[0].operand), InterfaceConfiguration)
416 self.assertEqual(cmd.actions[0].operand.interface_id, InterfaceType.LORAWAN_ABP)
417 self.assertEqual(type(cmd.actions[0].operand.interface_configuration), LoRaWANInterfaceConfigurationABP)
419 def test_parse_forward_LoRaWAN_iface_OTAA(self):
420 lorawan_config = LoRaWANInterfaceConfigurationOTAA(
421 adr_enabled=True,
422 request_ack=True,
423 app_port=0x01,
424 data_rate=0,
425 )
427 #bytes = bytearray(lorawan_config)
429 bytes = [
430 50, # forward
431 0x03, # LoRaWAN iface id
432 ]
434 bytes.extend(bytearray(lorawan_config))
436 cmd = Parser().parse(ConstBitStream(bytes=bytes), len(bytes))
437 self.assertEqual(len(cmd.actions), 1)
438 self.assertEqual(type(cmd.actions[0].operation), Forward)
439 self.assertEqual(type(cmd.actions[0].operand), InterfaceConfiguration)
440 self.assertEqual(cmd.actions[0].operand.interface_id, InterfaceType.LORAWAN_OTAA)
441 self.assertEqual(type(cmd.actions[0].operand.interface_configuration), LoRaWANInterfaceConfigurationOTAA)
443if __name__ == '__main__':
444 suite = unittest.TestLoader().loadTestsFromTestCase(TestParser)
445 unittest.TextTestRunner(verbosity=2).run(suite)