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