wiliot-certificate 1.3.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. gw_certificate/__init__.py +0 -0
  2. gw_certificate/ag/ut_defines.py +361 -0
  3. gw_certificate/ag/wlt_types.py +85 -0
  4. gw_certificate/ag/wlt_types_ag.py +5310 -0
  5. gw_certificate/ag/wlt_types_data.py +64 -0
  6. gw_certificate/api/extended_api.py +1547 -0
  7. gw_certificate/api_if/__init__.py +0 -0
  8. gw_certificate/api_if/api_validation.py +40 -0
  9. gw_certificate/api_if/gw_capabilities.py +18 -0
  10. gw_certificate/common/analysis_data_bricks.py +1455 -0
  11. gw_certificate/common/debug.py +63 -0
  12. gw_certificate/common/utils.py +219 -0
  13. gw_certificate/common/utils_defines.py +102 -0
  14. gw_certificate/common/wltPb_pb2.py +72 -0
  15. gw_certificate/common/wltPb_pb2.pyi +227 -0
  16. gw_certificate/gw_certificate.py +138 -0
  17. gw_certificate/gw_certificate_cli.py +70 -0
  18. gw_certificate/interface/ble_simulator.py +91 -0
  19. gw_certificate/interface/ble_sniffer.py +189 -0
  20. gw_certificate/interface/if_defines.py +35 -0
  21. gw_certificate/interface/mqtt.py +469 -0
  22. gw_certificate/interface/packet_error.py +22 -0
  23. gw_certificate/interface/pkt_generator.py +720 -0
  24. gw_certificate/interface/uart_if.py +193 -0
  25. gw_certificate/interface/uart_ports.py +20 -0
  26. gw_certificate/templates/results.html +241 -0
  27. gw_certificate/templates/stage.html +22 -0
  28. gw_certificate/templates/table.html +6 -0
  29. gw_certificate/templates/test.html +38 -0
  30. gw_certificate/tests/__init__.py +11 -0
  31. gw_certificate/tests/actions.py +131 -0
  32. gw_certificate/tests/bad_crc_to_PER_quantization.csv +51 -0
  33. gw_certificate/tests/connection.py +181 -0
  34. gw_certificate/tests/downlink.py +174 -0
  35. gw_certificate/tests/generic.py +161 -0
  36. gw_certificate/tests/registration.py +288 -0
  37. gw_certificate/tests/static/__init__.py +0 -0
  38. gw_certificate/tests/static/connection_defines.py +9 -0
  39. gw_certificate/tests/static/downlink_defines.py +9 -0
  40. gw_certificate/tests/static/generated_packet_table.py +209 -0
  41. gw_certificate/tests/static/packet_table.csv +10051 -0
  42. gw_certificate/tests/static/references.py +4 -0
  43. gw_certificate/tests/static/uplink_defines.py +20 -0
  44. gw_certificate/tests/throughput.py +244 -0
  45. gw_certificate/tests/uplink.py +683 -0
  46. wiliot_certificate-1.3.0a1.dist-info/LICENSE +21 -0
  47. wiliot_certificate-1.3.0a1.dist-info/METADATA +113 -0
  48. wiliot_certificate-1.3.0a1.dist-info/RECORD +51 -0
  49. wiliot_certificate-1.3.0a1.dist-info/WHEEL +5 -0
  50. wiliot_certificate-1.3.0a1.dist-info/entry_points.txt +2 -0
  51. wiliot_certificate-1.3.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,720 @@
