swarmit 0.2.0__py3-none-any.whl → 0.4.4__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.
- dotbot-firmware/doc/sphinx/conf.py +191 -0
- swarmit-0.4.4.dist-info/METADATA +127 -0
- swarmit-0.4.4.dist-info/RECORD +12 -0
- {swarmit-0.2.0.dist-info → swarmit-0.4.4.dist-info}/WHEEL +1 -1
- testbed/cli/main.py +265 -529
- testbed/swarmit/__init__.py +1 -0
- testbed/swarmit/adapter.py +142 -0
- testbed/swarmit/controller.py +664 -0
- testbed/swarmit/protocol.py +231 -0
- swarmit-0.2.0.dist-info/METADATA +0 -99
- swarmit-0.2.0.dist-info/RECORD +0 -7
- {swarmit-0.2.0.dist-info → swarmit-0.4.4.dist-info}/entry_points.txt +0 -0
- {swarmit-0.2.0.dist-info → swarmit-0.4.4.dist-info}/licenses/AUTHORS +0 -0
- {swarmit-0.2.0.dist-info → swarmit-0.4.4.dist-info}/licenses/LICENSE +0 -0
testbed/cli/main.py
CHANGED
@@ -3,426 +3,138 @@
|
|
3
3
|
import logging
|
4
4
|
import time
|
5
5
|
|
6
|
-
from dataclasses import dataclass
|
7
|
-
from enum import Enum
|
8
|
-
|
9
6
|
import click
|
10
7
|
import serial
|
11
8
|
import structlog
|
12
|
-
|
13
|
-
from
|
14
|
-
from cryptography.hazmat.primitives import hashes
|
15
|
-
|
9
|
+
from dotbot.serial_interface import SerialInterfaceException, get_default_port
|
10
|
+
from rich import print
|
16
11
|
from rich.console import Console
|
17
|
-
from rich.
|
18
|
-
|
19
|
-
|
20
|
-
from
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
12
|
+
from rich.pretty import pprint
|
13
|
+
|
14
|
+
from testbed.swarmit import __version__
|
15
|
+
from testbed.swarmit.controller import (
|
16
|
+
CHUNK_SIZE,
|
17
|
+
OTA_ACK_TIMEOUT_DEFAULT,
|
18
|
+
OTA_MAX_RETRIES_DEFAULT,
|
19
|
+
Controller,
|
20
|
+
ControllerSettings,
|
21
|
+
ResetLocation,
|
22
|
+
print_transfer_status,
|
27
23
|
)
|
28
24
|
|
25
|
+
SERIAL_PORT_DEFAULT = get_default_port()
|
26
|
+
BAUDRATE_DEFAULT = 1000000
|
27
|
+
MQTT_HOST_DEFAULT = "localhost"
|
28
|
+
MQTT_PORT_DEFAULT = 1883
|
29
|
+
# Default network ID for SwarmIT tests is 0x12**
|
30
|
+
# See https://crystalfree.atlassian.net/wiki/spaces/Mari/pages/3324903426/Registry+of+Mari+Network+IDs
|
31
|
+
SWARMIT_NETWORK_ID_DEFAULT = "1200"
|
29
32
|
|
30
|
-
SERIAL_PORT = get_default_port()
|
31
|
-
BAUDRATE = 1000000
|
32
|
-
CHUNK_SIZE = 128
|
33
|
-
HEADER = ProtocolHeader()
|
34
|
-
|
35
|
-
|
36
|
-
class NotificationType(Enum):
|
37
|
-
"""Types of notifications."""
|
38
|
-
|
39
|
-
SWARMIT_NOTIFICATION_STATUS = 0
|
40
|
-
SWARMIT_NOTIFICATION_OTA_START_ACK = 1
|
41
|
-
SWARMIT_NOTIFICATION_OTA_CHUNK_ACK = 2
|
42
|
-
SWARMIT_NOTIFICATION_EVENT_GPIO = 3
|
43
|
-
SWARMIT_NOTIFICATION_EVENT_LOG = 4
|
44
|
-
|
45
|
-
|
46
|
-
class RequestType(Enum):
|
47
|
-
"""Types of requests."""
|
48
|
-
|
49
|
-
SWARMIT_REQ_EXPERIMENT_START = 1
|
50
|
-
SWARMIT_REQ_EXPERIMENT_STOP = 2
|
51
|
-
SWARMIT_REQ_EXPERIMENT_STATUS = 3
|
52
|
-
SWARMIT_REQ_OTA_START = 4
|
53
|
-
SWARMIT_REQ_OTA_CHUNK = 5
|
54
|
-
|
55
|
-
|
56
|
-
class StatusType(Enum):
|
57
|
-
"""Types of device status."""
|
58
|
-
|
59
|
-
Ready = 0
|
60
|
-
Running = 1
|
61
|
-
Off = 2
|
62
|
-
|
63
|
-
|
64
|
-
@dataclass
|
65
|
-
class DataChunk:
|
66
|
-
"""Class that holds data chunks."""
|
67
|
-
|
68
|
-
index: int
|
69
|
-
size: int
|
70
|
-
data: bytes
|
71
|
-
|
72
|
-
|
73
|
-
def _header():
|
74
|
-
"""Return the header for the protocol."""
|
75
|
-
buffer = bytearray()
|
76
|
-
for field in ProtocolHeader().fields:
|
77
|
-
buffer += int(field.value).to_bytes(
|
78
|
-
length=field.length, byteorder="little", signed=field.signed
|
79
|
-
)
|
80
|
-
buffer += int(16).to_bytes(length=1, byteorder="little")
|
81
|
-
return buffer
|
82
|
-
|
83
|
-
|
84
|
-
class SwarmitFlash:
|
85
|
-
"""Class used to flash a firmware."""
|
86
|
-
|
87
|
-
def __init__(self, port, baudrate, firmware, known_devices):
|
88
|
-
self.serial = SerialInterface(port, baudrate, self.on_byte_received)
|
89
|
-
self.hdlc_handler = HDLCHandler()
|
90
|
-
self.start_ack_received = False
|
91
|
-
self.firmware = bytearray(firmware.read()) if firmware is not None else None
|
92
|
-
self.known_devices = known_devices
|
93
|
-
self.last_acked_index = -1
|
94
|
-
self.chunks = []
|
95
|
-
self.fw_hash = None
|
96
|
-
self.acked_ids = []
|
97
|
-
|
98
|
-
def on_byte_received(self, byte):
|
99
|
-
self.hdlc_handler.handle_byte(byte)
|
100
|
-
if self.hdlc_handler.state == HDLCState.READY:
|
101
|
-
payload = self.hdlc_handler.payload[25:]
|
102
|
-
if not payload:
|
103
|
-
return
|
104
|
-
deviceid_ack = hex(int.from_bytes(payload[0:8], byteorder="little"))
|
105
|
-
if deviceid_ack not in self.acked_ids:
|
106
|
-
self.acked_ids.append(deviceid_ack)
|
107
|
-
if payload[8] == NotificationType.SWARMIT_NOTIFICATION_OTA_START_ACK.value:
|
108
|
-
self.start_ack_received = True
|
109
|
-
elif (
|
110
|
-
payload[8] == NotificationType.SWARMIT_NOTIFICATION_OTA_CHUNK_ACK.value
|
111
|
-
):
|
112
|
-
self.last_acked_index = int.from_bytes(
|
113
|
-
payload[9:14], byteorder="little"
|
114
|
-
)
|
115
|
-
|
116
|
-
def _send_start_ota(self, device_id):
|
117
|
-
buffer = bytearray()
|
118
|
-
buffer += _header()
|
119
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
120
|
-
buffer += int(RequestType.SWARMIT_REQ_OTA_START.value).to_bytes(
|
121
|
-
length=1, byteorder="little"
|
122
|
-
)
|
123
|
-
buffer += len(self.firmware).to_bytes(length=4, byteorder="little")
|
124
|
-
buffer += self.fw_hash
|
125
|
-
self.serial.write(hdlc_encode(buffer))
|
126
|
-
|
127
|
-
def init(self, device_ids):
|
128
|
-
digest = hashes.Hash(hashes.SHA256())
|
129
|
-
chunks_count = int(len(self.firmware) / CHUNK_SIZE) + int(
|
130
|
-
len(self.firmware) % CHUNK_SIZE != 0
|
131
|
-
)
|
132
|
-
for chunk_idx in range(chunks_count):
|
133
|
-
if chunk_idx == chunks_count - 1:
|
134
|
-
chunk_size = len(self.firmware) % CHUNK_SIZE
|
135
|
-
else:
|
136
|
-
chunk_size = CHUNK_SIZE
|
137
|
-
data = self.firmware[
|
138
|
-
chunk_idx * CHUNK_SIZE : chunk_idx * CHUNK_SIZE + chunk_size
|
139
|
-
]
|
140
|
-
digest.update(data)
|
141
|
-
self.chunks.append(
|
142
|
-
DataChunk(
|
143
|
-
index=chunk_idx,
|
144
|
-
size=chunk_size,
|
145
|
-
data=data,
|
146
|
-
)
|
147
|
-
)
|
148
|
-
print(f"Radio chunks ({CHUNK_SIZE}B): {len(self.chunks)}")
|
149
|
-
self.fw_hash = digest.finalize()
|
150
|
-
if not device_ids:
|
151
|
-
print("Broadcast start ota notification...")
|
152
|
-
self._send_start_ota("0")
|
153
|
-
self.acked_ids = []
|
154
|
-
timeout = 0 # ms
|
155
|
-
while timeout < 10000 and sorted(self.acked_ids) != sorted(
|
156
|
-
self.known_devices
|
157
|
-
):
|
158
|
-
timeout += 1
|
159
|
-
time.sleep(0.0001)
|
160
|
-
else:
|
161
|
-
self.acked_ids = []
|
162
|
-
for device_id in device_ids:
|
163
|
-
print(f"Sending start ota notification to {device_id}...")
|
164
|
-
self._send_start_ota(device_id)
|
165
|
-
timeout = 0 # ms
|
166
|
-
while timeout < 10000 and device_id not in self.acked_ids:
|
167
|
-
timeout += 1
|
168
|
-
time.sleep(0.0001)
|
169
|
-
return self.acked_ids
|
170
|
-
|
171
|
-
def send_chunk(self, chunk, device_id):
|
172
|
-
send_time = time.time()
|
173
|
-
send = True
|
174
|
-
tries = 0
|
175
|
-
|
176
|
-
def is_chunk_acknowledged():
|
177
|
-
if device_id == "0":
|
178
|
-
return self.last_acked_index == chunk.index and sorted(
|
179
|
-
self.acked_ids
|
180
|
-
) == sorted(self.known_devices)
|
181
|
-
else:
|
182
|
-
return (
|
183
|
-
self.last_acked_index == chunk.index and device_id in self.acked_ids
|
184
|
-
)
|
185
|
-
|
186
|
-
self.acked_ids = []
|
187
|
-
while tries < 3:
|
188
|
-
if is_chunk_acknowledged():
|
189
|
-
break
|
190
|
-
if send is True:
|
191
|
-
buffer = bytearray()
|
192
|
-
buffer += _header()
|
193
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
194
|
-
buffer += int(RequestType.SWARMIT_REQ_OTA_CHUNK.value).to_bytes(
|
195
|
-
length=1, byteorder="little"
|
196
|
-
)
|
197
|
-
buffer += int(chunk.index).to_bytes(length=4, byteorder="little")
|
198
|
-
buffer += int(chunk.size).to_bytes(length=1, byteorder="little")
|
199
|
-
buffer += chunk.data
|
200
|
-
self.serial.write(hdlc_encode(buffer))
|
201
|
-
send_time = time.time()
|
202
|
-
tries += 1
|
203
|
-
time.sleep(0.001)
|
204
|
-
send = time.time() - send_time > 0.1
|
205
|
-
else:
|
206
|
-
raise Exception(f"chunk #{chunk.index} not acknowledged. Aborting.")
|
207
|
-
self.last_acked_index = -1
|
208
|
-
self.last_deviceid_ack = None
|
209
|
-
|
210
|
-
def transfer(self, device_ids):
|
211
|
-
data_size = len(self.firmware)
|
212
|
-
progress = tqdm(
|
213
|
-
range(0, data_size), unit="B", unit_scale=False, colour="green", ncols=100
|
214
|
-
)
|
215
|
-
progress.set_description(f"Loading firmware ({int(data_size / 1024)}kB)")
|
216
|
-
for chunk in self.chunks:
|
217
|
-
if not device_ids:
|
218
|
-
self.send_chunk(chunk, "0")
|
219
|
-
else:
|
220
|
-
for device_id in device_ids:
|
221
|
-
self.send_chunk(chunk, device_id)
|
222
|
-
progress.update(chunk.size)
|
223
|
-
progress.close()
|
224
|
-
|
225
|
-
|
226
|
-
class SwarmitStart:
|
227
|
-
"""Class used to start an experiment."""
|
228
|
-
|
229
|
-
def __init__(self, port, baudrate, known_devices):
|
230
|
-
self.serial = SerialInterface(port, baudrate, lambda x: None)
|
231
|
-
self.known_devices = known_devices
|
232
|
-
self.hdlc_handler = HDLCHandler()
|
233
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
234
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
235
|
-
|
236
|
-
def _send_start(self, device_id):
|
237
|
-
buffer = bytearray()
|
238
|
-
buffer += _header()
|
239
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
240
|
-
buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_START.value).to_bytes(
|
241
|
-
length=1, byteorder="little"
|
242
|
-
)
|
243
|
-
self.serial.write(hdlc_encode(buffer))
|
244
|
-
|
245
|
-
def start(self, device_ids):
|
246
|
-
if not device_ids:
|
247
|
-
self._send_start("0")
|
248
|
-
else:
|
249
|
-
for device_id in device_ids:
|
250
|
-
if device_id not in self.known_devices:
|
251
|
-
continue
|
252
|
-
self._send_start(device_id)
|
253
|
-
|
254
|
-
|
255
|
-
class SwarmitStop:
|
256
|
-
"""Class used to stop an experiment."""
|
257
|
-
|
258
|
-
def __init__(self, port, baudrate, known_devices):
|
259
|
-
self.serial = SerialInterface(port, baudrate, lambda x: None)
|
260
|
-
self.known_devices = known_devices
|
261
|
-
self.hdlc_handler = HDLCHandler()
|
262
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
263
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
264
|
-
|
265
|
-
def _send_stop(self, device_id):
|
266
|
-
buffer = bytearray()
|
267
|
-
buffer += _header()
|
268
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
269
|
-
buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_STOP.value).to_bytes(
|
270
|
-
length=1, byteorder="little"
|
271
|
-
)
|
272
|
-
self.serial.write(hdlc_encode(buffer))
|
273
|
-
|
274
|
-
def stop(self, device_ids):
|
275
|
-
if not device_ids:
|
276
|
-
self._send_stop("0")
|
277
|
-
else:
|
278
|
-
for device_id in device_ids:
|
279
|
-
if device_id not in self.known_devices:
|
280
|
-
continue
|
281
|
-
self._send_stop(device_id)
|
282
|
-
|
283
|
-
|
284
|
-
class SwarmitMonitor:
|
285
|
-
"""Class used to monitor an experiment."""
|
286
|
-
|
287
|
-
def __init__(self, port, baudrate, device_ids):
|
288
|
-
self.logger = LOGGER.bind(context=__name__)
|
289
|
-
self.hdlc_handler = HDLCHandler()
|
290
|
-
self.serial = SerialInterface(port, baudrate, self.on_byte_received)
|
291
|
-
self.last_deviceid_notification = None
|
292
|
-
self.device_ids = device_ids
|
293
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
294
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
295
|
-
|
296
|
-
def on_byte_received(self, byte):
|
297
|
-
if self.hdlc_handler is None:
|
298
|
-
return
|
299
|
-
self.hdlc_handler.handle_byte(byte)
|
300
|
-
if self.hdlc_handler.state == HDLCState.READY:
|
301
|
-
payload = self.hdlc_handler.payload[25:]
|
302
|
-
if not payload:
|
303
|
-
return
|
304
|
-
deviceid = int.from_bytes(payload[0:8], byteorder="little")
|
305
|
-
if self.device_ids and deviceid not in self.device_ids:
|
306
|
-
return
|
307
|
-
event = payload[8]
|
308
|
-
timestamp = int.from_bytes(payload[9:13], byteorder="little")
|
309
|
-
data_size = int(payload[13])
|
310
|
-
data = payload[14 : data_size + 14]
|
311
|
-
logger = self.logger.bind(
|
312
|
-
deviceid=hex(deviceid),
|
313
|
-
notification=event,
|
314
|
-
time=timestamp,
|
315
|
-
data_size=data_size,
|
316
|
-
data=data,
|
317
|
-
)
|
318
|
-
if event == NotificationType.SWARMIT_NOTIFICATION_EVENT_GPIO.value:
|
319
|
-
logger.info(f"GPIO event")
|
320
|
-
elif event == NotificationType.SWARMIT_NOTIFICATION_EVENT_LOG.value:
|
321
|
-
logger.info(f"LOG event")
|
322
|
-
|
323
|
-
def monitor(self):
|
324
|
-
while True:
|
325
|
-
time.sleep(0.01)
|
326
|
-
|
327
|
-
|
328
|
-
class SwarmitStatus:
|
329
|
-
"""Class used to get the status of experiment."""
|
330
33
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
34
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
35
|
+
@click.option(
|
36
|
+
"-p",
|
37
|
+
"--port",
|
38
|
+
type=str,
|
39
|
+
default=SERIAL_PORT_DEFAULT,
|
40
|
+
help=f"Serial port to use to send the bitstream to the gateway. Default: {SERIAL_PORT_DEFAULT}.",
|
41
|
+
)
|
42
|
+
@click.option(
|
43
|
+
"-b",
|
44
|
+
"--baudrate",
|
45
|
+
type=int,
|
46
|
+
default=BAUDRATE_DEFAULT,
|
47
|
+
help=f"Serial port baudrate. Default: {BAUDRATE_DEFAULT}.",
|
48
|
+
)
|
49
|
+
@click.option(
|
50
|
+
"-H",
|
51
|
+
"--mqtt-host",
|
52
|
+
type=str,
|
53
|
+
default=MQTT_HOST_DEFAULT,
|
54
|
+
help=f"MQTT host. Default: {MQTT_HOST_DEFAULT}.",
|
55
|
+
)
|
56
|
+
@click.option(
|
57
|
+
"-P",
|
58
|
+
"--mqtt-port",
|
59
|
+
type=int,
|
60
|
+
default=MQTT_PORT_DEFAULT,
|
61
|
+
help=f"MQTT port. Default: {MQTT_PORT_DEFAULT}.",
|
62
|
+
)
|
63
|
+
@click.option(
|
64
|
+
"-T",
|
65
|
+
"--mqtt-use_tls",
|
66
|
+
is_flag=True,
|
67
|
+
help="Use TLS with MQTT.",
|
68
|
+
)
|
69
|
+
@click.option(
|
70
|
+
"-n",
|
71
|
+
"--network-id",
|
72
|
+
type=str,
|
73
|
+
default=SWARMIT_NETWORK_ID_DEFAULT,
|
74
|
+
help=f"Marilib network ID to use. Default: 0x{SWARMIT_NETWORK_ID_DEFAULT}",
|
75
|
+
)
|
76
|
+
@click.option(
|
77
|
+
"-a",
|
78
|
+
"--adapter",
|
79
|
+
type=click.Choice(["edge", "cloud"], case_sensitive=True),
|
80
|
+
default="edge",
|
81
|
+
show_default=True,
|
82
|
+
help="Choose the adapter to communicate with the gateway.",
|
83
|
+
)
|
84
|
+
@click.option(
|
85
|
+
"-d",
|
86
|
+
"--devices",
|
87
|
+
type=str,
|
88
|
+
default="",
|
89
|
+
help="Subset list of device addresses to interact with, separated with ,",
|
90
|
+
)
|
91
|
+
@click.option(
|
92
|
+
"-v",
|
93
|
+
"--verbose",
|
94
|
+
is_flag=True,
|
95
|
+
help="Enable verbose mode.",
|
96
|
+
)
|
97
|
+
@click.version_option(__version__, "-V", "--version", prog_name="swarmit")
|
98
|
+
@click.pass_context
|
99
|
+
def main(
|
100
|
+
ctx,
|
101
|
+
port,
|
102
|
+
baudrate,
|
103
|
+
mqtt_host,
|
104
|
+
mqtt_port,
|
105
|
+
mqtt_use_tls,
|
106
|
+
network_id,
|
107
|
+
adapter,
|
108
|
+
devices,
|
109
|
+
verbose,
|
110
|
+
):
|
111
|
+
if ctx.invoked_subcommand != "monitor":
|
112
|
+
# Disable logging if not monitoring
|
113
|
+
structlog.configure(
|
114
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
115
|
+
logging.CRITICAL
|
116
|
+
),
|
411
117
|
)
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
118
|
+
ctx.ensure_object(dict)
|
119
|
+
ctx.obj["settings"] = ControllerSettings(
|
120
|
+
serial_port=port,
|
121
|
+
serial_baudrate=baudrate,
|
122
|
+
mqtt_host=mqtt_host,
|
123
|
+
mqtt_port=mqtt_port,
|
124
|
+
mqtt_use_tls=mqtt_use_tls,
|
125
|
+
network_id=int(network_id, 16),
|
126
|
+
adapter=adapter,
|
127
|
+
devices=[d for d in devices.split(",") if d],
|
128
|
+
verbose=verbose,
|
129
|
+
)
|
421
130
|
|
422
131
|
|
423
|
-
|
132
|
+
@main.command()
|
133
|
+
@click.pass_context
|
134
|
+
def start(ctx):
|
135
|
+
"""Start the user application."""
|
424
136
|
try:
|
425
|
-
|
137
|
+
controller = Controller(ctx.obj["settings"])
|
426
138
|
except (
|
427
139
|
SerialInterfaceException,
|
428
140
|
serial.serialutil.SerialException,
|
@@ -430,13 +142,19 @@ def swarmit_start(port, baudrate, devices, ready_devices):
|
|
430
142
|
console = Console()
|
431
143
|
console.print(f"[bold red]Error:[/] {exc}")
|
432
144
|
return
|
433
|
-
|
434
|
-
|
145
|
+
if controller.ready_devices:
|
146
|
+
controller.start()
|
147
|
+
else:
|
148
|
+
print("No device to start")
|
149
|
+
controller.terminate()
|
435
150
|
|
436
151
|
|
437
|
-
|
152
|
+
@main.command()
|
153
|
+
@click.pass_context
|
154
|
+
def stop(ctx):
|
155
|
+
"""Stop the user application."""
|
438
156
|
try:
|
439
|
-
|
157
|
+
controller = Controller(ctx.obj["settings"])
|
440
158
|
except (
|
441
159
|
SerialInterfaceException,
|
442
160
|
serial.serialutil.SerialException,
|
@@ -444,104 +162,53 @@ def swarmit_stop(port, baudrate, devices, running_devices):
|
|
444
162
|
console = Console()
|
445
163
|
console.print(f"[bold red]Error:[/] {exc}")
|
446
164
|
return
|
447
|
-
|
448
|
-
|
449
|
-
|
165
|
+
if controller.running_devices or controller.resetting_devices:
|
166
|
+
controller.stop()
|
167
|
+
else:
|
168
|
+
print("[bold]No device to stop[/]")
|
169
|
+
controller.terminate()
|
450
170
|
|
451
|
-
def swarmit_monitor(port, baudrate, devices):
|
452
|
-
try:
|
453
|
-
experiment = SwarmitMonitor(port, baudrate, devices)
|
454
|
-
except (
|
455
|
-
SerialInterfaceException,
|
456
|
-
serial.serialutil.SerialException,
|
457
|
-
) as exc:
|
458
|
-
console = Console()
|
459
|
-
console.print(f"[bold red]Error:[/] {exc}")
|
460
|
-
return
|
461
|
-
try:
|
462
|
-
experiment.monitor()
|
463
|
-
except KeyboardInterrupt:
|
464
|
-
print("Stopping monitor.")
|
465
171
|
|
172
|
+
@main.command()
|
173
|
+
@click.argument(
|
174
|
+
"locations",
|
175
|
+
type=str,
|
176
|
+
)
|
177
|
+
@click.pass_context
|
178
|
+
def reset(ctx, locations):
|
179
|
+
"""Reset robots locations.
|
466
180
|
|
467
|
-
|
181
|
+
Locations are provided as '<device_addr>:<x>,<y>-<device_addr>:<x>,<y>|...'
|
182
|
+
"""
|
468
183
|
try:
|
469
|
-
|
184
|
+
controller = Controller(ctx.obj["settings"])
|
470
185
|
except (
|
471
186
|
SerialInterfaceException,
|
472
187
|
serial.serialutil.SerialException,
|
473
188
|
) as exc:
|
474
189
|
console = Console()
|
475
190
|
console.print(f"[bold red]Error:[/] {exc}")
|
476
|
-
return
|
477
|
-
result = experiment.status(display)
|
478
|
-
experiment.serial.stop()
|
479
|
-
return result
|
480
|
-
|
191
|
+
return
|
481
192
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
)
|
489
|
-
|
490
|
-
"-b",
|
491
|
-
"--baudrate",
|
492
|
-
default=BAUDRATE,
|
493
|
-
help=f"Serial port baudrate. Default: {BAUDRATE}.",
|
494
|
-
)
|
495
|
-
@click.option(
|
496
|
-
"-d",
|
497
|
-
"--devices",
|
498
|
-
type=str,
|
499
|
-
default="",
|
500
|
-
help=f"Subset list of devices to interact with, separated with ,",
|
501
|
-
)
|
502
|
-
@click.pass_context
|
503
|
-
def main(ctx, port, baudrate, devices):
|
504
|
-
if ctx.invoked_subcommand != "monitor":
|
505
|
-
# Disable logging if not monitoring
|
506
|
-
structlog.configure(
|
507
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
|
193
|
+
devices = controller.settings.devices
|
194
|
+
if not devices:
|
195
|
+
print("No devices selected.")
|
196
|
+
return
|
197
|
+
locations = {
|
198
|
+
int(location.split(":")[0], 16): ResetLocation(
|
199
|
+
pos_x=int(float(location.split(":")[1].split(",")[0]) * 1e6),
|
200
|
+
pos_y=int(float(location.split(":")[1].split(",")[1]) * 1e6),
|
508
201
|
)
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
ready_devices = sorted(
|
520
|
-
[
|
521
|
-
device
|
522
|
-
for device, status in known_devices.items()
|
523
|
-
if status == StatusType.Ready
|
524
|
-
]
|
525
|
-
)
|
526
|
-
swarmit_start(
|
527
|
-
ctx.obj["port"], ctx.obj["baudrate"], list(ctx.obj["devices"]), ready_devices
|
528
|
-
)
|
529
|
-
|
530
|
-
|
531
|
-
@main.command()
|
532
|
-
@click.pass_context
|
533
|
-
def stop(ctx):
|
534
|
-
known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
|
535
|
-
running_devices = sorted(
|
536
|
-
[
|
537
|
-
device
|
538
|
-
for device, status in known_devices.items()
|
539
|
-
if status == StatusType.Running
|
540
|
-
]
|
541
|
-
)
|
542
|
-
swarmit_stop(
|
543
|
-
ctx.obj["port"], ctx.obj["baudrate"], list(ctx.obj["devices"]), running_devices
|
544
|
-
)
|
202
|
+
for location in locations.split("-")
|
203
|
+
}
|
204
|
+
if sorted(devices) and sorted(locations.keys()) != sorted(devices):
|
205
|
+
print("Selected devices and reset locations do not match.")
|
206
|
+
return
|
207
|
+
if not controller.ready_devices:
|
208
|
+
print("No device to reset.")
|
209
|
+
return
|
210
|
+
controller.reset(locations)
|
211
|
+
controller.terminate()
|
545
212
|
|
546
213
|
|
547
214
|
@main.command()
|
@@ -557,55 +224,124 @@ def stop(ctx):
|
|
557
224
|
is_flag=True,
|
558
225
|
help="Start the firmware once flashed.",
|
559
226
|
)
|
560
|
-
@click.
|
227
|
+
@click.option(
|
228
|
+
"-t",
|
229
|
+
"--ota-timeout",
|
230
|
+
type=float,
|
231
|
+
default=OTA_ACK_TIMEOUT_DEFAULT,
|
232
|
+
show_default=True,
|
233
|
+
help="Timeout in seconds for each OTA ACK message.",
|
234
|
+
)
|
235
|
+
@click.option(
|
236
|
+
"-r",
|
237
|
+
"--ota-max-retries",
|
238
|
+
type=int,
|
239
|
+
default=OTA_MAX_RETRIES_DEFAULT,
|
240
|
+
show_default=True,
|
241
|
+
help="Number of retries for each OTA message (start or chunk) transfer.",
|
242
|
+
)
|
243
|
+
@click.argument("firmware", type=click.File(mode="rb"), required=False)
|
561
244
|
@click.pass_context
|
562
|
-
def flash(ctx, yes, start, firmware):
|
245
|
+
def flash(ctx, yes, start, ota_timeout, ota_max_retries, firmware):
|
246
|
+
"""Flash a firmware to the robots."""
|
563
247
|
console = Console()
|
564
248
|
if firmware is None:
|
565
249
|
console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
|
566
250
|
ctx.exit()
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
)
|
575
|
-
if not ready_devices:
|
251
|
+
ctx.obj["settings"].ota_timeout = ota_timeout
|
252
|
+
ctx.obj["settings"].ota_max_retries = ota_max_retries
|
253
|
+
fw = bytearray(firmware.read())
|
254
|
+
controller = Controller(ctx.obj["settings"])
|
255
|
+
if not controller.ready_devices:
|
256
|
+
console.print("[bold red]Error:[/] No ready device found. Exiting.")
|
257
|
+
controller.terminate()
|
576
258
|
return
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
259
|
+
print(
|
260
|
+
f"Devices to flash ([bold white]{len(controller.ready_devices)}):[/]"
|
261
|
+
)
|
262
|
+
pprint(controller.ready_devices, expand_all=True)
|
263
|
+
if yes is False:
|
264
|
+
click.confirm("Do you want to continue?", default=True, abort=True)
|
265
|
+
|
266
|
+
start_data = controller.start_ota(fw)
|
267
|
+
if controller.settings.verbose:
|
268
|
+
print("\n[b]Start OTA response:[/]")
|
269
|
+
pprint(start_data, indent_guides=False, expand_all=True)
|
270
|
+
if start_data["missed"]:
|
271
|
+
console = Console()
|
272
|
+
console.print(
|
273
|
+
f"[bold red]Error:[/] {len(start_data["missed"])} acknowledgments "
|
274
|
+
f"are missing ({', '.join(sorted(set(start_data["missed"])))}). "
|
275
|
+
"Aborting."
|
585
276
|
)
|
586
|
-
|
587
|
-
|
588
|
-
|
277
|
+
controller.stop()
|
278
|
+
controller.terminate()
|
279
|
+
raise click.Abort()
|
280
|
+
print()
|
281
|
+
print(f"Image size: [bold cyan]{len(fw)}B[/]")
|
282
|
+
print(
|
283
|
+
f"Image hash: [bold cyan]{start_data["ota"].fw_hash.hex().upper()}[/]"
|
284
|
+
)
|
285
|
+
print(
|
286
|
+
f"Radio chunks ([bold]{CHUNK_SIZE}B[/bold]): {start_data["ota"].chunks}"
|
287
|
+
)
|
288
|
+
start_time = time.time()
|
289
|
+
data = controller.transfer(fw, start_data["acked"])
|
290
|
+
print(f"Elapsed: [bold cyan]{time.time() - start_time:.3f}s[/bold cyan]")
|
291
|
+
print_transfer_status(data, start_data["ota"])
|
292
|
+
if controller.settings.verbose:
|
293
|
+
print("\n[b]Transfer data:[/]")
|
294
|
+
pprint(data, indent_guides=False, expand_all=True)
|
295
|
+
if all([device.success for device in data.values()]) is False:
|
296
|
+
controller.terminate()
|
297
|
+
console = Console()
|
298
|
+
console.print("[bold red]Error:[/] Transfer failed.")
|
299
|
+
raise click.Abort()
|
300
|
+
|
589
301
|
if start is True:
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
list(ctx.obj["devices"]),
|
594
|
-
ready_devices,
|
595
|
-
)
|
302
|
+
time.sleep(1)
|
303
|
+
controller.start()
|
304
|
+
controller.terminate()
|
596
305
|
|
597
306
|
|
598
307
|
@main.command()
|
599
308
|
@click.pass_context
|
600
309
|
def monitor(ctx):
|
601
|
-
|
310
|
+
"""Monitor running applications."""
|
311
|
+
try:
|
312
|
+
controller = Controller(ctx.obj["settings"])
|
313
|
+
except (
|
314
|
+
SerialInterfaceException,
|
315
|
+
serial.serialutil.SerialException,
|
316
|
+
) as exc:
|
317
|
+
console = Console()
|
318
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
319
|
+
return {}
|
320
|
+
try:
|
321
|
+
controller.monitor()
|
322
|
+
except KeyboardInterrupt:
|
323
|
+
print("Stopping monitor.")
|
324
|
+
finally:
|
325
|
+
controller.terminate()
|
602
326
|
|
603
327
|
|
604
328
|
@main.command()
|
605
329
|
@click.pass_context
|
606
330
|
def status(ctx):
|
607
|
-
|
608
|
-
|
331
|
+
"""Print current status of the robots."""
|
332
|
+
controller = Controller(ctx.obj["settings"])
|
333
|
+
controller.status()
|
334
|
+
controller.terminate()
|
335
|
+
|
336
|
+
|
337
|
+
@main.command()
|
338
|
+
@click.argument("message", type=str, required=True)
|
339
|
+
@click.pass_context
|
340
|
+
def message(ctx, message):
|
341
|
+
"""Send a custom text message to the robots."""
|
342
|
+
controller = Controller(ctx.obj["settings"])
|
343
|
+
controller.send_message(message)
|
344
|
+
controller.terminate()
|
609
345
|
|
610
346
|
|
611
347
|
if __name__ == "__main__":
|