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.
@@ -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
+ }