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.
- gw_certificate/__init__.py +0 -0
- gw_certificate/ag/ut_defines.py +361 -0
- gw_certificate/ag/wlt_types.py +85 -0
- gw_certificate/ag/wlt_types_ag.py +5310 -0
- gw_certificate/ag/wlt_types_data.py +64 -0
- gw_certificate/api/extended_api.py +1547 -0
- gw_certificate/api_if/__init__.py +0 -0
- gw_certificate/api_if/api_validation.py +40 -0
- gw_certificate/api_if/gw_capabilities.py +18 -0
- gw_certificate/common/analysis_data_bricks.py +1455 -0
- gw_certificate/common/debug.py +63 -0
- gw_certificate/common/utils.py +219 -0
- gw_certificate/common/utils_defines.py +102 -0
- gw_certificate/common/wltPb_pb2.py +72 -0
- gw_certificate/common/wltPb_pb2.pyi +227 -0
- gw_certificate/gw_certificate.py +138 -0
- gw_certificate/gw_certificate_cli.py +70 -0
- gw_certificate/interface/ble_simulator.py +91 -0
- gw_certificate/interface/ble_sniffer.py +189 -0
- gw_certificate/interface/if_defines.py +35 -0
- gw_certificate/interface/mqtt.py +469 -0
- gw_certificate/interface/packet_error.py +22 -0
- gw_certificate/interface/pkt_generator.py +720 -0
- gw_certificate/interface/uart_if.py +193 -0
- gw_certificate/interface/uart_ports.py +20 -0
- gw_certificate/templates/results.html +241 -0
- gw_certificate/templates/stage.html +22 -0
- gw_certificate/templates/table.html +6 -0
- gw_certificate/templates/test.html +38 -0
- gw_certificate/tests/__init__.py +11 -0
- gw_certificate/tests/actions.py +131 -0
- gw_certificate/tests/bad_crc_to_PER_quantization.csv +51 -0
- gw_certificate/tests/connection.py +181 -0
- gw_certificate/tests/downlink.py +174 -0
- gw_certificate/tests/generic.py +161 -0
- gw_certificate/tests/registration.py +288 -0
- gw_certificate/tests/static/__init__.py +0 -0
- gw_certificate/tests/static/connection_defines.py +9 -0
- gw_certificate/tests/static/downlink_defines.py +9 -0
- gw_certificate/tests/static/generated_packet_table.py +209 -0
- gw_certificate/tests/static/packet_table.csv +10051 -0
- gw_certificate/tests/static/references.py +4 -0
- gw_certificate/tests/static/uplink_defines.py +20 -0
- gw_certificate/tests/throughput.py +244 -0
- gw_certificate/tests/uplink.py +683 -0
- wiliot_certificate-1.3.0a1.dist-info/LICENSE +21 -0
- wiliot_certificate-1.3.0a1.dist-info/METADATA +113 -0
- wiliot_certificate-1.3.0a1.dist-info/RECORD +51 -0
- wiliot_certificate-1.3.0a1.dist-info/WHEEL +5 -0
- wiliot_certificate-1.3.0a1.dist-info/entry_points.txt +2 -0
- 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}")
|