Skip to content
Merged
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
24 changes: 12 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
name: CI

# Run on every push and every PR against main.
# Run on every push to main and every PR against main.
on:
push:
branches: [main]
pull_request:
branches: [main]

# Cancel older runs of the same branch when a new commit lands.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
runs-on: ubuntu-latest
strategy:
# Test the range of Python versions we claim to support.
# See every version's result, not just the first failure.
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]

python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install (editable, with dev deps)
run: python -m pip install -e ".[dev]"

# Delegate to the Makefile so local and CI run identical commands.
- name: Lint + type-check
run: |
ruff check autonomy_stack tests
mypy

run: make lint
- name: Test
run: pytest
run: make test
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PYTHON ?= python
PKG := autonomy_stack
PKG := autonomy_stack

.PHONY: lint test run install
.PHONY: install lint format test check run

install:
$(PYTHON) -m pip install -e ".[dev]"
Expand All @@ -10,9 +10,16 @@ lint:
ruff check $(PKG) tests
mypy

# Auto-fix what's safely auto-fixable; format the rest.
format:
ruff format $(PKG) tests
ruff check --fix $(PKG) tests

test:
pytest

run:
$(PYTHON) -m $(PKG).nodes.replay --log logs/sample_session.parquet
# Single entry point for "is this PR good to go".
check: lint test

run:
$(PYTHON) -m $(PKG).nodes.replay --log logs/sample_session.parquet
21 changes: 21 additions & 0 deletions autonomy_stack/control/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass

from autonomy_stack.localization.pose import Pose
from autonomy_stack.planning.planner import Trajectory


@dataclass(frozen=True)
class Command:
"""A combined throttle + steering command. Both fields in [-1.0, 1.0]."""

timestamp: float
throttle: float # -1 full reverse, +1 full forward
steering: float # -1 full left, +1 full right


class Controller(ABC):
@abstractmethod
def step(self, trajectory: Trajectory, pose: Pose) -> Command:
"""Compute the next actuator command tracking the given trajectory."""
...
2 changes: 1 addition & 1 deletion autonomy_stack/hardware/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class Camera(ABC):
@abstractmethod
def read(self) -> npt.NDArray[np.uint8]:
"""Read one frame: an (H, W, 3) unit8 BGR image."""
"""Read one frame: an (H, W, 3) uint8 BGR image."""
...

@abstractmethod
Expand Down
25 changes: 25 additions & 0 deletions autonomy_stack/hardware/imu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass(frozen=True)
class ImuReading:
"""A single IMU sample. SI units; timestamp is a monotonic clock in seconds."""

timestamp: float
# Angular velocity (rad/s) in body frame: roll-rate, pitch-rate, yaw-rate.
gyro: tuple[float, float, float]
# Linear acceleration (m/s²) in body frame: x, y, z.
accel: tuple[float, float, float]


class IMU(ABC):
@abstractmethod
def read(self) -> ImuReading:
"""Read one sample. Blocks if no sample is available yet."""
...

@abstractmethod
def close(self) -> None:
"""Release the IMU resource."""
...
13 changes: 13 additions & 0 deletions autonomy_stack/hardware/steering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from abc import ABC, abstractmethod


class SteeringServo(ABC):
@abstractmethod
def set_angle(self, value: float) -> None:
"""Set steering in [-1.0, 1.0]. -1 full left, +1 full right, 0 centered."""
...

@abstractmethod
def center(self) -> None:
"""Return steering to center. Either successful or raise an exception."""
...
11 changes: 11 additions & 0 deletions autonomy_stack/localization/pose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class Pose:
"""2D pose of the car in the world frame."""

timestamp: float
x: float # meters
y: float # meters
heading: float # radians; 0 = +x axis, CCW positive
12 changes: 12 additions & 0 deletions autonomy_stack/perception/detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class Detection:
"""A detected lane feature or obstacle. Refine when Phase 3 perception lands."""

timestamp: float
label: str # e.g. "lane_left", "lane_right", "obstacle"
# Bounding box in normalized image coords: (x_min, y_min, x_max, y_max), each in [0, 1].
bbox: tuple[float, float, float, float]
confidence: float # [0.0, 1.0]
30 changes: 30 additions & 0 deletions autonomy_stack/planning/planner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from abc import ABC, abstractmethod
from collections.abc import Sequence
from dataclasses import dataclass

from autonomy_stack.localization.pose import Pose
from autonomy_stack.perception.detection import Detection


@dataclass(frozen=True)
class Waypoint:
"""A single trajectory point in the world frame."""

x: float # meters
y: float # meters
speed: float # m/s, target speed at this waypoint


@dataclass(frozen=True)
class Trajectory:
"""A planned path: a time-ordered sequence of waypoints."""

timestamp: float
waypoints: Sequence[Waypoint]


class Planner(ABC):
@abstractmethod
def plan(self, pose: Pose, detections: Sequence[Detection]) -> Trajectory:
"""Compute the next trajectory given current pose and latest detections."""
...
16 changes: 12 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ dependencies = [
]

[project.optional-dependencies]
dev = ["ruff>=0.6", "mypy>=1.11", "pytest>=8.0"]
pi = ["adafruit-circuitpython-pca9685>=3.4", "adafruit-circuitpython-bno055>=5.4"]
# Dev tools are upper-bounded so a future release can't silently break CI.
dev = [
"ruff>=0.6,<0.9",
"mypy>=1.11,<2.0",
"pytest>=8.0,<9.0",
]
pi = [
"adafruit-circuitpython-pca9685>=3.4",
"adafruit-circuitpython-bno055>=5.4",
]

[tool.hatch.build.targets.wheel]
packages = ["autonomy_stack"]

[tool.ruff]
target-version = "py311"
target-version = "py311" # lint for the minimum supported version
line-length = 100

[tool.ruff.lint]
Expand All @@ -39,4 +47,4 @@ ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
addopts = "-ra --strict-markers"
11 changes: 9 additions & 2 deletions tests/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@

import pytest

from autonomy_stack.control.controller import Controller
from autonomy_stack.hardware.camera import Camera
from autonomy_stack.hardware.imu import IMU
from autonomy_stack.hardware.motor import MotorDriver
from autonomy_stack.hardware.steering import SteeringServo
from autonomy_stack.planning.planner import Planner


@pytest.mark.parametrize("interface", [MotorDriver, Camera])
@pytest.mark.parametrize(
"interface",
[MotorDriver, SteeringServo, Camera, IMU, Planner, Controller],
)
def test_interfaces_cannot_be_instantiated(interface: type) -> None:
with pytest.raises(TypeError):
interface()
interface()
Loading