diff --git a/dotbot/examples/README.md b/dotbot/examples/README.md index f3a39963..d83387fd 100644 --- a/dotbot/examples/README.md +++ b/dotbot/examples/README.md @@ -1,27 +1,26 @@ -# DotBot Simulator Experiments +# DotBot Examples -This directory contains **experimental control scripts** for the DotBot simulator. -The goal is to prototype, test, and iterate on the testbed without needing to deploy anything, -with the same API that will run on a real testbed. **without touching the controller internals**. +This directory contains example scenarios for DotBots. +Examples can run against either real robots or the simulator, using the same controller APIs. +The simulator setup is documented as the default path because it is the most common way to reproduce experiments. +Each scenario has its own folder with dedicated instructions, initial states, and run commands. -All interaction with the simulator is done **via HTTP**, exactly like a real deployment. +## Available scenarios ---- +- `minimum_naming_game/`: naming game examples (with and without motion) +- `work_and_charge/`: work/charge alternation scenario +- `charging_station/`: queue-and-charge scenario -## 1. Start the simulator +We also provide a stop.py helper script to halt the simulator (without needing to stop robots via SwarmIT). -First, start the DotBot controller in **simulator mode** with the correct configuration: +## Common usage pattern (default: simulator) -```bash -dotbot-controller \ - --config-path config_sample.toml \ - -a dotbot-simulator -``` - -## 2. Run the experiments - -For example, if you want to run the charging station proof-of-concept +1. Pick a scenario and read its local `README.md`. +2. Set `simulator_init_state_path` in `config_sample.toml` as described by that scenario. +3. Start the controller in simulator mode: ```bash -python3 dotbot/examples/charging_station.py +python -m dotbot.controller_app --config-path config_sample.toml -a dotbot-simulator ``` + +4. Run the selected example using its documented command. diff --git a/dotbot/examples/charging_station/README.md b/dotbot/examples/charging_station/README.md new file mode 100644 index 00000000..e65fd730 --- /dev/null +++ b/dotbot/examples/charging_station/README.md @@ -0,0 +1,30 @@ +# Charging Station + +This demo runs a charging-station scenario: +robots first form a queue, then move through charging and parking phases. +It works with real robots or with the simulator via the same controller API. +The simulator setup below is the default path for reproducibility. + +## How to run (default: simulator) + +### 1. Specify the initial state + +Set `simulator_init_state_path` in `config_sample.toml` to: + +```toml +simulator_init_state_path = "dotbot/examples/charging_station/charging_station_init_state.toml" +``` + +### 2. Start the controller in simulator mode + +```bash +python -m dotbot.controller_app --config-path config_sample.toml -a dotbot-simulator +``` + +### 3. Run the charging-station scenario + +From the `PyDotBot/` root in a new terminal: + +```bash +python -m dotbot.examples.charging_station.charging_station +``` diff --git a/dotbot/examples/charging_station/__init__.py b/dotbot/examples/charging_station/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dotbot/examples/charging_station.py b/dotbot/examples/charging_station/charging_station.py similarity index 93% rename from dotbot/examples/charging_station.py rename to dotbot/examples/charging_station/charging_station.py index e5dcbaeb..ae813618 100644 --- a/dotbot/examples/charging_station.py +++ b/dotbot/examples/charging_station/charging_station.py @@ -3,12 +3,12 @@ import os from typing import Dict, List -from dotbot.examples.orca import ( +from dotbot.examples.common.orca import ( Agent, OrcaParams, compute_orca_velocity_for_agent, ) -from dotbot.examples.vec2 import Vec2 +from dotbot.examples.common.vec2 import Vec2 from dotbot.models import ( DotBotLH2Position, DotBotModel, @@ -300,7 +300,7 @@ def preferred_vel(dotbot: DotBotModel, goal: Vec2 | None) -> Vec2: def direction_to_rad(direction: float) -> float: rad = (direction + 90) * math.pi / 180.0 - return math.atan2(math.sin(rad), math.cos(rad)) # normalize to [-π, π] + return math.atan2(math.sin(rad), math.cos(rad)) # normalize to [-π, +π] async def compute_orca_velocity( @@ -342,6 +342,20 @@ async def main() -> None: # Phase 2: charging loop await charge_robots(client, ws, params) + except (asyncio.CancelledError, KeyboardInterrupt): + active_dotbots = await fetch_active_dotbots(client) + for dotbot in active_dotbots: + await ws.send( + WSWaypoints( + cmd="waypoints", + address=dotbot.address, + application=dotbot.application, + data=DotBotWaypoints( + threshold=0, + waypoints=[], + ), + ) + ) finally: await ws.close() diff --git a/dotbot/examples/charging_station_init_state.toml b/dotbot/examples/charging_station/charging_station_init_state.toml similarity index 100% rename from dotbot/examples/charging_station_init_state.toml rename to dotbot/examples/charging_station/charging_station_init_state.toml diff --git a/dotbot/examples/common/__init__.py b/dotbot/examples/common/__init__.py new file mode 100644 index 00000000..6d763454 --- /dev/null +++ b/dotbot/examples/common/__init__.py @@ -0,0 +1 @@ +"""Shared helpers for DotBot examples.""" diff --git a/dotbot/examples/orca.py b/dotbot/examples/common/orca.py similarity index 99% rename from dotbot/examples/orca.py rename to dotbot/examples/common/orca.py index 8c176526..437f4e6d 100644 --- a/dotbot/examples/orca.py +++ b/dotbot/examples/common/orca.py @@ -5,7 +5,7 @@ import math from dataclasses import dataclass -from dotbot.examples.vec2 import ( +from dotbot.examples.common.vec2 import ( Vec2, add, dot, diff --git a/dotbot/examples/sct.py b/dotbot/examples/common/sct.py similarity index 100% rename from dotbot/examples/sct.py rename to dotbot/examples/common/sct.py diff --git a/dotbot/examples/vec2.py b/dotbot/examples/common/vec2.py similarity index 100% rename from dotbot/examples/vec2.py rename to dotbot/examples/common/vec2.py diff --git a/dotbot/examples/minimum_naming_game/controller.py b/dotbot/examples/minimum_naming_game/controller.py index 0095d678..241ed610 100644 --- a/dotbot/examples/minimum_naming_game/controller.py +++ b/dotbot/examples/minimum_naming_game/controller.py @@ -1,6 +1,6 @@ import random -from dotbot.examples.sct import SCT +from dotbot.examples.common.sct import SCT from dotbot.models import ( DotBotLH2Position, DotBotModel, diff --git a/dotbot/examples/minimum_naming_game/controller_with_motion.py b/dotbot/examples/minimum_naming_game/controller_with_motion.py index edd590fc..48b65bd8 100644 --- a/dotbot/examples/minimum_naming_game/controller_with_motion.py +++ b/dotbot/examples/minimum_naming_game/controller_with_motion.py @@ -1,8 +1,8 @@ import math import random +from dotbot.examples.common.sct import SCT from dotbot.examples.minimum_naming_game.walk_avoid import walk_avoid -from dotbot.examples.sct import SCT from dotbot.models import ( DotBotLH2Position, DotBotModel, diff --git a/dotbot/examples/minimum_naming_game/minimum_naming_game_with_motion.py b/dotbot/examples/minimum_naming_game/minimum_naming_game_with_motion.py index c6f366c4..01a7a7d3 100644 --- a/dotbot/examples/minimum_naming_game/minimum_naming_game_with_motion.py +++ b/dotbot/examples/minimum_naming_game/minimum_naming_game_with_motion.py @@ -15,6 +15,7 @@ DotBotStatus, DotBotWaypoints, WSRgbLed, + WSWaypoints, ) from dotbot.protocol import ApplicationType from dotbot.rest import RestClient, rest_client @@ -190,6 +191,21 @@ async def main() -> None: # await asyncio.sleep(0.1) counter += 1 + except (asyncio.CancelledError, KeyboardInterrupt): + # stop all dotbots + active_dotbots = await fetch_active_dotbots(client) + for dotbot in active_dotbots: + await ws.send( + WSWaypoints( + cmd="waypoints", + address=dotbot.address, + application=dotbot.application, + data=DotBotWaypoints( + threshold=0, + waypoints=[], + ), + ) + ) finally: await ws.close() diff --git a/dotbot/examples/work_and_charge/controller.py b/dotbot/examples/work_and_charge/controller.py index 45cf4d15..65f08b27 100644 --- a/dotbot/examples/work_and_charge/controller.py +++ b/dotbot/examples/work_and_charge/controller.py @@ -1,6 +1,6 @@ import math -from dotbot.examples.sct import SCT +from dotbot.examples.common.sct import SCT from dotbot.models import ( DotBotLH2Position, ) diff --git a/dotbot/examples/work_and_charge/work_and_charge.py b/dotbot/examples/work_and_charge/work_and_charge.py index a0568414..4eeb98a7 100644 --- a/dotbot/examples/work_and_charge/work_and_charge.py +++ b/dotbot/examples/work_and_charge/work_and_charge.py @@ -6,12 +6,12 @@ import numpy as np from scipy.spatial import cKDTree -from dotbot.examples.orca import ( +from dotbot.examples.common.orca import ( Agent, OrcaParams, compute_orca_velocity_for_agent, ) -from dotbot.examples.vec2 import Vec2 +from dotbot.examples.common.vec2 import Vec2 from dotbot.examples.work_and_charge.controller import Controller from dotbot.models import ( DotBotLH2Position, @@ -327,6 +327,21 @@ async def main() -> None: ) await asyncio.sleep(DT) + except (asyncio.CancelledError, KeyboardInterrupt): + active_dotbots = await fetch_active_dotbots(client) + for dotbot in active_dotbots: + await ws.send( + WSWaypoints( + cmd="waypoints", + address=dotbot.address, + application=dotbot.application, + data=DotBotWaypoints( + threshold=0, + waypoints=[], + ), + ) + ) + return except Exception as e: print(f"Connection lost: {e}") print("Retrying in 1 seconds...") diff --git a/dotbot/tests/test_experiment_charging_station.py b/dotbot/tests/test_experiment_charging_station.py index 197bc4f2..e5e80749 100644 --- a/dotbot/tests/test_experiment_charging_station.py +++ b/dotbot/tests/test_experiment_charging_station.py @@ -5,7 +5,7 @@ import pytest -from dotbot.examples.charging_station import ( +from dotbot.examples.charging_station.charging_station import ( DT, PARK_SPACING, PARK_X, @@ -16,7 +16,7 @@ charge_robots, queue_robots, ) -from dotbot.examples.orca import OrcaParams +from dotbot.examples.common.orca import OrcaParams from dotbot.models import ( DotBotLH2Position, DotBotModel,