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,51 @@
|
|
|
1
|
+
bad_crc_percent,per_percent
|
|
2
|
+
2,7
|
|
3
|
+
4,11
|
|
4
|
+
6,16
|
|
5
|
+
8,20
|
|
6
|
+
10,25
|
|
7
|
+
12,29
|
|
8
|
+
14,33
|
|
9
|
+
16,37
|
|
10
|
+
18,41
|
|
11
|
+
20,45
|
|
12
|
+
22,49
|
|
13
|
+
24,52
|
|
14
|
+
26,55
|
|
15
|
+
28,58
|
|
16
|
+
30,61
|
|
17
|
+
32,64
|
|
18
|
+
34,67
|
|
19
|
+
36,69
|
|
20
|
+
38,72
|
|
21
|
+
40,74
|
|
22
|
+
42,76
|
|
23
|
+
44,78
|
|
24
|
+
46,81
|
|
25
|
+
48,83
|
|
26
|
+
50,84
|
|
27
|
+
52,86
|
|
28
|
+
54,87
|
|
29
|
+
56,89
|
|
30
|
+
58,90.5
|
|
31
|
+
60,91.5
|
|
32
|
+
62,92.5
|
|
33
|
+
64,93.5
|
|
34
|
+
66,94.5
|
|
35
|
+
68,95.5
|
|
36
|
+
70,96.5
|
|
37
|
+
72,97
|
|
38
|
+
74,97.5
|
|
39
|
+
76,98
|
|
40
|
+
78,98.5
|
|
41
|
+
80,99
|
|
42
|
+
82,99.25
|
|
43
|
+
84,99.5
|
|
44
|
+
86,99.75
|
|
45
|
+
88,100
|
|
46
|
+
90,100
|
|
47
|
+
92,100
|
|
48
|
+
94,100
|
|
49
|
+
96,100
|
|
50
|
+
98,100
|
|
51
|
+
100,100
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import pkg_resources
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from packaging import version
|
|
7
|
+
|
|
8
|
+
from gw_certificate.tests.static.connection_defines import *
|
|
9
|
+
from gw_certificate.common.debug import debug_print
|
|
10
|
+
from gw_certificate.api_if.gw_capabilities import GWCapabilities
|
|
11
|
+
from gw_certificate.tests.generic import INCONCLUSIVE_MINIMUM, PassCriteria, MINIMUM_SCORE, PERFECT_SCORE, GenericStage, GenericTest, INFORMATIVE
|
|
12
|
+
from gw_certificate.api_if.api_validation import validate_message, MESSAGE_TYPES
|
|
13
|
+
from gw_certificate.interface.mqtt import MqttClient, Serialization
|
|
14
|
+
from gw_certificate.interface.ble_sniffer import BLESniffer, BLESnifferContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConnectionStage(GenericStage):
|
|
18
|
+
def __init__(self, mqttc:MqttClient, **kwargs):
|
|
19
|
+
self.mqttc = mqttc
|
|
20
|
+
self.stage_tooltip = "Awaits the gateway to establish MQTT connection and upload it's configurations via the 'status' topic as it's first message"
|
|
21
|
+
self.__dict__.update(kwargs)
|
|
22
|
+
super().__init__(stage_name=type(self).__name__, **self.__dict__)
|
|
23
|
+
|
|
24
|
+
def run(self):
|
|
25
|
+
super().run()
|
|
26
|
+
self.stage_pass = MINIMUM_SCORE
|
|
27
|
+
input('The GW is expected to publish a configuration JSON/Protobuf message through the status topic upon connecting to mqtt:\n'
|
|
28
|
+
'Please unplug GW from power. Press enter when unplugged')
|
|
29
|
+
self.mqttc.flush_messages()
|
|
30
|
+
input('Please plug GW back to power. Press enter when plugged')
|
|
31
|
+
debug_print('Waiting for GW to connect... (Timeout 3 minutes)')
|
|
32
|
+
timeout = datetime.datetime.now() + datetime.timedelta(minutes=3)
|
|
33
|
+
self.status_message = None
|
|
34
|
+
|
|
35
|
+
while datetime.datetime.now() < timeout and self.status_message is None:
|
|
36
|
+
self.status_message = self.mqttc.get_status_message()
|
|
37
|
+
|
|
38
|
+
if self.status_message is not None:
|
|
39
|
+
ser = self.mqttc.get_serialization()
|
|
40
|
+
debug_print(self.status_message)
|
|
41
|
+
if ser == Serialization.JSON:
|
|
42
|
+
self.validation = validate_message(MESSAGE_TYPES.STATUS, self.status_message)
|
|
43
|
+
self.stage_pass = PERFECT_SCORE if self.validation[0] else MINIMUM_SCORE
|
|
44
|
+
else:
|
|
45
|
+
self.stage_pass = PERFECT_SCORE
|
|
46
|
+
# set GW Capabilities:
|
|
47
|
+
for key, value in self.status_message.items():
|
|
48
|
+
if key in GWCapabilities.get_capabilities() and type(value) is bool:
|
|
49
|
+
self.gw_capabilities.set_capability(key, value)
|
|
50
|
+
|
|
51
|
+
def generate_stage_report(self):
|
|
52
|
+
self.add_report_header()
|
|
53
|
+
|
|
54
|
+
if self.status_message is not None:
|
|
55
|
+
ser = self.mqttc.get_serialization()
|
|
56
|
+
debug_print(f'{ser.value} serialization detected')
|
|
57
|
+
self.add_to_stage_report(f'{ser.value} serialization detected')
|
|
58
|
+
self.add_to_stage_report('GW Status packet received:')
|
|
59
|
+
self.add_to_stage_report(f'{json.dumps(self.status_message)}\n')
|
|
60
|
+
|
|
61
|
+
for key, value in self.status_message.items():
|
|
62
|
+
if key in GWCapabilities.get_capabilities() and type(value) is bool:
|
|
63
|
+
self.add_to_stage_report(f'Capability set: {key} - {value}')
|
|
64
|
+
# Add reason test failed to report if neccessary
|
|
65
|
+
if self.stage_pass == MINIMUM_SCORE:
|
|
66
|
+
self.error_summary = "API (JSON structure) is invalid. "
|
|
67
|
+
self.add_to_stage_report(f'\n{len(self.validation[1])} validation errors:')
|
|
68
|
+
for error in self.validation[1]:
|
|
69
|
+
self.add_to_stage_report(error.message)
|
|
70
|
+
else:
|
|
71
|
+
self.error_summary = "No message recieved from GW in status topic after 3 mins."
|
|
72
|
+
self.add_to_stage_report(self.error_summary)
|
|
73
|
+
|
|
74
|
+
self.report_html = self.template_engine.render_template('stage.html', stage=self,
|
|
75
|
+
stage_report=self.report.split('\n'))
|
|
76
|
+
debug_print(self.report)
|
|
77
|
+
return super().generate_stage_report()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InterferenceAnalysisStage(GenericStage):
|
|
81
|
+
def __init__(self, sniffer:BLESniffer, **kwargs):
|
|
82
|
+
self.sniffer = sniffer
|
|
83
|
+
self.conversion_table_df = None
|
|
84
|
+
self.stage_tooltip = "Analyze BLE interference level (Bad CRC %)"
|
|
85
|
+
self.__dict__.update(kwargs)
|
|
86
|
+
|
|
87
|
+
# Stage shows warning if CER is >=50%
|
|
88
|
+
self.result_indication = INFORMATIVE
|
|
89
|
+
self.pass_min = 51
|
|
90
|
+
|
|
91
|
+
super().__init__(stage_name=type(self).__name__, **self.__dict__)
|
|
92
|
+
|
|
93
|
+
def get_data_from_quantization_csv(self):
|
|
94
|
+
relative_path = CSV_NAME
|
|
95
|
+
csv_path = pkg_resources.resource_filename(__name__, relative_path)
|
|
96
|
+
conversion_table_df = pd.read_csv(csv_path)
|
|
97
|
+
self.conversion_table_df = conversion_table_df
|
|
98
|
+
|
|
99
|
+
def interference_analysis(self):
|
|
100
|
+
"""Analyze the interference level (PER) before the test begins"""
|
|
101
|
+
self.report_buffer = []
|
|
102
|
+
|
|
103
|
+
def handle_wrap_around(a):
|
|
104
|
+
"handle a wrap around of the counter"
|
|
105
|
+
if a < 0:
|
|
106
|
+
a = a + MAX_UNSIGNED_32_BIT
|
|
107
|
+
return a
|
|
108
|
+
|
|
109
|
+
for channel in CHANNELS_TO_ANALYZE:
|
|
110
|
+
# Send the sniffer a command to retrive the counters and convert them to dict
|
|
111
|
+
start_cntrs = self.sniffer.get_pkts_cntrs(channel[0])
|
|
112
|
+
debug_print(f'Analyzing channel {channel[0]}... (30 seconds)')
|
|
113
|
+
time.sleep(CNTRS_LISTEN_TIME_SEC)
|
|
114
|
+
end_cntrs = self.sniffer.get_pkts_cntrs(channel[0])
|
|
115
|
+
|
|
116
|
+
if start_cntrs == None or end_cntrs == None:
|
|
117
|
+
debug_print(f'Channel {channel[0]} ({channel[1]} MHz) interference analysis was skipped beacause at least one counter is missing.')
|
|
118
|
+
self.report_buffer.append(f'Channel {channel[0]} ({channel[1]} MHz) Ambient Interference was not calculated, missing at least one counter.')
|
|
119
|
+
self.stage_pass = INCONCLUSIVE_MINIMUM
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Calculate the bad CRC percentage
|
|
123
|
+
diff_dict = dict()
|
|
124
|
+
for key in CNTRS_KEYS:
|
|
125
|
+
diff_dict[key] = handle_wrap_around(end_cntrs[key] - start_cntrs[key])
|
|
126
|
+
if (diff_dict[WLT_RX] + diff_dict[NON_WLT_RX]) > 0:
|
|
127
|
+
bad_crc_percentage = round((diff_dict[BAD_CRC] / (diff_dict[WLT_RX] + diff_dict[NON_WLT_RX])) * 100)
|
|
128
|
+
else:
|
|
129
|
+
bad_crc_percentage = 0
|
|
130
|
+
self.report_buffer.append(f'Channel {channel[0]} ({channel[1]} MHz) Ambient Interference (bad CRC percentage) is: {bad_crc_percentage}%.')
|
|
131
|
+
self.report_buffer.append(f'Good CRC packets = {diff_dict[NON_WLT_RX] + diff_dict[WLT_RX] - diff_dict[BAD_CRC]}, bad CRC packets: {diff_dict[BAD_CRC]}')
|
|
132
|
+
|
|
133
|
+
good_crc_percentage = 100 - bad_crc_percentage
|
|
134
|
+
if (self.stage_pass == MINIMUM_SCORE) or (good_crc_percentage < self.stage_pass):
|
|
135
|
+
self.stage_pass = good_crc_percentage
|
|
136
|
+
if self.stage_pass < self.pass_min:
|
|
137
|
+
self.error_summary = "High bad CRC rate within the current environment."
|
|
138
|
+
|
|
139
|
+
# Uncomment if you want to see PER of the site (will require print adjustments). Below, we use the truth table from the csv to match PER the bad CRC percentage. Require an update of the CSV to the bridge-GW case
|
|
140
|
+
# closest_index = (self.conversion_table_df['bad_crc_percent'] - bad_crc_percentage).abs().idxmin()
|
|
141
|
+
# per_percent = self.conversion_table_df.iloc[closest_index]['per_percent']
|
|
142
|
+
# self.add_to_stage_report(f'Channel {channel} PER is: {per_percent}%')
|
|
143
|
+
|
|
144
|
+
def run(self):
|
|
145
|
+
super().run()
|
|
146
|
+
# Run interference analysis
|
|
147
|
+
# Note: there is an infrastructure for converting bad_CRC % to PER, currently unused and commented since the quantization_csv does not match the bridge to GW case.
|
|
148
|
+
debug_print(f"Starting interference analysis for channels {[ch[0] for ch in CHANNELS_TO_ANALYZE]}. This will take {30 * len(CHANNELS_TO_ANALYZE)} seconds (total)")
|
|
149
|
+
# self.get_data_from_quantization_csv()
|
|
150
|
+
self.interference_analysis()
|
|
151
|
+
|
|
152
|
+
def generate_stage_report(self):
|
|
153
|
+
self.add_report_header()
|
|
154
|
+
for line in self.report_buffer:
|
|
155
|
+
self.add_to_stage_report(line)
|
|
156
|
+
|
|
157
|
+
self.report_html = self.template_engine.render_template('stage.html', stage=self,
|
|
158
|
+
stage_report=self.report.split('\n'))
|
|
159
|
+
debug_print(self.report)
|
|
160
|
+
return super().generate_stage_report()
|
|
161
|
+
|
|
162
|
+
STAGES = [ConnectionStage]
|
|
163
|
+
|
|
164
|
+
class ConnectionTest(GenericTest):
|
|
165
|
+
def __init__(self, **kwargs):
|
|
166
|
+
self.test_tooltip = "Stages related to cloud connectivity and environment"
|
|
167
|
+
self.__dict__.update(kwargs)
|
|
168
|
+
super().__init__(**self.__dict__, test_name=type(self).__name__)
|
|
169
|
+
stages = STAGES
|
|
170
|
+
if version.parse(self.uart.get_version()) >= version.parse(INTERFERENCE_ANALYSIS_FW_VER):
|
|
171
|
+
stages.append(InterferenceAnalysisStage)
|
|
172
|
+
self.stages = [stage(**self.__dict__) for stage in stages]
|
|
173
|
+
|
|
174
|
+
def run(self):
|
|
175
|
+
super().run()
|
|
176
|
+
self.test_pass = PERFECT_SCORE
|
|
177
|
+
for stage in self.stages:
|
|
178
|
+
stage.prepare_stage()
|
|
179
|
+
stage.run()
|
|
180
|
+
self.test_pass = PassCriteria.calc_for_test(self.test_pass, stage)
|
|
181
|
+
self.add_to_test_report(stage.generate_stage_report())
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import numpy as np
|
|
8
|
+
import plotly.express as px
|
|
9
|
+
from google.protobuf.json_format import MessageToDict
|
|
10
|
+
|
|
11
|
+
from gw_certificate.common import wltPb_pb2
|
|
12
|
+
from gw_certificate.common.debug import debug_print
|
|
13
|
+
from gw_certificate.interface.ble_sniffer import BLESniffer, BLESnifferContext
|
|
14
|
+
from gw_certificate.interface.if_defines import RX_CHANNELS
|
|
15
|
+
from gw_certificate.tests.static.downlink_defines import *
|
|
16
|
+
from gw_certificate.interface.mqtt import MqttClient
|
|
17
|
+
from gw_certificate.interface.pkt_generator import BrgPktGenerator
|
|
18
|
+
from gw_certificate.tests.generic import PassCriteria, PERFECT_SCORE, MINIMUM_SCORE, INCONCLUSIVE_MINIMUM, GenericTest, GenericStage
|
|
19
|
+
|
|
20
|
+
class GenericDownlinkStage(GenericStage):
|
|
21
|
+
def __init__(self, sniffer:BLESniffer, mqttc:MqttClient, pkt_gen:BrgPktGenerator, stage_name, **kwargs):
|
|
22
|
+
self.__dict__.update(kwargs)
|
|
23
|
+
super().__init__(stage_name=stage_name, **self.__dict__)
|
|
24
|
+
|
|
25
|
+
#Clients
|
|
26
|
+
self.sniffer = sniffer
|
|
27
|
+
self.mqttc = mqttc
|
|
28
|
+
self.pkt_gen = pkt_gen
|
|
29
|
+
|
|
30
|
+
#Stage Params
|
|
31
|
+
self.sent_pkts = []
|
|
32
|
+
self.sniffed_pkts = pd.DataFrame()
|
|
33
|
+
|
|
34
|
+
#Paths
|
|
35
|
+
self.sent_csv_path = os.path.join(self.test_dir, f'{self.stage_name}_sent.csv')
|
|
36
|
+
self.sniffed_csv_path = os.path.join(self.test_dir, f'{self.stage_name}_sniffed.csv')
|
|
37
|
+
self.graph_html_path = os.path.join(self.test_dir, f'{self.stage_name}.html')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def prepare_stage(self):
|
|
41
|
+
super().prepare_stage()
|
|
42
|
+
self.sniffer.flush_pkts()
|
|
43
|
+
|
|
44
|
+
def send_adv_payloads(self, tx_max_durations=TX_MAX_DURATIONS, retries=RETRIES):
|
|
45
|
+
sent_pkts = []
|
|
46
|
+
for tx_max_duration in tx_max_durations:
|
|
47
|
+
debug_print(f'Tx Max Duration {tx_max_duration}')
|
|
48
|
+
for retry in retries:
|
|
49
|
+
self.pkt_gen.increment_all()
|
|
50
|
+
brg_hb = self.pkt_gen.get_brg_hb()
|
|
51
|
+
tx_max_retries = tx_max_duration//100
|
|
52
|
+
sent_payload = self.mqttc.advertise_packet(raw_packet=brg_hb, tx_max_duration=tx_max_duration, use_retries=self.use_retries)
|
|
53
|
+
if isinstance(sent_payload, wltPb_pb2.DownlinkMessage):
|
|
54
|
+
sent_payload = MessageToDict(sent_payload)
|
|
55
|
+
# Decode b64-encoded bytes
|
|
56
|
+
sent_payload['txPacket']['payload'] = base64.b64decode(sent_payload['txPacket']['payload']).hex().upper()
|
|
57
|
+
else:
|
|
58
|
+
sent_payload = json.dumps(sent_payload)
|
|
59
|
+
debug_print(f'{sent_payload} sent to GW')
|
|
60
|
+
sent_pkts.append({'tx_max_duration': tx_max_duration, 'tx_max_retries': tx_max_retries,
|
|
61
|
+
'retry': retry, 'pkt': brg_hb[12:], 'payload': sent_payload, 'time_sent': datetime.datetime.now()})
|
|
62
|
+
time.sleep(max(MAX_RX_TX_PERIOD_SECS, (tx_max_duration/1000)*1.2))
|
|
63
|
+
time.sleep(10)
|
|
64
|
+
return sent_pkts
|
|
65
|
+
|
|
66
|
+
def process_sniffed_pkts(self, sent_pkts, sniffer:BLESniffer):
|
|
67
|
+
for pkt in sent_pkts:
|
|
68
|
+
# Get vars from dict
|
|
69
|
+
raw_packet = pkt['pkt']
|
|
70
|
+
# Get packets from sniffer
|
|
71
|
+
sniffed_pkts = sniffer.get_filtered_packets(raw_packet=raw_packet)
|
|
72
|
+
pkt['channel'] = str(sniffer.rx_channel)
|
|
73
|
+
pkt['num_pkts_received'] = len(sniffed_pkts)
|
|
74
|
+
self.sniffed_pkts = pd.concat([self.sniffed_pkts, sniffed_pkts.to_pandas()])
|
|
75
|
+
self.sent_pkts += sent_pkts
|
|
76
|
+
|
|
77
|
+
def generate_stage_report(self):
|
|
78
|
+
# Create graph and trendline
|
|
79
|
+
self.add_report_header()
|
|
80
|
+
self.sent_pkts = pd.DataFrame(self.sent_pkts)
|
|
81
|
+
x_value = ('tx_max_duration', 'TX Max Duration') if not self.use_retries else ('tx_max_retries', 'TX Max Retries')
|
|
82
|
+
fig = px.scatter(self.sent_pkts, x=x_value[0], y='num_pkts_received', color='channel', title=f'Packets Received by Sniffer / {x_value[1]}',
|
|
83
|
+
trendline='ols', labels={x_value[0]: x_value[1], 'num_pkts_received': 'Number of packets received', 'channel': "BLE Adv. Channel"})
|
|
84
|
+
fig.update_layout(scattermode="group", scattergap=0.95)
|
|
85
|
+
trendline_info = px.get_trendline_results(fig)
|
|
86
|
+
# Calculate whether stage pass/failed
|
|
87
|
+
self.stage_pass = PERFECT_SCORE
|
|
88
|
+
for channel, channel_df in trendline_info.groupby('BLE Adv. Channel'):
|
|
89
|
+
channel_pass = PERFECT_SCORE
|
|
90
|
+
channel_err_summary = ''
|
|
91
|
+
channel_pkts = self.sent_pkts[self.sent_pkts['channel'] == channel]
|
|
92
|
+
channel_trendline = channel_df['px_fit_results'].iloc[0]
|
|
93
|
+
slope = channel_trendline.params[1]
|
|
94
|
+
rsquared = channel_trendline.rsquared
|
|
95
|
+
# Determine Channel Pass
|
|
96
|
+
channel_pass, channel_err_summary = PassCriteria.calc_for_stage_downlink(rsquared, slope, self.stage_name)
|
|
97
|
+
if channel_pass < self.stage_pass:
|
|
98
|
+
self.stage_pass = channel_pass
|
|
99
|
+
self.error_summary = channel_err_summary
|
|
100
|
+
self.add_to_stage_report(f"Channel {channel}: {PassCriteria.to_string(channel_pass)}")
|
|
101
|
+
self.add_to_stage_report(f"- Total {len(channel_pkts['payload'])} MQTT payloads sent")
|
|
102
|
+
self.add_to_stage_report(f"- Total {sum(channel_pkts['num_pkts_received'])} BLE Packets received by sniffer")
|
|
103
|
+
self.add_to_stage_report(f"- R Value: {rsquared} | Slope: {slope}")
|
|
104
|
+
# Export all stage data
|
|
105
|
+
self.sent_pkts.to_csv(self.sent_csv_path)
|
|
106
|
+
self.add_to_stage_report(f'Sent data saved - {self.sent_csv_path}')
|
|
107
|
+
self.sniffed_pkts.to_csv(self.sniffed_csv_path)
|
|
108
|
+
self.add_to_stage_report(f'Sniffed data saved - {self.sniffed_csv_path}')
|
|
109
|
+
fig.write_html(self.graph_html_path)
|
|
110
|
+
self.add_to_stage_report(f'Graph saved - {self.graph_html_path}')
|
|
111
|
+
debug_print(self.report)
|
|
112
|
+
|
|
113
|
+
# Generate HTML
|
|
114
|
+
graph_div = fig.to_html(full_html=False, include_plotlyjs='cdn')
|
|
115
|
+
self.report_html = self.template_engine.render_template('stage.html', stage=self,
|
|
116
|
+
stage_report=self.report.split('\n'), graph = graph_div)
|
|
117
|
+
return self.report
|
|
118
|
+
|
|
119
|
+
class SanityStage(GenericDownlinkStage):
|
|
120
|
+
def __init__(self, **kwargs):
|
|
121
|
+
self.__dict__.update(kwargs)
|
|
122
|
+
self.stage_tooltip = ("Verifies that the gateway advertises requested packets. "
|
|
123
|
+
"These are requested via Advertisement actions published to the 'update' topic (MQTT)")
|
|
124
|
+
super().__init__(**self.__dict__, stage_name=type(self).__name__)
|
|
125
|
+
|
|
126
|
+
def run(self):
|
|
127
|
+
super().run()
|
|
128
|
+
for channel in RX_CHANNELS:
|
|
129
|
+
debug_print(f'RX Channel {channel}')
|
|
130
|
+
with BLESnifferContext(self.sniffer, channel) as sniffer:
|
|
131
|
+
# Send the packets
|
|
132
|
+
sent_pkts = self.send_adv_payloads(STAGE_CONFIGS[SANITY_STAGE][0], STAGE_CONFIGS[SANITY_STAGE][1])
|
|
133
|
+
# Process sniffed packets
|
|
134
|
+
self.process_sniffed_pkts(sent_pkts, sniffer)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
class CorrelationStage(GenericDownlinkStage):
|
|
138
|
+
def __init__(self, **kwargs):
|
|
139
|
+
self.__dict__.update(kwargs)
|
|
140
|
+
self.stage_tooltip = ("Checks how consistently the gateway advertises packets. "
|
|
141
|
+
"Expects a consistently increasing packets count with increasing 'txMaxDuration'")
|
|
142
|
+
super().__init__(**self.__dict__, stage_name=type(self).__name__)
|
|
143
|
+
|
|
144
|
+
def run(self):
|
|
145
|
+
super().run()
|
|
146
|
+
for channel in RX_CHANNELS:
|
|
147
|
+
debug_print(f'RX Channel {channel}')
|
|
148
|
+
with BLESnifferContext(self.sniffer, channel) as sniffer:
|
|
149
|
+
# Send the packets
|
|
150
|
+
sent_pkts = self.send_adv_payloads(STAGE_CONFIGS[CORRELATION_STAGE][0], STAGE_CONFIGS[CORRELATION_STAGE][1])
|
|
151
|
+
# Process sniffed packets
|
|
152
|
+
self.process_sniffed_pkts(sent_pkts, sniffer)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
STAGES = [SanityStage, CorrelationStage]
|
|
156
|
+
|
|
157
|
+
class DownlinkTest(GenericTest):
|
|
158
|
+
def __init__(self, **kwargs):
|
|
159
|
+
self.test_tooltip = "Stages examining gateway advertisements by issuing advertisement actions (txPacket)"
|
|
160
|
+
self.__dict__.update(kwargs)
|
|
161
|
+
super().__init__(**self.__dict__, test_name=type(self).__name__)
|
|
162
|
+
self.pkt_gen = BrgPktGenerator()
|
|
163
|
+
self.pkt_gen.set_bridge_id(DEFAULT_BRG_ID)
|
|
164
|
+
self.use_retries = self.topic_suffix == '-test'
|
|
165
|
+
self.stages = [stage(**self.__dict__) for stage in STAGES]
|
|
166
|
+
|
|
167
|
+
def run(self):
|
|
168
|
+
super().run()
|
|
169
|
+
self.test_pass = PERFECT_SCORE
|
|
170
|
+
for stage in self.stages:
|
|
171
|
+
stage.prepare_stage()
|
|
172
|
+
stage.run()
|
|
173
|
+
self.add_to_test_report(stage.generate_stage_report())
|
|
174
|
+
self.test_pass = PassCriteria.calc_for_test(self.test_pass, stage)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from gw_certificate.api.extended_api import ExtendedEdgeClient
|
|
5
|
+
from gw_certificate.common.debug import debug_print
|
|
6
|
+
from gw_certificate.api_if.gw_capabilities import GWCapabilities
|
|
7
|
+
from gw_certificate.interface.ble_simulator import BLESimulator
|
|
8
|
+
from gw_certificate.interface.if_defines import SEP
|
|
9
|
+
from gw_certificate.interface.mqtt import MqttClient
|
|
10
|
+
|
|
11
|
+
PASS_STATUS = {True: 'PASS', False: 'FAIL'}
|
|
12
|
+
|
|
13
|
+
# Score values for pass/inconclusive/fail
|
|
14
|
+
PERFECT_SCORE = 100
|
|
15
|
+
PASS_MINIMUM = 80
|
|
16
|
+
INCONCLUSIVE_MINIMUM = 70
|
|
17
|
+
INIT_INCONCLUSIVE_MINIMUM = 40
|
|
18
|
+
MINIMUM_SCORE = 0
|
|
19
|
+
|
|
20
|
+
# Results indications for stages. Must always be synced with frontend.
|
|
21
|
+
# 'score' shows as pass/inconclusive/fail. 'info' shows as info/warning.
|
|
22
|
+
SCORE_BASED = 'score'
|
|
23
|
+
INFORMATIVE = 'info'
|
|
24
|
+
OPTIONAL = 'optional'
|
|
25
|
+
|
|
26
|
+
class PassCriteria():
|
|
27
|
+
def __init__(self):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def to_string(pass_value:int) -> str:
|
|
32
|
+
if pass_value >= PASS_MINIMUM:
|
|
33
|
+
return 'Pass'
|
|
34
|
+
elif pass_value >= INCONCLUSIVE_MINIMUM:
|
|
35
|
+
return 'Inconclusive'
|
|
36
|
+
else:
|
|
37
|
+
return 'Fail'
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def missing_score(pass_value:int) -> int:
|
|
41
|
+
return PERFECT_SCORE - pass_value
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def calc_for_stage_uplink(pass_value:int, stage_name:str) -> int:
|
|
45
|
+
error_msg = "Insufficient amount of packets were scanned & uploaded by the gateway"
|
|
46
|
+
return pass_value, error_msg
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def calc_for_stage_stress(pass_value: int, stage_name:str) -> int:
|
|
50
|
+
return pass_value
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def calc_for_stage_downlink(rsquared, slope, stage_name:str):
|
|
54
|
+
error_msg = ''
|
|
55
|
+
if 'Sanity' in stage_name:
|
|
56
|
+
if rsquared > 0:
|
|
57
|
+
return PERFECT_SCORE, error_msg
|
|
58
|
+
else:
|
|
59
|
+
error_msg = 'No advertisements were received from the gateway.'
|
|
60
|
+
return MINIMUM_SCORE, error_msg
|
|
61
|
+
else:
|
|
62
|
+
if rsquared > 0.8 and slope > 0:
|
|
63
|
+
return PERFECT_SCORE, error_msg
|
|
64
|
+
elif rsquared > 0.5 and slope > 0:
|
|
65
|
+
error_msg = "The correlation between 'txMaxDuration' and the board advertisements is suboptimal."
|
|
66
|
+
return INCONCLUSIVE_MINIMUM, error_msg
|
|
67
|
+
else:
|
|
68
|
+
error_msg = "The correlation between 'txMaxDuration' and the board advertisements is weak."
|
|
69
|
+
return MINIMUM_SCORE, error_msg
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def calc_for_test(test_pass_value:int, stage) -> int:
|
|
73
|
+
if stage.stage_pass < test_pass_value:
|
|
74
|
+
if 'Geolocation' in stage.stage_name or 'info' in stage.result_indication:
|
|
75
|
+
return test_pass_value
|
|
76
|
+
else:
|
|
77
|
+
return stage.stage_pass
|
|
78
|
+
else:
|
|
79
|
+
return test_pass_value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GenericTest:
|
|
83
|
+
def __init__(self, mqttc: MqttClient,
|
|
84
|
+
gw_capabilities:GWCapabilities, gw_id, owner_id, test_name, ble_sim: BLESimulator = None, **kwargs):
|
|
85
|
+
# Clients
|
|
86
|
+
self.mqttc = mqttc
|
|
87
|
+
self.ble_sim = ble_sim
|
|
88
|
+
|
|
89
|
+
# Test-Related
|
|
90
|
+
self.gw_capabilities = gw_capabilities
|
|
91
|
+
self.report = ''
|
|
92
|
+
self.report_html = ''
|
|
93
|
+
self.test_pass = MINIMUM_SCORE
|
|
94
|
+
self.pass_min = PASS_MINIMUM
|
|
95
|
+
self.inconclusive_min = INCONCLUSIVE_MINIMUM
|
|
96
|
+
self.start_time = None
|
|
97
|
+
self.test_name = test_name
|
|
98
|
+
self.test_dir = os.path.join(self.certificate_dir, self.test_name)
|
|
99
|
+
self.env_dirs.create_dir(self.test_dir)
|
|
100
|
+
self.stages = []
|
|
101
|
+
self.test_tooltip = kwargs.get('test_tooltip', 'Missing tooltip')
|
|
102
|
+
self.result_indication = kwargs.get('result_indication', SCORE_BASED)
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
return self.test_name
|
|
106
|
+
|
|
107
|
+
def run(self):
|
|
108
|
+
self.start_time = datetime.datetime.now()
|
|
109
|
+
debug_print(f"Starting Test {self.test_name} : {self.start_time}")
|
|
110
|
+
|
|
111
|
+
def runtime(self):
|
|
112
|
+
return datetime.datetime.now() - self.start_time
|
|
113
|
+
|
|
114
|
+
def add_to_test_report(self, report):
|
|
115
|
+
self.report += '\n' + report
|
|
116
|
+
|
|
117
|
+
def create_test_html(self):
|
|
118
|
+
self.report_html = self.template_engine.render_template('test.html', test=self,
|
|
119
|
+
running_time = self.runtime())
|
|
120
|
+
|
|
121
|
+
def end_test(self):
|
|
122
|
+
self.create_test_html()
|
|
123
|
+
|
|
124
|
+
class GenericStage():
|
|
125
|
+
def __init__(self, stage_name, **kwargs):
|
|
126
|
+
#Stage Params
|
|
127
|
+
self.stage_name = stage_name
|
|
128
|
+
self.result_indication = kwargs.get('result_indication', SCORE_BASED)
|
|
129
|
+
self.stage_pass = MINIMUM_SCORE
|
|
130
|
+
self.pass_min = kwargs.get('pass_min', PASS_MINIMUM)
|
|
131
|
+
self.inconclusive_min = INCONCLUSIVE_MINIMUM
|
|
132
|
+
self.report = ''
|
|
133
|
+
self.report_html = ''
|
|
134
|
+
self.start_time = None
|
|
135
|
+
self.csv_path = os.path.join(self.test_dir, f'{self.stage_name}.csv')
|
|
136
|
+
self.stage_tooltip = kwargs.get('stage_tooltip', 'Missing tooltip')
|
|
137
|
+
self.error_summary = kwargs.get('error_summary', 'View the stage report for more info')
|
|
138
|
+
|
|
139
|
+
def __repr__(self):
|
|
140
|
+
return self.stage_name
|
|
141
|
+
|
|
142
|
+
def prepare_stage(self):
|
|
143
|
+
debug_print(f'### Starting Stage: {self.stage_name}')
|
|
144
|
+
|
|
145
|
+
def run(self):
|
|
146
|
+
self.start_time = datetime.datetime.now()
|
|
147
|
+
|
|
148
|
+
def add_to_stage_report(self, report):
|
|
149
|
+
self.report += f'{report}\n'
|
|
150
|
+
|
|
151
|
+
def generate_stage_report(self):
|
|
152
|
+
return self.report
|
|
153
|
+
|
|
154
|
+
def add_report_line_separator(self):
|
|
155
|
+
self.add_to_stage_report('-' * 50)
|
|
156
|
+
|
|
157
|
+
def add_report_header(self):
|
|
158
|
+
uncapitalize = lambda s: s[:1].lower() + s[1:] if s else ''
|
|
159
|
+
self.add_to_stage_report(f'Stage run time: {datetime.datetime.now() - self.start_time}')
|
|
160
|
+
self.add_to_stage_report(f'This stage {uncapitalize(self.stage_tooltip)}.')
|
|
161
|
+
self.add_report_line_separator()
|