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.
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 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, ProtocolHeader
23
- from dotbot.serial_interface import (
24
- SerialInterface,
25
- SerialInterfaceException,
26
- get_default_port,
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
- 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
-
331
- def __init__(self, port, baudrate):
332
- self.logger = LOGGER.bind(context=__name__)
333
- self.hdlc_handler = HDLCHandler()
334
- self.last_deviceid_notification = None
335
- self.status_data = {}
336
- self.resp_ids = []
337
- self.table = Table()
338
- self.table.add_column("Device ID", style="magenta", no_wrap=True)
339
- self.table.add_column("Status", style="green")
340
- self.serial = SerialInterface(port, baudrate, self.on_byte_received)
341
- # Just write a single byte to fake a DotBot gateway handshake
342
- self.serial.write(int(PROTOCOL_VERSION).to_bytes(length=1))
343
-
344
- def on_byte_received(self, byte):
345
- if self.hdlc_handler is None:
346
- return
347
- self.hdlc_handler.handle_byte(byte)
348
- if self.hdlc_handler.state == HDLCState.READY:
349
- payload = self.hdlc_handler.payload[25:]
350
- if not payload:
351
- return
352
- deviceid_resp = hex(int.from_bytes(payload[0:8], byteorder="little"))
353
- if deviceid_resp not in self.resp_ids:
354
- self.resp_ids.append(deviceid_resp)
355
- event = payload[8]
356
- if event == NotificationType.SWARMIT_NOTIFICATION_STATUS.value:
357
- status = StatusType(payload[9])
358
- self.status_data[deviceid_resp] = status
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
- self.serial.write(hdlc_encode(buffer))
372
- timeout = 0 # ms
373
- while timeout < 2000:
374
- timeout += 1
375
- time.sleep(0.0001)
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
- 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."""
387
84
  try:
388
- experiment = SwarmitFlash(port, baudrate, firmware, ready_devices)
389
- except (
390
- SerialInterfaceException,
391
- serial.serialutil.SerialException,
392
- ) as exc:
393
- console = Console()
394
- console.print("[bold red]Error:[/] {exc}")
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
- return False
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
- experiment.start(devices)
434
- 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()
435
117
 
436
118
 
437
- 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."""
438
129
  try:
439
- 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)
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
- experiment.stop(devices)
448
- 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()
449
171
 
450
172
 
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}")
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
- experiment.monitor()
463
- except KeyboardInterrupt:
464
- print("Stopping monitor.")
465
-
466
-
467
- def swarmit_status(port, baudrate, display=True):
468
- try:
469
- 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)
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
- result = experiment.status(display)
478
- experiment.serial.stop()
479
- return result
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.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)
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
- known_devices = swarmit_status(ctx.obj["port"], ctx.obj["baudrate"], display=False)
568
- ready_devices = sorted(
569
- [
570
- device
571
- for device, status in known_devices.items()
572
- if status == StatusType.Ready
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
- 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()
576
263
  return
577
- if (
578
- swarmit_flash(
579
- ctx.obj["port"],
580
- ctx.obj["baudrate"],
581
- firmware,
582
- yes,
583
- list(ctx.obj["devices"]),
584
- ready_devices,
585
- )
586
- 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)
587
276
  ):
588
- 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
+
589
300
  if start is True:
590
- swarmit_start(
591
- ctx.obj["port"],
592
- ctx.obj["baudrate"],
593
- list(ctx.obj["devices"]),
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
- 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()
602
336
 
603
337
 
604
338
  @main.command()
605
339
  @click.pass_context
606
340
  def status(ctx):
607
- 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:
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__":