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

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# 

19 

20# author: Christophe VG <contact@christophe.vg> 

21# unit tests for the D7 ALP parser 

22 

23import unittest 

24 

25from bitstring import ConstBitStream 

26 

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 

36 

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 

41 

42 

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 ] 

61 

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 

70 

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) 

75 

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) 

87 

88 def test_empty_data(self): 

89 alp_action_bytes = [ 

90 0x20, 

91 0x40, 

92 0x00, 

93 0x00 

94 ] 

95 

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) 

100 

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)) 

110 

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 ] 

119 

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) 

126 

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 ] 

135 

136 cmd_bytes = alp_action_bytes + alp_action_bytes + self.interface_status_action 

137 cmd = Parser().parse(ConstBitStream(bytes=cmd_bytes), len(cmd_bytes)) 

138 

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) 

144 

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) 

173 

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 ] 

192 

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) 

210 

211 #def test_interface_status_action_unknown_interface(self): 

212 # TODO  

213 

214 

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 ] 

223 

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 

227 

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 ] 

238 

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) 

243 

244 

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 ] 

257 

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) 

262 

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 ] 

275 

276 with self.assertRaises(ParseError): 

277 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data)) 

278 

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 ] 

289 

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) 

294 

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 ] 

307 

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) 

312 

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 ] 

325 

326 with self.assertRaises(ParseError): 

327 cmd = Parser().parse(ConstBitStream(bytes=cmd_data), len(cmd_data)) 

328 

329 

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) 

337 

338 def test_indirect_fwd(self): 

339 cmd_data = [ 

340 51, # indirect fwd, no overload 

341 64 # interface file id 

342 ] 

343 

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) 

350 

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 ] 

358 

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) 

366 

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 ] 

375 

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) 

379 

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 ] 

388 

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) 

392 

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 ) 

402 

403 #bytes = bytearray(lorawan_config) 

404 

405 bytes = [ 

406 50, # forward 

407 0x02, # LoRaWAN iface id 

408 ] 

409 

410 bytes.extend(bytearray(lorawan_config)) 

411 

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) 

418 

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 ) 

426 

427 #bytes = bytearray(lorawan_config) 

428 

429 bytes = [ 

430 50, # forward 

431 0x03, # LoRaWAN iface id 

432 ] 

433 

434 bytes.extend(bytearray(lorawan_config)) 

435 

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) 

442 

443if __name__ == '__main__': 

444 suite = unittest.TestLoader().loadTestsFromTestCase(TestParser) 

445 unittest.TextTestRunner(verbosity=2).run(suite)