swarmit 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ # Segger Studio specific files
2
+
3
+ *.emSession
4
+ *.jlink
5
+
6
+ # Python compiled files
7
+ *.pyc
8
+
9
+ # Git conflict files
10
+ *.orig
11
+
12
+ # Visual Studio specific files
13
+ .vscode/
14
+
15
+ # Segger Studio output directory
16
+ Output/
swarmit-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Inria
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
swarmit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.3
2
+ Name: swarmit
3
+ Version: 0.1.0
4
+ Summary: Run Your Own Robot Swarm Testbed.
5
+ Project-URL: Homepage, https://github.com/DotBots/swarmit
6
+ Project-URL: Bug Tracker, https://github.com/DotBots/swarmit/issues
7
+ Author-email: Alexandre Abadie <alexandre.abadie@inria.fr>
8
+ License: BSD
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.7
16
+ Requires-Dist: click==8.1.7
17
+ Requires-Dist: cryptography==43.0.1
18
+ Requires-Dist: pydotbot==0.21.2
19
+ Requires-Dist: pyserial==3.5
20
+ Requires-Dist: rich==13.8.1
21
+ Requires-Dist: structlog==24.4.0
22
+ Requires-Dist: tqdm==4.66.5
23
+ Description-Content-Type: text/markdown
24
+
25
+ # SwarmIT
26
+
27
+ SwarmIT provides a embedded C port for nRF53 as well as Python based services to
28
+ easily build and deploy a robotic swarm infrastructure testbed.
29
+ ARM TrustZone is used to create a sandboxed user environment on each device
30
+ under test, without requiring a control co-processor attached to it.
31
+
32
+ ## Features
33
+
34
+ - Experiment management: start, stop, monitor and status check
35
+ - Deploy a custom firmware on all or on a subset of robots of a swarm testbed
36
+ - Resilient robot state: even when crashed by buggy user code, the robot can be reprogrammed remotely and wirelessly
37
+
38
+ ## Usage
39
+
40
+ ### Embedded C code
41
+
42
+ SwarmIT embedded C code can be built using
43
+ [Segger Embedded Studio (SES)](https://www.segger.com/products/development-tools/embedded-studio/).
44
+
45
+ To provision a device, follow the following steps:
46
+ 1. open [netcore.emProject](device/network_core/netcore.emProject)
47
+ and [bootloader.emProject](device/bootloader/bootloader.emProject) in SES
48
+ 2. build and load the netcore application on the nRF53 network core,
49
+ 3. build and load the bootloader application on the nRF53 application core.
50
+
51
+ The device is now ready.
52
+
53
+ ### Python CLI script
54
+
55
+ The Python CLI script provides commands for flashing, starting and stopping user
56
+ code on the device, as well as monitoring and checking the status of devices
57
+ in the swarm.
58
+
59
+ Default usage:
60
+
61
+ ```
62
+ swarmit --help
63
+ Usage: swarmit [OPTIONS] COMMAND [ARGS]...
64
+
65
+ Options:
66
+ -p, --port TEXT Serial port to use to send the bitstream to the
67
+ gateway. Default: /dev/ttyACM0.
68
+ -b, --baudrate INTEGER Serial port baudrate. Default: 1000000.
69
+ -d, --devices TEXT Subset list of devices to interact with, separated
70
+ with ,
71
+ --help Show this message and exit.
72
+
73
+ Commands:
74
+ flash
75
+ monitor
76
+ start
77
+ status
78
+ stop
79
+ ```
@@ -0,0 +1,55 @@
1
+ # SwarmIT
2
+
3
+ SwarmIT provides a embedded C port for nRF53 as well as Python based services to
4
+ easily build and deploy a robotic swarm infrastructure testbed.
5
+ ARM TrustZone is used to create a sandboxed user environment on each device
6
+ under test, without requiring a control co-processor attached to it.
7
+
8
+ ## Features
9
+
10
+ - Experiment management: start, stop, monitor and status check
11
+ - Deploy a custom firmware on all or on a subset of robots of a swarm testbed
12
+ - Resilient robot state: even when crashed by buggy user code, the robot can be reprogrammed remotely and wirelessly
13
+
14
+ ## Usage
15
+
16
+ ### Embedded C code
17
+
18
+ SwarmIT embedded C code can be built using
19
+ [Segger Embedded Studio (SES)](https://www.segger.com/products/development-tools/embedded-studio/).
20
+
21
+ To provision a device, follow the following steps:
22
+ 1. open [netcore.emProject](device/network_core/netcore.emProject)
23
+ and [bootloader.emProject](device/bootloader/bootloader.emProject) in SES
24
+ 2. build and load the netcore application on the nRF53 network core,
25
+ 3. build and load the bootloader application on the nRF53 application core.
26
+
27
+ The device is now ready.
28
+
29
+ ### Python CLI script
30
+
31
+ The Python CLI script provides commands for flashing, starting and stopping user
32
+ code on the device, as well as monitoring and checking the status of devices
33
+ in the swarm.
34
+
35
+ Default usage:
36
+
37
+ ```
38
+ swarmit --help
39
+ Usage: swarmit [OPTIONS] COMMAND [ARGS]...
40
+
41
+ Options:
42
+ -p, --port TEXT Serial port to use to send the bitstream to the
43
+ gateway. Default: /dev/ttyACM0.
44
+ -b, --baudrate INTEGER Serial port baudrate. Default: 1000000.
45
+ -d, --devices TEXT Subset list of devices to interact with, separated
46
+ with ,
47
+ --help Show this message and exit.
48
+
49
+ Commands:
50
+ flash
51
+ monitor
52
+ start
53
+ status
54
+ stop
55
+ ```
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = [
3
+ "hatchling>=1.4.1",
4
+ ]
5
+ build-backend = "hatchling.build"
6
+
7
+ [tool.hatch.build]
8
+ include = [
9
+ "*.py"
10
+ ]
11
+ exclude = [
12
+ "device/",
13
+ "sample/",
14
+ ]
15
+
16
+ [project]
17
+ name = "swarmit"
18
+ version = "0.1.0"
19
+ authors = [
20
+ { name="Alexandre Abadie", email="alexandre.abadie@inria.fr" },
21
+ ]
22
+ dependencies = [
23
+ "click == 8.1.7",
24
+ "cryptography == 43.0.1",
25
+ "pydotbot == 0.21.2",
26
+ "pyserial == 3.5",
27
+ "rich == 13.8.1",
28
+ "structlog == 24.4.0",
29
+ "tqdm == 4.66.5",
30
+ ]
31
+ description = "Run Your Own Robot Swarm Testbed."
32
+ readme = "README.md"
33
+ license = { text="BSD" }
34
+ requires-python = ">=3.7"
35
+ classifiers = [
36
+ "Programming Language :: Python :: 3",
37
+ "License :: OSI Approved :: BSD License",
38
+ "Operating System :: MacOS",
39
+ "Operating System :: POSIX :: Linux",
40
+ "Operating System :: Microsoft :: Windows",
41
+ ]
42
+
43
+ [project.urls]
44
+ "Homepage" = "https://github.com/DotBots/swarmit"
45
+ "Bug Tracker" = "https://github.com/DotBots/swarmit/issues"
46
+
47
+ [project.scripts]
48
+ swarmit = "testbed.cli.main:main"
49
+
50
+ [tool.ruff]
51
+ select = ["E", "F"]
52
+ line-length = 88
53
+ ignore = ["E501"]
54
+
55
+ [tool.isort]
56
+ multi_line_output = 3 # Use Vertical Hanging Indent
57
+ profile = "black"
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env python
2
+
3
+ import logging
4
+ import time
5
+
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+ import click
10
+ import serial
11
+ import structlog
12
+
13
+ from tqdm import tqdm
14
+ from cryptography.hazmat.primitives import hashes
15
+
16
+ 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
+
314
+
315
+ class SwarmitStatus:
316
+ """Class used to get the status of experiment."""
317
+
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
+
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"
357
+ )
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
371
+
372
+
373
+ def swarmit_flash(port, baudrate, firmware, yes, devices, ready_devices):
374
+ 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."
398
+ )
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)
413
+ except (
414
+ SerialInterfaceException,
415
+ serial.serialutil.SerialException,
416
+ ) as exc:
417
+ console = Console()
418
+ console.print(f"[bold red]Error:[/] {exc}")
419
+ return
420
+ experiment.start(devices)
421
+ print("Experiment started.")
422
+
423
+
424
+ def swarmit_stop(port, baudrate, devices, running_devices):
425
+ try:
426
+ experiment = SwarmitStop(port, baudrate, running_devices)
427
+ except (
428
+ SerialInterfaceException,
429
+ serial.serialutil.SerialException,
430
+ ) as exc:
431
+ console = Console()
432
+ console.print(f"[bold red]Error:[/] {exc}")
433
+ return
434
+ experiment.stop(devices)
435
+ print("Experiment stopped.")
436
+
437
+
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}")
447
+ return
448
+ 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)
457
+ except (
458
+ SerialInterfaceException,
459
+ serial.serialutil.SerialException,
460
+ ) as exc:
461
+ console = Console()
462
+ 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
+ )
532
+
533
+
534
+ @main.command()
535
+ @click.option(
536
+ "-y",
537
+ "--yes",
538
+ is_flag=True,
539
+ help="Flash the firmware without prompt.",
540
+ )
541
+ @click.option(
542
+ "-s",
543
+ "--start",
544
+ is_flag=True,
545
+ help="Start the firmware once flashed.",
546
+ )
547
+ @click.argument("firmware", type=click.File(mode="rb", lazy=True), required=False)
548
+ @click.pass_context
549
+ def flash(ctx, yes, start, firmware):
550
+ console = Console()
551
+ if firmware is None:
552
+ console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
553
+ 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
+ ]
561
+ )
562
+ if not ready_devices:
563
+ 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
574
+ ):
575
+ return
576
+ if start is True:
577
+ swarmit_start(
578
+ ctx.obj["port"],
579
+ ctx.obj["baudrate"],
580
+ list(ctx.obj["devices"]),
581
+ ready_devices,
582
+ )
583
+
584
+
585
+ @main.command()
586
+ @click.pass_context
587
+ def monitor(ctx):
588
+ swarmit_monitor(ctx.obj["port"], ctx.obj["baudrate"], ctx.obj["devices"])
589
+
590
+
591
+ @main.command()
592
+ @click.pass_context
593
+ def status(ctx):
594
+ if not swarmit_status(ctx.obj["port"], ctx.obj["baudrate"]):
595
+ click.echo("No devices found.")
596
+
597
+
598
+ if __name__ == "__main__":
599
+ main(obj={})