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,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 }}: {% 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 }}: <span class="badge bg-secondary">Optional</span>
|
|
89
|
+
{% else %}
|
|
90
|
+
{{ 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 %}
|
|
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,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)
|