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,189 @@
1
+ import ast
2
+ import logging
3
+ import pandas as pd
4
+ import time
5
+ import datetime
6
+ import threading
7
+ import binascii
8
+
9
+ from gw_certificate.interface.uart_if import UARTInterface
10
+ from gw_certificate.common.debug import debug_print
11
+ from gw_certificate.interface.if_defines import *
12
+
13
+ class SnifferPkt():
14
+ def __init__(self, raw_output, time_received, rx_channel):
15
+ self.adva = raw_output[:12]
16
+ self.packet = raw_output[12:74]
17
+ try:
18
+ self.rssi = int.from_bytes(binascii.unhexlify(raw_output[74:]), 'big') * -1
19
+ except ValueError:
20
+ self.rssi = 99
21
+ self.time_received = time_received
22
+ self.rx_channel = rx_channel
23
+
24
+ def __repr__(self):
25
+ return f'CH{self.rx_channel}|{self.adva}{self.packet} RSSI:{self.rssi} {self.time_received}'
26
+
27
+ def to_dict(self):
28
+ return {'adva': self.adva, 'packet': self.packet, 'rssi': self.rssi, 'time_received': self.time_received, 'rx_channel': self.rx_channel}
29
+
30
+ class SnifferPkts():
31
+ def __init__(self, pkts=[]):
32
+ self.pkts = pkts
33
+
34
+ def __add__(self, other):
35
+ return SnifferPkts(self.pkts + other.pkts)
36
+
37
+ def __len__(self):
38
+ return len(self.pkts)
39
+
40
+ def __repr__(self):
41
+ return self.pkts
42
+
43
+ def process_pkt(self, raw_output, time_received, rx_channel, print_pkt=False, adva_filter=None):
44
+ pkt = SnifferPkt(raw_output, time_received, rx_channel)
45
+ if adva_filter:
46
+ if pkt.adva != adva_filter:
47
+ return None
48
+ self.pkts.append(pkt)
49
+ if print_pkt:
50
+ print(pkt)
51
+ return pkt
52
+
53
+ def filter_pkts(self, raw_packet=None, adva=None, time_range:tuple=None):
54
+ result = []
55
+ for pkt in self.pkts:
56
+ if (raw_packet is not None) and (pkt.packet == raw_packet) or \
57
+ (adva is not None) and (pkt.adva == adva) or \
58
+ (time_range is not None) and (time_range[0] < pkt.time_received < time_range[1]):
59
+ result.append(pkt)
60
+ return SnifferPkts(result)
61
+
62
+ def flush_pkts(self):
63
+ self.pkts = []
64
+
65
+ def to_list(self):
66
+ return [p.to_dict() for p in self.pkts]
67
+
68
+ def to_pandas(self):
69
+ return pd.DataFrame().from_dict(self.to_list())
70
+
71
+ def cur_time():
72
+ return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
73
+
74
+
75
+ class BLESniffer():
76
+ def __init__(self, uart:UARTInterface, print_pkt=False, logger_filepath=None, adva_filter=None):
77
+ self.uart = uart
78
+ self.listener_thread = None
79
+ self.sniffer_pkts = SnifferPkts()
80
+ self.listening = False
81
+ self.listener_lock = threading.Lock()
82
+ self.print = print_pkt
83
+ self.rx_channel = 0
84
+ self.adva_filter = None
85
+ if adva_filter is not None:
86
+ adva_filter = adva_filter.upper()
87
+ assert isinstance(adva_filter, str)\
88
+ and len(adva_filter) == 12\
89
+ and all(c in '0123456789ABCDEF' for c in adva_filter), "Input must be a 12-character hexadecimal string"
90
+ self.adva_filter = adva_filter
91
+ # Configure Logger
92
+ logger = logging.getLogger('ble_sniffer')
93
+ logger.setLevel(logging.DEBUG)
94
+ if logger_filepath is not None:
95
+ # create file handler
96
+ fh = logging.FileHandler(logger_filepath)
97
+ fh.setLevel(logging.DEBUG)
98
+ formatter = logging.Formatter('%(message)s')
99
+ fh.setFormatter(formatter)
100
+ logger.addHandler(fh)
101
+ logger.propagate = False # Do not send logs to 'root' logger
102
+ debug_print(f'BLE Sniffer Logger initialized at {logger_filepath}')
103
+ self.logger = logger
104
+
105
+ def packet_listener(self):
106
+ while self.listening:
107
+ line = self.uart.read_line()
108
+ if line is not None and len(line) == 76:
109
+ with self.listener_lock:
110
+ pkt = self.sniffer_pkts.process_pkt(line, datetime.datetime.now(), self.rx_channel, self.print, self.adva_filter)
111
+ if pkt:
112
+ self.logger.info(pkt)
113
+
114
+ # Change sniffing modes
115
+ def start_sniffer(self, rx_channel):
116
+ self.logger.debug(f'{cur_time()} | Starting Sniffer on CH{rx_channel}')
117
+ self.uart.set_sniffer(rx_channel)
118
+ self.rx_channel = rx_channel
119
+ self.listener_thread = threading.Thread(target=self.packet_listener)
120
+ self.listening = True
121
+ self.listener_thread.start()
122
+
123
+ def stop_sniffer(self):
124
+ self.logger.debug(f'{cur_time()} | Stopping Sniffer')
125
+ self.flush_pkts()
126
+ self.listening = False
127
+ if self.listener_thread is not None:
128
+ self.listener_thread.join()
129
+ self.listener_thread = None
130
+ self.uart.cancel_sniffer()
131
+ self.rx_channel = 0
132
+
133
+ def reset_sniffer(self, rx_channel):
134
+ self.logger.debug(f'{cur_time()} | Reseting Sniffer')
135
+ self.stop_sniffer()
136
+ self.start_sniffer(rx_channel)
137
+ self.flush_pkts()
138
+
139
+ # Data Handling
140
+ def get_all_pkts(self):
141
+ return self.sniffer_pkts
142
+
143
+ def get_filtered_packets(self, raw_packet=None, adva=None, time_range:tuple=None):
144
+ return self.sniffer_pkts.filter_pkts(raw_packet, adva, time_range)
145
+
146
+ def flush_pkts(self):
147
+ self.logger.debug(f'{cur_time()} | Flushing packets')
148
+ self.sniffer_pkts.flush_pkts()
149
+
150
+ def to_pandas(self):
151
+ return self.sniffer_pkts.to_pandas()
152
+
153
+ # Packet Counters
154
+ def get_pkts_cntrs(self, channel, lookout_time=DEFAULT_LOOKOUT_TIME):
155
+ self.logger.debug(f'{cur_time()} | Getting pkt counters for CH{channel}')
156
+ if self.rx_channel != channel:
157
+ self.uart.set_rx(channel)
158
+ self.rx_channel = channel
159
+
160
+ lookout_time = lookout_time
161
+ pkt_cntrs = None
162
+ self.uart.flush()
163
+ self.uart.write_ble_command(GET_LOGGER_COUNTERS)
164
+ start_time = time.time()
165
+ current_time = start_time
166
+ while current_time - start_time < lookout_time:
167
+ line = self.uart.read_line()
168
+ if line is not None and "'bad_crc'" in line:
169
+ start_of_cntr_index = line.find('{')
170
+ pkt_cntrs = line[start_of_cntr_index:]
171
+ debug_print(f"pkt_cntrs: {pkt_cntrs}")
172
+ return ast.literal_eval(pkt_cntrs)
173
+ current_time = time.time()
174
+ debug_print(f"No counter received within the time limit of {lookout_time} seconds")
175
+ return pkt_cntrs
176
+
177
+ class BLESnifferContext():
178
+ def __init__(self, ble_sniffer:BLESniffer, rx_channel):
179
+ self.ble_sniffer = ble_sniffer
180
+ self.rx_channel = rx_channel
181
+
182
+ def __enter__(self):
183
+ self.ble_sniffer.flush_pkts()
184
+ self.ble_sniffer.start_sniffer(self.rx_channel)
185
+ return self.ble_sniffer
186
+
187
+ def __exit__(self, exc_type, exc_value, traceback):
188
+ if self.ble_sniffer:
189
+ self.ble_sniffer.stop_sniffer()
@@ -0,0 +1,35 @@
1
+ RX_CHANNELS = [37, 38, 39]
2
+ RX_CHANNELS_SNIFFING_KIT = list(range(40))
3
+ SET_SNIFFER = '!set_logger_mode 2'
4
+ CANCEL_SNIFFER = '!set_logger_mode 0'
5
+ GET_LOGGER_COUNTERS = '!get_logger_counters'
6
+ GATEWAY_APP = '!gateway_app'
7
+ STOP_ADVERTISING = '!stop_advertising'
8
+ CANCEL = '!cancel'
9
+ RESET_GW = '!reset'
10
+ VERSION = '!version'
11
+ SEND_ALL_ADV_CHANNELS = 0
12
+ SERIAL_TIMEOUT = 0.1
13
+ SEP = "#"*100
14
+ LENGTH = '1E'
15
+ GAP_TYPE = '16'
16
+ DEFAULT_ADVA = 'FFFFFFFFFFFF'
17
+ DEFAULT_DUPLICATES = 3
18
+ GW_APP_VERSION_HEADER = 'WILIOT_GW_BLE_CHIP_SW_VER'
19
+ LOCATION = 'location'
20
+ DEFAULT_OUTPUT_POWER = 8
21
+ DEFAULT_DELAY = 20
22
+ BRIDGES = [i for i in range(3)]
23
+ MAX_NFPKT = 65535
24
+ MAX_RSSI = 255
25
+ ADVA_LENGTH = 12
26
+ GAP_LENGTH = 4
27
+ SERVICE_UUID_LENGTH = 4
28
+ GROUP_ID_LENGTH = 6
29
+ UPLINK_DUPLICATIONS = [i for i in range(3, 7)]
30
+ UPLINK_TIME_DELAYS = [20, 130, 255]
31
+ UNIFIED_DUPLICATIONS = [i for i in range(3, 7)]
32
+ UNIFIED_TIME_DELAYS = [20, 130, 255]
33
+ DEFAULT_LOOKOUT_TIME = 2
34
+
35
+ ADVA_PAYLOAD = 'adva_payload'
@@ -0,0 +1,469 @@
1
+ import copy
2
+ import datetime
3
+ from enum import Enum
4
+ import json
5
+ import logging
6
+ import time
7
+ from typing import Literal, Union
8
+ import uuid
9
+ import paho.mqtt.client as mqtt
10
+ import ssl
11
+ import base64
12
+ from google.protobuf.message import DecodeError
13
+ from google.protobuf.json_format import MessageToDict, Parse, ParseError, ParseDict
14
+
15
+ from gw_certificate.ag.ut_defines import PACKETS, PAYLOAD, MGMT_PKT, SIDE_INFO_PKT, GW_ID, NFPKT, GW_LOGS, UNIFIED_PKT
16
+ from gw_certificate.ag.wlt_types_ag import GROUP_ID_BRG2GW, GROUP_ID_SIDE_INFO, GROUP_ID_UNIFIED_PKT
17
+ from gw_certificate.ag.wlt_types_data import DATA_DEFAULT_GROUP_ID, DataPacket
18
+ from gw_certificate.common.debug import debug_print
19
+ from gw_certificate.common import wltPb_pb2
20
+
21
+
22
+
23
+ DATA_PKT = 'data_pkt'
24
+
25
+ class CustomBrokers(Enum):
26
+ HIVE = 'broker.hivemq.com'
27
+ EMQX = 'broker.emqx.io'
28
+ ECLIPSE = 'mqtt.eclipseprojects.io'
29
+
30
+ def get_broker_url(broker):
31
+ try:
32
+ broker_url = CustomBrokers[broker.upper()].value
33
+ debug_print(f"Broker URL: {broker_url}")
34
+ return broker_url
35
+ except KeyError:
36
+ raise KeyError(f"Broker '{broker}' not found in CustomBrokers.")
37
+
38
+ class Serialization(Enum):
39
+ UNKNOWN = "unknown"
40
+ JSON = "JSON"
41
+ PB = "Protobuf"
42
+
43
+ class GwAction(Enum):
44
+ DISABLE_DEV_MODE = "DevModeDisable"
45
+ REBOOT_GW ="rebootGw"
46
+ GET_GW_INFO ="getGwInfo"
47
+
48
+ class WltMqttMessage:
49
+ def __init__(self, body, topic):
50
+ self.body = body
51
+ self.mqtt_topic = topic
52
+ self.mqtt_timestamp = datetime.datetime.now()
53
+ self.body_ex = copy.deepcopy(body)
54
+ self.is_unified = False
55
+ if "data" in self.mqtt_topic and PACKETS in self.body_ex.keys():
56
+ for pkt in self.body_ex[PACKETS]:
57
+ data_pkt = DataPacket()
58
+ data_pkt.set(pkt[PAYLOAD])
59
+ if data_pkt.pkt != None:
60
+ if data_pkt.hdr.group_id == GROUP_ID_BRG2GW:
61
+ pkt[MGMT_PKT] = copy.deepcopy(data_pkt)
62
+ if data_pkt.hdr.group_id == GROUP_ID_SIDE_INFO:
63
+ pkt[SIDE_INFO_PKT] = copy.deepcopy(data_pkt)
64
+ if data_pkt.hdr.group_id == DATA_DEFAULT_GROUP_ID:
65
+ pkt[DATA_PKT] = copy.deepcopy(data_pkt)
66
+ if data_pkt.hdr.group_id == GROUP_ID_UNIFIED_PKT:
67
+ pkt[UNIFIED_PKT] = copy.deepcopy(data_pkt)
68
+ self.is_unified = True
69
+
70
+ def __repr__(self) -> str:
71
+ if self.body_ex != {}:
72
+ return str(self.body_ex)
73
+ return str(self.body)
74
+
75
+
76
+ class WltMqttMessages:
77
+ def __init__(self):
78
+ self.data = []
79
+ self.status = []
80
+ self.update = []
81
+ self.all = []
82
+
83
+ def insert(self, pkt):
84
+ self.all.append(pkt)
85
+ if "data" in pkt.mqtt_topic:
86
+ self.data.append(pkt)
87
+ elif "status" in pkt.mqtt_topic:
88
+ self.status.append(pkt)
89
+ elif "update" in pkt.mqtt_topic:
90
+ self.update.append(pkt)
91
+
92
+ def __repr__(self) -> str:
93
+ return f'Data {self.data} \n Status {self.status} \n Update {self.update}'
94
+
95
+ class MqttClient:
96
+
97
+ def __init__(self, gw_id, owner_id, logger_filepath=None, topic_suffix='', serialization=Serialization.UNKNOWN, broker='hive'):
98
+ # Set variables
99
+ self.gw_id = gw_id
100
+ self.owner_id = owner_id
101
+ self.broker_url = get_broker_url(broker)
102
+
103
+ # Configure logger
104
+ logger = logging.getLogger('mqtt')
105
+ logger.setLevel(logging.DEBUG)
106
+ if logger_filepath is not None:
107
+ # create file handler which logs even debug messages
108
+ fh = logging.FileHandler(logger_filepath)
109
+ fh.setLevel(logging.DEBUG)
110
+ formatter = logging.Formatter('%(asctime)s | %(message)s')
111
+ fh.setFormatter(formatter)
112
+ logger.addHandler(fh)
113
+ logger.propagate = False # Do not send logs to 'root' logger
114
+ debug_print(f'MQTT Logger initialized at {logger_filepath}')
115
+ self.logger = logger
116
+
117
+ # Configure Paho MQTT Client
118
+ client_id = f'GW_Certificate_{uuid.uuid4()}'
119
+ self.userdata = {'messages': WltMqttMessages(), 'gw_seen': False , 'logger': self.logger, 'serialization': serialization, 'published': []}
120
+ # Try-except is temporary until old users are up to date
121
+ try:
122
+ self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id, userdata=self.userdata)
123
+ except AttributeError:
124
+ print("\nGW Certificate now runs with latest paho-mqtt!\nPlease upgrade yours to version 2.0.0 (pip install --upgrade paho-mqtt)\n")
125
+ raise
126
+ self.client.enable_logger(logger=self.logger)
127
+ self.client.on_message = on_message
128
+ self.client.on_connect = on_connect
129
+ self.client.on_disconnect = on_disconnect
130
+ self.client.on_subscribe = on_subscribe
131
+ self.client.on_unsubscribe = on_unsubscribe
132
+ self.client.on_publish = on_publish
133
+ self.client.on_log = on_log
134
+ self.client.tls_set(tls_version=ssl.PROTOCOL_TLSv1_2)
135
+ debug_print(f'Connecting to MQTT broker: tls://{self.broker_url}:8883, Keepalive=60')
136
+ self.client.connect(self.broker_url, port=8883, keepalive=60)
137
+ # Set Topics
138
+ self.update_topic = f"update{topic_suffix}/{owner_id}/{gw_id}"
139
+ debug_print(f'Subscribe to {self.update_topic}...')
140
+ self.client.subscribe(self.update_topic)
141
+ self.data_topic = f"data{topic_suffix}/{owner_id}/{gw_id}"
142
+ debug_print(f'Subscribe to {self.data_topic}...')
143
+ self.client.subscribe(self.data_topic)
144
+ self.status_topic = f"status{topic_suffix}/{owner_id}/{gw_id}"
145
+ debug_print(f'Subscribe to {self.status_topic}...')
146
+ self.client.subscribe(self.status_topic)
147
+ self.client.loop_start()
148
+ while(not self.client.is_connected()):
149
+ debug_print(f'Waiting for MQTT connection...')
150
+ time.sleep(1)
151
+ debug_print('Connected to MQTT.')
152
+
153
+ def get_serialization(self):
154
+ """
155
+ return serialization type
156
+ """
157
+ return self.userdata['serialization']
158
+
159
+ # Downstream Interface
160
+ def send_action(self, action:GwAction):
161
+ """
162
+ Send an action to the gateway
163
+ :param action: GwAction - Required
164
+ """
165
+ assert isinstance(action, GwAction), 'Action Must be a GWAction!'
166
+ # JSON
167
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.JSON}:
168
+ raw_payload = json.dumps({"action": action.value})
169
+ self.userdata['published'].append(raw_payload.encode('utf-8'))
170
+ message_info = self.client.publish(self.update_topic, payload=raw_payload)
171
+ print('PublishJSON')
172
+ # PB
173
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.PB}:
174
+ payload = wltPb_pb2.DownlinkMessage()
175
+ payload.gatewayAction.action = action.value
176
+ raw_payload = payload.SerializeToString()
177
+ self.userdata['published'].append(raw_payload)
178
+ message_info = self.client.publish(self.update_topic, payload=raw_payload)
179
+ print('PublishPB')
180
+ message_info.wait_for_publish()
181
+ return message_info
182
+
183
+ def send_payload(self, payload, topic:Literal['update', 'data', 'status']='update'):
184
+ """
185
+ Send a payload to the gateway
186
+ :type payload: dict [JSON] / str [PB]
187
+ :param payload: payload to send
188
+ :type topic: Literal['update', 'data', 'status']
189
+ :param topic: defualts to update
190
+ """
191
+ def cast_to_proto(payload: Union[str, dict, wltPb_pb2.DownlinkMessage]):
192
+ if isinstance(payload, wltPb_pb2.DownlinkMessage):
193
+ return payload
194
+
195
+ if isinstance(payload, str):
196
+ payload = json.loads(payload)
197
+ # payload is now a dictionary
198
+
199
+ def add_proto_field_type(d):
200
+ for key, value in list(d.items()):
201
+ if isinstance(value, int):
202
+ d[key] = {"integerValue": value}
203
+ elif isinstance(value, float):
204
+ d[key] = {"numberValue": value}
205
+ elif isinstance(value, str):
206
+ d[key] = {"stringValue": value}
207
+ elif isinstance(value, bool):
208
+ d[key] = {"boolValue": value}
209
+ elif isinstance(value, dict):
210
+ add_proto_field_type(value) # Recursively handle nested dictionaries
211
+
212
+ if 'gatewayConfig' in payload:
213
+ config = payload['gatewayConfig'].get('config', {})
214
+ add_proto_field_type(config)
215
+
216
+ pb_message = wltPb_pb2.DownlinkMessage()
217
+ ParseDict(payload, pb_message, ignore_unknown_fields=True)
218
+
219
+ return pb_message
220
+
221
+ topic = {'update': self.update_topic,
222
+ 'data': self.data_topic,
223
+ 'status': self.status_topic}[topic]
224
+ # JSON
225
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.JSON}:
226
+ try:
227
+ raw_payload = json.dumps(payload)
228
+ # Add published payload to published list
229
+ self.userdata['published'].append(raw_payload.encode('utf-8'))
230
+ message_info = self.client.publish(topic, raw_payload)
231
+ except TypeError as e:
232
+ if self.get_serialization() != Serialization.UNKNOWN:
233
+ debug_print(f'Cannot pack payload as JSON!: {payload}')
234
+ raise e
235
+ # PB
236
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.PB}:
237
+ try:
238
+ pb_message = cast_to_proto(payload)
239
+ raw_payload = pb_message.SerializeToString()
240
+ # Add published payload to published list
241
+ self.userdata['published'].append(raw_payload)
242
+ message_info = self.client.publish(topic, raw_payload)
243
+ except ParseError as e:
244
+ if self.get_serialization() != Serialization.UNKNOWN:
245
+ debug_print(f'Cannot parse payload as PB message!: {payload}')
246
+ raise e
247
+ message_info.wait_for_publish()
248
+ return message_info
249
+
250
+ def advertise_packet(self, raw_packet, tx_max_duration=800, use_retries=False):
251
+ if len(raw_packet) < 62:
252
+ if len(raw_packet) == 54:
253
+ raw_packet = 'C6FC' + raw_packet
254
+ if len(raw_packet) == 58:
255
+ raw_packet = '1E16' + raw_packet
256
+ if len(raw_packet) > 62:
257
+ raw_packet = raw_packet[-62:]
258
+
259
+ assert len(raw_packet) == 62, 'Raw Packet must be 62 chars long!'
260
+
261
+ if self.get_serialization() == Serialization.PB: # PB Serialization
262
+ payload = wltPb_pb2.DownlinkMessage()
263
+ payload.txPacket.payload = bytes.fromhex(raw_packet)
264
+ payload.txPacket.maxRetries = int(tx_max_duration / 100)
265
+ payload.txPacket.maxDurationMs = tx_max_duration
266
+
267
+ else: # JSON Serialization
268
+ if use_retries:
269
+ payload = {
270
+ 'action': 0, # Advertise BLE Packet
271
+ 'txPacket': raw_packet, # Raw Packet
272
+ 'txMaxRetries': tx_max_duration / 100, # Tx Max Retries
273
+ }
274
+ else:
275
+ payload = {
276
+ 'txPacket': raw_packet, # Raw Packet
277
+ 'txMaxDurationMs': tx_max_duration, # Tx Max Duration
278
+ 'action': 0 # Advertise BLE Packet
279
+ }
280
+
281
+ self.send_payload(payload, topic='update')
282
+ return payload
283
+
284
+ def check_gw_seen(self):
285
+ return self.userdata['gw_seen']
286
+
287
+ def get_gw_info(self):
288
+ self.flush_messages()
289
+ self.send_action(GwAction.GET_GW_INFO)
290
+ time.sleep(5)
291
+ debug_print('---GW INFO---')
292
+ try:
293
+ gw_info = self.userdata['messages'].status[0]
294
+ debug_print(gw_info)
295
+ self.flush_messages()
296
+ return gw_info
297
+ except IndexError:
298
+ debug_print('No GW INFO')
299
+ self.flush_messages()
300
+ return False
301
+
302
+ def get_gw_configuration(self):
303
+ self.flush_messages()
304
+ self.send_action(GwAction.REBOOT_GW)
305
+ debug_print('---GW CONFIG---')
306
+ try:
307
+ debug_print(self.userdata['messages'].status)
308
+ return True
309
+ except KeyError:
310
+ return False
311
+
312
+ def exit_custom_mqtt(self, mqtt_mode:Literal['automatic', 'manual' ,'legacy']):
313
+ if mqtt_mode == 'legacy':
314
+ return self.send_action(GwAction.DISABLE_DEV_MODE)
315
+ elif mqtt_mode == 'automatic':
316
+ custom_mqtt = {
317
+ "customBroker": False,
318
+ "brokerUrl": "",
319
+ "port": 8883,
320
+ "username": "",
321
+ "password": "",
322
+ "updateTopic": f"update/{self.owner_id}/{self.gw_id}",
323
+ "statusTopic": f"status/{self.owner_id}/{self.gw_id}",
324
+ "dataTopic": f"data/{self.owner_id}/{self.gw_id}"
325
+ }
326
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.JSON}:
327
+ self.send_payload(custom_mqtt)
328
+ if self.get_serialization() in {Serialization.UNKNOWN, Serialization.PB}:
329
+ self.send_payload({'customBroker': custom_mqtt})
330
+ elif mqtt_mode == 'manual':
331
+ debug_print(f"Make sure GW {self.gw_id} is set to Wiliot MQTT broker")
332
+ return True
333
+
334
+ # Packet Handling
335
+ def flush_messages(self):
336
+ self.userdata = {'messages': WltMqttMessages(), 'gw_seen': False , 'logger': self.logger, 'serialization':self.get_serialization(), 'published':[]}
337
+ self.client.user_data_set(self.userdata)
338
+
339
+ def get_all_messages_from_topic(self, topic:Literal['status', 'data', 'update']):
340
+ return getattr(self.userdata['messages'], topic)
341
+
342
+ def get_all_pkts_from_topic(self, topic:Literal['status', 'data', 'update']):
343
+ pkts = []
344
+ if topic == 'data':
345
+ for p in eval(f'self.userdata["messages"].{topic}'):
346
+ gw_id = p.body_ex[GW_ID] if GW_ID in p.body_ex else ""
347
+ if PACKETS in p.body_ex:
348
+ for pkt in p.body_ex[PACKETS]:
349
+ pkt[GW_ID] = gw_id
350
+ pkts += [pkt]
351
+ return pkts
352
+
353
+ def get_status_message(self):
354
+ messages = self.get_all_messages_from_topic('status')
355
+ for message in messages:
356
+ if GW_LOGS not in message.body_ex.keys():
357
+ return message.body_ex
358
+ return None
359
+
360
+ def get_coupled_tags_pkts(self):
361
+ return [p for p in self.get_all_pkts_from_topic('data') if NFPKT in p]
362
+
363
+ def get_uncoupled_tags_pkts(self):
364
+ return [p for p in self.get_all_pkts_from_topic('data') if NFPKT not in p]
365
+
366
+ def get_all_tags_pkts(self):
367
+ return [p for p in self.get_all_pkts_from_topic('data')]
368
+
369
+ # MQTT Client callbacks
370
+
371
+ def on_connect(mqttc, userdata, flags, reason_code, properties):
372
+ message = f'MQTT: Connection, RC {reason_code}'
373
+ userdata['logger'].info(message)
374
+ # Properties and Flags
375
+ userdata['logger'].info(flags)
376
+ userdata['logger'].info(properties)
377
+
378
+ def on_disconnect(mqttc, userdata, flags, reason_code, properties):
379
+ if reason_code != 0:
380
+ userdata['logger'].info(f"MQTT: Unexpected disconnection. {reason_code}")
381
+ else:
382
+ userdata['logger'].info('MQTT: Disconnect')
383
+ userdata['logger'].info(flags)
384
+ userdata['logger'].info(properties)
385
+
386
+ def on_subscribe(mqttc, userdata, mid, reason_codes, properties):
387
+ userdata['logger'].info(f"MQTT: Subscribe, MessageID {mid}")
388
+ for sub_result, idx in enumerate(reason_codes):
389
+ userdata['logger'].info(f"[{idx}]: RC {sub_result}")
390
+ userdata['logger'].info(properties)
391
+
392
+ def on_unsubscribe(mqttc, userdata, mid, reason_codes, properties):
393
+ userdata['logger'].info(f"MQTT: Unsubscribe, MessageID {mid}")
394
+ for sub_result, idx in enumerate(reason_codes):
395
+ userdata['logger'].info(f"[{idx}]: RC {sub_result}")
396
+ userdata['logger'].info(properties)
397
+
398
+ def on_message(mqttc, userdata, message):
399
+ # Ignore messages published by MQTT Client
400
+ if message.payload in userdata['published']:
401
+ userdata['logger'].info(f'Received self-published payload - {message.topic}: {message.payload}')
402
+ return
403
+ # Try to parse message as JSON and determine if GW is working in JSON / PB mode
404
+ if userdata['serialization'] == Serialization.UNKNOWN:
405
+ try:
406
+ payload = message.payload.decode("utf-8")
407
+ userdata['logger'].info("Received JSON-Serialized packet - setting serialization to JSON")
408
+ debug_print("##### Received JSON-Serialized packet - setting serialization to JSON #####")
409
+ userdata['serialization'] = Serialization.JSON
410
+ except (json.JSONDecodeError, UnicodeDecodeError):
411
+ userdata['logger'].info("Received non-JSON-Serialized packet - setting serialization to PB")
412
+ debug_print("##### Received non-JSON-Serialized packet - setting serialization to PB #####")
413
+ userdata['serialization'] = Serialization.PB
414
+ if userdata['serialization'] == Serialization.JSON:
415
+ on_message_json(mqttc, userdata, message)
416
+ if userdata['serialization'] == Serialization.PB:
417
+ on_message_protobuf(mqttc, userdata, message)
418
+
419
+ def on_message_json(mqttc, userdata, message):
420
+ payload = message.payload.decode("utf-8")
421
+ data = json.loads(payload)
422
+ userdata['messages'].insert(WltMqttMessage(data, message.topic))
423
+ userdata['logger'].debug(f'{message.topic}: {payload}')
424
+ if(userdata['gw_seen'] is False):
425
+ userdata['gw_seen'] = True
426
+
427
+ def on_message_protobuf(mqttc, userdata, message):
428
+ pb_message = None
429
+ if 'status' in message.topic:
430
+ # Try to decode UplinkMessage
431
+ try:
432
+ pb_message = wltPb_pb2.UplinkMessage()
433
+ pb_message.ParseFromString(message.payload)
434
+ except DecodeError as e:
435
+ userdata['logger'].debug(f'{message.topic}: An exception occured:\n{e}\n(could be a JSON msg or pb msg that is not UplinkMessage)###########')
436
+ userdata['logger'].debug(f'Raw Payload: {message.payload}')
437
+ elif 'data' in message.topic:
438
+ # Try to decode GatewayData
439
+ try:
440
+ pb_message = wltPb_pb2.GatewayData()
441
+ pb_message.ParseFromString(message.payload)
442
+ # # Decode packets payloads as hex
443
+ # for idx, packet in enumerate(pb_message.packets):
444
+ # pb_message.packets[idx].payload = packet.payload.hex().upper().encode('utf-8')
445
+ except DecodeError as e:
446
+ userdata['logger'].debug(f'{message.topic}: An exception occured:\n{e}\n(could be a JSON msg or pb msg that is not UplinkMessage)###########')
447
+ userdata['logger'].debug(f'Raw Payload: {message.payload}')
448
+ else:
449
+ userdata['logger'].debug(f'Message from update topic, not decoding')
450
+ userdata['logger'].debug(f'{message.topic}: {message.payload}')
451
+ if pb_message is not None:
452
+ pb_message_dict = MessageToDict(pb_message)
453
+ # Decode b64 packet payloads to hex
454
+ if 'data' in message.topic and 'packets' in pb_message_dict.keys():
455
+ for idx, packet in enumerate(pb_message_dict['packets']):
456
+ pb_message_dict['packets'][idx]['payload'] = base64.b64decode(packet['payload']).hex().upper()
457
+ userdata['messages'].insert(WltMqttMessage(pb_message_dict, message.topic))
458
+ userdata['logger'].debug(f'{message.topic}: {pb_message.__class__.__name__}')
459
+ userdata['logger'].debug(f'{pb_message_dict}')
460
+ if(userdata['gw_seen'] is False):
461
+ userdata['gw_seen'] = True
462
+
463
+ def on_publish(mqttc, userdata, mid, reason_code, properties):
464
+ userdata['logger'].info(f"MQTT: Publish, MessageID {mid}, RC {reason_code}")
465
+ userdata['logger'].info(properties)
466
+
467
+ def on_log(mqttc, userdata, level, buf):
468
+ if (level < mqtt.MQTT_LOG_DEBUG):
469
+ userdata['logger'].info(f"MQTT: Log level={level}, Msg={buf}")