Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/build/
*.log
*.jsonl
41 changes: 41 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.20)
project(aerostack VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

option(AEROSTACK_BUILD_TESTS "Build unit tests" ON)

add_library(aerostack_lib
src/world/world.cpp
src/planning/astar_planner.cpp
src/control/local_avoidance.cpp
src/sim/mission_manager.cpp
src/sim/simulation_engine.cpp
src/telemetry/telemetry.cpp
)

target_include_directories(aerostack_lib PUBLIC include)

target_compile_options(aerostack_lib PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
)

add_executable(aerostack_sim src/app/main.cpp)
target_link_libraries(aerostack_sim PRIVATE aerostack_lib)

if(AEROSTACK_BUILD_TESTS)
include(CTest)
enable_testing()

add_executable(aerostack_tests tests/test_core.cpp)
target_link_libraries(aerostack_tests PRIVATE aerostack_lib)
add_test(NAME aerostack_tests COMMAND aerostack_tests)
add_test(NAME aerostack_smoke COMMAND aerostack_sim --scenario smoke --quiet --fail-if-incomplete)
add_test(NAME aerostack_swarm COMMAND aerostack_sim --scenario swarm --quiet --fail-if-incomplete)
add_test(NAME aerostack_reject_negative_drones COMMAND aerostack_sim --drones -1 --quiet)
set_tests_properties(aerostack_reject_negative_drones PROPERTIES WILL_FAIL TRUE)
add_test(NAME aerostack_reject_junk_missions COMMAND aerostack_sim --missions 12x --quiet)
set_tests_properties(aerostack_reject_junk_missions PROPERTIES WILL_FAIL TRUE)
endif()
123 changes: 122 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,122 @@
# aerostack
# AeroStack: C++20 Drone Swarm Simulator

A modular multi-drone autonomy simulator built with **modern C++20**. The project models a simplified autonomy stack for swarm mission execution:

- world model + obstacles
- global planning (A*)
- local collision avoidance (predictive safety check)
- mission manager + dynamic replanning
- simulation engine using `std::jthread` and `std::stop_token`
- event telemetry using `std::variant`

## Architecture

```text
/include/aerostack
types.hpp
world.hpp
planning.hpp
control.hpp
mission_manager.hpp
simulation_engine.hpp
telemetry.hpp
/src
world/
planning/
control/
sim/
telemetry/
app/main.cpp
/tests
test_core.cpp
```

### Core robotics-style components

- **World**: continuous 2D space with circular obstacles and drone/mission state.
- **AStarPlanner**: global path planner on a discretized grid.
- **PredictiveAvoidance**: local collision avoidance by simulating near-term future trajectories.
- **MissionManager**: assigns random goals and replans if goals become unreachable.
- **SimulationEngine**: runs the control loop in a dedicated `std::jthread` with cooperative shutdown.
- **TelemetryRecorder**: records simulation events and can export JSONL logs.

## Build

```bash
cmake -S . -B build
cmake --build build -j
```

## Run simulator

```bash
./build/aerostack_sim
```

The simulator is intended to be a terminal-first autonomy demo and browser replay generator. A run prints:

- the scenario geometry, obstacles, drones, and deterministic mission goals
- the autonomy components being exercised: A* planning, predictive avoidance, mission management, and telemetry
- live progress every few seconds of simulated time, including drone positions, speed, mission distance, battery, collision count, and telemetry count
- a final result summary with simulated time, drone count, completed missions, collision count, and telemetry events

It demonstrates the core shape of a robotics stack rather than a visual game/sandbox: missions are generated, drones plan global paths around obstacles, local control checks near-term safety, mission completion is detected, telemetry is recorded, and `replay.html` is regenerated for browser inspection.

Useful simulator variants:

```bash
# Fast deterministic smoke run: 2 drones, 2 short missions, exits nonzero if incomplete.
./build/aerostack_sim --scenario smoke --quiet --fail-if-incomplete

# Swarm demo: deterministic lanes through an obstacle field, expected to complete.
./build/aerostack_sim --scenario swarm --quiet --fail-if-incomplete

# Larger stress run with more work than drones; useful for finding congestion limits.
./build/aerostack_sim --scenario swarm --drones 20 --missions 40 --duration 120

# Terminal visualization without adding GUI dependencies.
./build/aerostack_sim --scenario demo --map --report-every 5

# Standalone interactive browser replay.
./build/aerostack_sim --scenario demo --quiet

# Export telemetry for plotting or a later UI.
./build/aerostack_sim --scenario swarm --telemetry-out swarm.jsonl --quiet
```

Every simulator run writes a fresh standalone browser replay to `replay.html` by default. Use `--html-out <path>` only when you want a different output path.

Supported options:

- `--scenario <demo|smoke|swarm>`: choose a preset.
- `--drones <n>`: set drone count.
- `--missions <n>`: set mission count; missions can exceed drones and are assigned round-robin.
- `--duration <seconds>`: set simulated runtime.
- `--report-every <seconds>`: set progress interval.
- `--map`: print a coarse ASCII map where digits are drones, `*` marks active goals, and `#` marks obstacles.
- `--quiet`: only print the final summary.
- `--telemetry-out <path>`: write simulation telemetry as JSONL.
- `--html-out <path>`: set the standalone browser replay path; default is `replay.html`.
- `--fail-if-incomplete`: return a nonzero exit code unless all missions complete.

## Run tests

```bash
ctest --test-dir build --output-on-failure
```

## Metrics emitted by the simulator

- `Missions completed`: how many assigned goals were reached within the simulated duration.
- `Detected collisions`: close-range drone/drone contacts observed during the run. The built-in smoke and swarm demos should stay at `0`.
- `Telemetry events`: control-loop tick and event records captured for replay/export.
- Per-drone progress lines: current position, speed, distance to the active goal, and battery level.
- `replay.html`: the latest interactive Canvas replay, overwritten on every simulator run unless `--html-out` points somewhere else.

## Next upgrades

- noisy GPS/IMU and Kalman filtering
- occupancy grid mapping
- battery-aware task allocation
- no-fly zones and wind disturbance model
- GUI frontend (raylib/SDL2/ImGui)
7 changes: 7 additions & 0 deletions configs/default_mission.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"world": {"width": 80, "height": 50},
"drones": 6,
"seed": 42,
"step_ms": 50,
"duration_s": 10
}
43 changes: 43 additions & 0 deletions include/aerostack/control.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once

