swarmit 0.2.0__py3-none-any.whl → 0.3.0__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.2.0.dist-info → swarmit-0.3.0.dist-info}/METADATA +4 -2
- swarmit-0.3.0.dist-info/RECORD +12 -0
- {swarmit-0.2.0.dist-info → swarmit-0.3.0.dist-info}/WHEEL +1 -1
- testbed/cli/main.py +293 -527
- testbed/swarmit/__init__.py +1 -0
- testbed/swarmit/adapter.py +94 -0
- testbed/swarmit/controller.py +620 -0
- testbed/swarmit/protocol.py +292 -0
- swarmit-0.2.0.dist-info/RECORD +0 -7
- {swarmit-0.2.0.dist-info → swarmit-0.3.0.dist-info}/entry_points.txt +0 -0
- {swarmit-0.2.0.dist-info → swarmit-0.3.0.dist-info}/licenses/AUTHORS +0 -0
- {swarmit-0.2.0.dist-info → swarmit-0.3.0.dist-info}/licenses/LICENSE +0 -0
testbed/cli/main.py
CHANGED
@@ -3,426 +3,94 @@
|
|
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
|
+
Controller,
|
18
|
+
ControllerSettings,
|
19
|
+
ResetLocation,
|
20
|
+
print_start_status,
|
21
|
+
print_status,
|
22
|
+
print_stop_status,
|
23
|
+
print_transfer_status,
|
27
24
|
)
|
28
25
|
|
26
|
+
SERIAL_PORT_DEFAULT = get_default_port()
|
27
|
+
BAUDRATE_DEFAULT = 1000000
|
29
28
|
|
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
29
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
"
|
330
|
-
|
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
|
-
self.table.add_row(
|
360
|
-
deviceid_resp,
|
361
|
-
f'{"[bold cyan]" if status == StatusType.Running else "[bold green]"}{status.name}',
|
362
|
-
)
|
363
|
-
|
364
|
-
def status(self, display=True):
|
365
|
-
buffer = bytearray()
|
366
|
-
buffer += _header()
|
367
|
-
buffer += int("0", 16).to_bytes(length=8, byteorder="little")
|
368
|
-
buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_STATUS.value).to_bytes(
|
369
|
-
length=1, byteorder="little"
|
30
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
31
|
+
@click.version_option(version=__version__)
|
32
|
+
@click.option(
|
33
|
+
"-p",
|
34
|
+
"--port",
|
35
|
+
default=SERIAL_PORT_DEFAULT,
|
36
|
+
help=f"Serial port to use to send the bitstream to the gateway. Default: {SERIAL_PORT_DEFAULT}.",
|
37
|
+
)
|
38
|
+
@click.option(
|
39
|
+
"-b",
|
40
|
+
"--baudrate",
|
41
|
+
default=BAUDRATE_DEFAULT,
|
42
|
+
help=f"Serial port baudrate. Default: {BAUDRATE_DEFAULT}.",
|
43
|
+
)
|
44
|
+
@click.option(
|
45
|
+
"-e",
|
46
|
+
"--edge",
|
47
|
+
is_flag=True,
|
48
|
+
default=False,
|
49
|
+
help="Use MQTT adapter to communicate with an edge gateway.",
|
50
|
+
)
|
51
|
+
@click.option(
|
52
|
+
"-d",
|
53
|
+
"--devices",
|
54
|
+
type=str,
|
55
|
+
default="",
|
56
|
+
help="Subset list of devices to interact with, separated with ,",
|
57
|
+
)
|
58
|
+
@click.pass_context
|
59
|
+
def main(ctx, port, baudrate, edge, devices):
|
60
|
+
if ctx.invoked_subcommand != "monitor":
|
61
|
+
# Disable logging if not monitoring
|
62
|
+
structlog.configure(
|
63
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
64
|
+
logging.CRITICAL
|
65
|
+
),
|
370
66
|
)
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
if self.status_data and display is True:
|
377
|
-
with Live(self.table, refresh_per_second=4) as live:
|
378
|
-
live.update(self.table)
|
379
|
-
for device_id, status in self.status_data.items():
|
380
|
-
if status != StatusType.Off:
|
381
|
-
continue
|
382
|
-
self.table.add_row(device_id, f"[bold red]{status.name}")
|
383
|
-
return self.status_data
|
67
|
+
ctx.ensure_object(dict)
|
68
|
+
ctx.obj["port"] = port
|
69
|
+
ctx.obj["baudrate"] = baudrate
|
70
|
+
ctx.obj["edge"] = edge
|
71
|
+
ctx.obj["devices"] = [e for e in devices.split(",") if e]
|
384
72
|
|
385
73
|
|
386
|
-
|
74
|
+
@main.command()
|
75
|
+
@click.option(
|
76
|
+
"-v",
|
77
|
+
"--verbose",
|
78
|
+
is_flag=True,
|
79
|
+
help="Print start result.",
|
80
|
+
)
|
81
|
+
@click.pass_context
|
82
|
+
def start(ctx, verbose):
|
83
|
+
"""Start the user application."""
|
387
84
|
try:
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
return False
|
396
|
-
|
397
|
-
start_time = time.time()
|
398
|
-
print(f"Image size: {len(experiment.firmware)}B")
|
399
|
-
print("")
|
400
|
-
if yes is False:
|
401
|
-
click.confirm("Do you want to continue?", default=True, abort=True)
|
402
|
-
ids = experiment.init(devices)
|
403
|
-
if (devices and sorted(ids) != sorted(devices)) or (
|
404
|
-
not devices and sorted(ids) != sorted(ready_devices)
|
405
|
-
):
|
406
|
-
console = Console()
|
407
|
-
console.print(
|
408
|
-
"[bold red]Error:[/] some acknowledgment are missing "
|
409
|
-
f'({", ".join(sorted(set(ready_devices).difference(set(ids))))}). '
|
410
|
-
"Aborting."
|
85
|
+
settings = ControllerSettings(
|
86
|
+
serial_port=ctx.obj["port"],
|
87
|
+
serial_baudrate=ctx.obj["baudrate"],
|
88
|
+
mqtt_host="argus.paris.inria.fr",
|
89
|
+
mqtt_port=8883,
|
90
|
+
edge=ctx.obj["edge"],
|
91
|
+
devices=list(ctx.obj["devices"]),
|
411
92
|
)
|
412
|
-
|
413
|
-
try:
|
414
|
-
experiment.transfer(devices)
|
415
|
-
except Exception as exc:
|
416
|
-
console = Console()
|
417
|
-
console.print(f"[bold red]Error:[/] transfer of image failed: {exc}")
|
418
|
-
return False
|
419
|
-
print(f"Elapsed: {time.time() - start_time:.3f}s")
|
420
|
-
return True
|
421
|
-
|
422
|
-
|
423
|
-
def swarmit_start(port, baudrate, devices, ready_devices):
|
424
|
-
try:
|
425
|
-
experiment = SwarmitStart(port, baudrate, ready_devices)
|
93
|
+
controller = Controller(settings)
|
426
94
|
except (
|
427
95
|
SerialInterfaceException,
|
428
96
|
serial.serialutil.SerialException,
|
@@ -430,13 +98,44 @@ def swarmit_start(port, baudrate, devices, ready_devices):
|
|
430
98
|
console = Console()
|
431
99
|
console.print(f"[bold red]Error:[/] {exc}")
|
432
100
|
return
|
433
|
-
|
434
|
-
|
101
|
+
if controller.ready_devices:
|
102
|
+
started = controller.start()
|
103
|
+
print_start_status(
|
104
|
+
sorted(started),
|
105
|
+
sorted(set(controller.ready_devices).difference(set(started))),
|
106
|
+
)
|
107
|
+
if verbose:
|
108
|
+
print("Started devices:")
|
109
|
+
pprint(started)
|
110
|
+
print("Not started devices:")
|
111
|
+
pprint(
|
112
|
+
sorted(set(controller.ready_devices).difference(set(started)))
|
113
|
+
)
|
114
|
+
else:
|
115
|
+
print("No device to start")
|
116
|
+
controller.terminate()
|
435
117
|
|
436
118
|
|
437
|
-
|
119
|
+
@main.command()
|
120
|
+
@click.option(
|
121
|
+
"-v",
|
122
|
+
"--verbose",
|
123
|
+
is_flag=True,
|
124
|
+
help="Print start result.",
|
125
|
+
)
|
126
|
+
@click.pass_context
|
127
|
+
def stop(ctx, verbose):
|
128
|
+
"""Stop the user application."""
|
438
129
|
try:
|
439
|
-
|
130
|
+
settings = ControllerSettings(
|
131
|
+
serial_port=ctx.obj["port"],
|
132
|
+
serial_baudrate=ctx.obj["baudrate"],
|
133
|
+
mqtt_host="argus.paris.inria.fr",
|
134
|
+
mqtt_port=8883,
|
135
|
+
edge=ctx.obj["edge"],
|
136
|
+
devices=list(ctx.obj["devices"]),
|
137
|
+
)
|
138
|
+
controller = Controller(settings)
|
440
139
|
except (
|
441
140
|
SerialInterfaceException,
|
442
141
|
serial.serialutil.SerialException,
|
@@ -444,104 +143,80 @@ def swarmit_stop(port, baudrate, devices, running_devices):
|
|
444
143
|
console = Console()
|
445
144
|
console.print(f"[bold red]Error:[/] {exc}")
|
446
145
|
return
|
447
|
-
|
448
|
-
|
146
|
+
if controller.running_devices or controller.resetting_devices:
|
147
|
+
stopped = controller.stop()
|
148
|
+
print_stop_status(
|
149
|
+
sorted(stopped),
|
150
|
+
sorted(
|
151
|
+
set(
|
152
|
+
controller.running_devices + controller.resetting_devices
|
153
|
+
).difference(set(stopped))
|
154
|
+
),
|
155
|
+
)
|
156
|
+
if verbose:
|
157
|
+
print("Started devices:")
|
158
|
+
pprint(stopped)
|
159
|
+
print("Not started devices:")
|
160
|
+
pprint(
|
161
|
+
sorted(
|
162
|
+
set(
|
163
|
+
controller.running_devices
|
164
|
+
+ controller.resetting_devices
|
165
|
+
).difference(set(stopped))
|
166
|
+
)
|
167
|
+
)
|
168
|
+
else:
|
169
|
+
print("No device to stop")
|
170
|
+
controller.terminate()
|
449
171
|
|
450
172
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
173
|
+
@main.command()
|
174
|
+
@click.argument(
|
175
|
+
"locations",
|
176
|
+
type=str,
|
177
|
+
)
|
178
|
+
@click.pass_context
|
179
|
+
def reset(ctx, locations):
|
180
|
+
"""Reset robots locations.
|
181
|
+
|
182
|
+
Locations are provided as '<device_id>:<x>,<y>-<device_id>:<x>,<y>|...'
|
183
|
+
"""
|
184
|
+
devices = ctx.obj["devices"]
|
185
|
+
if not devices:
|
186
|
+
print("No devices selected.")
|
187
|
+
return
|
188
|
+
locations = {
|
189
|
+
location.split(':')[0]: ResetLocation(
|
190
|
+
pos_x=int(float(location.split(':')[1].split(',')[0]) * 1e6),
|
191
|
+
pos_y=int(float(location.split(':')[1].split(',')[1]) * 1e6),
|
192
|
+
)
|
193
|
+
for location in locations.split("-")
|
194
|
+
}
|
195
|
+
if sorted(devices) and sorted(locations.keys()) != sorted(devices):
|
196
|
+
print("Selected devices and reset locations do not match.")
|
460
197
|
return
|
461
198
|
try:
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
199
|
+
settings = ControllerSettings(
|
200
|
+
serial_port=ctx.obj["port"],
|
201
|
+
serial_baudrate=ctx.obj["baudrate"],
|
202
|
+
mqtt_host="argus.paris.inria.fr",
|
203
|
+
mqtt_port=8883,
|
204
|
+
edge=ctx.obj["edge"],
|
205
|
+
devices=list(ctx.obj["devices"]),
|
206
|
+
)
|
207
|
+
controller = Controller(settings)
|
470
208
|
except (
|
471
209
|
SerialInterfaceException,
|
472
210
|
serial.serialutil.SerialException,
|
473
211
|
) as exc:
|
474
212
|
console = Console()
|
475
213
|
console.print(f"[bold red]Error:[/] {exc}")
|
476
|
-
return
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
@click.group()
|
483
|
-
@click.option(
|
484
|
-
"-p",
|
485
|
-
"--port",
|
486
|
-
default=SERIAL_PORT,
|
487
|
-
help=f"Serial port to use to send the bitstream to the gateway. Default: {SERIAL_PORT}.",
|
488
|
-
)
|
489
|
-
@click.option(
|
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),
|
508
|
-
)
|
509
|
-
ctx.ensure_object(dict)
|
510
|
-
ctx.obj["port"] = port
|
511
|
-
ctx.obj["baudrate"] = baudrate
|
512
|
-
ctx.obj["devices"] = [e for e in devices.split(",") if e]
|
513
|
-
|
514
|
-
|
515
|
-
@main.command()
|
516
|
-
@click.pass_context
|
517
|
-
def start(ctx):
|
518
|
-
known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
|
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
|
-
)
|
214
|
+
return
|
215
|
+
if not controller.ready_devices:
|
216
|
+
print("No device to reset.")
|
217
|
+
return
|
218
|
+
controller.reset(locations)
|
219
|
+
controller.terminate()
|
545
220
|
|
546
221
|
|
547
222
|
@main.command()
|
@@ -557,55 +232,146 @@ def stop(ctx):
|
|
557
232
|
is_flag=True,
|
558
233
|
help="Start the firmware once flashed.",
|
559
234
|
)
|
560
|
-
@click.
|
235
|
+
@click.option(
|
236
|
+
"-v",
|
237
|
+
"--verbose",
|
238
|
+
is_flag=True,
|
239
|
+
help="Print transfer data.",
|
240
|
+
)
|
241
|
+
@click.argument("firmware", type=click.File(mode="rb"), required=False)
|
561
242
|
@click.pass_context
|
562
|
-
def flash(ctx, yes, start, firmware):
|
243
|
+
def flash(ctx, yes, start, verbose, firmware):
|
244
|
+
"""Flash a firmware to the robots."""
|
563
245
|
console = Console()
|
564
246
|
if firmware is None:
|
565
247
|
console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
|
566
248
|
ctx.exit()
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
249
|
+
|
250
|
+
fw = bytearray(firmware.read())
|
251
|
+
settings = ControllerSettings(
|
252
|
+
serial_port=ctx.obj["port"],
|
253
|
+
serial_baudrate=ctx.obj["baudrate"],
|
254
|
+
mqtt_host="argus.paris.inria.fr",
|
255
|
+
mqtt_port=8883,
|
256
|
+
edge=ctx.obj["edge"],
|
257
|
+
devices=ctx.obj["devices"],
|
574
258
|
)
|
575
|
-
|
259
|
+
controller = Controller(settings)
|
260
|
+
if not controller.ready_devices:
|
261
|
+
console.print("[bold red]Error:[/] No ready devices found. Exiting.")
|
262
|
+
controller.terminate()
|
576
263
|
return
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
264
|
+
print(
|
265
|
+
f"Devices to flash ([bold white]{len(controller.ready_devices)}):[/]"
|
266
|
+
)
|
267
|
+
pprint(controller.ready_devices, expand_all=True)
|
268
|
+
if yes is False:
|
269
|
+
click.confirm("Do you want to continue?", default=True, abort=True)
|
270
|
+
|
271
|
+
devices = controller.settings.devices
|
272
|
+
start_data = controller.start_ota(fw)
|
273
|
+
if (devices and sorted(start_data.ids) != sorted(devices)) or (
|
274
|
+
not devices
|
275
|
+
and sorted(start_data.ids) != sorted(controller.ready_devices)
|
587
276
|
):
|
588
|
-
|
277
|
+
console = Console()
|
278
|
+
console.print(
|
279
|
+
"[bold red]Error:[/] some acknowledgments are missing "
|
280
|
+
f'({", ".join(sorted(set(controller.ready_devices).difference(set(start_data.ids))))}). '
|
281
|
+
"Aborting."
|
282
|
+
)
|
283
|
+
raise click.Abort()
|
284
|
+
print()
|
285
|
+
print(f"Image size: [bold cyan]{len(fw)}B[/]")
|
286
|
+
print(f"Image hash: [bold cyan]{start_data.fw_hash.hex().upper()}[/]")
|
287
|
+
print(f"Radio chunks ([bold]{CHUNK_SIZE}B[/bold]): {start_data.chunks}")
|
288
|
+
start_time = time.time()
|
289
|
+
data = controller.transfer(fw)
|
290
|
+
print(f"Elapsed: [bold cyan]{time.time() - start_time:.3f}s[/bold cyan]")
|
291
|
+
print_transfer_status(data, start_data)
|
292
|
+
if verbose:
|
293
|
+
pprint(data)
|
294
|
+
if not all([value.hashes_match for value in data.values()]):
|
295
|
+
controller.terminate()
|
296
|
+
console = Console()
|
297
|
+
console.print("[bold red]Error:[/] Hashes do not match.")
|
298
|
+
raise click.Abort()
|
299
|
+
|
589
300
|
if start is True:
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
ready_devices,
|
301
|
+
started = controller.start()
|
302
|
+
print_start_status(
|
303
|
+
sorted(started),
|
304
|
+
sorted(set(start_data.ids).difference(set(started))),
|
595
305
|
)
|
306
|
+
controller.terminate()
|
596
307
|
|
597
308
|
|
598
309
|
@main.command()
|
599
310
|
@click.pass_context
|
600
311
|
def monitor(ctx):
|
601
|
-
|
312
|
+
"""Monitor running applications."""
|
313
|
+
try:
|
314
|
+
settings = ControllerSettings(
|
315
|
+
serial_port=ctx.obj["port"],
|
316
|
+
serial_baudrate=ctx.obj["baudrate"],
|
317
|
+
mqtt_host="argus.paris.inria.fr",
|
318
|
+
mqtt_port=8883,
|
319
|
+
edge=ctx.obj["edge"],
|
320
|
+
devices=ctx.obj["devices"],
|
321
|
+
)
|
322
|
+
controller = Controller(settings)
|
323
|
+
except (
|
324
|
+
SerialInterfaceException,
|
325
|
+
serial.serialutil.SerialException,
|
326
|
+
) as exc:
|
327
|
+
console = Console()
|
328
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
329
|
+
return {}
|
330
|
+
try:
|
331
|
+
controller.monitor()
|
332
|
+
except KeyboardInterrupt:
|
333
|
+
print("Stopping monitor.")
|
334
|
+
finally:
|
335
|
+
controller.terminate()
|
602
336
|
|
603
337
|
|
604
338
|
@main.command()
|
605
339
|
@click.pass_context
|
606
340
|
def status(ctx):
|
607
|
-
|
341
|
+
"""Print current status of the robots."""
|
342
|
+
settings = ControllerSettings(
|
343
|
+
serial_port=ctx.obj["port"],
|
344
|
+
serial_baudrate=ctx.obj["baudrate"],
|
345
|
+
mqtt_host="argus.paris.inria.fr",
|
346
|
+
mqtt_port=8883,
|
347
|
+
edge=ctx.obj["edge"],
|
348
|
+
devices=ctx.obj["devices"],
|
349
|
+
)
|
350
|
+
controller = Controller(settings)
|
351
|
+
data = controller.status()
|
352
|
+
if not data:
|
608
353
|
click.echo("No devices found.")
|
354
|
+
else:
|
355
|
+
print_status(data)
|
356
|
+
controller.terminate()
|
357
|
+
|
358
|
+
|
359
|
+
@main.command()
|
360
|
+
@click.argument("message", type=str, required=True)
|
361
|
+
@click.pass_context
|
362
|
+
def message(ctx, message):
|
363
|
+
"""Send a custom text message to the robots."""
|
364
|
+
settings = ControllerSettings(
|
365
|
+
serial_port=ctx.obj["port"],
|
366
|
+
serial_baudrate=ctx.obj["baudrate"],
|
367
|
+
mqtt_host="argus.paris.inria.fr",
|
368
|
+
mqtt_port=8883,
|
369
|
+
edge=ctx.obj["edge"],
|
370
|
+
devices=ctx.obj["devices"],
|
371
|
+
)
|
372
|
+
controller = Controller(settings)
|
373
|
+
controller.send_message(message)
|
374
|
+
controller.terminate()
|
609
375
|
|
610
376
|
|
611
377
|
if __name__ == "__main__":
|