swarmit 0.2.0__tar.gz → 0.3.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.2.0 → swarmit-0.3.0}/PKG-INFO +4 -2
- swarmit-0.3.0/dotbot-firmware/doc/sphinx/conf.py +191 -0
- {swarmit-0.2.0 → swarmit-0.3.0}/pyproject.toml +15 -5
- swarmit-0.3.0/testbed/cli/main.py +378 -0
- swarmit-0.3.0/testbed/swarmit/__init__.py +1 -0
- swarmit-0.3.0/testbed/swarmit/adapter.py +94 -0
- swarmit-0.3.0/testbed/swarmit/controller.py +620 -0
- swarmit-0.3.0/testbed/swarmit/protocol.py +292 -0
- swarmit-0.2.0/testbed/cli/main.py +0 -612
- {swarmit-0.2.0 → swarmit-0.3.0}/.gitignore +0 -0
- {swarmit-0.2.0 → swarmit-0.3.0}/AUTHORS +0 -0
- {swarmit-0.2.0 → swarmit-0.3.0}/LICENSE +0 -0
- {swarmit-0.2.0 → swarmit-0.3.0}/README.md +0 -0
@@ -1,10 +1,11 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: swarmit
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Run Your Own Robot Swarm Testbed.
|
5
5
|
Project-URL: Homepage, https://github.com/DotBots/swarmit
|
6
6
|
Project-URL: Bug Tracker, https://github.com/DotBots/swarmit/issues
|
7
7
|
Author-email: Alexandre Abadie <alexandre.abadie@inria.fr>
|
8
|
+
License: BSD
|
8
9
|
License-File: AUTHORS
|
9
10
|
License-File: LICENSE
|
10
11
|
Classifier: License :: OSI Approved :: BSD License
|
@@ -15,6 +16,7 @@ Classifier: Programming Language :: Python :: 3
|
|
15
16
|
Requires-Python: >=3.7
|
16
17
|
Requires-Dist: click==8.1.7
|
17
18
|
Requires-Dist: cryptography==43.0.1
|
19
|
+
Requires-Dist: paho-mqtt>=2.1.0
|
18
20
|
Requires-Dist: pydotbot==0.22.0
|
19
21
|
Requires-Dist: pyserial==3.5
|
20
22
|
Requires-Dist: rich==13.8.1
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
2
|
+
|
3
|
+
import glob
|
4
|
+
import os
|
5
|
+
import shutil
|
6
|
+
import subprocess
|
7
|
+
import sys
|
8
|
+
|
9
|
+
|
10
|
+
project = 'DotBot-firmware'
|
11
|
+
copyright = '2023, Inria'
|
12
|
+
author = 'Alexandre Abadie'
|
13
|
+
|
14
|
+
# -- General configuration ----------------------------------------------------
|
15
|
+
extensions = [
|
16
|
+
'breathe',
|
17
|
+
"myst_parser",
|
18
|
+
"sphinx.ext.autodoc",
|
19
|
+
"sphinx.ext.autosummary",
|
20
|
+
"sphinx.ext.githubpages",
|
21
|
+
"sphinx.ext.graphviz",
|
22
|
+
"sphinx.ext.inheritance_diagram",
|
23
|
+
"sphinx.ext.todo",
|
24
|
+
"sphinx.ext.viewcode",
|
25
|
+
]
|
26
|
+
|
27
|
+
language = "en"
|
28
|
+
tls_verify = False
|
29
|
+
templates_path = ['_templates']
|
30
|
+
exclude_patterns = ["_build"]
|
31
|
+
nitpick_ignore_regex = [
|
32
|
+
(r'c:.*', r'[u]*int\d{1,2}_t'), # ignore int8_t, uint8_t, ...
|
33
|
+
(r'c:.*', r'NRF_.*'), # ignore NRF_ macros
|
34
|
+
(r'c:.*', r'[s]*size_t'), # ignore size_t and ssize_t
|
35
|
+
(r'c:.*', r'[U]*INT\d{1,2}_MAX'), # ignore INT8_MAX, UINT8_MAX, ...
|
36
|
+
]
|
37
|
+
|
38
|
+
# -- Options for breathe ------------------------------------------------------
|
39
|
+
breathe_projects = {"DotBot-firmware": "../doxygen/xml/"}
|
40
|
+
breathe_default_project = "DotBot-firmware"
|
41
|
+
breathe_show_include = False
|
42
|
+
breathe_domain_by_extension = {
|
43
|
+
"h" : "c",
|
44
|
+
}
|
45
|
+
|
46
|
+
myst_enable_extensions = ["html_image"]
|
47
|
+
|
48
|
+
# -- Options for HTML output --------------------------------------------------
|
49
|
+
html_theme = "pydata_sphinx_theme"
|
50
|
+
html_sourcelink_suffix = ""
|
51
|
+
html_static_path = ["_static"]
|
52
|
+
|
53
|
+
# Define the json_url for our version switcher.
|
54
|
+
json_url = "https://dotbot-firmware.readthedocs.io/en/latest/_static/switcher.json"
|
55
|
+
rtd_version = os.environ.get("READTHEDOCS_VERSION")
|
56
|
+
rtd_version_type = os.environ.get("READTHEDOCS_VERSION_TYPE")
|
57
|
+
rtd_git_identifier = os.environ.get("READTHEDOCS_GIT_IDENTIFIER")
|
58
|
+
# If READTHEDOCS_VERSION doesn't exist, we're not on RTD
|
59
|
+
# If it is an integer, we're in a PR build and the version isn't correct.
|
60
|
+
# If it's "latest" → change to "dev" (that's what we want the switcher to call it)
|
61
|
+
if not rtd_version or rtd_version.isdigit() or rtd_version == "latest":
|
62
|
+
rtd_version = "dev"
|
63
|
+
json_url = "_static/switcher.json"
|
64
|
+
elif rtd_version == "stable":
|
65
|
+
rtd_version = f"{rtd_git_identifier}"
|
66
|
+
elif rtd_version_type == "tag":
|
67
|
+
rtd_version = f"{rtd_git_identifier}"
|
68
|
+
|
69
|
+
html_theme_options = {
|
70
|
+
"external_links": [
|
71
|
+
{
|
72
|
+
"url": "https://github.com/DotBots/PyDotBot",
|
73
|
+
"name": "PyDotBot",
|
74
|
+
"attributes": {
|
75
|
+
"target" : "_blank",
|
76
|
+
"rel" : "noopener me",
|
77
|
+
},
|
78
|
+
},
|
79
|
+
{
|
80
|
+
"url": "https://github.com/DotBots/DotBot-hardware",
|
81
|
+
"name": "DotBot hardware",
|
82
|
+
"attributes": {
|
83
|
+
"target" : "_blank",
|
84
|
+
"rel" : "noopener me",
|
85
|
+
},
|
86
|
+
},
|
87
|
+
],
|
88
|
+
"icon_links": [
|
89
|
+
{
|
90
|
+
"name": "GitHub",
|
91
|
+
"url": "https://github.com/DotBots/DotBot-firmware",
|
92
|
+
"icon": "fa-brands fa-github",
|
93
|
+
},
|
94
|
+
],
|
95
|
+
"header_links_before_dropdown": 4,
|
96
|
+
"logo": {
|
97
|
+
"text": "DotBot firmware",
|
98
|
+
},
|
99
|
+
"navbar_align": "left",
|
100
|
+
"navbar_center": ["version-switcher", "navbar-nav"],
|
101
|
+
"switcher": {
|
102
|
+
"json_url": json_url,
|
103
|
+
"version_match": rtd_version,
|
104
|
+
},
|
105
|
+
"footer_start": ["copyright"],
|
106
|
+
"footer_center": ["sphinx-version"],
|
107
|
+
}
|
108
|
+
|
109
|
+
# -- Options for autosummary/autodoc output -----------------------------------
|
110
|
+
autosummary_generate = True
|
111
|
+
autodoc_typehints = "description"
|
112
|
+
autodoc_member_order = "groupwise"
|
113
|
+
|
114
|
+
# Hook for building doxygen documentation -------------------------------------
|
115
|
+
|
116
|
+
def run_doxygen(app):
|
117
|
+
"""Run the doxygen make command."""
|
118
|
+
doxygen_path = "../doxygen"
|
119
|
+
try:
|
120
|
+
retcode = subprocess.call(f"make -C {doxygen_path}", shell=True)
|
121
|
+
if retcode < 0:
|
122
|
+
sys.stderr.write(f"doxygen terminated by signal {-retcode}")
|
123
|
+
except OSError as e:
|
124
|
+
sys.stderr.write(f"doxygen execution failed: {e}")
|
125
|
+
|
126
|
+
# Hook for generating linked README.md files --------------------------------------------
|
127
|
+
|
128
|
+
README_INCLUDE_TEMPLATE = """```{{include}} {path_to_readme}
|
129
|
+
:relative-images:
|
130
|
+
:relative-docs: ../../
|
131
|
+
```
|
132
|
+
"""
|
133
|
+
|
134
|
+
def generate_readme(app, prefix, dest):
|
135
|
+
projects_dir = os.path.join(app.srcdir, "../../projects/")
|
136
|
+
projects = [os.path.basename(project) for project in glob.glob(f"{projects_dir}/{prefix}*")]
|
137
|
+
output_dir = os.path.join(app.srcdir, dest)
|
138
|
+
if not os.path.exists(output_dir):
|
139
|
+
os.makedirs(output_dir, exist_ok=True)
|
140
|
+
for project in projects:
|
141
|
+
with open(os.path.join(output_dir, f"{project}.md"), "w") as f:
|
142
|
+
f.write(README_INCLUDE_TEMPLATE.format(path_to_readme=f"../../../projects/{project}/README.md"))
|
143
|
+
|
144
|
+
|
145
|
+
def generate_projects_readme(app):
|
146
|
+
for prefix, dest in [("01", "_examples"), ("03app", "_projects")]:
|
147
|
+
generate_readme(app, prefix, dest)
|
148
|
+
|
149
|
+
|
150
|
+
API_INCLUDE_TEMPLATE = """{title}
|
151
|
+
=================================
|
152
|
+
|
153
|
+
.. doxygengroup:: {module}
|
154
|
+
.. doxygenfile:: {header}
|
155
|
+
|
156
|
+
"""
|
157
|
+
EXCLUDE_MODULES = [
|
158
|
+
"board_config",
|
159
|
+
"soft_ed25519",
|
160
|
+
"soft_edsign",
|
161
|
+
"soft_f25519",
|
162
|
+
"soft_fprime",
|
163
|
+
"soft_sha256",
|
164
|
+
"soft_sha512",
|
165
|
+
]
|
166
|
+
|
167
|
+
|
168
|
+
def generate_api_files(app):
|
169
|
+
output_dir = os.path.join(app.srcdir, "_api")
|
170
|
+
if not os.path.exists(output_dir):
|
171
|
+
os.makedirs(output_dir, exist_ok=True)
|
172
|
+
for module in ["bsp", "crypto", "drv"]:
|
173
|
+
module_dir = os.path.join(app.srcdir, f"../../{module}/")
|
174
|
+
submodules = [os.path.basename(project).split(".")[0] for project in glob.glob(f"{module_dir}/*.h")]
|
175
|
+
submodules = [module for module in submodules if module not in EXCLUDE_MODULES]
|
176
|
+
for submodule in submodules:
|
177
|
+
with open(os.path.join(output_dir, f"{module}_{submodule}.rst"), "w") as f:
|
178
|
+
f.write(
|
179
|
+
API_INCLUDE_TEMPLATE.format(
|
180
|
+
title=f"{submodule.capitalize()}",
|
181
|
+
module=f"{module}_{submodule}",
|
182
|
+
header=f"{module}/{submodule}.h"
|
183
|
+
)
|
184
|
+
)
|
185
|
+
|
186
|
+
|
187
|
+
def setup(app):
|
188
|
+
"""Add hook for building doxygen documentation."""
|
189
|
+
app.connect("builder-inited", run_doxygen)
|
190
|
+
app.connect("builder-inited", generate_api_files)
|
191
|
+
app.connect("builder-inited", generate_projects_readme)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
[build-system]
|
2
|
-
requires = [
|
2
|
+
requires = [
|
3
|
+
"hatchling>=1.4.1",
|
4
|
+
]
|
3
5
|
build-backend = "hatchling.build"
|
4
6
|
|
5
7
|
[tool.hatch.build]
|
@@ -11,9 +13,12 @@ exclude = [
|
|
11
13
|
"sample/",
|
12
14
|
]
|
13
15
|
|
16
|
+
[tool.hatch.version]
|
17
|
+
path = "testbed/swarmit/__init__.py"
|
18
|
+
|
14
19
|
[project]
|
15
20
|
name = "swarmit"
|
16
|
-
|
21
|
+
dynamic = ["version"]
|
17
22
|
authors = [
|
18
23
|
{ name="Alexandre Abadie", email="alexandre.abadie@inria.fr" },
|
19
24
|
]
|
@@ -25,10 +30,11 @@ dependencies = [
|
|
25
30
|
"rich == 13.8.1",
|
26
31
|
"structlog == 24.4.0",
|
27
32
|
"tqdm == 4.66.5",
|
33
|
+
"paho-mqtt >= 2.1.0",
|
28
34
|
]
|
29
35
|
description = "Run Your Own Robot Swarm Testbed."
|
30
36
|
readme = "README.md"
|
31
|
-
license
|
37
|
+
license = { text="BSD" }
|
32
38
|
requires-python = ">=3.7"
|
33
39
|
classifiers = [
|
34
40
|
"Programming Language :: Python :: 3",
|
@@ -46,10 +52,14 @@ classifiers = [
|
|
46
52
|
swarmit = "testbed.cli.main:main"
|
47
53
|
|
48
54
|
[tool.ruff]
|
49
|
-
select = ["E", "F"]
|
55
|
+
lint.select = ["E", "F"]
|
50
56
|
line-length = 88
|
51
|
-
ignore = ["E501"]
|
57
|
+
lint.ignore = ["E501"]
|
52
58
|
|
53
59
|
[tool.isort]
|
54
60
|
multi_line_output = 3 # Use Vertical Hanging Indent
|
55
61
|
profile = "black"
|
62
|
+
|
63
|
+
[tool.black]
|
64
|
+
line-length = 79
|
65
|
+
skip-string-normalization = true
|
@@ -0,0 +1,378 @@
|
|
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
|
+
Controller,
|
18
|
+
ControllerSettings,
|
19
|
+
ResetLocation,
|
20
|
+
print_start_status,
|
21
|
+
print_status,
|
22
|
+
print_stop_status,
|
23
|
+
print_transfer_status,
|
24
|
+
)
|
25
|
+
|
26
|
+
SERIAL_PORT_DEFAULT = get_default_port()
|
27
|
+
BAUDRATE_DEFAULT = 1000000
|
28
|
+
|
29
|
+
|
30
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
31
|
+
@click.version_option(version=__version__)
|
32
|
+
@click.option(
|
33
|
+
"-p",
|
34
|
+
"--port",
|
35
|
+
default=SERIAL_PORT_DEFAULT,
|
36
|
+
help=f"Serial port to use to send the bitstream to the gateway. Default: {SERIAL_PORT_DEFAULT}.",
|
37
|
+
)
|
38
|
+
@click.option(
|
39
|
+
"-b",
|
40
|
+
"--baudrate",
|
41
|
+
default=BAUDRATE_DEFAULT,
|
42
|
+
help=f"Serial port baudrate. Default: {BAUDRATE_DEFAULT}.",
|
43
|
+
)
|
44
|
+
@click.option(
|
45
|
+
"-e",
|
46
|
+
"--edge",
|
47
|
+
is_flag=True,
|
48
|
+
default=False,
|
49
|
+
help="Use MQTT adapter to communicate with an edge gateway.",
|
50
|
+
)
|
51
|
+
@click.option(
|
52
|
+
"-d",
|
53
|
+
"--devices",
|
54
|
+
type=str,
|
55
|
+
default="",
|
56
|
+
help="Subset list of devices to interact with, separated with ,",
|
57
|
+
)
|
58
|
+
@click.pass_context
|
59
|
+
def main(ctx, port, baudrate, edge, devices):
|
60
|
+
if ctx.invoked_subcommand != "monitor":
|
61
|
+
# Disable logging if not monitoring
|
62
|
+
structlog.configure(
|
63
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
64
|
+
logging.CRITICAL
|
65
|
+
),
|
66
|
+
)
|
67
|
+
ctx.ensure_object(dict)
|
68
|
+
ctx.obj["port"] = port
|
69
|
+
ctx.obj["baudrate"] = baudrate
|
70
|
+
ctx.obj["edge"] = edge
|
71
|
+
ctx.obj["devices"] = [e for e in devices.split(",") if e]
|
72
|
+
|
73
|
+
|
74
|
+
@main.command()
|
75
|
+
@click.option(
|
76
|
+
"-v",
|
77
|
+
"--verbose",
|
78
|
+
is_flag=True,
|
79
|
+
help="Print start result.",
|
80
|
+
)
|
81
|
+
@click.pass_context
|
82
|
+
def start(ctx, verbose):
|
83
|
+
"""Start the user application."""
|
84
|
+
try:
|
85
|
+
settings = ControllerSettings(
|
86
|
+
serial_port=ctx.obj["port"],
|
87
|
+
serial_baudrate=ctx.obj["baudrate"],
|
88
|
+
mqtt_host="argus.paris.inria.fr",
|
89
|
+
mqtt_port=8883,
|
90
|
+
edge=ctx.obj["edge"],
|
91
|
+
devices=list(ctx.obj["devices"]),
|
92
|
+
)
|
93
|
+
controller = Controller(settings)
|
94
|
+
except (
|
95
|
+
SerialInterfaceException,
|
96
|
+
serial.serialutil.SerialException,
|
97
|
+
) as exc:
|
98
|
+
console = Console()
|
99
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
100
|
+
return
|
101
|
+
if controller.ready_devices:
|
102
|
+
started = controller.start()
|
103
|
+
print_start_status(
|
104
|
+
sorted(started),
|
105
|
+
sorted(set(controller.ready_devices).difference(set(started))),
|
106
|
+
)
|
107
|
+
if verbose:
|
108
|
+
print("Started devices:")
|
109
|
+
pprint(started)
|
110
|
+
print("Not started devices:")
|
111
|
+
pprint(
|
112
|
+
sorted(set(controller.ready_devices).difference(set(started)))
|
113
|
+
)
|
114
|
+
else:
|
115
|
+
print("No device to start")
|
116
|
+
controller.terminate()
|
117
|
+
|
118
|
+
|
119
|
+
@main.command()
|
120
|
+
@click.option(
|
121
|
+
"-v",
|
122
|
+
"--verbose",
|
123
|
+
is_flag=True,
|
124
|
+
help="Print start result.",
|
125
|
+
)
|
126
|
+
@click.pass_context
|
127
|
+
def stop(ctx, verbose):
|
128
|
+
"""Stop the user application."""
|
129
|
+
try:
|
130
|
+
settings = ControllerSettings(
|
131
|
+
serial_port=ctx.obj["port"],
|
132
|
+
serial_baudrate=ctx.obj["baudrate"],
|
133
|
+
mqtt_host="argus.paris.inria.fr",
|
134
|
+
mqtt_port=8883,
|
135
|
+
edge=ctx.obj["edge"],
|
136
|
+
devices=list(ctx.obj["devices"]),
|
137
|
+
)
|
138
|
+
controller = Controller(settings)
|
139
|
+
except (
|
140
|
+
SerialInterfaceException,
|
141
|
+
serial.serialutil.SerialException,
|
142
|
+
) as exc:
|
143
|
+
console = Console()
|
144
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
145
|
+
return
|
146
|
+
if controller.running_devices or controller.resetting_devices:
|
147
|
+
stopped = controller.stop()
|
148
|
+
print_stop_status(
|
149
|
+
sorted(stopped),
|
150
|
+
sorted(
|
151
|
+
set(
|
152
|
+
controller.running_devices + controller.resetting_devices
|
153
|
+
).difference(set(stopped))
|
154
|
+
),
|
155
|
+
)
|
156
|
+
if verbose:
|
157
|
+
print("Started devices:")
|
158
|
+
pprint(stopped)
|
159
|
+
print("Not started devices:")
|
160
|
+
pprint(
|
161
|
+
sorted(
|
162
|
+
set(
|
163
|
+
controller.running_devices
|
164
|
+
+ controller.resetting_devices
|
165
|
+
).difference(set(stopped))
|
166
|
+
)
|
167
|
+
)
|
168
|
+
else:
|
169
|
+
print("No device to stop")
|
170
|
+
controller.terminate()
|
171
|
+
|
172
|
+
|
173
|
+
@main.command()
|
174
|
+
@click.argument(
|
175
|
+
"locations",
|
176
|
+
type=str,
|
177
|
+
)
|
178
|
+
@click.pass_context
|
179
|
+
def reset(ctx, locations):
|
180
|
+
"""Reset robots locations.
|
181
|
+
|
182
|
+
Locations are provided as '<device_id>:<x>,<y>-<device_id>:<x>,<y>|...'
|
183
|
+
"""
|
184
|
+
devices = ctx.obj["devices"]
|
185
|
+
if not devices:
|
186
|
+
print("No devices selected.")
|
187
|
+
return
|
188
|
+
locations = {
|
189
|
+
location.split(':')[0]: ResetLocation(
|
190
|
+
pos_x=int(float(location.split(':')[1].split(',')[0]) * 1e6),
|
191
|
+
pos_y=int(float(location.split(':')[1].split(',')[1]) * 1e6),
|
192
|
+
)
|
193
|
+
for location in locations.split("-")
|
194
|
+
}
|
195
|
+
if sorted(devices) and sorted(locations.keys()) != sorted(devices):
|
196
|
+
print("Selected devices and reset locations do not match.")
|
197
|
+
return
|
198
|
+
try:
|
199
|
+
settings = ControllerSettings(
|
200
|
+
serial_port=ctx.obj["port"],
|
201
|
+
serial_baudrate=ctx.obj["baudrate"],
|
202
|
+
mqtt_host="argus.paris.inria.fr",
|
203
|
+
mqtt_port=8883,
|
204
|
+
edge=ctx.obj["edge"],
|
205
|
+
devices=list(ctx.obj["devices"]),
|
206
|
+
)
|
207
|
+
controller = Controller(settings)
|
208
|
+
except (
|
209
|
+
SerialInterfaceException,
|
210
|
+
serial.serialutil.SerialException,
|
211
|
+
) as exc:
|
212
|
+
console = Console()
|
213
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
214
|
+
return
|
215
|
+
if not controller.ready_devices:
|
216
|
+
print("No device to reset.")
|
217
|
+
return
|
218
|
+
controller.reset(locations)
|
219
|
+
controller.terminate()
|
220
|
+
|
221
|
+
|
222
|
+
@main.command()
|
223
|
+
@click.option(
|
224
|
+
"-y",
|
225
|
+
"--yes",
|
226
|
+
is_flag=True,
|
227
|
+
help="Flash the firmware without prompt.",
|
228
|
+
)
|
229
|
+
@click.option(
|
230
|
+
"-s",
|
231
|
+
"--start",
|
232
|
+
is_flag=True,
|
233
|
+
help="Start the firmware once flashed.",
|
234
|
+
)
|
235
|
+
@click.option(
|
236
|
+
"-v",
|
237
|
+
"--verbose",
|
238
|
+
is_flag=True,
|
239
|
+
help="Print transfer data.",
|
240
|
+
)
|
241
|
+
@click.argument("firmware", type=click.File(mode="rb"), required=False)
|
242
|
+
@click.pass_context
|
243
|
+
def flash(ctx, yes, start, verbose, firmware):
|
244
|
+
"""Flash a firmware to the robots."""
|
245
|
+
console = Console()
|
246
|
+
if firmware is None:
|
247
|
+
console.print("[bold red]Error:[/] Missing firmware file. Exiting.")
|
248
|
+
ctx.exit()
|
249
|
+
|
250
|
+
fw = bytearray(firmware.read())
|
251
|
+
settings = ControllerSettings(
|
252
|
+
serial_port=ctx.obj["port"],
|
253
|
+
serial_baudrate=ctx.obj["baudrate"],
|
254
|
+
mqtt_host="argus.paris.inria.fr",
|
255
|
+
mqtt_port=8883,
|
256
|
+
edge=ctx.obj["edge"],
|
257
|
+
devices=ctx.obj["devices"],
|
258
|
+
)
|
259
|
+
controller = Controller(settings)
|
260
|
+
if not controller.ready_devices:
|
261
|
+
console.print("[bold red]Error:[/] No ready devices found. Exiting.")
|
262
|
+
controller.terminate()
|
263
|
+
return
|
264
|
+
print(
|
265
|
+
f"Devices to flash ([bold white]{len(controller.ready_devices)}):[/]"
|
266
|
+
)
|
267
|
+
pprint(controller.ready_devices, expand_all=True)
|
268
|
+
if yes is False:
|
269
|
+
click.confirm("Do you want to continue?", default=True, abort=True)
|
270
|
+
|
271
|
+
devices = controller.settings.devices
|
272
|
+
start_data = controller.start_ota(fw)
|
273
|
+
if (devices and sorted(start_data.ids) != sorted(devices)) or (
|
274
|
+
not devices
|
275
|
+
and sorted(start_data.ids) != sorted(controller.ready_devices)
|
276
|
+
):
|
277
|
+
console = Console()
|
278
|
+
console.print(
|
279
|
+
"[bold red]Error:[/] some acknowledgments are missing "
|
280
|
+
f'({", ".join(sorted(set(controller.ready_devices).difference(set(start_data.ids))))}). '
|
281
|
+
"Aborting."
|
282
|
+
)
|
283
|
+
raise click.Abort()
|
284
|
+
print()
|
285
|
+
print(f"Image size: [bold cyan]{len(fw)}B[/]")
|
286
|
+
print(f"Image hash: [bold cyan]{start_data.fw_hash.hex().upper()}[/]")
|
287
|
+
print(f"Radio chunks ([bold]{CHUNK_SIZE}B[/bold]): {start_data.chunks}")
|
288
|
+
start_time = time.time()
|
289
|
+
data = controller.transfer(fw)
|
290
|
+
print(f"Elapsed: [bold cyan]{time.time() - start_time:.3f}s[/bold cyan]")
|
291
|
+
print_transfer_status(data, start_data)
|
292
|
+
if verbose:
|
293
|
+
pprint(data)
|
294
|
+
if not all([value.hashes_match for value in data.values()]):
|
295
|
+
controller.terminate()
|
296
|
+
console = Console()
|
297
|
+
console.print("[bold red]Error:[/] Hashes do not match.")
|
298
|
+
raise click.Abort()
|
299
|
+
|
300
|
+
if start is True:
|
301
|
+
started = controller.start()
|
302
|
+
print_start_status(
|
303
|
+
sorted(started),
|
304
|
+
sorted(set(start_data.ids).difference(set(started))),
|
305
|
+
)
|
306
|
+
controller.terminate()
|
307
|
+
|
308
|
+
|
309
|
+
@main.command()
|
310
|
+
@click.pass_context
|
311
|
+
def monitor(ctx):
|
312
|
+
"""Monitor running applications."""
|
313
|
+
try:
|
314
|
+
settings = ControllerSettings(
|
315
|
+
serial_port=ctx.obj["port"],
|
316
|
+
serial_baudrate=ctx.obj["baudrate"],
|
317
|
+
mqtt_host="argus.paris.inria.fr",
|
318
|
+
mqtt_port=8883,
|
319
|
+
edge=ctx.obj["edge"],
|
320
|
+
devices=ctx.obj["devices"],
|
321
|
+
)
|
322
|
+
controller = Controller(settings)
|
323
|
+
except (
|
324
|
+
SerialInterfaceException,
|
325
|
+
serial.serialutil.SerialException,
|
326
|
+
) as exc:
|
327
|
+
console = Console()
|
328
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
329
|
+
return {}
|
330
|
+
try:
|
331
|
+
controller.monitor()
|
332
|
+
except KeyboardInterrupt:
|
333
|
+
print("Stopping monitor.")
|
334
|
+
finally:
|
335
|
+
controller.terminate()
|
336
|
+
|
337
|
+
|
338
|
+
@main.command()
|
339
|
+
@click.pass_context
|
340
|
+
def status(ctx):
|
341
|
+
"""Print current status of the robots."""
|
342
|
+
settings = ControllerSettings(
|
343
|
+
serial_port=ctx.obj["port"],
|
344
|
+
serial_baudrate=ctx.obj["baudrate"],
|
345
|
+
mqtt_host="argus.paris.inria.fr",
|
346
|
+
mqtt_port=8883,
|
347
|
+
edge=ctx.obj["edge"],
|
348
|
+
devices=ctx.obj["devices"],
|
349
|
+
)
|
350
|
+
controller = Controller(settings)
|
351
|
+
data = controller.status()
|
352
|
+
if not data:
|
353
|
+
click.echo("No devices found.")
|
354
|
+
else:
|
355
|
+
print_status(data)
|
356
|
+
controller.terminate()
|
357
|
+
|
358
|
+
|
359
|
+
@main.command()
|
360
|
+
@click.argument("message", type=str, required=True)
|
361
|
+
@click.pass_context
|
362
|
+
def message(ctx, message):
|
363
|
+
"""Send a custom text message to the robots."""
|
364
|
+
settings = ControllerSettings(
|
365
|
+
serial_port=ctx.obj["port"],
|
366
|
+
serial_baudrate=ctx.obj["baudrate"],
|
367
|
+
mqtt_host="argus.paris.inria.fr",
|
368
|
+
mqtt_port=8883,
|
369
|
+
edge=ctx.obj["edge"],
|
370
|
+
devices=ctx.obj["devices"],
|
371
|
+
)
|
372
|
+
controller = Controller(settings)
|
373
|
+
controller.send_message(message)
|
374
|
+
controller.terminate()
|
375
|
+
|
376
|
+
|
377
|
+
if __name__ == "__main__":
|
378
|
+
main(obj={})
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.3.0"
|