diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 43eef5b2..4d2786f3 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -52,6 +52,28 @@ jobs:
with:
verbose: true
+ control_loop:
+ name: check custom control loop
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ - name: Checkout DotBot-libs repo
+ uses: actions/checkout@v4
+ with:
+ repository: DotBots/DotBot-libs
+ path: DotBot-libs
+ - name: Build control loop library
+ run: |
+ cmake -DDOTBOT_LIBS_DIR=${PWD}/DotBot-libs -S utils/control_loop -B build
+ cmake --build build
+
doc:
name: check documentation
runs-on: ubuntu-latest
@@ -92,7 +114,7 @@ jobs:
path: ./dotbot/frontend/build
package:
- needs: [test, doc, frontend]
+ needs: [test, doc, frontend, control_loop]
name: build source package
runs-on: ${{ matrix.os }}
strategy:
diff --git a/doc/conf.py b/doc/conf.py
index 88e624b6..52eb0e2f 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -46,6 +46,9 @@
("py:class", r"Query"),
("py:class", r"PydanticUndefined"),
("py:class", r"queue.Queue"),
+ ("py:class", r"_ctypes.Structure"),
+ ("py:class", r"pathlib._local.Path"),
+ ("py:class", r"pathlib.Path"),
]
# -- Options for HTML output -------------------------------------------------
diff --git a/dotbot/dotbot_simulator.py b/dotbot/dotbot_simulator.py
index 3cfb1c7f..bacbad87 100644
--- a/dotbot/dotbot_simulator.py
+++ b/dotbot/dotbot_simulator.py
@@ -6,6 +6,7 @@
"""Dotbot simulator for the DotBot project."""
+import ctypes
import queue
import random
import threading
@@ -13,6 +14,7 @@
from dataclasses import dataclass
from enum import Enum
from math import atan2, cos, pi, sin, sqrt
+from pathlib import Path
from typing import Callable, List
import toml
@@ -24,16 +26,17 @@
from dotbot.protocol import PayloadDotBotAdvertisement, PayloadType
Kv = 400 # motor speed constant in RPM
-r = 50 # motor reduction ratio
-R = 25 # wheel radius in mm
+R = 50 # motor reduction ratio
+D = 50 # wheel diameter in mm
L = 70 # distance between the two wheels in mm
MIN_PWM_TO_MOVE = 30 # minimum PWM value to overcome static friction and start moving
# Control parameters for the automatic mode
MOTOR_SPEED = 60
-ANGULAR_SPEED_GAIN = 1.2
-REDUCE_SPEED_FACTOR = 0.7
+ANGULAR_SPEED_GAIN = 2
+REDUCE_SPEED_FACTOR = 0.8
REDUCE_SPEED_ANGLE = 25
+DIRECTION_THRESHOLD = 50 # threshold to update the direction (50mm)
SIMULATOR_STEP_DELTA_T = 0.02 # 20 ms
@@ -59,7 +62,11 @@ def wheel_speed_from_pwm(pwm: float) -> float:
"""Convert a PWM value to a wheel speed in mm/s."""
if abs(pwm) < MIN_PWM_TO_MOVE:
return 0.0
- return pwm * R * Kv / (r * 127)
+ if pwm > 100:
+ pwm = 100
+ if pwm < -100:
+ pwm = -100
+ return pwm * D * Kv / (R * 127)
class DotBotSimulatorMode(Enum):
@@ -81,10 +88,26 @@ class SimulatedDotBotSettings(BaseModel):
address: str
pos_x: int
pos_y: int
- theta: float
+ direction: int = -1000
calibrated: int = 0xFF
motor_left_error: float = 0
motor_right_error: float = 0
+ custom_control_loop_library: Path = None
+
+
+class RobotControl(ctypes.Structure):
+ _fields_ = [
+ ("pos_x", ctypes.c_uint32),
+ ("pos_y", ctypes.c_uint32),
+ ("direction", ctypes.c_int16),
+ ("waypoints_length", ctypes.c_uint8),
+ ("waypoint_idx", ctypes.c_uint8),
+ ("waypoint_x", ctypes.c_uint32),
+ ("waypoint_y", ctypes.c_uint32),
+ ("waypoint_threshold", ctypes.c_uint32),
+ ("pwm_left", ctypes.c_int8),
+ ("pwm_right", ctypes.c_int8),
+ ]
class SimulatedNetworkSettings(BaseModel):
@@ -103,13 +126,16 @@ def __init__(self, settings: SimulatedDotBotSettings, tx_queue: queue.Queue):
self.address = settings.address.lower()
self.pos_x = settings.pos_x
self.pos_y = settings.pos_y
- self.theta = settings.theta
+ self.theta = settings.direction * -1 if settings.direction != -1000 else 0
self.motor_left_error = settings.motor_left_error
self.motor_right_error = settings.motor_right_error
+ self.custom_control_loop_library = settings.custom_control_loop_library
+ self._control_loop_func = self._init_control_loop()
self.time_elapsed_s = 0
self.pwm_left = 0
self.pwm_right = 0
+ self.direction = settings.direction
self.calibrated = settings.calibrated
self.waypoint_threshold = 0
@@ -130,6 +156,13 @@ def __init__(self, settings: SimulatedDotBotSettings, tx_queue: queue.Queue):
self.advertise_thread.start()
self.control_thread.start()
self.main_thread.start()
+ self.logger.info(
+ "DotBot simulator initialized",
+ pos_x=self.pos_x,
+ pos_y=self.pos_y,
+ direction=self.direction,
+ theta=self.theta,
+ )
@property
def header(self):
@@ -144,39 +177,34 @@ def _diff_drive_model_update(self):
pos_y_old = self.pos_y
theta_old = self.theta
- if self.pwm_left > 100:
- self.pwm_left = 100
- if self.pwm_right > 100:
- self.pwm_right = 100
- if self.pwm_left < -100:
- self.pwm_left = -100
- if self.pwm_right < -100:
- self.pwm_right = -100
-
# Compute each wheel's real speed considering the motor error and the minimum PWM to move
v_left_real = wheel_speed_from_pwm(self.pwm_left) * (1 - self.motor_left_error)
v_right_real = wheel_speed_from_pwm(self.pwm_right) * (
1 - self.motor_right_error
)
- V = (v_left_real + v_right_real) / 2
- w = (v_left_real - v_right_real) / L
- x_dot = V * cos(theta_old - pi)
- y_dot = V * sin(theta_old - pi)
+ V = (v_right_real + v_left_real) / 2
+ w = (v_right_real - v_left_real) / L
+ x_dot = V * cos(theta_old * pi / 180 - pi / 2)
+ y_dot = V * sin(theta_old * pi / 180 + pi / 2)
+ dx = x_dot * SIMULATOR_STEP_DELTA_T
+ dy = y_dot * SIMULATOR_STEP_DELTA_T
+
+ self.pos_x = pos_x_old + dx
+ self.pos_y = pos_y_old + dy
+ self.theta = (theta_old + w * SIMULATOR_STEP_DELTA_T * 180 / pi) % 360
- self.pos_x = pos_x_old + x_dot * SIMULATOR_STEP_DELTA_T
- self.pos_y = pos_y_old + y_dot * SIMULATOR_STEP_DELTA_T
- self.theta = (theta_old + w * SIMULATOR_STEP_DELTA_T) % (2 * pi)
+ if sqrt(dx**2 + dy**2):
+ self.direction = int(-1 * atan2(dx, dy) * 180 / pi) % 360
self.logger.debug(
"State updated",
- pos_x=self.pos_x,
- pos_y=self.pos_y,
- theta=self.theta,
- pwm_left=self.pwm_left,
- pwm_right=self.pwm_right,
- v_left_real=v_left_real,
- v_right_real=v_right_real,
+ pos_x=int(self.pos_x),
+ pos_y=int(self.pos_y),
+ theta=int(self.theta),
+ direction=int(self.direction),
+ pwm_left=int(self.pwm_left),
+ pwm_right=int(self.pwm_right),
)
self.time_elapsed_s += SIMULATOR_STEP_DELTA_T
@@ -189,12 +217,63 @@ def update_state(self):
if is_stopped:
break
- def _compute_automatic_control(self):
- if self.controller_mode != DotBotSimulatorMode.AUTOMATIC:
- return
+ def _init_control_loop(self) -> callable:
+ """Initialize the control loop, potentially loading a custom control loop library."""
+ if self.custom_control_loop_library is not None:
+ self.custom_control_loop_library = ctypes.CDLL(
+ self.custom_control_loop_library
+ )
+ self.custom_robot_control = RobotControl()
+ self.custom_robot_control.waypoint_idx = 0
+ return self._control_loop_custom
+ else:
+ return self._control_loop_default
+
+ def _control_loop_custom(self):
+ """Control loop using a custom control loop library."""
+ self.custom_robot_control.pos_x = int(self.pos_x)
+ self.custom_robot_control.pos_y = int(self.pos_y)
+ self.custom_robot_control.direction = self.direction
+ self.custom_robot_control.waypoints_length = len(self.waypoints)
+ self.custom_robot_control.waypoint_x = int(
+ self.waypoints[self.custom_robot_control.waypoint_idx].pos_x
+ )
+ self.custom_robot_control.waypoint_y = int(
+ self.waypoints[self.custom_robot_control.waypoint_idx].pos_y
+ )
+ self.custom_robot_control.waypoint_threshold = int(self.waypoint_threshold)
+ # Call the custom control loop function from the shared library
+ self.custom_control_loop_library.update_control(
+ ctypes.byref(self.custom_robot_control)
+ )
+
+ # Update the PWM values based on the output of the custom control loop
+ self.pwm_left = self.custom_robot_control.pwm_left
+ self.pwm_right = self.custom_robot_control.pwm_right
+
+ if self.custom_robot_control.waypoint_idx >= len(self.waypoints):
+ self.logger.info("Last waypoint reached")
+ self.pwm_left = 0
+ self.pwm_right = 0
+ self.waypoint_index = 0
+ self.custom_robot_control.waypoint_idx = 0
+ self.controller_mode = DotBotSimulatorMode.MANUAL
- delta_x = self.pos_x - self.waypoints[self.waypoint_index].pos_x
- delta_y = self.pos_y - self.waypoints[self.waypoint_index].pos_y
+ self.logger.info(
+ "Custom loop",
+ pwm_left=self.pwm_left,
+ pwm_right=self.pwm_right,
+ direction=self.direction,
+ waypoint_index=self.custom_robot_control.waypoint_idx,
+ waypoints_length=self.custom_robot_control.waypoints_length,
+ waypoint_threshold=self.custom_robot_control.waypoint_threshold,
+ waypoint_x=self.custom_robot_control.waypoint_x,
+ waypoint_y=self.custom_robot_control.waypoint_y,
+ )
+
+ def _control_loop_default(self):
+ delta_x = self.waypoints[self.waypoint_index].pos_x - self.pos_x
+ delta_y = self.waypoints[self.waypoint_index].pos_y - self.pos_y
distance_to_target = sqrt(delta_x**2 + delta_y**2)
# check if we are close enough to the "next" waypoint
@@ -210,46 +289,49 @@ def _compute_automatic_control(self):
self.pwm_right = 0
self.waypoint_index = 0
self.controller_mode = DotBotSimulatorMode.MANUAL
- else:
- robot_angle = self.theta
- angle_to_target = atan2(delta_y, delta_x)
- if robot_angle >= pi:
- robot_angle = robot_angle - 2 * pi
-
- error_angle = ((angle_to_target - robot_angle + pi) % (2 * pi)) - pi
- self.logger.info(
- "Moving to waypoint",
- robot_angle=robot_angle,
- angle_to_target=angle_to_target,
- error_angle=error_angle,
- )
-
- speed_reduction_factor: float = 1.0
- if distance_to_target < self.waypoint_threshold * 2:
- speed_reduction_factor = REDUCE_SPEED_FACTOR
-
- error_angle_degrees = error_angle * 180 / pi
- if (
- error_angle_degrees > REDUCE_SPEED_ANGLE
- or error_angle_degrees < -REDUCE_SPEED_ANGLE
- ):
- speed_reduction_factor = REDUCE_SPEED_FACTOR
- angular_speed = error_angle * MOTOR_SPEED * ANGULAR_SPEED_GAIN
- self.pwm_left = MOTOR_SPEED * speed_reduction_factor + angular_speed
- self.pwm_right = MOTOR_SPEED * speed_reduction_factor - angular_speed
+ return
+
+ angle_to_target = -1 * atan2(delta_x, delta_y) * 180 / pi
+ robot_angle = self.direction
+ if robot_angle >= 180:
+ robot_angle -= 360
+ elif robot_angle < -180:
+ robot_angle += 360
+
+ error_angle = angle_to_target - robot_angle
+ if error_angle >= 180:
+ error_angle -= 360
+ elif error_angle < -180:
+ error_angle += 360
+
+ speed_reduction_factor: float = 1.0
+ if distance_to_target < self.waypoint_threshold * 2:
+ speed_reduction_factor = REDUCE_SPEED_FACTOR
+ if error_angle > REDUCE_SPEED_ANGLE or error_angle < -REDUCE_SPEED_ANGLE:
+ speed_reduction_factor = REDUCE_SPEED_FACTOR
+
+ angular_speed = (error_angle / 180) * MOTOR_SPEED * ANGULAR_SPEED_GAIN
+ self.pwm_left = MOTOR_SPEED * speed_reduction_factor + angular_speed
+ self.pwm_right = MOTOR_SPEED * speed_reduction_factor - angular_speed
self.logger.info(
- "Control loop update",
- v_left=self.pwm_left,
- v_right=self.pwm_right,
- theta=self.theta,
+ "Loop update",
+ robot_angle=int(robot_angle),
+ angle_to_target=int(angle_to_target),
+ error_angle=int(error_angle),
+ angular_speed=int(angular_speed),
+ pwm_left=int(self.pwm_left),
+ pwm_right=int(self.pwm_right),
+ theta=int(self.theta),
+ waypoint=f"{self.waypoint_index}/{len(self.waypoints)}",
)
def control_thread(self):
"""Control thread to update the state of the dotbot simulator."""
while self._stop_event.is_set() is False:
- with self._lock:
- self._compute_automatic_control()
+ if self.controller_mode == DotBotSimulatorMode.AUTOMATIC:
+ with self._lock:
+ self._control_loop_func()
is_stopped = self._stop_event.wait(SIMULATOR_UPDATE_INTERVAL_S)
if is_stopped:
break
@@ -262,7 +344,7 @@ def advertise(self):
packet=Packet.from_payload(
PayloadDotBotAdvertisement(
calibrated=self.calibrated,
- direction=int(self.theta * 180 / pi + 90),
+ direction=self.direction,
pos_x=int(self.pos_x) if self.pos_x >= 0 else 0,
pos_y=int(self.pos_y) if self.pos_y >= 0 else 0,
pos_z=0,
@@ -288,21 +370,16 @@ def rx_frame(self):
self.controller_mode = DotBotSimulatorMode.MANUAL
self.pwm_left = frame.packet.payload.left_y
self.pwm_right = frame.packet.payload.right_y
- self.logger.info(
- "RAW command received",
- v_left=self.pwm_left,
- v_right=self.pwm_right,
- )
-
if self.pwm_left > 127:
self.pwm_left = self.pwm_left - 256
if self.pwm_right > 127:
self.pwm_right = self.pwm_right - 256
-
+ self.logger.info(
+ "RAW command received",
+ pwm_left=self.pwm_left,
+ pwm_right=self.pwm_right,
+ )
elif frame.payload_type == PayloadType.LH2_WAYPOINTS:
- self.pwm_left = 0
- self.pwm_right = 0
- self.controller_mode = DotBotSimulatorMode.MANUAL
self.waypoint_threshold = frame.packet.payload.threshold
self.waypoints = frame.packet.payload.waypoints
self.logger.info(
@@ -312,6 +389,10 @@ def rx_frame(self):
)
if self.waypoints:
self.controller_mode = DotBotSimulatorMode.AUTOMATIC
+ else:
+ self.pwm_left = 0
+ self.pwm_right = 0
+ self.controller_mode = DotBotSimulatorMode.MANUAL
def stop(self):
self.logger.info(f"Stopping DotBot {self.address} simulator...")
diff --git a/dotbot/examples/charging_station/charging_station.py b/dotbot/examples/charging_station/charging_station.py
index dfc0f30a..2ba763f1 100644
--- a/dotbot/examples/charging_station/charging_station.py
+++ b/dotbot/examples/charging_station/charging_station.py
@@ -149,7 +149,7 @@ async def disengage_from_charger(client: RestClient, dotbot_address: str):
address=dotbot_address,
application=dotbot.application,
command=DotBotMoveRawCommandModel(
- left_x=0, left_y=-100, right_x=0, right_y=-100
+ left_x=0, left_y=-80, right_x=0, right_y=-80
),
)
await asyncio.sleep(0.1)
@@ -164,7 +164,7 @@ async def disengage_from_charger(client: RestClient, dotbot_address: str):
address=dotbot_address,
application=dotbot.application,
command=DotBotMoveRawCommandModel(
- left_x=0, left_y=100, right_x=0, right_y=100
+ left_x=0, left_y=80, right_x=0, right_y=80
),
)
await asyncio.sleep(0.1)
diff --git a/dotbot/examples/charging_station/charging_station_init_state.toml b/dotbot/examples/charging_station/charging_station_init_state.toml
index 6ef74e1e..d11d3ce2 100644
--- a/dotbot/examples/charging_station/charging_station_init_state.toml
+++ b/dotbot/examples/charging_station/charging_station_init_state.toml
@@ -1,61 +1,69 @@
[[dotbots]]
address = "BADCAFE111111111" # DotBot unique address
-calibrated = 0x01 # optional, defaults to only first lighthouse calibrated
-pos_x = 400_000 # [0, 1_000_000]
-pos_y = 200_000 # [0, 1_000_000]
-theta = 0.0 # [0.0, 2pi]
+calibrated = 0xff # optional, defaults to all possible lighthouses calibrated
+pos_x = 400 # [0, 1_000_000]
+pos_y = 200 # [0, 1_000_000]
+direction = 0 # [0, 360]
[[dotbots]]
address = "DEADBEEF22222222"
-pos_x = 300_000
-pos_y = 100_000
-theta = 1.57
+calibrated = 0xff
+pos_x = 300
+pos_y = 200
+direction = 90
[[dotbots]]
address = "B0B0F00D33333333"
-pos_x = 1_000_000
-pos_y = 1_000_000
-theta = 1.57
+calibrated = 0xff
+pos_x = 1_000
+pos_y = 1_000
+direction = 90
[[dotbots]]
address = "BADC0DE444444444"
-pos_x = 500_000
-pos_y = 500_000
-theta = 3.14
+calibrated = 0xff
+pos_x = 500
+pos_y = 500
+direction = 180
[[dotbots]]
address = "5555555555555555"
-pos_x = 10_000
-pos_y = 870_000
-theta = 3.14
+calibrated = 0xff
+pos_x = 810
+pos_y = 870
+direction = 180
[[dotbots]]
address = "6666666666666666"
-pos_x = 280_000
-pos_y = 880_000
-theta = 3.14
+calibrated = 0xff
+pos_x = 280
+pos_y = 880
+direction = 180
[[dotbots]]
address = "7777777777777777"
-pos_x = 120_000
-pos_y = 90_000
-theta = 3.14
+calibrated = 0xff
+pos_x = 120
+pos_y = 990
+direction = 180
[[dotbots]]
address = "8888888888888888"
-pos_x = 800_000
-pos_y = 560_000
-theta = 3.14
+calibrated = 0xff
+pos_x = 800
+pos_y = 560
+direction = 180
[[dotbots]]
address = "9999999999999999"
-pos_x = 990_000
-pos_y = 180_000
-theta = 1.14
-
+calibrated = 0xff
+pos_x = 990
+pos_y = 180
+direction = 65
[[dotbots]]
address = "A000000000000000"
-pos_x = 880_000
-pos_y = 80_000
-theta = 2.14
+calibrated = 0xff
+pos_x = 900
+pos_y = 800
+direction = 123
diff --git a/dotbot/examples/minimum_naming_game/gen_init_pose.py b/dotbot/examples/minimum_naming_game/gen_init_pose.py
index 1be4766e..ffbeb1aa 100644
--- a/dotbot/examples/minimum_naming_game/gen_init_pose.py
+++ b/dotbot/examples/minimum_naming_game/gen_init_pose.py
@@ -1,21 +1,26 @@
-import math
import random
-from pathlib import Path
-
-# --- Configuration ---
-WIDTH_NODES = 5 # Robots per row
-HEIGHT_NODES = 5 # Number of rows
-SEP_X = 240 # Separation between columns
-SEP_Y = 240 # Separation between rows
+import click
+from rich import print
-def format_with_underscores(value):
- """Formats an integer with underscores every three digits."""
- return f"{value:_}"
+# --- Configuration ---
+WIDTH_NODES_DEFAULT = 5 # Robots per row
+HEIGHT_NODES_DEFAULT = 5 # Number of rows
+START_X_DEFAULT = 120 # Starting X position in mm
+START_Y_DEFAULT = 120 # Starting Y position in mm
+SEP_X_DEFAULT = 240 # Separation between columns
+SEP_Y_DEFAULT = 240 # Separation between rows
def generate_lattice_toml(
- width_count, height_count, sep_x, sep_y, start_x=120, start_y=120
+ width_count,
+ height_count,
+ start_x,
+ start_y,
+ sep_x,
+ sep_y,
+ direction=None,
+ control_loop_library_path=None,
):
output_lines = []
@@ -28,8 +33,9 @@ def generate_lattice_toml(
pos_x = start_x + (col * sep_x)
pos_y = start_y + (row * sep_y)
- # Randomize theta between 0 and 2*pi
- random_theta = round(random.uniform(0, 2 * math.pi), 2)
+ # Randomize direction between 0 and 360
+ if direction is None:
+ direction = random.randint(0, 360)
# Manually build the TOML entry string to preserve underscores
output_lines.append("[[dotbots]]")
@@ -37,18 +43,97 @@ def generate_lattice_toml(
output_lines.append("calibrated = 0xff")
output_lines.append(f"pos_x = {pos_x:_}")
output_lines.append(f"pos_y = {pos_y:_}")
- output_lines.append(f"theta = {random_theta}")
+ output_lines.append(f"direction = {direction}")
+ if control_loop_library_path is not None:
+ output_lines.append(
+ f'control_loop_library_path = "{control_loop_library_path}"'
+ )
output_lines.append("") # Empty line for readability
return "\n".join(output_lines)
-def generate_dotbot_list():
- # Generate
- toml_string = generate_lattice_toml(WIDTH_NODES, HEIGHT_NODES, SEP_X, SEP_Y)
+@click.command()
+@click.argument(
+ "output_path",
+ type=click.Path(dir_okay=False, writable=True),
+ default="init_state.toml",
+)
+@click.option(
+ "--width",
+ type=int,
+ default=WIDTH_NODES_DEFAULT,
+ help=f"Number of robots per row. Defaults to {WIDTH_NODES_DEFAULT}",
+)
+@click.option(
+ "--height",
+ type=int,
+ default=HEIGHT_NODES_DEFAULT,
+ help=f"Number of rows. Defaults to {HEIGHT_NODES_DEFAULT}",
+)
+@click.option(
+ "--start-x",
+ type=int,
+ default=START_X_DEFAULT,
+ help=f"Starting X position in mm. Defaults to {START_X_DEFAULT}",
+)
+@click.option(
+ "--start-y",
+ type=int,
+ default=START_Y_DEFAULT,
+ help=f"Starting Y position in mm. Defaults to {START_Y_DEFAULT}",
+)
+@click.option(
+ "--sep-x",
+ type=int,
+ default=SEP_X_DEFAULT,
+ help=f"Separation in mm between columns. Defaults to {SEP_X_DEFAULT}",
+)
+@click.option(
+ "--sep-y",
+ type=int,
+ default=SEP_Y_DEFAULT,
+ help=f"Separation in mm between rows. Defaults to {SEP_Y_DEFAULT}",
+)
+@click.option(
+ "--direction",
+ type=int,
+ default=None,
+ help="Default robot direction. Defaults to random generated direction",
+)
+@click.option(
+ "--control-loop-library-path",
+ type=click.Path(exists=True, dir_okay=False),
+ help="Path to an optional .so control loop library.",
+)
+def main(
+ output_path,
+ width,
+ height,
+ start_x,
+ start_y,
+ sep_x,
+ sep_y,
+ direction,
+ control_loop_library_path,
+):
+ print(f"\nGenerating configuration with {width * height} robots.\n")
+ print(f" - Layout (row x col) : {height} x {width}")
+ print(f" - Start coordinate (X,Y) : {start_x}, {start_y}")
+ print(f" - Separation (X,Y) : {sep_x}, {sep_y}\n")
+
+ toml_string = generate_lattice_toml(
+ width,
+ height,
+ start_x,
+ start_y,
+ sep_x,
+ sep_y,
+ direction,
+ control_loop_library_path,
+ )
# Save to file
- output_path = Path(__file__).resolve().parent / "init_state.toml"
with open(output_path, "w") as f:
f.write(toml_string)
@@ -56,4 +141,4 @@ def generate_dotbot_list():
if __name__ == "__main__":
- generate_dotbot_list()
+ main()
diff --git a/dotbot/examples/minimum_naming_game/init_state.toml b/dotbot/examples/minimum_naming_game/init_state.toml
index aa46b34f..2b8b720d 100644
--- a/dotbot/examples/minimum_naming_game/init_state.toml
+++ b/dotbot/examples/minimum_naming_game/init_state.toml
@@ -3,172 +3,172 @@ address = "AAAAAAAA00000001"
calibrated = 0xff
pos_x = 120
pos_y = 120
-theta = 4.79
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000002"
calibrated = 0xff
pos_x = 360
pos_y = 120
-theta = 5.85
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000003"
calibrated = 0xff
pos_x = 600
pos_y = 120
-theta = 4.34
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000004"
calibrated = 0xff
pos_x = 840
pos_y = 120
-theta = 2.86
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000005"
calibrated = 0xff
pos_x = 1_080
pos_y = 120
-theta = 4.62
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000006"
calibrated = 0xff
pos_x = 120
pos_y = 360
-theta = 2.88
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000007"
calibrated = 0xff
pos_x = 360
pos_y = 360
-theta = 0.84
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000008"
calibrated = 0xff
pos_x = 600
pos_y = 360
-theta = 4.11
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000009"
calibrated = 0xff
pos_x = 840
pos_y = 360
-theta = 1.36
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000A"
calibrated = 0xff
pos_x = 1_080
pos_y = 360
-theta = 1.7
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000B"
calibrated = 0xff
pos_x = 120
pos_y = 600
-theta = 1.79
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000C"
calibrated = 0xff
pos_x = 360
pos_y = 600
-theta = 5.67
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000D"
calibrated = 0xff
pos_x = 600
pos_y = 600
-theta = 5.6
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000E"
calibrated = 0xff
pos_x = 840
pos_y = 600
-theta = 3.07
+direction = 26
[[dotbots]]
address = "AAAAAAAA0000000F"
calibrated = 0xff
pos_x = 1_080
pos_y = 600
-theta = 3.97
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000010"
calibrated = 0xff
pos_x = 120
pos_y = 840
-theta = 0.89
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000011"
calibrated = 0xff
pos_x = 360
pos_y = 840
-theta = 5.04
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000012"
calibrated = 0xff
pos_x = 600
pos_y = 840
-theta = 1.56
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000013"
calibrated = 0xff
pos_x = 840
pos_y = 840
-theta = 2.1
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000014"
calibrated = 0xff
pos_x = 1_080
pos_y = 840
-theta = 2.5
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000015"
calibrated = 0xff
pos_x = 120
pos_y = 1_080
-theta = 0.63
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000016"
calibrated = 0xff
pos_x = 360
pos_y = 1_080
-theta = 6.22
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000017"
calibrated = 0xff
pos_x = 600
pos_y = 1_080
-theta = 3.68
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000018"
calibrated = 0xff
pos_x = 840
pos_y = 1_080
-theta = 3.07
+direction = 26
[[dotbots]]
address = "AAAAAAAA00000019"
calibrated = 0xff
pos_x = 1_080
pos_y = 1_080
-theta = 3.32
+direction = 26
diff --git a/dotbot/examples/minimum_naming_game/init_state_with_motion.toml b/dotbot/examples/minimum_naming_game/init_state_with_motion.toml
index 5837d93d..b923dca0 100644
--- a/dotbot/examples/minimum_naming_game/init_state_with_motion.toml
+++ b/dotbot/examples/minimum_naming_game/init_state_with_motion.toml
@@ -1,62 +1,62 @@
[[dotbots]]
address = "AAAAAAAA00000001"
calibrated = 0xff
-pos_x = 240
-pos_y = 240
-theta = 3.5
+pos_x = 300
+pos_y = 300
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000002"
calibrated = 0xff
-pos_x = 720
-pos_y = 240
-theta = 6.0
+pos_x = 600
+pos_y = 300
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000003"
calibrated = 0xff
-pos_x = 1_200
-pos_y = 240
-theta = 1.64
+pos_x = 900
+pos_y = 300
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000004"
calibrated = 0xff
-pos_x = 240
-pos_y = 720
-theta = 1.23
+pos_x = 300
+pos_y = 600
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000005"
calibrated = 0xff
-pos_x = 720
-pos_y = 720
-theta = 4.16
+pos_x = 600
+pos_y = 600
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000006"
calibrated = 0xff
-pos_x = 1_200
-pos_y = 720
-theta = 3.81
+pos_x = 900
+pos_y = 600
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000007"
calibrated = 0xff
-pos_x = 240
-pos_y = 1_200
-theta = 2.8
+pos_x = 300
+pos_y = 900
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000008"
calibrated = 0xff
-pos_x = 720
-pos_y = 1_200
-theta = 1.54
+pos_x = 600
+pos_y = 900
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000009"
calibrated = 0xff
-pos_x = 1_200
-pos_y = 1_200
-theta = 6.03
+pos_x = 900
+pos_y = 900
+direction = 180
diff --git a/dotbot/examples/work_and_charge/gen_init_pose.py b/dotbot/examples/work_and_charge/gen_init_pose.py
index 0576e361..1e6780d0 100644
--- a/dotbot/examples/work_and_charge/gen_init_pose.py
+++ b/dotbot/examples/work_and_charge/gen_init_pose.py
@@ -1,4 +1,5 @@
-from pathlib import Path
+
+import click
# Configuration Constants
NUM_ROBOTS = 8 # Total robots to generate
@@ -7,10 +8,21 @@
X_LEFT = 100
START_Y = 200
Y_STEP = 200
-THETA = 3.14
+DIRECTION = 180
-def generate_dotbot_script():
+@click.command()
+@click.argument(
+ "output_path",
+ type=click.Path(dir_okay=False, writable=True),
+ default="init_state.toml",
+)
+@click.option(
+ "--control-loop-library-path",
+ type=click.Path(exists=True, dir_okay=False),
+ help="Path to an optional .so control loop library.",
+)
+def main(output_path, control_loop_library_path):
lines = []
@@ -35,12 +47,13 @@ def generate_dotbot_script():
f"calibrated = 0xff\n"
f"pos_x = {pos_x}\n"
f"pos_y = {pos_y}\n"
- f"theta = {THETA}\n"
+ f"direction = {DIRECTION}\n"
)
+ if control_loop_library_path is not None:
+ block += f'control_loop_library_path = "{control_loop_library_path}"\n'
lines.append(block)
# Save to file
- output_path = Path(__file__).resolve().parent / "init_state.toml"
with open(output_path, "w") as f:
f.write("\n".join(lines))
@@ -48,4 +61,4 @@ def generate_dotbot_script():
if __name__ == "__main__":
- generate_dotbot_script()
+ main()
diff --git a/dotbot/examples/work_and_charge/init_state.toml b/dotbot/examples/work_and_charge/init_state.toml
index 0e70945a..5ae98e63 100644
--- a/dotbot/examples/work_and_charge/init_state.toml
+++ b/dotbot/examples/work_and_charge/init_state.toml
@@ -3,53 +3,53 @@ address = "AAAAAAAA00000001"
calibrated = 0xff
pos_x = 800
pos_y = 200
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000002"
calibrated = 0xff
pos_x = 100
pos_y = 400
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000003"
calibrated = 0xff
pos_x = 800
pos_y = 600
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000004"
calibrated = 0xff
pos_x = 100
pos_y = 800
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000005"
calibrated = 0xff
pos_x = 800
pos_y = 1_000
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000006"
calibrated = 0xff
pos_x = 100
pos_y = 1_200
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000007"
calibrated = 0xff
pos_x = 800
pos_y = 1_400
-theta = 3.14
+direction = 180
[[dotbots]]
address = "AAAAAAAA00000008"
calibrated = 0xff
pos_x = 100
pos_y = 1_600
-theta = 3.14
+direction = 180
diff --git a/dotbot/frontend/src/DotBotsMap.js b/dotbot/frontend/src/DotBotsMap.js
index 4b08bbc4..f80da0bf 100644
--- a/dotbot/frontend/src/DotBotsMap.js
+++ b/dotbot/frontend/src/DotBotsMap.js
@@ -91,7 +91,7 @@ const DotBotsMapPoint = React.memo((props) => {
const posX = props.mapSize * parseInt(props.dotbot.lh2_position.x) / props.areaSize.width;
const posY = props.mapSize * parseInt(props.dotbot.lh2_position.y) / props.areaSize.width;
- const rotation = (props.dotbot.direction) ? props.dotbot.direction : 0;
+ const rotation = (props.dotbot.direction !== -1000) ? props.dotbot.direction : 0;
const radius = (props.dotbot.address === props.active || hovered) ? props.mapSize * (dotbotRadius + 5) / props.areaSize.width : props.mapSize * dotbotRadius / props.areaSize.width;
const directionShift = (props.dotbot.address === props.active || hovered) ? 2: 1;
const directionSize = (props.dotbot.address === props.active || hovered) ? props.mapSize * (dotbotRadius + 5) / props.areaSize.width : props.mapSize * dotbotRadius / props.areaSize.width;
@@ -134,12 +134,12 @@ const DotBotsMapPoint = React.memo((props) => {
))
)}
-
{
props.updateActive(props.dotbot.address === props.active ? inactiveAddress : props.dotbot.address)
@@ -149,10 +149,11 @@ const DotBotsMapPoint = React.memo((props) => {
onMouseLeave={onMouseLeave} >
{`${props.dotbot.address}@${posX}x${posY}`}
- {(props.dotbot.direction) &&
+ {(props.dotbot.direction !== -1000) &&
+
- }
+ }
>
)
})
diff --git a/pyproject.toml b/pyproject.toml
index 57420ee6..652b0db2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ dependencies = [
"pynput >= 1.8.1",
"pyserial >= 3.5",
"qrkey >= 0.12.1",
+ "rich >= 14.0.0",
"structlog >= 24.4.0",
"uvicorn >= 0.32.0",
"websockets >= 13.1.0",
diff --git a/simulator_init_state.toml b/simulator_init_state.toml
index 6c09d66f..4869d438 100644
--- a/simulator_init_state.toml
+++ b/simulator_init_state.toml
@@ -6,22 +6,22 @@ address = "BADCAFE111111111" # DotBot unique address
calibrated = 0xff # optional, defaults to only first lighthouse calibrated
pos_x = 400 # [0, 2_000] in mm
pos_y = 400 # [0, 2_000]
-theta = 0.0 # [0.0, 2pi]
+direction = 0 # [0, 360] in degrees, 0 is facing north, increasing clockwise
[[dotbots]]
address = "DEADBEEF22222222"
pos_x = 400
pos_y = 200
-theta = 1.57
+direction = 180
[[dotbots]]
address = "B0B0F00D33333333"
pos_x = 1_500
pos_y = 1_500
-theta = 1.57
+direction = 180
[[dotbots]]
address = "BADC0DE444444444"
pos_x = 1_000
pos_y = 1_000
-theta = 3.14
+direction = 360
diff --git a/utils/control_loop/.gitignore b/utils/control_loop/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/utils/control_loop/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/utils/control_loop/CMakeLists.txt b/utils/control_loop/CMakeLists.txt
new file mode 100644
index 00000000..3578431f
--- /dev/null
+++ b/utils/control_loop/CMakeLists.txt
@@ -0,0 +1,34 @@
+cmake_minimum_required(VERSION 3.10)
+project(dotbot_control_loop VERSION 1.0 LANGUAGES C)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_BUILD_TYPE Release)
+
+# Set default dotbot version
+if(NOT DEFINED DOTBOT_VERSION)
+ set(DOTBOT_VERSION 3)
+endif()
+
+# Require DOTBOT_LIBS_DIR
+if(NOT DEFINED DOTBOT_LIBS_DIR)
+ message(FATAL_ERROR "DOTBOT_LIBS_DIR must be set")
+endif()
+
+# Create shared library
+add_library(${PROJECT_NAME} SHARED
+ ${DOTBOT_LIBS_DIR}/drv/control_loop/control_loop.c
+)
+
+# Set include directories
+target_include_directories(${PROJECT_NAME} PRIVATE
+ ${DOTBOT_LIBS_DIR}/drv
+)
+
+# Set preprocessor definition
+target_compile_definitions(${PROJECT_NAME} PRIVATE
+ BOARD_DOTBOT_V${DOTBOT_VERSION}
+)
+
+# Add fast-math flag
+target_compile_options(${PROJECT_NAME} PRIVATE -ffast-math)
diff --git a/utils/control_loop/README.md b/utils/control_loop/README.md
new file mode 100644
index 00000000..7be1dbfa
--- /dev/null
+++ b/utils/control_loop/README.md
@@ -0,0 +1,47 @@
+
+# Custom Control Loop Library
+
+## Overview
+
+This guide explains how to build a custom control loop library from the C
+version provided by DotBot-libs and integrate it with the simulator using FFI
+(Foreign Function Interface).
+
+## Building with CMake
+
+### Prerequisites
+
+- CMake 3.10 or higher
+- C compiler (gcc, clang, or MSVC)
+
+### Build Steps
+
+```bash
+cmake -DDOTBOT_LIBS_DIR= -DDOTBOT_VERSION= -B build .
+make -C build
+```
+
+This creates the `build/` subdirectory and generates there the
+`libdotbot_control_loop.so` library file.
+
+`DOTBOT_LIBS_DIR` variable must be set to tell the build system where to find
+the dotbot control loop library.
+If not set default value for `DOTBOT_VERSION` is 3.
+
+
+3. The compiled shared library will be in the `build/` directory (`.so` on Linux,
+`.dylib` on macOS, `.dll` on Windows).
+
+## Integration with Simulator
+
+### Configuration
+
+Add your custom control loop library to `simulator_init_state.toml`:
+
+```toml
+[[robots]]
+address = "DECAFBAD4B0B0AAA"
+custom_control_loop_library = "/path/to/libdotbot_control_loop.so"
+```
+
+Each robot configuration can specify its own library independently.