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.
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 tqdm import tqdm
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.live import Live
18
- from rich.table import Table
19
-
20
- from dotbot.logger import LOGGER
21
- from dotbot.hdlc import hdlc_encode, HDLCHandler, HDLCState
22
- from dotbot.protocol import PROTOCOL_VERSION
23
- from dotbot.serial_interface import SerialInterface, SerialInterfaceException, get_default_port
24
-
25
-
26
- SERIAL_PORT = get_default_port()
27
- BAUDRATE = 1000000
28
- CHUNK_SIZE = 128
29
- SWARMIT_PREAMBLE = bytes([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07])
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
- class SwarmitStatus:
316
- """Class used to get the status of experiment."""
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
- def on_byte_received(self, byte):
332
- if self.hdlc_handler is None:
333
- return
334
- self.hdlc_handler.handle_byte(byte)
335
- if self.hdlc_handler.state == HDLCState.READY:
336
- payload = self.hdlc_handler.payload
337
- if not payload:
338
- return
339
- deviceid_resp = hex(int.from_bytes(payload[0:8], byteorder="little"))
340
- if deviceid_resp not in self.resp_ids:
341
- self.resp_ids.append(deviceid_resp)
342
- event = payload[8]
343
- if event == NotificationType.SWARMIT_NOTIFICATION_STATUS.value:
344
- status = StatusType(payload[9])
345
- self.status_data[deviceid_resp] = status
346
- self.table.add_row(
347
- deviceid_resp,
348
- f"{"[bold cyan]" if status == StatusType.Running else "[bold green]"}{status.name}",
349
- )
350
-
351
- def status(self, display=True):
352
- buffer = bytearray()
353
- buffer += SWARMIT_PREAMBLE
354
- buffer += int("0", 16).to_bytes(length=8, byteorder="little")
355
- buffer += int(RequestType.SWARMIT_REQ_EXPERIMENT_STATUS.value).to_bytes(
356
- 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
+ ),
357
66
  )
358
- self.serial.write(hdlc_encode(buffer))
359
- timeout = 0 # ms
360
- while timeout < 200:
361
- timeout += 1
362
- time.sleep(0.0001)
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
- def swarmit_flash(port, baudrate, firmware, yes, devices, ready_devices):
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
- experiment = SwarmitFlash(port, baudrate, firmware, ready_devices)
376
- except (
377
- SerialInterfaceException,
378
- serial.serialutil.SerialException,
379
- ) as exc:
380
- console = Console()
381
- console.print("[bold red]Error:[/] {exc}")
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
- return False
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
- experiment.start(devices)
421
- print("Experiment started.")
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
- def swarmit_stop(port, baudrate, devices, running_devices):
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
- experiment = SwarmitStop(port, baudrate, running_devices)
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
- experiment.stop(devices)
435
- print("Experiment stopped.")
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
- def swarmit_monitor(port, baudrate, devices):
439
- try:
440
- experiment = SwarmitMonitor(port, baudrate, devices)
441
- except (
442
- SerialInterfaceException,
443
- serial.serialutil.SerialException,
444
- ) as exc:
445
- console = Console()
446
- console.print(f"[bold red]Error:[/] {exc}")
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
- experiment.monitor()
450
- except KeyboardInterrupt:
451
- print("Stopping monitor.")
452
-
453
-
454
- def swarmit_status(port, baudrate, display=True):
455
- try:
456
- experiment = SwarmitStatus(port, baudrate)
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
- result = experiment.status(display)
465
- experiment.serial.stop()
466
- return result
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.argument("firmware", type=click.File(mode="rb", lazy=True), required=False)
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
- known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
555
- ready_devices = sorted(
556
- [
557
- device
558
- for device, status in known_devices.items()
559
- if status == StatusType.Ready
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
- if not ready_devices:
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
- if (
565
- swarmit_flash(
566
- ctx.obj["port"],
567
- ctx.obj["baudrate"],
568
- firmware,
569
- yes,
570
- list(ctx.obj["devices"]),
571
- ready_devices,
572
- )
573
- is False
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
- return
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
- swarmit_start(
578
- ctx.obj["port"],
579
- ctx.obj["baudrate"],
580
- list(ctx.obj["devices"]),
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
- swarmit_monitor(ctx.obj["port"], ctx.obj["baudrate"], ctx.obj["devices"])
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
- if not swarmit_status(ctx.obj["port"], ctx.obj["baudrate"]):
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__":