swarmit 0.3.0__tar.gz → 0.4.5__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.3.0 → swarmit-0.4.5}/.gitignore +6 -0
- swarmit-0.4.5/PKG-INFO +128 -0
- swarmit-0.4.5/README.md +103 -0
- {swarmit-0.3.0 → swarmit-0.4.5}/pyproject.toml +8 -5
- swarmit-0.4.5/testbed/cli/main.py +348 -0
- swarmit-0.4.5/testbed/swarmit/__init__.py +1 -0
- swarmit-0.4.5/testbed/swarmit/adapter.py +142 -0
- swarmit-0.4.5/testbed/swarmit/controller.py +664 -0
- {swarmit-0.3.0 → swarmit-0.4.5}/testbed/swarmit/protocol.py +38 -99
- swarmit-0.3.0/PKG-INFO +0 -101
- swarmit-0.3.0/README.md +0 -75
- swarmit-0.3.0/testbed/cli/main.py +0 -378
- swarmit-0.3.0/testbed/swarmit/__init__.py +0 -1
- swarmit-0.3.0/testbed/swarmit/adapter.py +0 -94
- swarmit-0.3.0/testbed/swarmit/controller.py +0 -620
- {swarmit-0.3.0 → swarmit-0.4.5}/AUTHORS +0 -0
- {swarmit-0.3.0 → swarmit-0.4.5}/LICENSE +0 -0
- {swarmit-0.3.0 → swarmit-0.4.5}/dotbot-firmware/doc/sphinx/conf.py +0 -0
swarmit-0.4.5/PKG-INFO
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: swarmit
|
3
|
+
Version: 0.4.5
|
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: AUTHORS
|
10
|
+
License-File: LICENSE
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
12
|
+
Classifier: Operating System :: MacOS
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
16
|
+
Requires-Python: >=3.7
|
17
|
+
Requires-Dist: click==8.1.7
|
18
|
+
Requires-Dist: cryptography==43.0.1
|
19
|
+
Requires-Dist: marilib-pkg>=0.6.0
|
20
|
+
Requires-Dist: pydotbot>=0.24.1
|
21
|
+
Requires-Dist: rich==14.0.0
|
22
|
+
Requires-Dist: structlog==24.4.0
|
23
|
+
Requires-Dist: tqdm==4.66.5
|
24
|
+
Description-Content-Type: text/markdown
|
25
|
+
|
26
|
+
# SwarmIT
|
27
|
+
|
28
|
+
SwarmIT provides a embedded C port for nRF53 as well as Python based services to
|
29
|
+
easily build and deploy a robotic swarm infrastructure testbed.
|
30
|
+
ARM TrustZone is used to create a sandboxed user environment on each device
|
31
|
+
under test, without requiring a control co-processor attached to it.
|
32
|
+
|
33
|
+
<video src="https://github.com/user-attachments/assets/eff63b07-216a-41fb-9062-2e0e56f03c20" type="video/mp4" controls width="100%">
|
34
|
+
</video>
|
35
|
+
|
36
|
+
## Features
|
37
|
+
|
38
|
+
- Experiment management: start, stop, monitor and status check
|
39
|
+
- Deploy a custom firmware on all or on a subset of robots of a swarm testbed
|
40
|
+
- Resilient robot state: even when crashed by buggy user code, the robot can be reprogrammed remotely and wirelessly
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
### Get the code
|
45
|
+
|
46
|
+
Swarmit depends on the [DotBot-firmware](https://github.com/DotBots/DotBot-firmware)
|
47
|
+
and [Mari](https://github.com/DotBots/mari) repositories. They are included
|
48
|
+
in the codebase as [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules).
|
49
|
+
|
50
|
+
Use the following command to clone the Swarmit codebase locally:
|
51
|
+
|
52
|
+
```
|
53
|
+
git clone --recurse-submodules https://github.com/DotBots/swarmit.git
|
54
|
+
```
|
55
|
+
|
56
|
+
### Embedded C code
|
57
|
+
|
58
|
+
SwarmIT embedded C code can be built using
|
59
|
+
[Segger Embedded Studio (SES)](https://www.segger.com/products/development-tools/embedded-studio/).
|
60
|
+
Use Tools > Package manager to install the CMSIS 5 CMSIS-CORE, CMSIS-DSP and nRF packages.
|
61
|
+
|
62
|
+
To provision a device, follow the following steps:
|
63
|
+
1. open [netcore.emProject](swarmit-netcore.emProject)
|
64
|
+
and [bootloader.emProject](swarmit-bootloader-dotbot-v3.emProject)
|
65
|
+
(or [bootloader.emProject](swarmit-bootloader-dotbot-v2.emProject) depending on
|
66
|
+
your robot version) in SES
|
67
|
+
2. build and load the netcore application on the nRF53 network core,
|
68
|
+
3. build and load the bootloader application on the nRF53 application core.
|
69
|
+
|
70
|
+
The device is now ready.
|
71
|
+
|
72
|
+
### Gateway
|
73
|
+
|
74
|
+
The communication between the computer and the swarm devices is performed via a
|
75
|
+
gateway board connected via USB to the computer.
|
76
|
+
|
77
|
+
This gateway uses the [mari](https://github.com/dotbots/mari) network stack and
|
78
|
+
must run the Mari gateway firmware.
|
79
|
+
|
80
|
+
The documentation to setup a Mari gateway is located
|
81
|
+
[here](https://github.com/DotBots/mari/wiki/Getting-started#running-mari-network-on-your-computer).
|
82
|
+
|
83
|
+
### Python CLI script
|
84
|
+
|
85
|
+
The Python CLI script provides commands for flashing, starting and stopping user
|
86
|
+
code on the device, as well as monitoring and checking the status of devices
|
87
|
+
in the swarm.
|
88
|
+
|
89
|
+
The Python CLI script connects via a virtual COM port to the gateway connected to
|
90
|
+
the computer.
|
91
|
+
|
92
|
+
The Python CLI script is available on PyPI. Install it using:
|
93
|
+
|
94
|
+
```
|
95
|
+
pip install swarmit
|
96
|
+
```
|
97
|
+
|
98
|
+
Print usage using `swarmit --help`:
|
99
|
+
|
100
|
+
```
|
101
|
+
Usage: swarmit [OPTIONS] COMMAND [ARGS]...
|
102
|
+
|
103
|
+
Options:
|
104
|
+
-p, --port TEXT Serial port to use to send the bitstream to
|
105
|
+
the gateway. Default: /dev/ttyACM0.
|
106
|
+
-b, --baudrate INTEGER Serial port baudrate. Default: 1000000.
|
107
|
+
-H, --mqtt-host TEXT MQTT host. Default: localhost.
|
108
|
+
-P, --mqtt-port INTEGER MQTT port. Default: 1883.
|
109
|
+
-T, --mqtt-use_tls Use TLS with MQTT.
|
110
|
+
-n, --network-id INTEGER Marilib network ID to use. Default: 1
|
111
|
+
-a, --adapter [edge|cloud]
|
112
|
+
Choose the adapter to communicate with the
|
113
|
+
gateway. [default: edge]
|
114
|
+
-d, --devices TEXT Subset list of devices to interact with,
|
115
|
+
separated with ,
|
116
|
+
-v, --verbose Enable verbose mode.
|
117
|
+
-V, --version Show the version and exit.
|
118
|
+
-h, --help Show this message and exit.
|
119
|
+
|
120
|
+
Commands:
|
121
|
+
flash Flash a firmware to the robots.
|
122
|
+
message Send a custom text message to the robots.
|
123
|
+
monitor Monitor running applications.
|
124
|
+
reset Reset robots locations.
|
125
|
+
start Start the user application.
|
126
|
+
status Print current status of the robots.
|
127
|
+
stop Stop the user application.
|
128
|
+
```
|
swarmit-0.4.5/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
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
|
+
<video src="https://github.com/user-attachments/assets/eff63b07-216a-41fb-9062-2e0e56f03c20" type="video/mp4" controls width="100%">
|
9
|
+
</video>
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
- Experiment management: start, stop, monitor and status check
|
14
|
+
- Deploy a custom firmware on all or on a subset of robots of a swarm testbed
|
15
|
+
- Resilient robot state: even when crashed by buggy user code, the robot can be reprogrammed remotely and wirelessly
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Get the code
|
20
|
+
|
21
|
+
Swarmit depends on the [DotBot-firmware](https://github.com/DotBots/DotBot-firmware)
|
22
|
+
and [Mari](https://github.com/DotBots/mari) repositories. They are included
|
23
|
+
in the codebase as [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules).
|
24
|
+
|
25
|
+
Use the following command to clone the Swarmit codebase locally:
|
26
|
+
|
27
|
+
```
|
28
|
+
git clone --recurse-submodules https://github.com/DotBots/swarmit.git
|
29
|
+
```
|
30
|
+
|
31
|
+
### Embedded C code
|
32
|
+
|
33
|
+
SwarmIT embedded C code can be built using
|
34
|
+
[Segger Embedded Studio (SES)](https://www.segger.com/products/development-tools/embedded-studio/).
|
35
|
+
Use Tools > Package manager to install the CMSIS 5 CMSIS-CORE, CMSIS-DSP and nRF packages.
|
36
|
+
|
37
|
+
To provision a device, follow the following steps:
|
38
|
+
1. open [netcore.emProject](swarmit-netcore.emProject)
|
39
|
+
and [bootloader.emProject](swarmit-bootloader-dotbot-v3.emProject)
|
40
|
+
(or [bootloader.emProject](swarmit-bootloader-dotbot-v2.emProject) depending on
|
41
|
+
your robot version) in SES
|
42
|
+
2. build and load the netcore application on the nRF53 network core,
|
43
|
+
3. build and load the bootloader application on the nRF53 application core.
|
44
|
+
|
45
|
+
The device is now ready.
|
46
|
+
|
47
|
+
### Gateway
|
48
|
+
|
49
|
+
The communication between the computer and the swarm devices is performed via a
|
50
|
+
gateway board connected via USB to the computer.
|
51
|
+
|
52
|
+
This gateway uses the [mari](https://github.com/dotbots/mari) network stack and
|
53
|
+
must run the Mari gateway firmware.
|
54
|
+
|
55
|
+
The documentation to setup a Mari gateway is located
|
56
|
+
[here](https://github.com/DotBots/mari/wiki/Getting-started#running-mari-network-on-your-computer).
|
57
|
+
|
58
|
+
### Python CLI script
|
59
|
+
|
60
|
+
The Python CLI script provides commands for flashing, starting and stopping user
|
61
|
+
code on the device, as well as monitoring and checking the status of devices
|
62
|
+
in the swarm.
|
63
|
+
|
64
|
+
The Python CLI script connects via a virtual COM port to the gateway connected to
|
65
|
+
the computer.
|
66
|
+
|
67
|
+
The Python CLI script is available on PyPI. Install it using:
|
68
|
+
|
69
|
+
```
|
70
|
+
pip install swarmit
|
71
|
+
```
|
72
|
+
|
73
|
+
Print usage using `swarmit --help`:
|
74
|
+
|
75
|
+
```
|
76
|
+
Usage: swarmit [OPTIONS] COMMAND [ARGS]...
|
77
|
+
|
78
|
+
Options:
|
79
|
+
-p, --port TEXT Serial port to use to send the bitstream to
|
80
|
+
the gateway. Default: /dev/ttyACM0.
|
81
|
+
-b, --baudrate INTEGER Serial port baudrate. Default: 1000000.
|
82
|
+
-H, --mqtt-host TEXT MQTT host. Default: localhost.
|
83
|
+
-P, --mqtt-port INTEGER MQTT port. Default: 1883.
|
84
|
+
-T, --mqtt-use_tls Use TLS with MQTT.
|
85
|
+
-n, --network-id INTEGER Marilib network ID to use. Default: 1
|
86
|
+
-a, --adapter [edge|cloud]
|
87
|
+
Choose the adapter to communicate with the
|
88
|
+
gateway. [default: edge]
|
89
|
+
-d, --devices TEXT Subset list of devices to interact with,
|
90
|
+
separated with ,
|
91
|
+
-v, --verbose Enable verbose mode.
|
92
|
+
-V, --version Show the version and exit.
|
93
|
+
-h, --help Show this message and exit.
|
94
|
+
|
95
|
+
Commands:
|
96
|
+
flash Flash a firmware to the robots.
|
97
|
+
message Send a custom text message to the robots.
|
98
|
+
monitor Monitor running applications.
|
99
|
+
reset Reset robots locations.
|
100
|
+
start Start the user application.
|
101
|
+
status Print current status of the robots.
|
102
|
+
stop Stop the user application.
|
103
|
+
```
|
@@ -16,6 +16,9 @@ exclude = [
|
|
16
16
|
[tool.hatch.version]
|
17
17
|
path = "testbed/swarmit/__init__.py"
|
18
18
|
|
19
|
+
[tool.hatch.metadata]
|
20
|
+
allow-direct-references = true
|
21
|
+
|
19
22
|
[project]
|
20
23
|
name = "swarmit"
|
21
24
|
dynamic = ["version"]
|
@@ -25,12 +28,11 @@ authors = [
|
|
25
28
|
dependencies = [
|
26
29
|
"click == 8.1.7",
|
27
30
|
"cryptography == 43.0.1",
|
28
|
-
"pydotbot
|
29
|
-
"
|
30
|
-
"rich == 13.8.1",
|
31
|
+
"pydotbot >= 0.24.1",
|
32
|
+
"rich == 14.0.0",
|
31
33
|
"structlog == 24.4.0",
|
32
34
|
"tqdm == 4.66.5",
|
33
|
-
"
|
35
|
+
"marilib-pkg >= 0.6.0",
|
34
36
|
]
|
35
37
|
description = "Run Your Own Robot Swarm Testbed."
|
36
38
|
readme = "README.md"
|
@@ -54,7 +56,8 @@ swarmit = "testbed.cli.main:main"
|
|
54
56
|
[tool.ruff]
|
55
57
|
lint.select = ["E", "F"]
|
56
58
|
line-length = 88
|
57
|
-
lint.ignore = ["E501"]
|
59
|
+
lint.ignore = ["E501", "E722"]
|
60
|
+
exclude = ["dotbot-firmware"]
|
58
61
|
|
59
62
|
[tool.isort]
|
60
63
|
multi_line_output = 3 # Use Vertical Hanging Indent
|
@@ -0,0 +1,348 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
|
6
|
+
import click
|
7
|
+
import serial
|
8
|
+
import structlog
|
9
|
+
from dotbot.serial_interface import SerialInterfaceException, get_default_port
|
10
|
+
from rich import print
|
11
|
+
from rich.console import Console
|
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,
|
23
|
+
)
|
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"
|
32
|
+
|
33
|
+
|
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
|
+
),
|
117
|
+
)
|
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
|
+
)
|
130
|
+
|
131
|
+
|
132
|
+
@main.command()
|
133
|
+
@click.pass_context
|
134
|
+
def start(ctx):
|
135
|
+
"""Start the user application."""
|
136
|
+
try:
|
137
|
+
controller = Controller(ctx.obj["settings"])
|
138
|
+
except (
|
139
|
+
SerialInterfaceException,
|
140
|
+
serial.serialutil.SerialException,
|
141
|
+
) as exc:
|
142
|
+
console = Console()
|
143
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
144
|
+
return
|
145
|
+
if controller.ready_devices:
|
146
|
+
controller.start()
|
147
|
+
else:
|
148
|
+
print("No device to start")
|
149
|
+
controller.terminate()
|
150
|
+
|
151
|
+
|
152
|
+
@main.command()
|
153
|
+
@click.pass_context
|
154
|
+
def stop(ctx):
|
155
|
+
"""Stop the user application."""
|
156
|
+
try:
|
157
|
+
controller = Controller(ctx.obj["settings"])
|
158
|
+
except (
|
159
|
+
SerialInterfaceException,
|
160
|
+
serial.serialutil.SerialException,
|
161
|
+
) as exc:
|
162
|
+
console = Console()
|
163
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
164
|
+
return
|
165
|
+
if controller.running_devices or controller.resetting_devices:
|
166
|
+
controller.stop()
|
167
|
+
else:
|
168
|
+
print("[bold]No device to stop[/]")
|
169
|
+
controller.terminate()
|
170
|
+
|
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.
|
180
|
+
|
181
|
+
Locations are provided as '<device_addr>:<x>,<y>-<device_addr>:<x>,<y>|...'
|
182
|
+
"""
|
183
|
+
try:
|
184
|
+
controller = Controller(ctx.obj["settings"])
|
185
|
+
except (
|
186
|
+
SerialInterfaceException,
|
187
|
+
serial.serialutil.SerialException,
|
188
|
+
) as exc:
|
189
|
+
console = Console()
|
190
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
191
|
+
return
|
192
|
+
|
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),
|
201
|
+
)
|
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()
|
212
|
+
|
213
|
+
|
214
|
+
@main.command()
|
215
|
+
@click.option(
|
216
|
+
"-y",
|
217
|
+
"--yes",
|
218
|
+
is_flag=True,
|
219
|
+
help="Flash the firmware without prompt.",
|
220
|
+
)
|
221
|
+
@click.option(
|
222
|
+
"-s",
|
223
|
+
"--start",
|
224
|
+
is_flag=True,
|
225
|
+
help="Start the firmware once flashed.",
|
226
|
+
)
|
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)
|
244
|
+
@click.pass_context
|
245
|
+
def flash(ctx, yes, start, ota_timeout, ota_max_retries, firmware):
|
246
|
+
"""Flash a firmware to the robots."""
|
247
|
+
console = Console()
|
248
|
+
if firmware is None:
|
249
|
+
console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
|
250
|
+
ctx.exit()
|
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()
|
258
|
+
return
|
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."
|
276
|
+
)
|
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
|
+
|
301
|
+
if start is True:
|
302
|
+
time.sleep(1)
|
303
|
+
controller.start()
|
304
|
+
controller.terminate()
|
305
|
+
|
306
|
+
|
307
|
+
@main.command()
|
308
|
+
@click.pass_context
|
309
|
+
def monitor(ctx):
|
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()
|
326
|
+
|
327
|
+
|
328
|
+
@main.command()
|
329
|
+
@click.pass_context
|
330
|
+
def status(ctx):
|
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()
|
345
|
+
|
346
|
+
|
347
|
+
if __name__ == "__main__":
|
348
|
+
main(obj={})
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.4.5"
|