1
+ import binascii
2
+ import datetime
3
+ from typing import Literal, List
4
+ import numpy as np
5
+ import random
6
+ import os
7
+
8
+ from gw_certificate.ag.ut_defines import (
9
+ BRIDGE_ID,
10
+ NFPKT,
11
+ PAYLOAD,
12
+ RSSI,
13
+ TIMESTAMP,
14
+ )
15
+ from gw_certificate.interface.if_defines import ADVA_LENGTH, GAP_LENGTH, GROUP_ID_LENGTH, SERVICE_UUID_LENGTH
16
+ from gw_certificate.ag.wlt_types import *
17
+ from gw_certificate.ag.wlt_types_ag import (
18
+ GROUP_ID_SIDE_INFO,
19
+ GROUP_ID_BRG2GW,
20
+ GROUP_ID_GW2BRG,
21
+ Hdr,
22
+ SideInfo,
23
+ )
24
+ from gw_certificate.ag.wlt_types_data import DataPacket
25
+
26
+ # TODO Find a relevant place for this define that is not auto-generated.
27
+ BRG_LATENCY = "brg_latency"
28
+
29
+ # Shared Functions
30
+ def generate_random_adva(device:Literal['Tag', 'Bridge']):
31
+ """generate random 6-byte AdvA
32
+
33
+ :return: AdvA (12 hex chars)
34
+ :rtype: str
35
+ """
36
+ adva = os.urandom(6).hex().upper()
37
+ if device == 'Tag':
38
+ adva = apply_adva_bitmask(adva, 'non_resolvable')
39
+ if device == 'Bridge':
40
+ adva = apply_adva_bitmask(adva, 'random_static')
41
+ return adva
42
+
43
+ def apply_adva_bitmask(adva, address_type:Literal['random_static', 'non_resolvable']):
44
+ """apply AdvA Bitmask
45
+ BRGs AdvA comply with BTLE Random Static Address
46
+ Pixel AdvA comply with BTLE Random Private Non-Resolvable Address
47
+
48
+ :param adva: AdvA
49
+ :type adva: str
50
+ :return: AdvA (12 hex chars)
51
+ :rtype: str
52
+ """
53
+ masked_adva = bytearray(binascii.unhexlify(adva))
54
+ if address_type == 'random_static':
55
+ masked_adva[5] = masked_adva[5] | 0b11000000
56
+ if address_type == 'non_resolvable':
57
+ masked_adva[5] = masked_adva[5] & 0b00111111
58
+ masked_adva = masked_adva.hex().upper()
59
+ return masked_adva
60
+
61
+ def init_adva(adva=None, device:Literal['Tag', 'Bridge']='Tag'):
62
+ """Init function helper
63
+
64
+ :param adva: AdvA, defaults to None
65
+ :type adva: str, optional
66
+ :return: AdvA (12 hex chars)
67
+ :rtype: str
68
+ """
69
+ if adva is None:
70
+ adva = generate_random_adva(device)
71
+
72
+ adva_type = ''
73
+ if device == 'Bridge':
74
+ adva_type = 'random_static'
75
+ elif device == 'Tag':
76
+ adva_type = 'non_resolvable'
77
+
78
+ adva = apply_adva_bitmask(adva, adva_type)
79
+
80
+ return adva
81
+
82
+ def increment_circular(x, num_to_inc, bits):
83
+ """Increment object of {bits} size circularly
84
+ :param x: number to increment
85
+ :type x: int
86
+ :param num_to_inc: number to increment
87
+ :type num_to_inc: int
88
+ :param bits: bit size
89
+ :type bits: int
90
+ """
91
+ return (x + num_to_inc) % (2 ^ bits)
92
+
93
+ def decrease_circular(x, num_to_dec, bits):
94
+ """Decrease object of {bits} size circularly
95
+ :param x: number to decrease
96
+ :type x: int
97
+ :param num_to_dec: number to decrease
98
+ :type num_to_dec: int
99
+ :param bits: bit size
100
+ :type bits: int
101
+ """
102
+ return (x-num_to_dec) % (2 ^ bits)
103
+
104
+ def generate_random_bridge_id():
105
+ """Generate random 6 byte bridge ID
106
+
107
+ :return: bridge ID - (12 hex chars, int)
108
+ :rtype: tuple (str, int)
109
+ """
110
+ bridge_id = os.urandom(6).hex().upper()
111
+ bridge_id_int = int.from_bytes(binascii.unhexlify(bridge_id), "big")
112
+ return bridge_id, bridge_id_int
113
+
114
+
115
+ class TagPktGenerator:
116
+ """Tag Packet Generator - represents 1 Wiliot Pixel"""
117
+ def __init__(self, adva=None, group_id=None, service_uuid:Literal["tag", "bridge"]='bridge') -> None:
118
+ """
119
+ :param adva: AdvA, defaults to None
120
+ :type adva: str, optional
121
+ :param group_id: Tag's group id, defaults to 0x020000 if none given
122
+ :type group_id: int, optional
123
+ :param service_uuid: set the generator's service uuid ("tag"=0xAFFD, "bridge"=0xC6FC)
124
+ :type service_uuid: string
125
+ """
126
+ self.adva = init_adva(adva, device='Tag')
127
+ self.data_packet = DataPacket(group_id=group_id, service_uuid=service_uuid)
128
+ pass
129
+
130
+ def __repr__(self) -> str:
131
+ return f'Tag: {self.adva}, PktID: {self.get_pkt_id()[0]}'
132
+
133
+ def get_pkt_id(self):
134
+ """Get generator's current packet ID
135
+
136
+ :return: 6 byte packet ID - (12 hex chars, int)
137
+ :rtype: tuple (str, int)
138
+ """
139
+ pkt_id_int = self.data_packet.generic.pkt_id
140
+ pkt_id_bytes = pkt_id_int.to_bytes(4, "big").hex()
141
+ return pkt_id_bytes, pkt_id_int
142
+
143
+ def set_pkt_id(self, pkt_id_int):
144
+ """Set generator's packet ID
145
+
146
+ :param pkt_id_int: packet ID
147
+ :type pkt_id_int: int
148
+ """
149
+ assert 0 < pkt_id_int < (2 ** 32), "PacketID Must be a 32 bit unsigned integer!"
150
+ self.data_packet.generic.pkt_id = pkt_id_int
151
+
152
+ def randomize_pkt_id(self):
153
+ """Randomize generator's packet ID"""
154
+ pkt_id_int = int.from_bytes(os.urandom(4), "big")
155
+ self.set_pkt_id(pkt_id_int)
156
+
157
+ def randomize_packet_payload(self):
158
+ """Randomize Generator's packet payload (keep packet ID as is)"""
159
+ self.data_packet.generic.payload = int.from_bytes(os.urandom(20), "big")
160
+
161
+ def randomize_packet_payload_unified(self):
162
+ """Randomize Generator's packet payload (keep packet ID as is) for unified packet"""
163
+ self.data_packet.pkt.nonce_n_unique_id = int.from_bytes(os.urandom(10), "big")
164
+ self.data_packet.pkt.mic = int.from_bytes(os.urandom(3), "big")
165
+ self.data_packet.pkt.data = int.from_bytes(os.urandom(8), "big")
166
+ self.data_packet.data_hdr.group_id_major == GROUP_ID_UNIFIED_PKT
167
+
168
+ def increment_pkt_data(self):
169
+ packet = self.get_packet()
170
+ data = packet[(ADVA_LENGTH+GAP_LENGTH+SERVICE_UUID_LENGTH+GROUP_ID_LENGTH):]
171
+ incremented_data = [bytes([((int(data[i:i+2], 16) + 1) % 256)]).hex().upper() for i in range(0, len(data), 2)]
172
+ incremented_packet = packet[:ADVA_LENGTH+GAP_LENGTH+SERVICE_UUID_LENGTH+GROUP_ID_LENGTH] + ''.join(incremented_data)
173
+ self.set_packet(incremented_packet)
174
+
175
+ def get_packet(self):
176
+ """Get current packet from generator (hex string)
177
+
178
+ :return: Tag's Packet [12 char AdvA + 62 char BLE Packet]
179
+ :rtype: str
180
+ """
181
+ return self.adva + self.data_packet.dump()
182
+
183
+ def set_packet(self, pkt):
184
+ """set packet to input pkt"""
185
+ if len(pkt) == 74:
186
+ self.adva = pkt[:12]
187
+ pkt = pkt[12:]
188
+ assert len(pkt) == 62, "packet must be 74 / 62 hex chars long!"
189
+ self.data_packet.set(pkt)
190
+
191
+ def get_expected_mqtt(self):
192
+ timestamp = int(datetime.datetime.now().timestamp() * 1000)
193
+ expected = {
194
+ TIMESTAMP: timestamp,
195
+ PAYLOAD: self.get_packet()[ADVA_LENGTH+GAP_LENGTH:]
196
+ }
197
+ return expected
198
+
199
+
200
+
201
+ class BrgPktGenerator:
202
+ """Bridge Packet Generator - represents 1 wiliot Bridge"""
203
+ def __init__(self, bridge_id:str=None):
204
+ """
205
+ :param bridge_id: bridge ID, defaults to randomly generated if None
206
+ :type bridge_id: str, optional
207
+ """
208
+ if bridge_id is None:
209
+ self.bridge_id, self.bridge_id_int = generate_random_bridge_id()
210
+ else:
211
+ self.bridge_id = bridge_id
212
+ self.bridge_id_int = int.from_bytes(binascii.unhexlify(bridge_id), "big")
213
+ self.adva = apply_adva_bitmask(self.bridge_id, 'random_static')
214
+ # Data packet init
215
+ self.tag_list:List[TagPktGenerator] = []
216
+ self.append_data_pkt()
217
+ # SI packet init
218
+ self.si_list:List[WltPkt]
219
+ self.update_si_list()
220
+
221
+ # BRG CFG packet init
222
+ self.brg_cfg = WltPkt(
223
+ hdr=Hdr(group_id=GROUP_ID_BRG2GW),
224
+ generic=Brg2GwCfgV7(
225
+ msg_type=BRG_MGMT_MSG_TYPE_CFG_INFO,
226
+ major_ver=3,
227
+ minor_ver=12,
228
+ build_ver=35,
229
+ brg_mac=self.bridge_id_int,
230
+ ),
231
+ )
232
+ self.brg_hb = WltPkt(
233
+ hdr=Hdr(group_id=GROUP_ID_BRG2GW),
234
+ generic=Brg2GwHbV7(
235
+ msg_type=BRG_MGMT_MSG_TYPE_HB, brg_mac=self.bridge_id_int
236
+ ),
237
+ )
238
+ self.brg_seq_id = 0
239
+
240
+ def __repr__(self):
241
+ return f'BRG {self.bridge_id}, Tags {self.tag_list}'
242
+
243
+ def set_bridge_id(self, brg_mac):
244
+ """Set generator's bridge ID
245
+
246
+ :param brg_mac: bridge ID (12 hex chars)
247
+ :type brg_mac: str
248
+ """
249
+ self.bridge_id = brg_mac
250
+ self.bridge_id_int = int.from_bytes(binascii.unhexlify(brg_mac), "big")
251
+ self.brg_cfg = WltPkt(
252
+ hdr=Hdr(group_id=GROUP_ID_BRG2GW),
253
+ generic=Brg2GwCfgV7(
254
+ msg_type=BRG_MGMT_MSG_TYPE_CFG_INFO,
255
+ major_ver=3,
256
+ minor_ver=12,
257
+ build_ver=35,
258
+ brg_mac=self.bridge_id_int,
259
+ ),
260
+ )
261
+ self.brg_hb = WltPkt(
262
+ hdr=Hdr(group_id=GROUP_ID_BRG2GW),
263
+ generic=Brg2GwHbV7(
264
+ msg_type=BRG_MGMT_MSG_TYPE_HB, brg_mac=self.bridge_id_int
265
+ ),
266
+ )
267
+
268
+ def set_random_bridge_id(self):
269
+ bridge_id, bridge_id_int = generate_random_bridge_id()
270
+ self.set_bridge_id(bridge_id)
271
+
272
+ # Data Packet
273
+
274
+ def append_data_pkt(self, data_pkt=None):
275
+ """Append data packet to tag list
276
+ if no data packet is input, a new TagPktGenerator object will be appended to tag list
277
+
278
+ :param data_pkt: 62 hex char string, defaults to None
279
+ :type data_pkt: str, optional
280
+ """
281
+ idx_to_append = len(self.tag_list)
282
+ self.tag_list.append(None)
283
+ self.set_data_pkt(tag_idx=idx_to_append, data_pkt=data_pkt)
284
+
285
+ def set_data_pkt(self, tag_idx, data_pkt=None):
286
+ """Set tag generator at specific idx's data packet from string
287
+ if no data packet is input, a new TagPktGenerator object will be created at tag_idx
288
+
289
+ :param tag_idx: tag index
290
+ :type tag_idx: int
291
+ :param data_pkt: data packet (62 hex chars), defaults to None
292
+ :type data_pkt: str, optional
293
+ """
294
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
295
+ tag_pkt_gen = TagPktGenerator()
296
+ if data_pkt is not None:
297
+ tag_pkt_gen.set_packet(data_pkt)
298
+ self.tag_list[tag_idx] = tag_pkt_gen
299
+
300
+ # SideInfo Packet
301
+
302
+ def generate_si_from_pkt_id(self, pkt_id_int):
303
+ """Generate SI from packet ID
304
+
305
+ :param pkt_id_int: packet ID
306
+ :type pkt_id_int: int
307
+ :return: WltPkt for SideInfo, with RSSI, NFPKT zeroed out
308
+ :rtype: WltPkt(hdr:Hdr, generic:SideInfo)
309
+ """
310
+ assert 0 <= pkt_id_int < (2 ** 32), "Packet ID must be a 32 bit unsigned integer!"
311
+ si_packet = WltPkt(
312
+ hdr=Hdr(group_id=GROUP_ID_SIDE_INFO),
313
+ generic=SideInfo(brg_mac=self.bridge_id_int, pkt_id=pkt_id_int),
314
+ )
315
+ return si_packet
316
+
317
+ def update_si_list(self):
318
+ """Update the SI list according to current pixel's packet IDs"""
319
+ si_table = []
320
+ for tag in self.tag_list:
321
+ tag_pkt_id_bytes, tag_pkt_id_int = tag.get_pkt_id()
322
+ si_table.append(self.generate_si_from_pkt_id(tag_pkt_id_int))
323
+ self.si_list = si_table
324
+
325
+ def randomize_si_rssi_nfpkt(self):
326
+ """Randomize all SI's rssi and nfpkt values
327
+ """
328
+ for si in self.si_list:
329
+ rssi = int.from_bytes(os.urandom(1), "big")
330
+ nfpkt = int.from_bytes(os.urandom(2), "big")
331
+ si.generic.rssi = rssi
332
+ si.generic.nfpkt = nfpkt
333
+
334
+ def randomize_unified_rssi_nfpkt_latency_gpacing(self):
335
+ """Randomize all rssi, nfpkt, global pacing and brg latency values
336
+ """
337
+ for tag in self.tag_list:
338
+ tag.data_packet.pkt.rssi = random.randint(0,63) + 40
339
+ tag.data_packet.pkt.nfpkt = int.from_bytes(os.urandom(1), "big")
340
+ tag.data_packet.pkt.brg_latency = random.randint(0,63)
341
+ tag.data_packet.pkt.global_pacing_group = random.randint(0,15)
342
+ tag.data_packet.hdr.group_id = 0x00003F
343
+ tag.data_packet.data_hdr.group_id_major = GROUP_ID_UNIFIED_PKT
344
+ tag.data_packet.data_hdr.group_id_minor = 0x0000
345
+
346
+ def set_rssi_nfpkt(self, rssi, nfpkt, tag_idx:int=None):
347
+ if tag_idx is None:
348
+ tag_idx = 0
349
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
350
+ self.si_list[tag_idx].generic.rssi = rssi
351
+ self.si_list[tag_idx].generic.nfpkt = nfpkt
352
+
353
+ # Coupling (Shared Data + SideInfo)
354
+
355
+ def set_tag_list(self, tag_list:List[TagPktGenerator]):
356
+ self.tag_list = tag_list
357
+ self.update_si_list()
358
+
359
+ def increment_pkt_id(self, tag_idx:int=None):
360
+ """Increment tag generator at specific idx's data packet's packet ID
361
+
362
+ :param tag_idx: tag index, defaults to None
363
+ :type tag_idx: int, optional
364
+ """
365
+ if tag_idx is None:
366
+ tag_idx = 0
367
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
368
+ self.tag_list[tag_idx].generic.pkt_id = increment_circular(self.tag_list[tag_idx].generic.pkt_id, 1, 32)
369
+ self.update_si_list()
370
+
371
+ def randomize_data_packet(self, tag_idx:int=None):
372
+ """Randomize tag generator at specific idx's data packet (payload + ID),
373
+ Update SI to match
374
+
375
+ :param tag_idx: tag index, defaults to None
376
+ :type tag_idx: int, optional
377
+ """
378
+ if tag_idx is None:
379
+ tag_idx = 0
380
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
381
+ tag = self.tag_list[tag_idx]
382
+ # randomize packet ID
383
+ tag.randomize_pkt_id()
384
+ # randomize packet payload
385
+ tag.randomize_packet_payload()
386
+ # update SI
387
+ self.update_si_list()
388
+
389
+ def randomize_packet_unified(self, tag_idx:int=None):
390
+ """Randomize tag generator at specific idx's data packet (payload + ID)
391
+
392
+ :param tag_idx: tag index, defaults to None
393
+ :type tag_idx: int, optional
394
+ """
395
+ if tag_idx is None:
396
+ tag_idx = 0
397
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
398
+ tag = self.tag_list[tag_idx]
399
+ # randomize packet ID
400
+ tag.randomize_pkt_id()
401
+ # randomize packet payload
402
+ tag.randomize_packet_payload_unified()
403
+
404
+ def get_data(self, tag_idx:int):
405
+ """Get tag at index's data packet (AdvA + BLE Packet)
406
+
407
+ :param tag_idx: tag index
408
+ :type tag_idx: int
409
+ :return: data packet (74 hex char)
410
+ :rtype: str
411
+ """
412
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
413
+ return self.tag_list[tag_idx].get_packet()
414
+
415
+ def get_si(self, tag_idx:int):
416
+ """Get tag at index's sideinfo packet (AdvA + BLE Packet)
417
+
418
+ :param tag_idx: _description_
419
+ :type tag_idx: int
420
+ :return: _description_
421
+ :rtype: _type_
422
+ """
423
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
424
+ return self.adva + self.si_list[tag_idx].dump()
425
+
426
+ def get_expected_coupled_mqtt(self, tag_idx:int=None):
427
+ if tag_idx is None:
428
+ tag_idx = 0
429
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
430
+ si = self.si_list[tag_idx]
431
+ """generates expected MQTT packet"""
432
+ timestamp = int(datetime.datetime.now().timestamp() * 1000)
433
+ expected = {
434
+ TIMESTAMP: timestamp,
435
+ BRIDGE_ID: self.bridge_id,
436
+ NFPKT: si.generic.nfpkt,
437
+ RSSI: si.generic.rssi,
438
+ PAYLOAD: self.get_data(tag_idx)[(ADVA_LENGTH+GAP_LENGTH):]
439
+ }
440
+ return expected
441
+
442
+ def get_expected_uncoupled_mqtt(self, tag_idx:int=None):
443
+ if tag_idx is None:
444
+ tag_idx = 0
445
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
446
+ timestamp = int(datetime.datetime.now().timestamp() * 1000)
447
+ expected_data = {
448
+ TIMESTAMP: timestamp,
449
+ PAYLOAD: self.get_data(tag_idx)[(ADVA_LENGTH+GAP_LENGTH):]
450
+ }
451
+ expected_si = {
452
+ TIMESTAMP: timestamp,
453
+ PAYLOAD: self.get_si(tag_idx)[(ADVA_LENGTH+GAP_LENGTH):]
454
+ }
455
+ return [expected_data, expected_si]
456
+
457
+ def get_expected_mqtt_unified(self, full_data_pkt, tag_idx:int=None):
458
+ if tag_idx is None:
459
+ tag_idx = 0
460
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
461
+
462
+ def extract_param_from_pkt():
463
+ # Take only the relevant digis from the data packet
464
+ param_hex_string = full_data_pkt[46:-22]
465
+ if len(param_hex_string) != 6:
466
+ raise ValueError("The parameters hex representation must be 6 digits long")
467
+
468
+ # Convert to decimal from binari and assign
469
+ param_binari_string = bin(int(param_hex_string, 16))[2:].zfill(24)
470
+ nfpkt = int(param_binari_string[:8], 2)
471
+ rssi = int(param_binari_string[8:14], 2)
472
+ # Note: rssi here is -40 then the actual value. This portion is to be added on cloud
473
+ brg_latency = int(param_binari_string[14:20], 2)
474
+ brg_global_pacing_group = int(param_binari_string[20:], 2)
475
+ return [nfpkt, rssi, brg_latency, brg_global_pacing_group]
476
+
477
+ """generates expected MQTT packet"""
478
+ timestamp = int(datetime.datetime.now().timestamp() * 1000)
479
+ pkt_param = extract_param_from_pkt()
480
+
481
+ expected_data = {
482
+ TIMESTAMP: timestamp,
483
+ BRIDGE_ID: self.bridge_id,
484
+ NFPKT: pkt_param[0],
485
+ RSSI: pkt_param[1],
486
+ BRG_LATENCY: pkt_param[2],
487
+ BRG_GLOBAL_PACING_GROUP: pkt_param[3],
488
+ PAYLOAD: full_data_pkt[(ADVA_LENGTH+GAP_LENGTH):]
489
+ }
490
+ return [expected_data]
491
+
492
+ def get_expected_hb_mqtt(self):
493
+ timestamp = int(datetime.datetime.now().timestamp() * 1000)
494
+ expected = {
495
+ TIMESTAMP: timestamp,
496
+ PAYLOAD: self.get_brg_hb()[(ADVA_LENGTH+GAP_LENGTH):]
497
+ }
498
+ return expected
499
+ # Management packets (Heartbeat / CFG)
500
+
501
+ def increment_brg_seq_id(self):
502
+ """Increment Bridge sequenceID (Relevant for CFG/HB Packets only)"""
503
+ self.brg_seq_id = increment_circular(self.brg_seq_id, 2, 8)
504
+ self.brg_cfg.generic.seq_id = decrease_circular(self.brg_seq_id, 2, 8)
505
+ self.brg_hb.generic.seq_id = decrease_circular(self.brg_seq_id, 1, 8)
506
+
507
+ def increment_hb_counters(self, num=1):
508
+ """Update bridge HB counters values
509
+
510
+ :param num: num to increment by, defaults to 1
511
+ :type num: int, optional
512
+ """
513
+ self.brg_hb.generic.non_wlt_rx_pkts_ctr = increment_circular(self.brg_hb.generic.non_wlt_rx_pkts_ctr, num, 24)
514
+ self.brg_hb.generic.bad_crc_pkts_ctr = increment_circular(self.brg_hb.generic.bad_crc_pkts_ctr, num, 24)
515
+ self.brg_hb.generic.wlt_rx_pkts_ctr = increment_circular(self.brg_hb.generic.wlt_rx_pkts_ctr, num, 24)
516
+ self.brg_hb.generic.wlt_tx_pkts_ctr = increment_circular(self.brg_hb.generic.wlt_tx_pkts_ctr, num, 16)
517
+ self.brg_hb.generic.tags_ctr = len(self.tag_list)
518
+
519
+ def increment_all(self):
520
+ """Increment Bridge sequenceID and HB counters"""
521
+ self.increment_brg_seq_id()
522
+ self.increment_hb_counters()
523
+
524
+ def get_brg_cfg(self):
525
+ """Get bridge CFG Packet
526
+
527
+ :return: CFG Packet (74 hex chars)
528
+ :rtype: str
529
+ """
530
+ return self.adva + self.brg_cfg.dump()
531
+
532
+ def get_brg_hb(self):
533
+ """Get bridge HB Packet
534
+
535
+ :return: HB Packet (74 hex chars)
536
+ :rtype: str
537
+ """
538
+ return self.adva + self.brg_hb.dump()
539
+
540
+ # General
541
+
542
+ def get_existing_data_si(self, tag_idx:int=None) -> dict:
543
+ """Get tag at tag_idx's Data packet and relevant SideInfo packet
544
+
545
+ :param tag_idx: tag index, defaults to None
546
+ :type tag_idx: int, optional
547
+ :return: dictionary with data packet and sideinfo packet
548
+ :rtype: dict
549
+ """
550
+ if tag_idx is None:
551
+ tag_idx = 0
552
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
553
+ existing_packets = {
554
+ "data_packet": self.get_data(tag_idx),
555
+ "si_packet": self.get_si(tag_idx)
556
+ }
557
+ return existing_packets
558
+
559
+ def get_existing_data_unified(self, tag_idx:int=None) -> dict:
560
+ """Get tag at tag_idx's Data packet for unified packet
561
+
562
+ :param tag_idx: tag index, defaults to None
563
+ :type tag_idx: int, optional
564
+ :return: dictionary with data packet and sideinfo packet
565
+ :rtype: dict
566
+ """
567
+ if tag_idx is None:
568
+ tag_idx = 0
569
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
570
+ existing_packets = {
571
+ "data_packet": self.get_data(tag_idx)
572
+ }
573
+ return existing_packets
574
+
575
+ def get_new_data_si(self, tag_idx:int=None) -> dict:
576
+ """Generate new Data+SideInfo packets for tag at tag_idx
577
+
578
+ :param tag_idx: tag index, defaults to None
579
+ :type tag_idx: int, optional
580
+ :return: dictionary with data packet and sideinfo packet
581
+ :rtype: dict
582
+ """
583
+ if tag_idx is None:
584
+ tag_idx = 0
585
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
586
+ self.randomize_data_packet(tag_idx)
587
+ self.randomize_si_rssi_nfpkt()
588
+ self.increment_brg_seq_id()
589
+ self.increment_hb_counters()
590
+ return self.get_existing_data_si(tag_idx)
591
+
592
+ def get_new_data_unified(self, tag_idx:int=None) -> dict:
593
+ """Generate new unified Data packets for tag at tag_idx
594
+
595
+ :param tag_idx: tag index, defaults to None
596
+ :type tag_idx: int, optional
597
+ :return: dictionary with data packet
598
+ :rtype: dict
599
+ """
600
+ if tag_idx is None:
601
+ tag_idx = 0
602
+ assert tag_idx <= len(self.tag_list), f'Tag index must be in {[i for i in range(len(self.tag_list))]}'
603
+ # set the packet type to unified
604
+ self.tag_list[tag_idx].data_packet.pkt = UnifiedEchoPkt()
605
+ self.tag_list[tag_idx].adva = init_adva(self.bridge_id, 'Bridge')
606
+ self.randomize_packet_unified(tag_idx)
607
+ self.randomize_unified_rssi_nfpkt_latency_gpacing()
608
+ self.increment_brg_seq_id()
609
+ self.increment_hb_counters()
610
+ return self.get_existing_data_unified(tag_idx)
611
+
612
+ class BrgPktGeneratorNetwork:
613
+ """Bridge Packet Generator Network - Represents multiple bridges setup at same location and echoing the same tags"""
614
+ def __init__(self, num_brgs=3):
615
+ """
616
+ :param num_brgs: number of bridges in array, defaults to 3
617
+ :type num_brgs: int, optional
618
+ """
619
+ assert num_brgs > 1, 'BrgArray cannot be smaller than 1!'
620
+ self.brg_list = [BrgPktGenerator() for brg in range(num_brgs)]
621
+ self.primary_brg = self.brg_list[0]
622
+ self.secondary_brgs = self.brg_list[1:]
623
+ for brg in self.secondary_brgs:
624
+ brg.set_tag_list(self.primary_brg.tag_list)
625
+
626
+ def get_new_pkt_pairs(self) -> list:
627
+ """Get data/si pairs for all bridges in bridge network
628
+
629
+ :return: List of dictionaries containing {bridge_id, data_packet, si_packet} key:value pairs
630
+ :rtype: list[dict]
631
+ """
632
+ pkts = []
633
+ new_pkt_primary = self.primary_brg.get_new_data_si()
634
+ pkts.append(new_pkt_primary)
635
+ for brg in self.secondary_brgs:
636
+ brg.update_si_list()
637
+ brg.randomize_si_rssi_nfpkt()
638
+ pkts.append(brg.get_existing_data_si())
639
+ for idx, pkt in enumerate(pkts):
640
+ pkt.update({'bridge_id': self.brg_list[idx].bridge_id})
641
+ return pkts
642
+
643
+ def get_new_pkt_unified(self) -> list:
644
+ """Get new unified pkt for all bridges in bridge network
645
+
646
+ :return: List of dictionaries containing {bridge_id, data_packet} key:value pairs
647
+ :rtype: list[dict]
648
+ """
649
+ pkts = []
650
+ new_pkt_primary = self.primary_brg.get_new_data_unified()
651
+ pkts.append(new_pkt_primary)
652
+ for brg in self.secondary_brgs:
653
+ brg.randomize_unified_rssi_nfpkt_latency_gpacing()
654
+ brg.tag_list[0].adva = init_adva(brg.bridge_id, 'Bridge')
655
+ pkts.append(brg.get_existing_data_unified())
656
+ for idx, pkt in enumerate(pkts):
657
+ pkt.update({'bridge_id': self.brg_list[idx].bridge_id})
658
+ return pkts
659
+
660
+ def get_scattered_pkt_pairs(self, delay):
661
+ """Get data/si pairs with scattered time delays
662
+
663
+ :param delay: bridges rxTxPeriod equivalent (millisecond)
664
+ :type delay: int
665
+ """
666
+ assert delay / len(self.brg_list) >= 10, 'Cannot get scattered PKT pairs for parameters! decrease num of brgs or increase delay'
667
+ start_times = sorted(random.sample(list(np.arange(0, delay-10+1, 10)), k=len(self.brg_list)))
668
+ start_times.append((start_times[0] + delay))
669
+ pkts = self.get_new_pkt_pairs()
670
+
671
+ for idx, pkt in enumerate(pkts):
672
+ # minimum time between UART packet commands = 10ms
673
+ pkt['time_delay'] = max(start_times[idx+1] - start_times[idx], 10)
674
+ return pkts
675
+
676
+ class CloudPktGenerator:
677
+ """Cloud Packet Generator - Represents """
678
+ def __init__(self):
679
+ self.bridge_id, self.bridge_id_int = generate_random_bridge_id()
680
+ pass
681
+
682
+ def generate_config_pkt(self, seq_id=None, bridge_id=None):
683
+ if seq_id == None:
684
+ seq_id = int.from_bytes(os.urandom(1), 'big')
685
+ if bridge_id is not None:
686
+ bridge_id_int = int.from_bytes(binascii.unhexlify(bridge_id), "big")
687
+
688
+ pkt = WltPkt(Hdr(group_id=GROUP_ID_GW2BRG), generic=Gw2BrgCfgV7(
689
+ msg_type=BRG_MGMT_MSG_TYPE_CFG_SET,
690
+ global_pacing_group=random.getrandbits(4),
691
+ output_power_sub_1_ghz=random.getrandbits(4),
692
+ seq_id=random.getrandbits(8),
693
+ brg_mac=bridge_id_int,
694
+ unused0=0,
695
+ pkt_types_mask=random.getrandbits(5),
696
+ unused1=0,
697
+ rxtx_period=random.getrandbits(8),
698
+ tx_period=random.getrandbits(8),
699
+ energy_pattern_idx=random.getrandbits(8),
700
+ output_power_2_4=random.getrandbits(8),
701
+ pacer_interval=random.getrandbits(16),
702
+ unused2=0,
703
+ tx_prob=random.getrandbits(3),
704
+ tx_repetition=random.getrandbits(4),
705
+ transmit_time_sub_1_ghz=random.getrandbits(4),
706
+ sub1g_freq_profile=random.getrandbits(4),
707
+ ))
708
+ return pkt.dump()
709
+
710
+ # def generate_action_pkt(self, action: Literal["blink", "reboot"], seq_id=None, bridge_id=None):
711
+ # if seq_id == None:
712
+ # seq_id = int.from_bytes(os.urandom(1), 'big')
713
+ # if bridge_id is not None:
714
+ # bridge_id_int = int.from_bytes(binascii.unhexlify(bridge_id), "big")
715
+
716
+ # pkt = WltPkt(Hdr(group_id=GROUP_ID_GW2BRG), generic=ActionV7(
717
+ # msg_type=BRG_MGMT_MSG_TYPE_ACTION,
718
+ # seq_id=seq_id,
719
+ # action_id=
720
+ # )