swarmit 0.2.0__py3-none-any.whl → 0.4.4__py3-none-any.whl

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