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,193 @@
1
+ import datetime
2
+ import subprocess
3
+ import serial
4
+ import serial.tools.list_ports
5
+ import time
6
+ import os.path
7
+ import platform
8
+ from packaging import version
9
+ import pkg_resources
10
+
11
+ from gw_certificate.common.debug import debug_print
12
+ from gw_certificate.interface.if_defines import *
13
+
14
+ LATEST_VERSION = '4.4.15'
15
+ LATEST_VERSION_FILE = f'{LATEST_VERSION}_app.zip'
16
+ LATEST_VERSION_PATH = pkg_resources.resource_filename(__name__, LATEST_VERSION_FILE)
17
+
18
+ LATEST_VERSION_SNIFFING_KIT = '4.1.19'
19
+ LATEST_VERSION_FILE_SNIFFING_KIT = f'{LATEST_VERSION_SNIFFING_KIT}_app.zip'
20
+ LATEST_VERSION_PATH_SNIFFING_KIT = pkg_resources.resource_filename(__name__, LATEST_VERSION_FILE_SNIFFING_KIT)
21
+
22
+ class UARTError(Exception):
23
+ pass
24
+
25
+ class UARTInterface:
26
+ def __init__(self, comport, update_fw=True, sniffing_kit_flag=False):
27
+ self.comport = comport
28
+ self.serial = serial.Serial(port=comport, baudrate=921600, timeout=SERIAL_TIMEOUT)
29
+ self.serial.flushInput()
30
+ self.gw_app_rx = None
31
+ self.sniffing_kit_flag = sniffing_kit_flag
32
+ version_supported = self.check_fw_supported()
33
+ if not version_supported and update_fw:
34
+ update_status = self.update_firmware()
35
+ if not update_status:
36
+ raise UARTError('Update Failed! Update FW manually using NRF Tools')
37
+ if self.fw_version >= version.Version('3.17.0'):
38
+ self.write_ble_command(GATEWAY_APP)
39
+ self.flush()
40
+ debug_print(f'Serial Connection {comport} Initialized')
41
+
42
+ @staticmethod
43
+ def get_comports():
44
+ ports = serial.tools.list_ports.comports()
45
+ debug_print(SEP + "\nAvailable ports:")
46
+ for port, desc, hwid in sorted(ports):
47
+ debug_print("{}: {} [{}]".format(port, desc, hwid))
48
+ debug_print(SEP + "\n")
49
+ return ports
50
+
51
+ def read_line(self):
52
+ # This reads a line from the ble device (from the serial connection using ble_ser),
53
+ # strips it from white spaces and then decodes to string from bytes using the "utf-8" protocol.
54
+ answer = self.serial.readline().strip().decode("utf-8", "ignore")
55
+ if len(answer) == 0:
56
+ return None
57
+ return answer
58
+
59
+ def write_ble_command(self, cmd, read=False):
60
+ # This function writes a command (cmd) to the ble using a serial connection (ble_ser) that are provided to it beforehand.. and returns the answer from the device as string
61
+ debug_print("Write to BLE: {}".format(cmd))
62
+ # Shows on terminal what command is about to be printed to the BLE device
63
+ bytes_to_write = bytes(cmd.encode("utf-8")) + b'\r\n'
64
+ self.serial.write(bytes_to_write)
65
+ answer = None
66
+ if read:
67
+ # The "bytes" function converts the command from string to bytes by the specified "utf-8" protocol then we use .write to send the byte sequence to the ble device using the serial connection that we have for this port (ble_ser)
68
+ # Pauses the program for execution for 0.01sec. This is done to allow the device to process the command and provide a response before reading the response.
69
+ time.sleep(1)
70
+ answer = self.read_line()
71
+ debug_print(answer)
72
+ return answer
73
+
74
+ def flush(self):
75
+ self.serial.close()
76
+ self.serial.open()
77
+ self.serial.flushInput()
78
+ self.serial.flush()
79
+ self.serial.reset_output_buffer()
80
+
81
+ def reset_gw(self):
82
+ self.flush()
83
+ self.write_ble_command(RESET_GW)
84
+ self.gw_app_rx = None
85
+ time.sleep(3)
86
+ self.write_ble_command(STOP_ADVERTISING)
87
+ time.sleep(3)
88
+
89
+
90
+ def cancel(self):
91
+ self.write_ble_command(CANCEL)
92
+
93
+ def set_rx(self, rx_channel):
94
+ if self.sniffing_kit_flag:
95
+ assert rx_channel in RX_CHANNELS_SNIFFING_KIT
96
+ else:
97
+ assert rx_channel in RX_CHANNELS
98
+ if self.gw_app_rx is None:
99
+ self.reset_gw()
100
+
101
+ if self.fw_version >= version.Version('3.17.0'):
102
+ # from 3.17.0, only full_cfg can be used to configure channels. sending it with:
103
+ # Data coupling(DC) off, wifi(NW) and mqtt(MQ) on.
104
+ rx_ch_to_fw_enums = {37: 2, 38: 3, 39: 4}
105
+ if self.sniffing_kit_flag:
106
+ my_dict = {i: 5 + i for i in range(37)}
107
+ rx_ch_to_fw_enums.update(my_dict)
108
+ cmd = f'!full_cfg DM {rx_ch_to_fw_enums[rx_channel]} DC 0 NW 1 MQ 1 CH {rx_channel}'
109
+ # cmd = '!gateway_app'
110
+ else:
111
+ cmd = f'!gateway_app {rx_channel} 30 0 17'
112
+
113
+ self.write_ble_command(cmd)
114
+ self.gw_app_rx = rx_channel
115
+
116
+ def set_sniffer(self, rx_channel):
117
+ self.set_rx(rx_channel)
118
+ self.flush()
119
+ time.sleep(1)
120
+ if self.fw_version >= version.Version('4.1.0'):
121
+ self.write_ble_command(f'{SET_SNIFFER} {rx_channel}')
122
+ else:
123
+ self.write_ble_command(SET_SNIFFER)
124
+ self.flush()
125
+ time.sleep(1)
126
+
127
+ def cancel_sniffer(self):
128
+ self.write_ble_command(CANCEL_SNIFFER)
129
+ self.flush()
130
+
131
+ def get_version(self):
132
+ self.reset_gw()
133
+ self.flush()
134
+ timeout = datetime.datetime.now() + datetime.timedelta(seconds=15)
135
+ while datetime.datetime.now() < timeout:
136
+ raw_version = self.write_ble_command(VERSION, read=True)
137
+ if raw_version is not None:
138
+ if GW_APP_VERSION_HEADER in raw_version:
139
+ return raw_version.split(' ')[0].split('=')[1]
140
+ return None
141
+
142
+ def check_fw_supported(self):
143
+ current_version = self.get_version()
144
+ if current_version is None:
145
+ raise UARTError("Cannot initialize board! Please try disconnecting and connecting USB cable")
146
+ current_version = version.parse(current_version)
147
+ hex_version = version.parse(os.path.splitext(os.path.basename(LATEST_VERSION_PATH))[0].split('_')[0])
148
+ self.fw_version = current_version
149
+ if current_version >= hex_version:
150
+ debug_print(f'GW Running version {current_version}')
151
+ self.fw_version = current_version
152
+ return True
153
+ return False
154
+
155
+ def update_firmware(self):
156
+ NRFUTIL_MAP = {"Linux": "nrfutil-linux", "Darwin": "nrfutil-mac", "Windows": "nrfutil.exe"}
157
+ nrfutil_file = pkg_resources.resource_filename(__name__, NRFUTIL_MAP[platform.system()])
158
+ # In order to support NRF UART FW update with protobuf > 3.20.0
159
+ os.environ['PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION'] = 'python'
160
+
161
+ self.write_ble_command('!reset', read=True)
162
+ self.write_ble_command('!move_to_bootloader', read=True)
163
+ self.serial.close()
164
+ p = None
165
+ if self.sniffing_kit_flag:
166
+ p = subprocess.Popen(f'{nrfutil_file} dfu serial --package "{LATEST_VERSION_PATH_SNIFFING_KIT}" -p {self.comport} -fc 0 -b 115200 -t 10', shell=True)
167
+ else:
168
+ p = subprocess.Popen(f'{nrfutil_file} dfu serial --package "{LATEST_VERSION_PATH}" -p {self.comport} -fc 0 -b 115200 -t 10', shell=True)
169
+ p.wait()
170
+ return_code = p.returncode
171
+ debug_print('Waiting for device to update...')
172
+ timeout = datetime.datetime.now() + datetime.timedelta(minutes=2)
173
+ current_ver = ''
174
+ time.sleep(15)
175
+ self.serial.open()
176
+ self.flush()
177
+ while GW_APP_VERSION_HEADER not in current_ver and datetime.datetime.now() < timeout:
178
+ current_ver = self.write_ble_command(VERSION, read=True)
179
+ if current_ver is None:
180
+ current_ver = ''
181
+ time.sleep(1)
182
+ if self.sniffing_kit_flag:
183
+ if current_ver.split(' ')[0].split('=')[1] != LATEST_VERSION_SNIFFING_KIT:
184
+ return False
185
+ else:
186
+ if current_ver.split(' ')[0].split('=')[1] != LATEST_VERSION:
187
+ return False
188
+ if return_code == 0:
189
+ self.fw_version = version.parse(current_ver.split(' ')[0].split('=')[1])
190
+ return True
191
+ return False
192
+
193
+ ### TODO: Sequence List
@@ -0,0 +1,20 @@
1
+ import sys
2
+ import serial.tools.list_ports
3
+
4
+ def get_uart_ports(silent=True):
5
+ ports = serial.tools.list_ports.comports()
6
+ uart_ports = []
7
+ if not silent:
8
+ sys.stdout.write('[')
9
+ for port, desc, hwid in sorted(ports):
10
+ if 'USB to UART' not in desc:
11
+ continue
12
+ if not silent:
13
+ sys.stdout.write(f'("{port}", "{desc}", "{hwid}"),')
14
+ uart_ports.append(port)
15
+ if not silent:
16
+ sys.stdout.write(']')
17
+ return uart_ports
18
+
19
+ if __name__ == '__main__':
20
+ get_uart_ports(silent=False)
@@ -0,0 +1,241 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <!-- Required meta tags -->
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>{{ page_title }}</title>
8
+ <link rel="icon" href="https://www.wiliot.com/favicon.ico" type="image/x-icon" />
9
+ <!-- JQuery -->
10
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
11
+ <!-- Bootstrap CSS -->
12
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet">
13
+ <!-- DataTables -->
14
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/jquery.dataTables.css" />
15
+ <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.js"></script>
16
+
17
+ <style>
18
+ #card-body {
19
+ height: auto;
20
+ overflow:auto;
21
+ }
22
+ .pass {
23
+ color: green;
24
+ }
25
+ .inconclusive {
26
+ color: rgb(160, 160, 2);
27
+ }
28
+ .fail {
29
+ color: red;
30
+ }
31
+
32
+ </style>
33
+ <title>{{ title }}</title>
34
+ </head>
35
+ <body>
36
+ <br>
37
+ <div class="text-center">
38
+ <img src="https://www.wiliot.com/src/img/svg/logo.svg" class="img-fluid" alt="">
39
+ </div>
40
+ <br>
41
+ <div class="container">
42
+ <h1> GW Certificate | version {{version}} | {{ gw_id }} | {{ datetime }}</h1>
43
+ <div class="card">
44
+ <div class="card-header">
45
+ Tests Summary
46
+ </div>
47
+ <div class="card-body" id="card-body">
48
+ <div class="test-list">
49
+ {% for test in tests %}
50
+ <div class="test">
51
+ <span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ test.test_tooltip }}">
52
+ {% if test.result_indication == 'info' %}
53
+ <h5>{{ test.test_name }}: {% if test.test_pass >= test.pass_min %}<span class="badge bg-info text-dark">Info</span>{% else %}<span class="badge bg-warning text-dark">Warning</span>{% endif %}</h5>
54
+ {% elif test.result_indication == 'optional' %}
55
+ <h5>{{ test.test_name }}: <span class="badge bg-secondary">Optional</span></h5>
56
+ {% else %}
57
+ <h5>{{ test.test_name }}: {% if test.test_pass >= test.pass_min %}<span class="badge bg-success">Pass</span>{% elif test.test_pass >= test.inconclusive_min %}<span class="badge bg-warning text-dark">Inconclusive</span>{% else %}<span class="badge bg-danger">Fail</span>{% endif %}</h5>
58
+ {% endif %}
59
+ </span>
60
+ <br>
61
+ {% for stage in test.stages %}
62
+ <span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ stage.stage_tooltip }}" >
63
+ {% if stage.result_indication == 'info' %}
64
+ <h6> - {{ stage.stage_name }}: {% if stage.stage_pass >= stage.pass_min %}<span class="badge bg-info text-dark">Info</span>{% else %}<span class="badge bg-warning text-dark">Warning</span>{% endif %} </h6>
65
+ {% else %}
66
+ <h6> - {{ stage.stage_name }}: {% if stage.stage_pass >= stage.pass_min %}<span class="badge bg-success">Pass</span>{% elif stage.stage_pass >= stage.inconclusive_min %}<span class="badge bg-warning text-dark">Inconclusive</span>{% else %}<span class="badge bg-danger">Fail</span>{% endif %} </h6>
67
+ {% endif %}
68
+ </span>
69
+ <br>
70
+ {% endfor %}
71
+ </div>
72
+ {% endfor %}
73
+ </div>
74
+ <i> <sub>Hover over each line for a brief explainer </sub> </i>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <br>
79
+ <div class="container">
80
+ {% for test in tests %}
81
+ <div class="accordion" id="accordion{{ loop.index }}">
82
+ <div class="accordion-item">
83
+ <h2 class="accordion-header" id="heading{{ loop.index }}">
84
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ loop.index }}" aria-expanded="false" aria-controls="collapse{{ loop.index }}">
85
+ {% if test.result_indication == 'info' %}
86
+ {{ test.test_name }}:&nbsp; {% if test.test_pass >= test.pass_min %}<span class="badge bg-info text-dark">Info</span>{% else %}<span class="badge bg-warning text-dark">Warning</span>{% endif %}
87
+ {% elif test.result_indication == 'optional' %}
88
+ {{ test.test_name }}:&nbsp; <span class="badge bg-secondary">Optional</span>
89
+ {% else %}
90
+ {{ test.test_name }}:&nbsp; {% if test.test_pass >= test.pass_min %}<span class="badge bg-success">Pass</span>{% elif test.test_pass >= test.inconclusive_min %}<span class="badge bg-warning text-dark">Inconclusive</span>{% else %}<span class="badge bg-danger">Fail</span>{% endif %}
91
+ {% endif %}
92
+ </button>
93
+ </h2>
94
+ <div id="collapse{{ loop.index }}" class="accordion-collapse collapse" aria-labelledby="heading{{ loop.index }}" data-bs-parent="#accordion{{ loop.index }}">
95
+ <div class="accordion-body">
96
+ {{ test.report_html }}
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ {% endfor %}
102
+ <div class="accordion" id="accordion_log">
103
+ <div class="accordion-item">
104
+ <h2 class="accordion-header" id="heading_log">
105
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse_log" aria-expanded="false" aria-controls="collapse_log">
106
+ GW Certificate Log
107
+ </button>
108
+ </h2>
109
+ <div id="collapse_log" class="accordion-collapse collapse" aria-labelledby="heading_log" data-bs-parent="#accordion_log">
110
+ <div class="accordion-body">
111
+ <div class="container">
112
+ <div class="card">
113
+ <div class="card-header">
114
+ GW Certificate Log
115
+ </div>
116
+ <div class="card-body" id="card-body" style="height: 500px; overflow: auto">
117
+ <samp>
118
+ {% for line in log %}
119
+ {{ line | safe}}<br />
120
+ {% endfor %}
121
+ </samp>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ <br>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ <div class="accordion" id="accordion_mqtt_log">
131
+ <div class="accordion-item">
132
+ <h2 class="accordion-header" id="heading_mqtt_log">
133
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse_mqtt_log" aria-expanded="false" aria-controls="collapse_mqtt_log">
134
+ MQTT Log
135
+ </button>
136
+ </h2>
137
+ <div id="collapse_mqtt_log" class="accordion-collapse collapse" aria-labelledby="heading_mqtt_log" data-bs-parent="#accordion_log">
138
+ <div class="accordion-body">
139
+ <div class="container">
140
+ <div class="card">
141
+ <div class="card-header">
142
+ MQTT Log
143
+ </div>
144
+ <div class="card-body" id="card-body" style="height: 500px; overflow: auto">
145
+ <samp>
146
+ {% for line in mqtt_log %}
147
+ {{ line | safe}}<br />
148
+ {% endfor %}
149
+ </samp>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ <br>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ <div class="accordion" id="accordion_sniffer_log">
159
+ <div class="accordion-item">
160
+ <h2 class="accordion-header" id="heading_sniffer_log">
161
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse_sniffer_log" aria-expanded="false" aria-controls="collapse_sniffer_log">
162
+ Sniffer Log
163
+ </button>
164
+ </h2>
165
+ <div id="collapse_sniffer_log" class="accordion-collapse collapse" aria-labelledby="heading_sniffer_log" data-bs-parent="#accordion_log">
166
+ <div class="accordion-body">
167
+ <div class="container">
168
+ <div class="card">
169
+ <div class="card-header">
170
+ Sniffer Log
171
+ </div>
172
+ <div class="card-body" id="card-body" style="height: 500px; overflow: auto">
173
+ <samp>
174
+ {% for line in sniffer_log %}
175
+ {{ line | safe}}<br />
176
+ {% endfor %}
177
+ </samp>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ <br>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- <br>
188
+ <div class="text-center">
189
+ <br>
190
+ <h6>Certified Partners</h6>
191
+ </div>
192
+ <div id="certifiedPartnersCarousel" class="carousel slide" data-bs-ride="carousel" data-bs-pause="false" data-bs-interval="10000">
193
+ <div class="carousel-inner">
194
+ <div class="carousel-item active">
195
+ <div class="row d-flex flex-wrap align-items-center" style="height: 50px;">
196
+ <img src="https://www.wiliot.com/src/uploads/_partnersListingLogo/Screenshot-2024-07-08-at-9.36.37-AM.png" class="d-block mx-auto" style="width: 120px;" alt="Image missing">
197
+ </div>
198
+ </div>
199
+ <div class="carousel-item">
200
+ <div class="row d-flex flex-wrap align-items-center" style="height: 50px;">
201
+ <img src="https://www.wiliot.com/src/uploads/_partnersListingLogo/rigado.png" class="d-block mx-auto" style="width: 120px;" alt="Image missing">
202
+ </div>
203
+ </div>
204
+ <div class="carousel-item">
205
+ <div class="row d-flex flex-wrap align-items-center" style="height: 50px;">
206
+ <img src="https://www.minew.com/wp-content/uploads/2024/06/mine-logo-black.png" class="d-block mx-auto" style="width: 120px;" alt="Image missing">
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ <br> -->
212
+
213
+ <!-- Accordion listener to adjust table -->
214
+ <script defer type="text/javascript">
215
+ var accordions = document.getElementsByClassName('accordion')
216
+ Array.prototype.forEach.call(accordions, (el) => {
217
+ el.addEventListener('shown.bs.collapse', function () {
218
+ DataTable.tables({ visible: true, api: true }).columns.adjust();
219
+ });
220
+ });
221
+ </script>
222
+
223
+
224
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
225
+ integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
226
+ crossorigin="anonymous"></script>
227
+ <script>
228
+ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
229
+ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
230
+ return new bootstrap.Tooltip(tooltipTriggerEl)
231
+ })
232
+ </script>
233
+ <style>
234
+ .tooltip-inner {
235
+ max-width: 1000px;
236
+ /* width: 570px; */
237
+ font-size:larger;
238
+ }
239
+ </style>
240
+ </body>
241
+ </html>
@@ -0,0 +1,22 @@
1
+ <div class="card">
2
+ <div class="card-header" id="{{ stage.stage_name }}">
3
+ {% if stage.result_indication == 'info' %}
4
+ {{ stage.stage_name }}: {% if stage.stage_pass >= stage.pass_min %}<span class="badge bg-info text-dark">Info</span>{% else %}<span class="badge bg-warning text-dark">Warning</span>{% endif %}
5
+ {% else %}
6
+ {{ stage.stage_name }}: {% if stage.stage_pass >= stage.pass_min %}<span class="badge bg-success">Pass</span>{% elif stage.stage_pass >= stage.inconclusive_min %}<span class="badge bg-warning text-dark">Inconclusive</span>{% else %}<span class="badge bg-danger">Fail</span>{% endif %}
7
+ {% endif %}
8
+
9
+ </div>
10
+ <div class="card-body" id="card-body">
11
+ <samp>
12
+ {% for line in stage_report %}
13
+ {{ line | safe}}<br />
14
+ {% endfor %}
15
+ </samp>
16
+ </div>
17
+ <!-- Insert table here if available-->
18
+ {{ table|safe }}
19
+ <!-- Insert graph here if available-->
20
+ {{ graph|safe }}
21
+ </div>
22
+ <br>
@@ -0,0 +1,6 @@
1
+ {{ dataframe }}
2
+
3
+ {% set let_line = 'let ' + table_id + ' = new DataTable("#' + table_id + '", {scrollY: "300px", scrollX: true});' %}
4
+ <script defer type="text/javascript">
5
+ {{ let_line }}
6
+ </script>
@@ -0,0 +1,38 @@
1
+ <div class="container">
2
+ {% if test.result_indication == 'info' %}
3
+ <h3>{{ test.test_name }}: {% if test.test_pass >= test.pass_min %}<span class="badge bg-info text-dark">Info</span>{% else %}<span class="badge bg-warning text-dark">Warning</span>{% endif %}</h3>
4
+ {% elif test.result_indication == 'optional' %}
5
+ <h3>{{ test.test_name }}: <span class="badge bg-secondary">Optional</span></h3>
6
+ {% else %}
7
+ <h3>{{ test.test_name }}: {% if test.test_pass >= test.pass_min %}<span class="badge bg-success">Pass</span>{% elif test.test_pass >= test.inconclusive_min %}<span class="badge bg-warning text-dark">Inconclusive</span>{% else %}<span class="badge bg-danger">Fail</span>{% endif %}</h3>
8
+ {% endif %}
9
+ <h4>Running Time: {{ running_time }}</h4>
10
+
11
+ {% if test.test_pass < test.pass_min %}
12
+ <h5>Error summary:</h5>
13
+ {% for stage in test.stages %}
14
+ {% if stage.result_indication == 'info' %}
15
+ {% if stage.stage_pass < stage.pass_min %}
16
+ <div class="alert alert-warning" role="alert">
17
+ <a href="#{{ stage.stage_name }}" class="alert-link">{{ stage.stage_name }}</a>: {{ stage.error_summary }}
18
+ </div>
19
+ {% endif %}
20
+ {% else %}
21
+ {% if stage.stage_pass < stage.inconclusive_min %}
22
+ <div class="alert alert-danger" role="alert">
23
+ <a href="#{{ stage.stage_name }}" class="alert-link">{{ stage.stage_name }}</a>: {{ stage.error_summary }}
24
+ </div>
25
+ {% elif stage.stage_pass < stage.pass_min %}
26
+ <div class="alert alert-warning" role="alert">
27
+ <a href="#{{ stage.stage_name }}" class="alert-link">{{ stage.stage_name }}</a>: {{ stage.error_summary }}
28
+ </div>
29
+ {% endif %}
30
+ {% endif %}
31
+ {% endfor %}
32
+ {% endif %}
33
+ <hr>
34
+ {% for stage in test.stages %}
35
+ {{ stage.report_html }}
36
+ {% endfor %}
37
+ </div>
38
+ <br>
@@ -0,0 +1,11 @@
1
+ from gw_certificate.tests.registration import RegistrationTest
2
+ from gw_certificate.tests.connection import ConnectionTest
3
+ from gw_certificate.tests.uplink import UplinkTest
4
+ from gw_certificate.tests.downlink import DownlinkTest
5
+ from gw_certificate.tests.actions import ActionsTest
6
+ from gw_certificate.tests.throughput import StressTest
7
+
8
+ TESTS = [RegistrationTest, ConnectionTest, UplinkTest, DownlinkTest, ActionsTest, StressTest]
9
+
10
+ TESTS_DEFAULT = [ConnectionTest, UplinkTest, DownlinkTest, ActionsTest, StressTest]
11
+ TESTS_NO_UART = [RegistrationTest, ActionsTest]
@@ -0,0 +1,131 @@
1
+ import datetime
2
+ import os
3
+ import time
4
+ import pandas as pd
5
+
6
+ from gw_certificate.common.debug import debug_print
7
+ from gw_certificate.interface.mqtt import MqttClient, GwAction
8
+ from gw_certificate.tests.generic import PassCriteria, PERFECT_SCORE, MINIMUM_SCORE, INCONCLUSIVE_MINIMUM, GenericTest, GenericStage, OPTIONAL
9
+ from gw_certificate.tests.static.references import GW_ACTIONS_DOC
10
+
11
+
12
+ class GenericActionsStage(GenericStage):
13
+ def __init__(self, mqttc:MqttClient, stage_name, **kwargs):
14
+ self.__dict__.update(kwargs)
15
+ super().__init__(stage_name=stage_name, **self.__dict__)
16
+
17
+ #Clients
18
+ self.mqttc = mqttc
19
+
20
+ #Stage Params
21
+ self.action = ""
22
+
23
+ #Paths
24
+ self.summary_csv_path = os.path.join(self.test_dir, f'{self.stage_name}_summary.csv')
25
+
26
+
27
+ def prepare_stage(self):
28
+ super().prepare_stage()
29
+ self.mqttc.flush_messages()
30
+
31
+ def generate_stage_report(self):
32
+ self.add_report_header()
33
+
34
+ class GatewayInfoStage(GenericActionsStage):
35
+ def __init__(self, **kwargs):
36
+ super().__init__(**kwargs, stage_name=type(self).__name__)
37
+ self.stage_tooltip = "Issues a Gateway Info action to the gateway. Expects the gateway to publish a response"
38
+ self.error_summary = "Did not receive a response to the Gateway Info action"
39
+ self.action = "getGwInfo"
40
+ self.response = None
41
+
42
+ def run(self):
43
+ super().run()
44
+ self.gw_info = self.mqttc.get_gw_info()
45
+
46
+ def generate_stage_report(self):
47
+ super().generate_stage_report()
48
+
49
+ # Calculate whether stage pass/failed
50
+ if not self.gw_info or "gatewayInfo" not in self.gw_info.body or len(self.gw_info.body) > 1:
51
+ self.stage_pass = MINIMUM_SCORE
52
+ self.add_to_stage_report(f'Did not receive a response to the Gateway Info action. For more info visit:')
53
+ self.add_to_stage_report(f'{GW_ACTIONS_DOC}')
54
+ else:
55
+ self.stage_pass = PERFECT_SCORE
56
+ self.response = repr(self.gw_info)
57
+ self.add_to_stage_report('A Gateway Info response was receieved:')
58
+ self.add_to_stage_report(self.response)
59
+
60
+ # Export all stage data
61
+ csv_data = {'Action': [self.action], 'Response': [self.response], 'Pass': [self.stage_pass > self.pass_min]}
62
+ pd.DataFrame(csv_data).to_csv(self.summary_csv_path)
63
+ self.add_to_stage_report(f'\nStage summary saved - {self.summary_csv_path}')
64
+ debug_print(self.report)
65
+
66
+ # Generate HTML
67
+ self.report_html = self.template_engine.render_template('stage.html', stage=self,
68
+ stage_report=self.report.split('\n'))
69
+ return self.report
70
+
71
+
72
+ class RebootStage(GenericActionsStage):
73
+ def __init__(self, **kwargs):
74
+ super().__init__(**kwargs, stage_name=type(self).__name__)
75
+ self.stage_tooltip = "Issues reboot action to the gateway. Expects it to reboot"
76
+ self.error_summary = "The gateway did not reboot as expected"
77
+ self.action = "rebootGw"
78
+
79
+ def run(self):
80
+ super().run()
81
+ timeout = datetime.datetime.now() + datetime.timedelta(minutes=3)
82
+ self.status_message = None
83
+
84
+ self.mqttc.send_action(GwAction.REBOOT_GW)
85
+ while datetime.datetime.now() < timeout and self.status_message is None:
86
+ self.status_message = self.mqttc.get_status_message()
87
+ time.sleep(5)
88
+
89
+ def generate_stage_report(self):
90
+ super().generate_stage_report()
91
+
92
+ # Calculate whether stage pass/failed
93
+ if (self.status_message is None or
94
+ (not any('gatewayconf' == key.lower() for key in self.status_message.keys()) and
95
+ not any('gatewaystatus' == key.lower() for key in self.status_message.keys()))):
96
+ self.stage_pass = MINIMUM_SCORE
97
+ self.add_to_stage_report(f"The gateway did not validly reboot")
98
+ self.add_to_stage_report(f"Gateways are expected to upload a status(configuration) message upon establishing MQTT connection, which wasn't received.")
99
+ else:
100
+ self.stage_pass = PERFECT_SCORE
101
+ self.add_to_stage_report(f"Gateway rebooted and uploaded a configuration message, as expected.")
102
+
103
+ # Export all stage data
104
+ csv_data = {'Action': [self.action], 'Pass': [self.stage_pass > self.pass_min]}
105
+ pd.DataFrame(csv_data).to_csv(self.summary_csv_path)
106
+ self.add_to_stage_report(f'\nStage summary saved - {self.summary_csv_path}')
107
+
108
+ # Generate HTML
109
+ self.report_html = self.template_engine.render_template('stage.html', stage=self,
110
+ stage_report=self.report.split('\n'))
111
+ return self.report
112
+
113
+
114
+ STAGES = [GatewayInfoStage, RebootStage]
115
+
116
+ class ActionsTest(GenericTest):
117
+ def __init__(self, **kwargs):
118
+ self.__dict__.update(kwargs)
119
+ super().__init__(**self.__dict__, test_name=type(self).__name__)
120
+ self.test_tooltip = "Stages publishing different actions (via the 'update' topic). Optional"
121
+ self.result_indication = OPTIONAL
122
+ self.stages = [stage(**self.__dict__) for stage in STAGES]
123
+
124
+ def run(self):
125
+ super().run()
126
+ self.test_pass = PERFECT_SCORE
127
+ for stage in self.stages:
128
+ stage.prepare_stage()
129
+ stage.run()
130
+ self.add_to_test_report(stage.generate_stage_report())
131
+ self.test_pass = PassCriteria.calc_for_test(self.test_pass, stage)