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.
- swarmit-0.1.0/.gitignore +16 -0
- swarmit-0.1.0/LICENSE +28 -0
- swarmit-0.1.0/PKG-INFO +79 -0
- swarmit-0.1.0/README.md +55 -0
- swarmit-0.1.0/pyproject.toml +57 -0
- swarmit-0.1.0/testbed/cli/main.py +599 -0
swarmit-0.1.0/.gitignore
ADDED
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
|
+
```
|
swarmit-0.1.0/README.md
ADDED
@@ -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={})
|