#include <concepts>
#include <vector>

#include "aerostack/types.hpp"

namespace aerostack {

template <typename T>
concept LocalAvoidance = requires(T avoidance, const DroneState& self, const std::vector<DroneState>& others,
const std::vector<Obstacle>& obstacles, const Vec2& desired_velocity, double dt) {
{ avoidance.safe_velocity(self, others, obstacles, desired_velocity, dt) } -> std::same_as<Vec2>;
};

class PredictiveAvoidance {
public:
PredictiveAvoidance(double horizon_seconds = 2.0, double min_separation = 1.5)
: horizon_seconds_(horizon_seconds), min_separation_(min_separation) {}

Vec2 safe_velocity(const DroneState& self,
const std::vector<DroneState>& others,
const std::vector<Obstacle>& obstacles,
const Vec2& desired_velocity,
double dt) const;

private:
[[nodiscard]] bool unsafe(const DroneState& self,
const std::vector<DroneState>& others,
const std::vector<Obstacle>& obstacles,
const Vec2& v,
double dt) const;
[[nodiscard]] Vec2 separation_velocity(const DroneState& self, const std::vector<DroneState>& others) const;
[[nodiscard]] bool obstacle_unsafe(const DroneState& self,
const std::vector<Obstacle>& obstacles,
const Vec2& v,
double dt) const;

double horizon_seconds_;
double min_separation_;
};

} // namespace aerostack
22 changes: 22 additions & 0 deletions include/aerostack/mission_manager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <random>

#include "aerostack/planning.hpp"
#include "aerostack/telemetry.hpp"
#include "aerostack/world.hpp"

namespace aerostack {

class MissionManager {
public:
MissionManager();

void seed_random_missions(World& world, std::size_t mission_count);
void update(World& world, const AStarPlanner& planner, TelemetryRecorder& telemetry);

private:
std::mt19937 rng_;
};

} // namespace aerostack
25 changes: 25 additions & 0 deletions include/aerostack/planning.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include <concepts>
#include <optional>
#include <vector>

#include "aerostack/types.hpp"

namespace aerostack {

template <typename T>
concept GlobalPlanner = requires(T planner, const WorldSnapshot& world, const Vec2& start, const Vec2& goal) {
{ planner.plan(world, start, goal) } -> std::same_as<std::optional<std::vector<Vec2>>>;
};

class AStarPlanner {
public:
explicit AStarPlanner(double grid_resolution = 1.0) : grid_resolution_(grid_resolution) {}
std::optional<std::vector<Vec2>> plan(const WorldSnapshot& world, const Vec2& start, const Vec2& goal) const;

private:
double grid_resolution_;
};

} // namespace aerostack
47 changes: 47 additions & 0 deletions include/aerostack/simulation_engine.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <atomic>
#include <chrono>
#include <thread>
#include <unordered_map>

#include "aerostack/control.hpp"
#include "aerostack/mission_manager.hpp"

namespace aerostack {

struct DroneMetrics {
double path_length{0.0};
double battery_used{0.0};
std::size_t replans{0};
};

class SimulationEngine {
public:
SimulationEngine(World world, AStarPlanner planner, PredictiveAvoidance avoidance, MissionManager mission_manager,
TelemetryRecorder telemetry);

void run_for(std::chrono::seconds duration, std::chrono::milliseconds step);
void stop();
void step(double dt);

[[nodiscard]] WorldSnapshot snapshot() const { return world_.snapshot(); }
[[nodiscard]] const TelemetryRecorder& telemetry() const { return telemetry_; }
[[nodiscard]] std::size_t collisions() const { return collisions_.load(); }

private:
void loop(std::stop_token st, std::chrono::milliseconds step);

World world_;
AStarPlanner planner_;
PredictiveAvoidance avoidance_;
MissionManager mission_manager_;
TelemetryRecorder telemetry_;
std::atomic<std::size_t> collisions_{0};
std::unordered_map<std::size_t, std::vector<Vec2>> planned_paths_;
std::unordered_map<std::size_t, std::size_t> path_index_;
std::unordered_map<std::size_t, DroneMetrics> metrics_;
std::jthread thread_;
};

} // namespace aerostack
39 changes: 39 additions & 0 deletions include/aerostack/telemetry.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include <filesystem>
#include <variant>
#include <vector>

#include "aerostack/types.hpp"

namespace aerostack {

struct TickEvent {
double sim_time{};
std::vector<DroneState> drones;
};

struct ReplanEvent {
std::size_t drone_id{};
Vec2 new_goal;
};

struct CollisionEvent {
std::size_t a{};
std::size_t b{};
double sim_time{};
};

using TelemetryEvent = std::variant<TickEvent, ReplanEvent, CollisionEvent>;

class TelemetryRecorder {
public:
void record(TelemetryEvent event);
[[nodiscard]] const std::vector<TelemetryEvent>& events() const { return events_; }
void write_jsonl(const std::filesystem::path& output) const;

private:
std::vector<TelemetryEvent> events_;
};

} // namespace aerostack
Loading