phyelds-argos 8.9.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.
- phyelds_argos-8.9.0/PKG-INFO +75 -0
- phyelds_argos-8.9.0/README.md +58 -0
- phyelds_argos-8.9.0/pyproject.toml +21 -0
- phyelds_argos-8.9.0/src/phyelds/argos/__init__.py +1 -0
- phyelds_argos-8.9.0/src/phyelds/argos/build.py +47 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/CMakeLists.txt +39 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/controller.cpp +176 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/controller.hpp +32 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/devices.cpp +220 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/devices.hpp +34 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/gui.cpp +33 -0
- phyelds_argos-8.9.0/src/phyelds/argos/native/python.hpp +70 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phyelds-argos
|
|
3
|
+
Version: 8.9.0
|
|
4
|
+
Summary: Aggregate programs on ARGoS robots, communicating over simulated radios
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Gianluca Aguzzi
|
|
7
|
+
Author-email: gianluca.aguzzi@unibo.it
|
|
8
|
+
Requires-Python: >=3.12,<3.14
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: phyelds (>=8.9.0,<9.0.0)
|
|
14
|
+
Project-URL: Repository, https://github.com/phyelds/phyelds
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# phyelds-argos
|
|
18
|
+
|
|
19
|
+
Run [phyelds](https://github.com/phyelds/phyelds) aggregate programs on any ARGoS3 robot:
|
|
20
|
+
foot-bot, e-puck, eye-bot, Spiri, Pi-Puck, BuilderBot, drone, block or prototype.
|
|
21
|
+
ARGoS owns the robots, physics and radios. Each robot runs a `phyelds.vm.device.Device`;
|
|
22
|
+
its bytes travel through the robot's simulated radio, one round per tick.
|
|
23
|
+
|
|
24
|
+
Install this package into Python 3.12 or 3.13, with ARGoS3, a C++17 compiler and
|
|
25
|
+
CMake 3.18+ installed. Build the plugin with the same Python that has your programs:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m phyelds.argos.build --build-dir build
|
|
29
|
+
# Without Qt/OpenGL (the controller still drives the LEDs):
|
|
30
|
+
python -m phyelds.argos.build --build-dir build-headless --no-gui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package ships C++ sources, not a binary tied to a particular ARGoS/Python ABI.
|
|
34
|
+
The command compiles `libphyelds_argos.so` and, unless `--no-gui` is used,
|
|
35
|
+
`libphyelds_argos_qt.so`. It uses the calling interpreter and its development
|
|
36
|
+
headers/shared library, rather than whichever `python3` happens to be on the PATH.
|
|
37
|
+
Rebuild after moving or replacing that Python environment.
|
|
38
|
+
|
|
39
|
+
In an ARGoS experiment use `phyelds_controller`, with
|
|
40
|
+
`library="build/libphyelds_argos"` and `<params program="your_module:your_function"
|
|
41
|
+
source="0" max_speed="0.1" />`. Robot names must end in a unique unsigned 32-bit
|
|
42
|
+
integer, for example `fb0`, `fb1`. The controller uses the devices the experiment
|
|
43
|
+
declares: it needs the `positioning` sensor and a `range_and_bearing` or
|
|
44
|
+
`simple_radios` radio (actuator and sensor); motion (`differential_steering`,
|
|
45
|
+
`pipuck_differential_drive`, `builderbot_differential_drive`, `quadrotor_speed`,
|
|
46
|
+
`drone_flight_system`), proximity sensors and LEDs are optional, and outputs a robot
|
|
47
|
+
cannot act on are ignored. Positioning supplies **yaw only**, unless `gps="true"`.
|
|
48
|
+
|
|
49
|
+
Programs read `time` (seconds), `source` and `heading`; `obstacle` with a proximity
|
|
50
|
+
sensor or rangefinders; the `neighbors_distances` and `neighbors_vectors` maps, keyed
|
|
51
|
+
by numeric robot ID, with the range-and-bearing radio; and `position` with
|
|
52
|
+
`gps="true"`, which simple radios need for geometry. Distances are metres; vectors and
|
|
53
|
+
headings share the global compass frame. The standard geometry helpers use the
|
|
54
|
+
measured sensors without GPS.
|
|
55
|
+
|
|
56
|
+
Programs write `velocity` (an iterable with two or three components, m/s), `led`
|
|
57
|
+
(three integer RGB channels, 0–255) and `label` (text), through `store`. Missing
|
|
58
|
+
outputs mean zero velocity, black LEDs and an empty label. With the GUI enabled,
|
|
59
|
+
`<user_functions library="build/libphyelds_argos_qt" label="phyelds_qtuser_functions" />`
|
|
60
|
+
draws the LED colour as a halo and the label above each robot.
|
|
61
|
+
|
|
62
|
+
Radio frames are `[u32 sender][u16 length][Device payload]`, with the header encoded by
|
|
63
|
+
ARGoS `CByteArray`, padded with zeros on the range-and-bearing radio. Compact paths are
|
|
64
|
+
used by default; `paths="debug"` is available for debugging, but all communicating
|
|
65
|
+
robots must use the same mode and program version. Use a sufficiently large
|
|
66
|
+
`rab_data_size` (4096 in the examples).
|
|
67
|
+
Oversize messages fail explicitly; they are never truncated. Bad frame lengths are
|
|
68
|
+
ignored. Payload/codec errors stop the experiment with the robot ID and Python traceback.
|
|
69
|
+
`trace="true"` logs each robot's label after every round for diagnostics.
|
|
70
|
+
|
|
71
|
+
See the [integration guide](https://phyelds.github.io/phyelds/integration/argos/) for
|
|
72
|
+
an experiment and the five-example project. The host build needs no Boost.Python,
|
|
73
|
+
pybind11, FCPP or phyelds simulator. Python execution is protected by the GIL even
|
|
74
|
+
when ARGoS uses worker threads; individual rounds are not executed in parallel.
|
|
75
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# phyelds-argos
|
|
2
|
+
|
|
3
|
+
Run [phyelds](https://github.com/phyelds/phyelds) aggregate programs on any ARGoS3 robot:
|
|
4
|
+
foot-bot, e-puck, eye-bot, Spiri, Pi-Puck, BuilderBot, drone, block or prototype.
|
|
5
|
+
ARGoS owns the robots, physics and radios. Each robot runs a `phyelds.vm.device.Device`;
|
|
6
|
+
its bytes travel through the robot's simulated radio, one round per tick.
|
|
7
|
+
|
|
8
|
+
Install this package into Python 3.12 or 3.13, with ARGoS3, a C++17 compiler and
|
|
9
|
+
CMake 3.18+ installed. Build the plugin with the same Python that has your programs:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python -m phyelds.argos.build --build-dir build
|
|
13
|
+
# Without Qt/OpenGL (the controller still drives the LEDs):
|
|
14
|
+
python -m phyelds.argos.build --build-dir build-headless --no-gui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package ships C++ sources, not a binary tied to a particular ARGoS/Python ABI.
|
|
18
|
+
The command compiles `libphyelds_argos.so` and, unless `--no-gui` is used,
|
|
19
|
+
`libphyelds_argos_qt.so`. It uses the calling interpreter and its development
|
|
20
|
+
headers/shared library, rather than whichever `python3` happens to be on the PATH.
|
|
21
|
+
Rebuild after moving or replacing that Python environment.
|
|
22
|
+
|
|
23
|
+
In an ARGoS experiment use `phyelds_controller`, with
|
|
24
|
+
`library="build/libphyelds_argos"` and `<params program="your_module:your_function"
|
|
25
|
+
source="0" max_speed="0.1" />`. Robot names must end in a unique unsigned 32-bit
|
|
26
|
+
integer, for example `fb0`, `fb1`. The controller uses the devices the experiment
|
|
27
|
+
declares: it needs the `positioning` sensor and a `range_and_bearing` or
|
|
28
|
+
`simple_radios` radio (actuator and sensor); motion (`differential_steering`,
|
|
29
|
+
`pipuck_differential_drive`, `builderbot_differential_drive`, `quadrotor_speed`,
|
|
30
|
+
`drone_flight_system`), proximity sensors and LEDs are optional, and outputs a robot
|
|
31
|
+
cannot act on are ignored. Positioning supplies **yaw only**, unless `gps="true"`.
|
|
32
|
+
|
|
33
|
+
Programs read `time` (seconds), `source` and `heading`; `obstacle` with a proximity
|
|
34
|
+
sensor or rangefinders; the `neighbors_distances` and `neighbors_vectors` maps, keyed
|
|
35
|
+
by numeric robot ID, with the range-and-bearing radio; and `position` with
|
|
36
|
+
`gps="true"`, which simple radios need for geometry. Distances are metres; vectors and
|
|
37
|
+
headings share the global compass frame. The standard geometry helpers use the
|
|
38
|
+
measured sensors without GPS.
|
|
39
|
+
|
|
40
|
+
Programs write `velocity` (an iterable with two or three components, m/s), `led`
|
|
41
|
+
(three integer RGB channels, 0–255) and `label` (text), through `store`. Missing
|
|
42
|
+
outputs mean zero velocity, black LEDs and an empty label. With the GUI enabled,
|
|
43
|
+
`<user_functions library="build/libphyelds_argos_qt" label="phyelds_qtuser_functions" />`
|
|
44
|
+
draws the LED colour as a halo and the label above each robot.
|
|
45
|
+
|
|
46
|
+
Radio frames are `[u32 sender][u16 length][Device payload]`, with the header encoded by
|
|
47
|
+
ARGoS `CByteArray`, padded with zeros on the range-and-bearing radio. Compact paths are
|
|
48
|
+
used by default; `paths="debug"` is available for debugging, but all communicating
|
|
49
|
+
robots must use the same mode and program version. Use a sufficiently large
|
|
50
|
+
`rab_data_size` (4096 in the examples).
|
|
51
|
+
Oversize messages fail explicitly; they are never truncated. Bad frame lengths are
|
|
52
|
+
ignored. Payload/codec errors stop the experiment with the robot ID and Python traceback.
|
|
53
|
+
`trace="true"` logs each robot's label after every round for diagnostics.
|
|
54
|
+
|
|
55
|
+
See the [integration guide](https://phyelds.github.io/phyelds/integration/argos/) for
|
|
56
|
+
an experiment and the five-example project. The host build needs no Boost.Python,
|
|
57
|
+
pybind11, FCPP or phyelds simulator. Python execution is protected by the GIL even
|
|
58
|
+
when ARGoS uses worker threads; individual rounds are not executed in parallel.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "phyelds-argos"
|
|
3
|
+
version = "8.9.0"
|
|
4
|
+
description = "Aggregate programs on ARGoS robots, communicating over simulated radios"
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
authors = ["Gianluca Aguzzi <gianluca.aguzzi@unibo.it>", "Davide Domini <davide.domini@unibo.it>"]
|
|
7
|
+
repository = "https://github.com/phyelds/phyelds"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
packages = [{ include = "phyelds/argos", from = "src" }]
|
|
10
|
+
include = [{ path = "src/phyelds/argos/native/**", format = ["sdist", "wheel"] }]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = ">=3.12,<3.14"
|
|
14
|
+
phyelds = "^8.9.0"
|
|
15
|
+
|
|
16
|
+
[tool.pytest.ini_options]
|
|
17
|
+
testpaths = ["tests"]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
21
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ARGoS integration: build the native host with ``python -m phyelds.argos.build``."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Build the ARGoS plugin against the Python running this command."""
|
|
2
|
+
import argparse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_plugin(build_dir: Path, *, gui: bool = True) -> Path:
|
|
9
|
+
"""Compile the host and return the controller plugin path.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
build_dir: Directory for CMake artifacts and the compiled plugins.
|
|
13
|
+
gui: Whether to also build the Qt/OpenGL halo and label renderer.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The absolute path of the controller library.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
subprocess.CalledProcessError: If configuration or compilation fails.
|
|
20
|
+
FileNotFoundError: If CMake or the built controller cannot be found.
|
|
21
|
+
"""
|
|
22
|
+
build_dir = Path(build_dir).absolute()
|
|
23
|
+
source = Path(__file__).parent / "native"
|
|
24
|
+
subprocess.run([
|
|
25
|
+
"cmake", "-S", str(source), "-B", str(build_dir),
|
|
26
|
+
f"-DPython3_EXECUTABLE={sys.executable}", "-DCMAKE_BUILD_TYPE=Release",
|
|
27
|
+
f"-DPHYELDS_ARGOS_GUI={'ON' if gui else 'OFF'}",
|
|
28
|
+
], check=True)
|
|
29
|
+
subprocess.run(["cmake", "--build", str(build_dir), "--parallel"], check=True)
|
|
30
|
+
suffix = ".dylib" if sys.platform == "darwin" else ".so"
|
|
31
|
+
plugin = build_dir / f"libphyelds_argos{suffix}"
|
|
32
|
+
if not plugin.is_file():
|
|
33
|
+
raise FileNotFoundError(plugin)
|
|
34
|
+
return plugin
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
"""Compile the plugins using command-line options."""
|
|
39
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
40
|
+
parser.add_argument("--build-dir", type=Path, default=Path("build"))
|
|
41
|
+
parser.add_argument("--no-gui", action="store_true", help="do not require Qt/OpenGL")
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
print(build_plugin(args.build_dir, gui=not args.no_gui))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(phyelds_argos CXX)
|
|
3
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
4
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
5
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
6
|
+
|
|
7
|
+
# ARGoSConfig.cmake looks for the modules installed beside it only under the system
|
|
8
|
+
# prefixes: an ARGoS found through CMAKE_PREFIX_PATH would not find its own modules.
|
|
9
|
+
find_path(ARGOS_CMAKE_DIR FindARGoSQTOpenGL.cmake
|
|
10
|
+
PATHS ${CMAKE_PREFIX_PATH} ENV CMAKE_PREFIX_PATH
|
|
11
|
+
PATH_SUFFIXES share/argos3/cmake NO_DEFAULT_PATH)
|
|
12
|
+
find_package(ARGoS REQUIRED)
|
|
13
|
+
# Find both together so an interpreter can never be paired with another Python's ABI.
|
|
14
|
+
set(Python3_USE_STATIC_LIBS FALSE)
|
|
15
|
+
find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter Development.Embed)
|
|
16
|
+
if(Python3_VERSION VERSION_GREATER_EQUAL "3.14")
|
|
17
|
+
message(FATAL_ERROR "phyelds currently supports Python 3.12 and 3.13")
|
|
18
|
+
endif()
|
|
19
|
+
include_directories(${ARGOS_INCLUDE_DIRS})
|
|
20
|
+
link_directories(${ARGOS_LIBRARY_DIR})
|
|
21
|
+
|
|
22
|
+
add_library(phyelds_argos SHARED controller.cpp devices.cpp)
|
|
23
|
+
target_compile_definitions(phyelds_argos PRIVATE
|
|
24
|
+
PHYELDS_PYTHON_EXECUTABLE="${Python3_EXECUTABLE}")
|
|
25
|
+
target_compile_options(phyelds_argos PRIVATE -Wall -Wextra)
|
|
26
|
+
target_link_libraries(phyelds_argos PRIVATE Python3::Python
|
|
27
|
+
argos3core_simulator argos3plugin_simulator_footbot argos3plugin_simulator_epuck
|
|
28
|
+
argos3plugin_simulator_eyebot argos3plugin_simulator_pipuck argos3plugin_simulator_builderbot
|
|
29
|
+
argos3plugin_simulator_drone
|
|
30
|
+
argos3plugin_simulator_genericrobot ${ARGOS_LDFLAGS})
|
|
31
|
+
|
|
32
|
+
option(PHYELDS_ARGOS_GUI "Build Qt/OpenGL halo and label renderer" ON)
|
|
33
|
+
if(PHYELDS_ARGOS_GUI)
|
|
34
|
+
find_package(ARGoSQTOpenGL REQUIRED)
|
|
35
|
+
add_library(phyelds_argos_qt SHARED gui.cpp)
|
|
36
|
+
target_include_directories(phyelds_argos_qt PRIVATE ${ARGOS_QTOPENGL_INCLUDE_DIRS})
|
|
37
|
+
target_link_libraries(phyelds_argos_qt PRIVATE phyelds_argos
|
|
38
|
+
argos3plugin_simulator_qtopengl ${ARGOS_QTOPENGL_LIBRARIES})
|
|
39
|
+
endif()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#include <cmath>
|
|
2
|
+
#include <algorithm>
|
|
3
|
+
#include <limits>
|
|
4
|
+
#include <regex>
|
|
5
|
+
#include "controller.hpp"
|
|
6
|
+
#include "python.hpp"
|
|
7
|
+
#include <argos3/core/simulator/simulator.h>
|
|
8
|
+
#include <argos3/core/simulator/space/space.h>
|
|
9
|
+
#include <argos3/core/utility/logging/argos_log.h>
|
|
10
|
+
|
|
11
|
+
using namespace argos;
|
|
12
|
+
using namespace phyelds_argos;
|
|
13
|
+
|
|
14
|
+
CPhyeldsController::~CPhyeldsController() {
|
|
15
|
+
if (m_device && Py_IsInitialized()) {
|
|
16
|
+
Gil gil;
|
|
17
|
+
Py_DECREF(m_device);
|
|
18
|
+
}
|
|
19
|
+
// The interpreter belongs to the process, never to an individual robot.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void CPhyeldsController::Init(TConfigurationNode& node) {
|
|
23
|
+
try {
|
|
24
|
+
GetNodeAttribute(node, "program", m_program);
|
|
25
|
+
GetNodeAttributeOrDefault(node, "source", m_source, m_source);
|
|
26
|
+
GetNodeAttributeOrDefault(node, "max_speed", m_max_speed, m_max_speed);
|
|
27
|
+
GetNodeAttributeOrDefault(node, "paths", m_paths, m_paths);
|
|
28
|
+
GetNodeAttributeOrDefault(node, "gps", m_gps, m_gps);
|
|
29
|
+
GetNodeAttributeOrDefault(node, "trace", m_trace, m_trace);
|
|
30
|
+
if (!std::isfinite(m_max_speed) || m_max_speed < 0)
|
|
31
|
+
throw std::runtime_error("max_speed must be finite and nonnegative");
|
|
32
|
+
if (m_paths != "compact" && m_paths != "debug")
|
|
33
|
+
throw std::runtime_error("paths must be compact or debug");
|
|
34
|
+
std::smatch match;
|
|
35
|
+
auto id = GetId();
|
|
36
|
+
if (!std::regex_match(id, match, std::regex(".*?([0-9]+)$")))
|
|
37
|
+
throw std::runtime_error("robot IDs must end in a unique integer (e.g. fb0)");
|
|
38
|
+
auto uid = std::stoull(match[1].str());
|
|
39
|
+
if (uid > std::numeric_limits<UInt32>::max()) throw std::runtime_error("robot UID exceeds u32");
|
|
40
|
+
m_uid = static_cast<UInt32>(uid);
|
|
41
|
+
m_devices = FindDevices(*this);
|
|
42
|
+
InitializePython();
|
|
43
|
+
Reset();
|
|
44
|
+
} catch (std::exception& error) {
|
|
45
|
+
THROW_ARGOSEXCEPTION_NESTED(GetId() << " initializing " << m_program, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void CPhyeldsController::Reset() {
|
|
50
|
+
try {
|
|
51
|
+
Gil gil;
|
|
52
|
+
auto separator = m_program.find(':');
|
|
53
|
+
if (separator == std::string::npos || separator == 0 || separator + 1 == m_program.size())
|
|
54
|
+
throw std::runtime_error("program must be module:function");
|
|
55
|
+
auto module = Checked(PyImport_ImportModule(m_program.substr(0, separator).c_str()));
|
|
56
|
+
auto program = Checked(PyObject_GetAttrString(module.get(), m_program.substr(separator + 1).c_str()));
|
|
57
|
+
if (!PyCallable_Check(program.get())) throw std::runtime_error("program is not callable");
|
|
58
|
+
auto runtime = Checked(PyImport_ImportModule("phyelds.vm.device"));
|
|
59
|
+
auto device = Checked(PyObject_GetAttrString(runtime.get(), "Device"));
|
|
60
|
+
auto args = Checked(Py_BuildValue("(IO)", m_uid, program.get()));
|
|
61
|
+
auto kwargs = Checked(Py_BuildValue("{s:s}", "paths", m_paths.c_str()));
|
|
62
|
+
auto replacement = Checked(PyObject_Call(device.get(), args.get(), kwargs.get()));
|
|
63
|
+
Py_XDECREF(m_device);
|
|
64
|
+
m_device = replacement.release();
|
|
65
|
+
Label.clear();
|
|
66
|
+
Color = CColor::BLACK;
|
|
67
|
+
if (m_devices.Drive) m_devices.Drive(CVector3::ZERO);
|
|
68
|
+
if (m_devices.Light) m_devices.Light(Color);
|
|
69
|
+
m_devices.Send(CByteArray());
|
|
70
|
+
} catch (std::exception& error) {
|
|
71
|
+
THROW_ARGOSEXCEPTION_NESTED(GetId() << " resetting " << m_program, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void CPhyeldsController::ControlStep() {
|
|
76
|
+
try {
|
|
77
|
+
Gil gil;
|
|
78
|
+
Step();
|
|
79
|
+
} catch (std::exception& error) {
|
|
80
|
+
THROW_ARGOSEXCEPTION_NESTED(GetId() << " executing " << m_program, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
void CPhyeldsController::Step() {
|
|
85
|
+
auto const& pose = m_devices.Positioning->GetReading();
|
|
86
|
+
CRadians yaw, pitch, roll;
|
|
87
|
+
pose.Orientation.ToEulerAngles(yaw, pitch, roll);
|
|
88
|
+
auto ranges = Checked(PyDict_New());
|
|
89
|
+
auto vectors = Checked(PyDict_New());
|
|
90
|
+
for (auto& heard : m_devices.Receive()) {
|
|
91
|
+
CByteArray& data = heard.Data;
|
|
92
|
+
if (data.Size() < 6) continue;
|
|
93
|
+
UInt32 sender;
|
|
94
|
+
UInt16 length;
|
|
95
|
+
data >> sender >> length;
|
|
96
|
+
if (!length || length > data.Size() || sender == m_uid) continue;
|
|
97
|
+
auto received = Checked(PyObject_CallMethod(m_device, "receive", "Iy#", sender,
|
|
98
|
+
reinterpret_cast<char const*>(data.ToCArray()), static_cast<Py_ssize_t>(length)));
|
|
99
|
+
if (!heard.Offset) continue;
|
|
100
|
+
CVector3 offset = heard.Offset->Rotate(pose.Orientation);
|
|
101
|
+
auto key = Checked(PyLong_FromUnsignedLong(sender));
|
|
102
|
+
auto distance = Checked(PyFloat_FromDouble(offset.Length()));
|
|
103
|
+
auto vector = Checked(Py_BuildValue("(ddd)", offset.GetX(), offset.GetY(), offset.GetZ()));
|
|
104
|
+
Check(PyDict_SetItem(ranges.get(), key.get(), distance.get()));
|
|
105
|
+
Check(PyDict_SetItem(vectors.get(), key.get(), vector.get()));
|
|
106
|
+
}
|
|
107
|
+
auto sensors = Checked(PyDict_New());
|
|
108
|
+
Real time = CSimulator::GetInstance().GetSpace().GetSimulationClock()
|
|
109
|
+
* CPhysicsEngine::GetSimulationClockTick();
|
|
110
|
+
Put(sensors.get(), "time", Checked(PyFloat_FromDouble(time)));
|
|
111
|
+
Put(sensors.get(), "source", Checked(PyBool_FromLong(m_source == m_uid)));
|
|
112
|
+
Put(sensors.get(), "heading", Checked(Py_BuildValue("(ddd)", Cos(yaw), Sin(yaw), 0.0)));
|
|
113
|
+
if (m_devices.Obstacle) {
|
|
114
|
+
CVector2 obstacle = m_devices.Obstacle().Rotate(yaw);
|
|
115
|
+
Put(sensors.get(), "obstacle", Checked(Py_BuildValue("(ddd)", obstacle.GetX(), obstacle.GetY(), 0.0)));
|
|
116
|
+
}
|
|
117
|
+
if (m_devices.Measures) {
|
|
118
|
+
Put(sensors.get(), "neighbors_distances", std::move(ranges));
|
|
119
|
+
Put(sensors.get(), "neighbors_vectors", std::move(vectors));
|
|
120
|
+
}
|
|
121
|
+
if (m_gps) {
|
|
122
|
+
auto const& position = pose.Position;
|
|
123
|
+
Put(sensors.get(), "position",
|
|
124
|
+
Checked(Py_BuildValue("(ddd)", position.GetX(), position.GetY(), position.GetZ())));
|
|
125
|
+
}
|
|
126
|
+
auto payload = Checked(PyObject_CallMethod(m_device, "run_round", "O", sensors.get()));
|
|
127
|
+
if (!PyBytes_Check(payload.get())) throw std::runtime_error("Device.run_round must return bytes");
|
|
128
|
+
auto length = static_cast<size_t>(PyBytes_Size(payload.get()));
|
|
129
|
+
if (length > std::numeric_limits<UInt16>::max())
|
|
130
|
+
throw std::runtime_error("payload of " + std::to_string(length) + " bytes exceeds the 16-bit frame length");
|
|
131
|
+
CByteArray data;
|
|
132
|
+
data << m_uid << static_cast<UInt16>(length);
|
|
133
|
+
data.AddBuffer(reinterpret_cast<UInt8 const*>(PyBytes_AsString(payload.get())), length);
|
|
134
|
+
m_devices.Send(data);
|
|
135
|
+
|
|
136
|
+
auto outputs = Checked(PyObject_GetAttrString(m_device, "outputs"));
|
|
137
|
+
Label.clear();
|
|
138
|
+
if (auto label = PyDict_GetItemString(outputs.get(), "label")) {
|
|
139
|
+
auto text = Checked(PyObject_Str(label));
|
|
140
|
+
auto utf8 = PyUnicode_AsUTF8(text.get());
|
|
141
|
+
if (!utf8) Checked(nullptr);
|
|
142
|
+
Label = utf8;
|
|
143
|
+
}
|
|
144
|
+
Color = CColor::BLACK;
|
|
145
|
+
if (auto led = PyDict_GetItemString(outputs.get(), "led")) {
|
|
146
|
+
auto channels = Checked(PySequence_Fast(led, "led must be an RGB iterable"));
|
|
147
|
+
if (PySequence_Fast_GET_SIZE(channels.get()) != 3) throw std::runtime_error("led must have three channels");
|
|
148
|
+
UInt8 rgb[3];
|
|
149
|
+
for (size_t index = 0; index < 3; ++index) {
|
|
150
|
+
double value = Number(PySequence_Fast_GET_ITEM(channels.get(), index));
|
|
151
|
+
if (value < 0 || value > 255) throw std::runtime_error("led channels must be in [0,255]");
|
|
152
|
+
rgb[index] = static_cast<UInt8>(value);
|
|
153
|
+
}
|
|
154
|
+
Color = CColor(rgb[0], rgb[1], rgb[2]);
|
|
155
|
+
}
|
|
156
|
+
if (m_devices.Light) m_devices.Light(Color);
|
|
157
|
+
CVector3 velocity;
|
|
158
|
+
if (auto value = PyDict_GetItemString(outputs.get(), "velocity")) {
|
|
159
|
+
auto components = Checked(PySequence_Fast(value, "velocity must be an iterable"));
|
|
160
|
+
auto size = PySequence_Fast_GET_SIZE(components.get());
|
|
161
|
+
if (size != 2 && size != 3) throw std::runtime_error("velocity must have two or three components");
|
|
162
|
+
velocity.Set(Number(PySequence_Fast_GET_ITEM(components.get(), 0)),
|
|
163
|
+
Number(PySequence_Fast_GET_ITEM(components.get(), 1)),
|
|
164
|
+
size == 3 ? Number(PySequence_Fast_GET_ITEM(components.get(), 2)) : 0);
|
|
165
|
+
if (velocity.Length() > m_max_speed) velocity *= m_max_speed / velocity.Length();
|
|
166
|
+
}
|
|
167
|
+
if (m_devices.Drive) m_devices.Drive(velocity);
|
|
168
|
+
if (m_trace) LOG << GetId() << " round: " << Label << std::endl;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void CPhyeldsController::Destroy() {
|
|
172
|
+
LOG << GetId() << " final: " << Label << std::endl;
|
|
173
|
+
LOG.Flush();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
REGISTER_CONTROLLER(CPhyeldsController, "phyelds_controller")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Native host only: all aggregate state and serialization belong to phyelds Device.
|
|
2
|
+
#pragma once
|
|
3
|
+
#include <argos3/core/control_interface/ci_controller.h>
|
|
4
|
+
#include <argos3/core/utility/datatypes/color.h>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include "devices.hpp"
|
|
7
|
+
|
|
8
|
+
struct _object;
|
|
9
|
+
|
|
10
|
+
class CPhyeldsController : public argos::CCI_Controller {
|
|
11
|
+
public:
|
|
12
|
+
~CPhyeldsController() override;
|
|
13
|
+
void Init(argos::TConfigurationNode& node) override;
|
|
14
|
+
void Reset() override;
|
|
15
|
+
void ControlStep() override;
|
|
16
|
+
void Destroy() override;
|
|
17
|
+
|
|
18
|
+
std::string Label;
|
|
19
|
+
argos::CColor Color = argos::CColor::BLACK;
|
|
20
|
+
|
|
21
|
+
private:
|
|
22
|
+
void Step();
|
|
23
|
+
phyelds_argos::Devices m_devices;
|
|
24
|
+
_object* m_device = nullptr;
|
|
25
|
+
std::string m_program;
|
|
26
|
+
std::string m_paths = "compact";
|
|
27
|
+
argos::UInt32 m_uid = 0;
|
|
28
|
+
argos::SInt64 m_source = -1;
|
|
29
|
+
argos::Real m_max_speed = 0.1;
|
|
30
|
+
bool m_gps = false;
|
|
31
|
+
bool m_trace = false;
|
|
32
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#include "devices.hpp"
|
|
2
|
+
#include <algorithm>
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <utility>
|
|
7
|
+
#include <argos3/plugins/robots/builderbot/control_interface/ci_builderbot_differential_drive_actuator.h>
|
|
8
|
+
#include <argos3/plugins/robots/builderbot/control_interface/ci_builderbot_leds_actuator.h>
|
|
9
|
+
#include <argos3/plugins/robots/builderbot/control_interface/ci_builderbot_rangefinders_sensor.h>
|
|
10
|
+
#include <argos3/plugins/robots/drone/control_interface/ci_drone_flight_system_actuator.h>
|
|
11
|
+
#include <argos3/plugins/robots/drone/control_interface/ci_drone_flight_system_sensor.h>
|
|
12
|
+
#include <argos3/plugins/robots/drone/control_interface/ci_drone_leds_actuator.h>
|
|
13
|
+
#include <argos3/plugins/robots/e-puck/control_interface/ci_epuck_proximity_sensor.h>
|
|
14
|
+
#include <argos3/plugins/robots/eye-bot/control_interface/ci_eyebot_proximity_sensor.h>
|
|
15
|
+
#include <argos3/plugins/robots/foot-bot/control_interface/ci_footbot_proximity_sensor.h>
|
|
16
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_differential_steering_actuator.h>
|
|
17
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_directional_leds_actuator.h>
|
|
18
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_leds_actuator.h>
|
|
19
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_quadrotor_speed_actuator.h>
|
|
20
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_range_and_bearing_actuator.h>
|
|
21
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_range_and_bearing_sensor.h>
|
|
22
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_simple_radios_actuator.h>
|
|
23
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_simple_radios_sensor.h>
|
|
24
|
+
#include <argos3/plugins/robots/pi-puck/control_interface/ci_pipuck_differential_drive_actuator.h>
|
|
25
|
+
#include <argos3/plugins/robots/pi-puck/control_interface/ci_pipuck_leds_actuator.h>
|
|
26
|
+
#include <argos3/plugins/robots/pi-puck/control_interface/ci_pipuck_rangefinders_sensor.h>
|
|
27
|
+
|
|
28
|
+
using namespace argos;
|
|
29
|
+
|
|
30
|
+
namespace phyelds_argos {
|
|
31
|
+
namespace {
|
|
32
|
+
constexpr Real CM_PER_M = 100; // range-and-bearing ranges and differential_steering speeds are in cm
|
|
33
|
+
constexpr Real BUILDERBOT_RANGE = 0.05; // what its rangefinders report when they see nothing
|
|
34
|
+
|
|
35
|
+
CRadians Yaw(CCI_PositioningSensor const* positioning) {
|
|
36
|
+
CRadians yaw, pitch, roll;
|
|
37
|
+
positioning->GetReading().Orientation.ToEulerAngles(yaw, pitch, roll);
|
|
38
|
+
return yaw;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wheel speeds (m/s) that turn a differential drive towards a velocity in its own frame,
|
|
42
|
+
// and drive it forward as its heading aligns.
|
|
43
|
+
std::pair<Real, Real> Wheels(CVector2 const& velocity) {
|
|
44
|
+
Real speed = velocity.Length();
|
|
45
|
+
if (speed < 0.005) return {0, 0};
|
|
46
|
+
Real angle = velocity.Angle().GetValue();
|
|
47
|
+
Real forward = speed * std::max<Real>(0, std::cos(angle));
|
|
48
|
+
Real turn = speed * angle;
|
|
49
|
+
return {forward - turn, forward + turn};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A rangefinder distance as a proximity reading: exp(-distance) in range, as ARGoS
|
|
53
|
+
// computes the readings of the foot-bot, e-puck and eye-bot, and 0 beyond it.
|
|
54
|
+
Real Closeness(Real distance, Real range) {
|
|
55
|
+
return distance < range ? std::exp(-distance) : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The horizontal direction a rangefinder looks along, in the frame of the robot.
|
|
59
|
+
CVector2 Looking(CQuaternion const& orientation) {
|
|
60
|
+
CVector3 ray(CVector3::Z);
|
|
61
|
+
ray.Rotate(orientation);
|
|
62
|
+
return {ray.GetX(), ray.GetY()};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The repulsion of a ring of proximity sensors with angles, as the foot-bot's.
|
|
66
|
+
template <class Sensor> std::function<CVector2()> Ring(Sensor* sensor) {
|
|
67
|
+
return [sensor] {
|
|
68
|
+
CVector2 repulsion;
|
|
69
|
+
for (auto const& reading : sensor->GetReadings()) repulsion -= CVector2(reading.Value, reading.Angle);
|
|
70
|
+
return repulsion;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
void FindRadio(CCI_Controller& robot, Devices& devices) {
|
|
75
|
+
if (robot.HasActuator("range_and_bearing")) {
|
|
76
|
+
auto* out = robot.GetActuator<CCI_RangeAndBearingActuator>("range_and_bearing");
|
|
77
|
+
auto* in = robot.GetSensor<CCI_RangeAndBearingSensor>("range_and_bearing");
|
|
78
|
+
devices.Measures = true;
|
|
79
|
+
devices.Receive = [in] {
|
|
80
|
+
std::vector<Heard> heard;
|
|
81
|
+
for (auto const& packet : in->GetReadings()) {
|
|
82
|
+
CVector3 offset;
|
|
83
|
+
offset.FromSphericalCoords(packet.Range / CM_PER_M,
|
|
84
|
+
CRadians::PI_OVER_TWO - packet.VerticalBearing, packet.HorizontalBearing);
|
|
85
|
+
heard.push_back({packet.Data, offset});
|
|
86
|
+
}
|
|
87
|
+
return heard;
|
|
88
|
+
};
|
|
89
|
+
devices.Send = [out](CByteArray const& frame) {
|
|
90
|
+
if (frame.Size() > out->GetSize())
|
|
91
|
+
throw std::runtime_error("payload of " + std::to_string(frame.Size() - 6)
|
|
92
|
+
+ " bytes exceeds rab_data_size (including 6-byte header)");
|
|
93
|
+
CByteArray data(frame);
|
|
94
|
+
data.Resize(out->GetSize());
|
|
95
|
+
out->SetData(data);
|
|
96
|
+
};
|
|
97
|
+
} else if (robot.HasActuator("simple_radios")) {
|
|
98
|
+
auto* out = robot.GetActuator<CCI_SimpleRadiosActuator>("simple_radios");
|
|
99
|
+
auto* in = robot.GetSensor<CCI_SimpleRadiosSensor>("simple_radios");
|
|
100
|
+
// Every interface: a block has a radio on each face, a BuilderBot both wifi and NFC.
|
|
101
|
+
devices.Receive = [in] {
|
|
102
|
+
std::vector<Heard> heard;
|
|
103
|
+
for (auto const& interface : in->GetInterfaces())
|
|
104
|
+
for (auto const& message : interface.Messages) heard.push_back({message, std::nullopt});
|
|
105
|
+
return heard;
|
|
106
|
+
};
|
|
107
|
+
devices.Send = [out](CByteArray const& frame) {
|
|
108
|
+
if (frame.Size() == 0) return; // simple radios send each message once
|
|
109
|
+
for (auto& interface : out->GetInterfaces()) interface.Messages.push_back(frame);
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
throw std::runtime_error("the robot needs a range_and_bearing or a simple_radios radio");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void FindLocomotion(CCI_Controller& robot, Devices& devices) {
|
|
117
|
+
auto* positioning = devices.Positioning;
|
|
118
|
+
auto local = [positioning](CVector3 const& velocity) {
|
|
119
|
+
return CVector2(velocity.GetX(), velocity.GetY()).Rotate(-Yaw(positioning));
|
|
120
|
+
};
|
|
121
|
+
if (robot.HasActuator("differential_steering")) {
|
|
122
|
+
auto* wheels = robot.GetActuator<CCI_DifferentialSteeringActuator>("differential_steering");
|
|
123
|
+
devices.Drive = [wheels, local](CVector3 const& velocity) {
|
|
124
|
+
auto [left, right] = Wheels(local(velocity));
|
|
125
|
+
wheels->SetLinearVelocity(left * CM_PER_M, right * CM_PER_M);
|
|
126
|
+
};
|
|
127
|
+
} else if (robot.HasActuator("pipuck_differential_drive")) {
|
|
128
|
+
auto* wheels = robot.GetActuator<CCI_PiPuckDifferentialDriveActuator>("pipuck_differential_drive");
|
|
129
|
+
devices.Drive = [wheels, local](CVector3 const& velocity) {
|
|
130
|
+
auto [left, right] = Wheels(local(velocity));
|
|
131
|
+
wheels->SetLinearVelocity(left, right);
|
|
132
|
+
};
|
|
133
|
+
} else if (robot.HasActuator("builderbot_differential_drive")) {
|
|
134
|
+
auto* wheels = robot.GetActuator<CCI_BuilderBotDifferentialDriveActuator>("builderbot_differential_drive");
|
|
135
|
+
// Its right wheel is mirrored: (v, -v) drives the BuilderBot straight ahead.
|
|
136
|
+
devices.Drive = [wheels, local](CVector3 const& velocity) {
|
|
137
|
+
auto [left, right] = Wheels(local(velocity));
|
|
138
|
+
wheels->SetTargetVelocityLeft(left);
|
|
139
|
+
wheels->SetTargetVelocityRight(-right);
|
|
140
|
+
};
|
|
141
|
+
} else if (robot.HasActuator("quadrotor_speed")) {
|
|
142
|
+
auto* rotors = robot.GetActuator<CCI_QuadRotorSpeedActuator>("quadrotor_speed");
|
|
143
|
+
devices.Drive = [rotors](CVector3 const& velocity) { rotors->SetLinearVelocity(velocity); };
|
|
144
|
+
} else if (robot.HasActuator("drone_flight_system")) {
|
|
145
|
+
auto* flight = robot.GetActuator<CCI_DroneFlightSystemActuator>("drone_flight_system");
|
|
146
|
+
auto* state = robot.GetSensor<CCI_DroneFlightSystemSensor>("drone_flight_system");
|
|
147
|
+
// The flight system flies towards a target, in the frame the drone started in, at
|
|
148
|
+
// 1/s of the distance: a target one second ahead is flown at the velocity. At zero
|
|
149
|
+
// velocity the drone keeps its last target, so it holds its place without drifting.
|
|
150
|
+
devices.Drive = [flight, state, positioning](CVector3 velocity) {
|
|
151
|
+
if (velocity == CVector3::ZERO) return;
|
|
152
|
+
velocity.RotateZ(CRadians(state->GetOrientation().GetZ()) - Yaw(positioning));
|
|
153
|
+
flight->SetTargetPosition(state->GetPosition() + velocity);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
void FindObstacles(CCI_Controller& robot, Devices& devices) {
|
|
159
|
+
if (robot.HasSensor("footbot_proximity")) {
|
|
160
|
+
devices.Obstacle = Ring(robot.GetSensor<CCI_FootBotProximitySensor>("footbot_proximity"));
|
|
161
|
+
} else if (robot.HasSensor("epuck_proximity")) {
|
|
162
|
+
devices.Obstacle = Ring(robot.GetSensor<CCI_EPuckProximitySensor>("epuck_proximity"));
|
|
163
|
+
} else if (robot.HasSensor("eyebot_proximity")) {
|
|
164
|
+
devices.Obstacle = Ring(robot.GetSensor<CCI_EyeBotProximitySensor>("eyebot_proximity"));
|
|
165
|
+
} else if (robot.HasSensor("pipuck_rangefinders")) {
|
|
166
|
+
auto* sensor = robot.GetSensor<CCI_PiPuckRangefindersSensor>("pipuck_rangefinders");
|
|
167
|
+
devices.Obstacle = [sensor] {
|
|
168
|
+
CVector2 repulsion;
|
|
169
|
+
sensor->Visit([&repulsion](auto const& rangefinder) {
|
|
170
|
+
auto const& [anchor, position, orientation, range] = rangefinder.Configuration;
|
|
171
|
+
repulsion -= Looking(orientation) * Closeness(rangefinder.Proximity, range);
|
|
172
|
+
});
|
|
173
|
+
return repulsion;
|
|
174
|
+
};
|
|
175
|
+
} else if (robot.HasSensor("builderbot_rangefinders")) {
|
|
176
|
+
auto* sensor = robot.GetSensor<CCI_BuilderBotRangefindersSensor>("builderbot_rangefinders");
|
|
177
|
+
devices.Obstacle = [sensor] {
|
|
178
|
+
CVector2 repulsion;
|
|
179
|
+
// The ring on its body, but for the front pair (6, 7) that faces its own end
|
|
180
|
+
// effector, as do the rangefinders on the end effector.
|
|
181
|
+
for (auto const* rangefinder : sensor->GetInterfaces())
|
|
182
|
+
if (rangefinder->Anchor == "origin" && rangefinder->Label != "6" && rangefinder->Label != "7")
|
|
183
|
+
repulsion -= Looking(rangefinder->OrientationOffset)
|
|
184
|
+
* Closeness(rangefinder->Proximity, BUILDERBOT_RANGE);
|
|
185
|
+
return repulsion;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void FindLights(CCI_Controller& robot, Devices& devices) {
|
|
191
|
+
if (robot.HasActuator("leds")) {
|
|
192
|
+
auto* leds = robot.GetActuator<CCI_LEDsActuator>("leds");
|
|
193
|
+
devices.Light = [leds](CColor const& color) { leds->SetAllColors(color); };
|
|
194
|
+
} else if (robot.HasActuator("directional_leds")) {
|
|
195
|
+
auto* leds = robot.GetActuator<CCI_DirectionalLEDsActuator>("directional_leds");
|
|
196
|
+
devices.Light = [leds](CColor const& color) { leds->SetAllColors(color); };
|
|
197
|
+
} else if (robot.HasActuator("builderbot_leds")) {
|
|
198
|
+
auto* leds = robot.GetActuator<CCI_BuilderBotLEDsActuator>("builderbot_leds");
|
|
199
|
+
devices.Light = [leds](CColor const& color) { leds->SetLEDs(color); };
|
|
200
|
+
} else if (robot.HasActuator("drone_leds")) {
|
|
201
|
+
auto* leds = robot.GetActuator<CCI_DroneLEDsActuator>("drone_leds");
|
|
202
|
+
devices.Light = [leds](CColor const& color) { leds->SetLEDs(color); };
|
|
203
|
+
} else if (robot.HasActuator("pipuck_leds")) {
|
|
204
|
+
// The Pi-Puck ring LEDs are only on or off.
|
|
205
|
+
auto* leds = robot.GetActuator<CCI_PiPuckLEDsActuator>("pipuck_leds");
|
|
206
|
+
devices.Light = [leds](CColor const& color) { leds->SetRingLEDs(color != CColor::BLACK); };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Devices FindDevices(CCI_Controller& robot) {
|
|
212
|
+
Devices devices;
|
|
213
|
+
devices.Positioning = robot.GetSensor<CCI_PositioningSensor>("positioning");
|
|
214
|
+
FindRadio(robot, devices);
|
|
215
|
+
FindLocomotion(robot, devices);
|
|
216
|
+
FindObstacles(robot, devices);
|
|
217
|
+
FindLights(robot, devices);
|
|
218
|
+
return devices;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The devices of any ARGoS robot, behind the few operations an aggregate round needs.
|
|
2
|
+
#pragma once
|
|
3
|
+
#include <argos3/core/control_interface/ci_controller.h>
|
|
4
|
+
#include <argos3/core/utility/datatypes/byte_array.h>
|
|
5
|
+
#include <argos3/core/utility/datatypes/color.h>
|
|
6
|
+
#include <argos3/core/utility/math/vector2.h>
|
|
7
|
+
#include <argos3/core/utility/math/vector3.h>
|
|
8
|
+
#include <argos3/plugins/robots/generic/control_interface/ci_positioning_sensor.h>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <optional>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
namespace phyelds_argos {
|
|
14
|
+
// A frame from the radio; a range-and-bearing radio also measures where its sender is.
|
|
15
|
+
struct Heard {
|
|
16
|
+
argos::CByteArray Data;
|
|
17
|
+
std::optional<argos::CVector3> Offset; // metres, in the frame of the robot
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// What a robot can do, from the devices its controller declares. A device the robot
|
|
21
|
+
// lacks is an empty function: its outputs are ignored, so one program fits a mixed swarm.
|
|
22
|
+
struct Devices {
|
|
23
|
+
argos::CCI_PositioningSensor* Positioning = nullptr;
|
|
24
|
+
bool Measures = false; // whether Receive measures where the senders are
|
|
25
|
+
std::function<std::vector<Heard>()> Receive;
|
|
26
|
+
std::function<void(argos::CByteArray const&)> Send; // an empty frame silences the radio
|
|
27
|
+
std::function<argos::CVector2()> Obstacle; // repulsion, in the frame of the robot
|
|
28
|
+
std::function<void(argos::CVector3 const&)> Drive; // m/s, in the global frame
|
|
29
|
+
std::function<void(argos::CColor const&)> Light;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Find the devices declared for a robot; it needs a radio and the positioning sensor.
|
|
33
|
+
Devices FindDevices(argos::CCI_Controller& robot);
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#include <algorithm>
|
|
2
|
+
#include <argos3/core/simulator/entity/composable_entity.h>
|
|
3
|
+
#include <argos3/core/simulator/entity/controllable_entity.h>
|
|
4
|
+
#include <argos3/core/simulator/entity/embodied_entity.h>
|
|
5
|
+
#include <argos3/core/simulator/simulator.h>
|
|
6
|
+
#include <argos3/core/simulator/space/space.h>
|
|
7
|
+
#include <argos3/plugins/simulator/visualizations/qt-opengl/qtopengl_user_functions.h>
|
|
8
|
+
#include "controller.hpp"
|
|
9
|
+
|
|
10
|
+
// Under every robot a phyelds controller drives, whatever its type, a halo of its LED
|
|
11
|
+
// colour, and its label above it.
|
|
12
|
+
class CPhyeldsQTUserFunctions : public argos::CQTOpenGLUserFunctions {
|
|
13
|
+
public:
|
|
14
|
+
void DrawInWorld() override {
|
|
15
|
+
QFont font;
|
|
16
|
+
font.setPixelSize(12);
|
|
17
|
+
for (auto& [id, entity] : argos::CSimulator::GetInstance().GetSpace().GetEntitiesByType("controller")) {
|
|
18
|
+
auto& controllable = *argos::any_cast<argos::CControllableEntity*>(entity);
|
|
19
|
+
auto* controller = dynamic_cast<CPhyeldsController*>(&controllable.GetController());
|
|
20
|
+
auto& robot = controllable.GetParent();
|
|
21
|
+
if (!controller || !robot.HasComponent("body")) continue;
|
|
22
|
+
auto const& box = robot.GetComponent<argos::CEmbodiedEntity>("body").GetBoundingBox();
|
|
23
|
+
argos::CVector3 base = (box.MinCorner + box.MaxCorner) * 0.5;
|
|
24
|
+
base.SetZ(box.MinCorner.GetZ());
|
|
25
|
+
argos::Real radius = 0.75 * std::max(box.MaxCorner.GetX() - box.MinCorner.GetX(),
|
|
26
|
+
box.MaxCorner.GetY() - box.MinCorner.GetY());
|
|
27
|
+
DrawCylinder(base, argos::CQuaternion(), radius, 0.01, controller->Color);
|
|
28
|
+
DrawText(argos::CVector3(base.GetX(), base.GetY(), box.MaxCorner.GetZ() + 0.05),
|
|
29
|
+
controller->Label, argos::CColor::BLACK, font);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
REGISTER_QTOPENGL_USER_FUNCTIONS(CPhyeldsQTUserFunctions, "phyelds_qtuser_functions")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The GIL must outlive every owned Python reference in its scope.
|
|
2
|
+
#pragma once
|
|
3
|
+
#define PY_SSIZE_T_CLEAN
|
|
4
|
+
#include <Python.h>
|
|
5
|
+
#include <cmath>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
#include <string>
|
|
10
|
+
|
|
11
|
+
namespace phyelds_argos {
|
|
12
|
+
struct PythonDeleter {
|
|
13
|
+
void operator()(PyObject* value) const { Py_XDECREF(value); }
|
|
14
|
+
};
|
|
15
|
+
using Object = std::unique_ptr<PyObject, PythonDeleter>;
|
|
16
|
+
|
|
17
|
+
inline Object Checked(PyObject* value) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
// Preserve the original traceback; the controller adds its ID and program.
|
|
20
|
+
PyErr_Print();
|
|
21
|
+
throw std::runtime_error("Python call failed (see traceback above)");
|
|
22
|
+
}
|
|
23
|
+
return Object(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
inline void Check(int result) {
|
|
27
|
+
if (result < 0) Checked(nullptr);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
inline void InitializePython() {
|
|
31
|
+
static std::once_flag once;
|
|
32
|
+
std::call_once(once, [] {
|
|
33
|
+
if (Py_IsInitialized()) return;
|
|
34
|
+
PyConfig config;
|
|
35
|
+
PyConfig_InitPythonConfig(&config);
|
|
36
|
+
config.parse_argv = 0;
|
|
37
|
+
config.install_signal_handlers = 0;
|
|
38
|
+
PyStatus status = PyConfig_SetBytesString(&config, &config.program_name, PHYELDS_PYTHON_EXECUTABLE);
|
|
39
|
+
if (!PyStatus_Exception(status)) status = Py_InitializeFromConfig(&config);
|
|
40
|
+
std::string error;
|
|
41
|
+
if (PyStatus_Exception(status))
|
|
42
|
+
error = status.err_msg ? status.err_msg : "could not initialize Python";
|
|
43
|
+
PyConfig_Clear(&config);
|
|
44
|
+
if (!error.empty()) throw std::runtime_error(error);
|
|
45
|
+
// ARGoS can dispatch controllers to worker threads. Each call takes the GIL.
|
|
46
|
+
PyEval_SaveThread();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class Gil {
|
|
51
|
+
public:
|
|
52
|
+
Gil() : m_state(PyGILState_Ensure()) {}
|
|
53
|
+
~Gil() { PyGILState_Release(m_state); }
|
|
54
|
+
Gil(Gil const&) = delete;
|
|
55
|
+
Gil& operator=(Gil const&) = delete;
|
|
56
|
+
private:
|
|
57
|
+
PyGILState_STATE m_state;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
inline void Put(PyObject* dict, char const* name, Object value) {
|
|
61
|
+
Check(PyDict_SetItemString(dict, name, value.get()));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
inline double Number(PyObject* value) {
|
|
65
|
+
double number = PyFloat_AsDouble(value);
|
|
66
|
+
if (PyErr_Occurred()) Checked(nullptr);
|
|
67
|
+
if (!std::isfinite(number)) throw std::runtime_error("actuator components must be finite");
|
|
68
|
+
return number;
|
|
69
|
+
}
|
|
70
|
+
}
|