swarmit 0.1.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.1.0.dist-info → swarmit-0.3.0.dist-info}/METADATA +25 -3
- swarmit-0.3.0.dist-info/RECORD +12 -0
- {swarmit-0.1.0.dist-info → swarmit-0.3.0.dist-info}/WHEEL +1 -1
- swarmit-0.3.0.dist-info/licenses/AUTHORS +1 -0
- testbed/cli/main.py +294 -515
- 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.1.0.dist-info/RECORD +0 -6
- {swarmit-0.1.0.dist-info → swarmit-0.3.0.dist-info}/entry_points.txt +0 -0
- {swarmit-0.1.0.dist-info → swarmit-0.3.0.dist-info}/licenses/LICENSE +0 -0
testbed/cli/main.py
CHANGED
@@ -3,413 +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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
class NotificationType(Enum):
|
33
|
-
"""Types of notifications."""
|
34
|
-
|
35
|
-
SWARMIT_NOTIFICATION_STATUS = 0
|
36
|
-
SWARMIT_NOTIFICATION_OTA_START_ACK = 1
|
37
|
-
SWARMIT_NOTIFICATION_OTA_CHUNK_ACK = 2
|
38
|
-
SWARMIT_NOTIFICATION_EVENT_GPIO = 3
|
39
|
-
SWARMIT_NOTIFICATION_EVENT_LOG = 4
|
40
|
-
|
41
|
-
|
42
|
-
class RequestType(Enum):
|
43
|
-
"""Types of requests."""
|
44
|
-
|
45
|
-
SWARMIT_REQ_EXPERIMENT_START = 1
|
46
|
-
SWARMIT_REQ_EXPERIMENT_STOP = 2
|
47
|
-
SWARMIT_REQ_EXPERIMENT_STATUS = 3
|
48
|
-
SWARMIT_REQ_OTA_START = 4
|
49
|
-
SWARMIT_REQ_OTA_CHUNK = 5
|
50
|
-
|
51
|
-
|
52
|
-
class StatusType(Enum):
|
53
|
-
"""Types of device status."""
|
54
|
-
|
55
|
-
Ready = 0
|
56
|
-
Running = 1
|
57
|
-
Off = 2
|
58
|
-
|
59
|
-
|
60
|
-
@dataclass
|
61
|
-
class DataChunk:
|
62
|
-
"""Class that holds data chunks."""
|
63
|
-
|
64
|
-
index: int
|
65
|
-
size: int
|
66
|
-
data: bytes
|
67
|
-
|
68
|
-
|
69
|
-
class SwarmitFlash:
|
70
|
-
"""Class used to flash a firmware."""
|
71
|
-
|
72
|
-
def __init__(self, port, baudrate, firmware, known_devices):
|
73
|
-
self.serial = SerialInterface(port, baudrate, self.on_byte_received)
|
74
|
-
self.hdlc_handler = HDLCHandler()
|
75
|
-
self.start_ack_received = False
|
76
|
-
self.firmware = bytearray(firmware.read()) if firmware is not None else None
|
77
|
-
self.known_devices = known_devices
|
78
|
-
self.last_acked_index = -1
|
79
|
-
self.chunks = []
|
80
|
-
self.fw_hash = None
|
81
|
-
self.acked_ids = []
|
82
|
-
# Just write a single byte to fake a gateway handshake
|
83
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
84
|
-
|
85
|
-
def on_byte_received(self, byte):
|
86
|
-
self.hdlc_handler.handle_byte(byte)
|
87
|
-
if self.hdlc_handler.state == HDLCState.READY:
|
88
|
-
payload = self.hdlc_handler.payload
|
89
|
-
if not payload:
|
90
|
-
return
|
91
|
-
deviceid_ack = hex(int.from_bytes(payload[0:8], byteorder="little"))
|
92
|
-
if deviceid_ack not in self.acked_ids:
|
93
|
-
self.acked_ids.append(deviceid_ack)
|
94
|
-
if payload[8] == NotificationType.SWARMIT_NOTIFICATION_OTA_START_ACK.value:
|
95
|
-
self.start_ack_received = True
|
96
|
-
elif (
|
97
|
-
payload[8] == NotificationType.SWARMIT_NOTIFICATION_OTA_CHUNK_ACK.value
|
98
|
-
):
|
99
|
-
self.last_acked_index = int.from_bytes(
|
100
|
-
payload[9:14], byteorder="little"
|
101
|
-
)
|
102
|
-
|
103
|
-
def _send_start_ota(self, device_id):
|
104
|
-
buffer = bytearray()
|
105
|
-
buffer += SWARMIT_PREAMBLE
|
106
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
107
|
-
buffer += int(RequestType.SWARMIT_REQ_OTA_START.value).to_bytes(
|
108
|
-
length=1, byteorder="little"
|
109
|
-
)
|
110
|
-
buffer += len(self.firmware).to_bytes(length=4, byteorder="little")
|
111
|
-
buffer += self.fw_hash
|
112
|
-
self.serial.write(hdlc_encode(buffer))
|
113
|
-
|
114
|
-
def init(self, device_ids):
|
115
|
-
digest = hashes.Hash(hashes.SHA256())
|
116
|
-
chunks_count = int(len(self.firmware) / CHUNK_SIZE) + int(
|
117
|
-
len(self.firmware) % CHUNK_SIZE != 0
|
118
|
-
)
|
119
|
-
for chunk_idx in range(chunks_count):
|
120
|
-
if chunk_idx == chunks_count - 1:
|
121
|
-
chunk_size = len(self.firmware) % CHUNK_SIZE
|
122
|
-
else:
|
123
|
-
chunk_size = CHUNK_SIZE
|
124
|
-
data = self.firmware[
|
125
|
-
chunk_idx * CHUNK_SIZE : chunk_idx * CHUNK_SIZE + chunk_size
|
126
|
-
]
|
127
|
-
digest.update(data)
|
128
|
-
self.chunks.append(
|
129
|
-
DataChunk(
|
130
|
-
index=chunk_idx,
|
131
|
-
size=chunk_size,
|
132
|
-
data=data,
|
133
|
-
)
|
134
|
-
)
|
135
|
-
print(f"Radio chunks ({CHUNK_SIZE}B): {len(self.chunks)}")
|
136
|
-
self.fw_hash = digest.finalize()
|
137
|
-
if not device_ids:
|
138
|
-
print("Broadcast start ota notification...")
|
139
|
-
self._send_start_ota("0")
|
140
|
-
self.acked_ids = []
|
141
|
-
timeout = 0 # ms
|
142
|
-
while timeout < 10000 and sorted(self.acked_ids) != sorted(
|
143
|
-
self.known_devices
|
144
|
-
):
|
145
|
-
timeout += 1
|
146
|
-
time.sleep(0.0001)
|
147
|
-
else:
|
148
|
-
self.acked_ids = []
|
149
|
-
for device_id in device_ids:
|
150
|
-
print(f"Sending start ota notification to {device_id}...")
|
151
|
-
self._send_start_ota(device_id)
|
152
|
-
timeout = 0 # ms
|
153
|
-
while timeout < 10000 and device_id not in self.acked_ids:
|
154
|
-
timeout += 1
|
155
|
-
time.sleep(0.0001)
|
156
|
-
return self.acked_ids
|
157
|
-
|
158
|
-
def send_chunk(self, chunk, device_id):
|
159
|
-
send_time = time.time()
|
160
|
-
send = True
|
161
|
-
tries = 0
|
162
|
-
|
163
|
-
def is_chunk_acknowledged():
|
164
|
-
if device_id == "0":
|
165
|
-
return self.last_acked_index == chunk.index and sorted(
|
166
|
-
self.acked_ids
|
167
|
-
) == sorted(self.known_devices)
|
168
|
-
else:
|
169
|
-
return (
|
170
|
-
self.last_acked_index == chunk.index and device_id in self.acked_ids
|
171
|
-
)
|
172
|
-
|
173
|
-
self.acked_ids = []
|
174
|
-
while tries < 3:
|
175
|
-
if is_chunk_acknowledged():
|
176
|
-
break
|
177
|
-
if send is True:
|
178
|
-
buffer = bytearray()
|
179
|
-
buffer += SWARMIT_PREAMBLE
|
180
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
181
|
-
buffer += int(RequestType.SWARMIT_REQ_OTA_CHUNK.value).to_bytes(
|
182
|
-
length=1, byteorder="little"
|
183
|
-
)
|
184
|
-
buffer += int(chunk.index).to_bytes(length=4, byteorder="little")
|
185
|
-
buffer += int(chunk.size).to_bytes(length=1, byteorder="little")
|
186
|
-
buffer += chunk.data
|
187
|
-
self.serial.write(hdlc_encode(buffer))
|
188
|
-
send_time = time.time()
|
189
|
-
tries += 1
|
190
|
-
time.sleep(0.001)
|
191
|
-
send = time.time() - send_time > 0.1
|
192
|
-
else:
|
193
|
-
raise Exception(f"chunk #{chunk.index} not acknowledged. Aborting.")
|
194
|
-
self.last_acked_index = -1
|
195
|
-
self.last_deviceid_ack = None
|
196
|
-
|
197
|
-
def transfer(self, device_ids):
|
198
|
-
data_size = len(self.firmware)
|
199
|
-
progress = tqdm(
|
200
|
-
range(0, data_size), unit="B", unit_scale=False, colour="green", ncols=100
|
201
|
-
)
|
202
|
-
progress.set_description(f"Loading firmware ({int(data_size / 1024)}kB)")
|
203
|
-
for chunk in self.chunks:
|
204
|
-
if not device_ids:
|
205
|
-
self.send_chunk(chunk, "0")
|
206
|
-
else:
|
207
|
-
for device_id in device_ids:
|
208
|
-
self.send_chunk(chunk, device_id)
|
209
|
-
progress.update(chunk.size)
|
210
|
-
progress.close()
|
211
|
-
|
212
|
-
|
213
|
-
class SwarmitStart:
|
214
|
-
"""Class used to start an experiment."""
|
215
|
-
|
216
|
-
def __init__(self, port, baudrate, known_devices):
|
217
|
-
self.serial = SerialInterface(port, baudrate, lambda x: None)
|
218
|
-
self.known_devices = known_devices
|
219
|
-
self.hdlc_handler = HDLCHandler()
|
220
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
221
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
222
|
-
|
223
|
-
def _send_start(self, device_id):
|
224
|
-
buffer = bytearray()
|
225
|
-
buffer += SWARMIT_PREAMBLE
|
226
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
227
|
-
buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_START.value).to_bytes(
|
228
|
-
length=1, byteorder="little"
|
229
|
-
)
|
230
|
-
self.serial.write(hdlc_encode(buffer))
|
231
|
-
|
232
|
-
def start(self, device_ids):
|
233
|
-
if not device_ids:
|
234
|
-
self._send_start("0")
|
235
|
-
else:
|
236
|
-
for device_id in device_ids:
|
237
|
-
if device_id not in self.known_devices:
|
238
|
-
continue
|
239
|
-
self._send_start(device_id)
|
240
|
-
|
241
|
-
|
242
|
-
class SwarmitStop:
|
243
|
-
"""Class used to stop an experiment."""
|
244
|
-
|
245
|
-
def __init__(self, port, baudrate, known_devices):
|
246
|
-
self.serial = SerialInterface(port, baudrate, lambda x: None)
|
247
|
-
self.known_devices = known_devices
|
248
|
-
self.hdlc_handler = HDLCHandler()
|
249
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
250
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
251
|
-
|
252
|
-
def _send_stop(self, device_id):
|
253
|
-
buffer = bytearray()
|
254
|
-
buffer += SWARMIT_PREAMBLE
|
255
|
-
buffer += int(device_id, 16).to_bytes(length=8, byteorder="little")
|
256
|
-
buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_STOP.value).to_bytes(
|
257
|
-
length=1, byteorder="little"
|
258
|
-
)
|
259
|
-
self.serial.write(hdlc_encode(buffer))
|
260
|
-
|
261
|
-
def stop(self, device_ids):
|
262
|
-
if not device_ids:
|
263
|
-
self._send_stop("0")
|
264
|
-
else:
|
265
|
-
for device_id in device_ids:
|
266
|
-
if device_id not in self.known_devices:
|
267
|
-
continue
|
268
|
-
self._send_stop(device_id)
|
269
|
-
|
270
|
-
|
271
|
-
class SwarmitMonitor:
|
272
|
-
"""Class used to monitor an experiment."""
|
273
|
-
|
274
|
-
def __init__(self, port, baudrate, device_ids):
|
275
|
-
self.logger = LOGGER.bind(context=__name__)
|
276
|
-
self.hdlc_handler = HDLCHandler()
|
277
|
-
self.serial = SerialInterface(port, baudrate, self.on_byte_received)
|
278
|
-
self.last_deviceid_notification = None
|
279
|
-
self.device_ids = device_ids
|
280
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
281
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
282
|
-
|
283
|
-
def on_byte_received(self, byte):
|
284
|
-
if self.hdlc_handler is None:
|
285
|
-
return
|
286
|
-
self.hdlc_handler.handle_byte(byte)
|
287
|
-
if self.hdlc_handler.state == HDLCState.READY:
|
288
|
-
payload = self.hdlc_handler.payload
|
289
|
-
if not payload:
|
290
|
-
return
|
291
|
-
deviceid = int.from_bytes(payload[0:8], byteorder="little")
|
292
|
-
if self.device_ids and deviceid not in self.device_ids:
|
293
|
-
return
|
294
|
-
event = payload[8]
|
295
|
-
timestamp = int.from_bytes(payload[9:13], byteorder="little")
|
296
|
-
data_size = int(payload[13])
|
297
|
-
data = payload[14 : data_size + 14]
|
298
|
-
logger = self.logger.bind(
|
299
|
-
deviceid=hex(deviceid),
|
300
|
-
notification=event,
|
301
|
-
time=timestamp,
|
302
|
-
data_size=data_size,
|
303
|
-
data=data,
|
304
|
-
)
|
305
|
-
if event == NotificationType.SWARMIT_NOTIFICATION_EVENT_GPIO.value:
|
306
|
-
logger.info(f"GPIO event")
|
307
|
-
elif event == NotificationType.SWARMIT_NOTIFICATION_EVENT_LOG.value:
|
308
|
-
logger.info(f"LOG event")
|
309
|
-
|
310
|
-
def monitor(self):
|
311
|
-
while True:
|
312
|
-
time.sleep(0.01)
|
313
|
-
|
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,
|
24
|
+
)
|
314
25
|
|
315
|
-
|
316
|
-
|
26
|
+
SERIAL_PORT_DEFAULT = get_default_port()
|
27
|
+
BAUDRATE_DEFAULT = 1000000
|
317
28
|
|
318
|
-
def __init__(self, port, baudrate):
|
319
|
-
self.logger = LOGGER.bind(context=__name__)
|
320
|
-
self.hdlc_handler = HDLCHandler()
|
321
|
-
self.serial = SerialInterface(port, baudrate, self.on_byte_received)
|
322
|
-
self.last_deviceid_notification = None
|
323
|
-
# Just write a single byte to fake a DotBot gateway handshake
|
324
|
-
self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
|
325
|
-
self.status_data = {}
|
326
|
-
self.resp_ids = []
|
327
|
-
self.table = Table()
|
328
|
-
self.table.add_column("Device ID", style="magenta", no_wrap=True)
|
329
|
-
self.table.add_column("Status", style="green")
|
330
29
|
|
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
|
-
|
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
|
+
),
|
357
66
|
)
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
if self.status_data and display is True:
|
364
|
-
with Live(self.table, refresh_per_second=4) as live:
|
365
|
-
live.update(self.table)
|
366
|
-
for device_id, status in self.status_data.items():
|
367
|
-
if status != StatusType.Off:
|
368
|
-
continue
|
369
|
-
self.table.add_row(device_id, f"[bold red]{status.name}")
|
370
|
-
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]
|
371
72
|
|
372
73
|
|
373
|
-
|
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."""
|
374
84
|
try:
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
return False
|
383
|
-
|
384
|
-
start_time = time.time()
|
385
|
-
print(f"Image size: {len(experiment.firmware)}B")
|
386
|
-
print("")
|
387
|
-
if yes is False:
|
388
|
-
click.confirm("Do you want to continue?", default=True, abort=True)
|
389
|
-
ids = experiment.init(devices)
|
390
|
-
if (devices and sorted(ids) != sorted(devices)) or (
|
391
|
-
not devices and sorted(ids) != sorted(ready_devices)
|
392
|
-
):
|
393
|
-
console = Console()
|
394
|
-
console.print(
|
395
|
-
"[bold red]Error:[/] some acknowledgment are missing "
|
396
|
-
f"({", ".join(sorted(set(ready_devices).difference(set(ids))))}). "
|
397
|
-
"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"]),
|
398
92
|
)
|
399
|
-
|
400
|
-
try:
|
401
|
-
experiment.transfer(devices)
|
402
|
-
except Exception as exc:
|
403
|
-
console = Console()
|
404
|
-
console.print(f"[bold red]Error:[/] transfer of image failed: {exc}")
|
405
|
-
return False
|
406
|
-
print(f"Elapsed: {time.time() - start_time:.3f}s")
|
407
|
-
return True
|
408
|
-
|
409
|
-
|
410
|
-
def swarmit_start(port, baudrate, devices, ready_devices):
|
411
|
-
try:
|
412
|
-
experiment = SwarmitStart(port, baudrate, ready_devices)
|
93
|
+
controller = Controller(settings)
|
413
94
|
except (
|
414
95
|
SerialInterfaceException,
|
415
96
|
serial.serialutil.SerialException,
|
@@ -417,13 +98,44 @@ def swarmit_start(port, baudrate, devices, ready_devices):
|
|
417
98
|
console = Console()
|
418
99
|
console.print(f"[bold red]Error:[/] {exc}")
|
419
100
|
return
|
420
|
-
|
421
|
-
|
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()
|
422
117
|
|
423
118
|
|
424
|
-
|
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."""
|
425
129
|
try:
|
426
|
-
|
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)
|
427
139
|
except (
|
428
140
|
SerialInterfaceException,
|
429
141
|
serial.serialutil.SerialException,
|
@@ -431,104 +143,80 @@ def swarmit_stop(port, baudrate, devices, running_devices):
|
|
431
143
|
console = Console()
|
432
144
|
console.print(f"[bold red]Error:[/] {exc}")
|
433
145
|
return
|
434
|
-
|
435
|
-
|
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()
|
436
171
|
|
437
172
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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.")
|
447
197
|
return
|
448
198
|
try:
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
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)
|
457
208
|
except (
|
458
209
|
SerialInterfaceException,
|
459
210
|
serial.serialutil.SerialException,
|
460
211
|
) as exc:
|
461
212
|
console = Console()
|
462
213
|
console.print(f"[bold red]Error:[/] {exc}")
|
463
|
-
return
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
@click.group()
|
470
|
-
@click.option(
|
471
|
-
"-p",
|
472
|
-
"--port",
|
473
|
-
default=SERIAL_PORT,
|
474
|
-
help=f"Serial port to use to send the bitstream to the gateway. Default: {SERIAL_PORT}.",
|
475
|
-
)
|
476
|
-
@click.option(
|
477
|
-
"-b",
|
478
|
-
"--baudrate",
|
479
|
-
default=BAUDRATE,
|
480
|
-
help=f"Serial port baudrate. Default: {BAUDRATE}.",
|
481
|
-
)
|
482
|
-
@click.option(
|
483
|
-
"-d",
|
484
|
-
"--devices",
|
485
|
-
type=str,
|
486
|
-
default="",
|
487
|
-
help=f"Subset list of devices to interact with, separated with ,",
|
488
|
-
)
|
489
|
-
@click.pass_context
|
490
|
-
def main(ctx, port, baudrate, devices):
|
491
|
-
if ctx.invoked_subcommand != "monitor":
|
492
|
-
# Disable logging if not monitoring
|
493
|
-
structlog.configure(
|
494
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
|
495
|
-
)
|
496
|
-
ctx.ensure_object(dict)
|
497
|
-
ctx.obj["port"] = port
|
498
|
-
ctx.obj["baudrate"] = baudrate
|
499
|
-
ctx.obj["devices"] = [e for e in devices.split(",") if e]
|
500
|
-
|
501
|
-
|
502
|
-
@main.command()
|
503
|
-
@click.pass_context
|
504
|
-
def start(ctx):
|
505
|
-
known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
|
506
|
-
ready_devices = sorted(
|
507
|
-
[
|
508
|
-
device
|
509
|
-
for device, status in known_devices.items()
|
510
|
-
if status == StatusType.Ready
|
511
|
-
]
|
512
|
-
)
|
513
|
-
swarmit_start(
|
514
|
-
ctx.obj["port"], ctx.obj["baudrate"], list(ctx.obj["devices"]), ready_devices
|
515
|
-
)
|
516
|
-
|
517
|
-
|
518
|
-
@main.command()
|
519
|
-
@click.pass_context
|
520
|
-
def stop(ctx):
|
521
|
-
known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
|
522
|
-
running_devices = sorted(
|
523
|
-
[
|
524
|
-
device
|
525
|
-
for device, status in known_devices.items()
|
526
|
-
if status == StatusType.Running
|
527
|
-
]
|
528
|
-
)
|
529
|
-
swarmit_stop(
|
530
|
-
ctx.obj["port"], ctx.obj["baudrate"], list(ctx.obj["devices"]), running_devices
|
531
|
-
)
|
214
|
+
return
|
215
|
+
if not controller.ready_devices:
|
216
|
+
print("No device to reset.")
|
217
|
+
return
|
218
|
+
controller.reset(locations)
|
219
|
+
controller.terminate()
|
532
220
|
|
533
221
|
|
534
222
|
@main.command()
|
@@ -544,55 +232,146 @@ def stop(ctx):
|
|
544
232
|
is_flag=True,
|
545
233
|
help="Start the firmware once flashed.",
|
546
234
|
)
|
547
|
-
@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)
|
548
242
|
@click.pass_context
|
549
|
-
def flash(ctx, yes, start, firmware):
|
243
|
+
def flash(ctx, yes, start, verbose, firmware):
|
244
|
+
"""Flash a firmware to the robots."""
|
550
245
|
console = Console()
|
551
246
|
if firmware is None:
|
552
247
|
console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
|
553
248
|
ctx.exit()
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
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"],
|
561
258
|
)
|
562
|
-
|
259
|
+
controller = Controller(settings)
|
260
|
+
if not controller.ready_devices:
|
261
|
+
console.print("[bold red]Error:[/] No ready devices found. Exiting.")
|
262
|
+
controller.terminate()
|
563
263
|
return
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
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)
|
574
276
|
):
|
575
|
-
|
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
|
+
|
576
300
|
if start is True:
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
ready_devices,
|
301
|
+
started = controller.start()
|
302
|
+
print_start_status(
|
303
|
+
sorted(started),
|
304
|
+
sorted(set(start_data.ids).difference(set(started))),
|
582
305
|
)
|
306
|
+
controller.terminate()
|
583
307
|
|
584
308
|
|
585
309
|
@main.command()
|
586
310
|
@click.pass_context
|
587
311
|
def monitor(ctx):
|
588
|
-
|
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()
|
589
336
|
|
590
337
|
|
591
338
|
@main.command()
|
592
339
|
@click.pass_context
|
593
340
|
def status(ctx):
|
594
|
-
|
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:
|
595
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()
|
596
375
|
|
597
376
|
|
598
377
|
if __name__ == "__main__":
|