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
22 changes: 22 additions & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
%{
configs: [
%{
name: "default",
checks: [
{Credo.Check.Readability.MaxLineLength, max_length: 120},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.UnlessWithElse, []}
],
plugins: [{ExSlop, []}]
}
]
}
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
190 changes: 190 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
ELIXIR_VERSION: "1.18"
OTP_VERSION: "27"
MIX_ENV: test

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Check formatting
run: mix format --check-formatted
- name: Check unused dependencies
run: mix deps.unlock --check-unused
- name: Run credo
run: mix credo --strict

test:
name: Test (Elixir ${{ matrix.elixir }}, OTP ${{ matrix.otp }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- elixir: "1.20"
otp: "29"
- elixir: "1.19"
otp: "28"
- elixir: "1.18"
otp: "27"
- elixir: "1.17"
otp: "26"
env:
MIX_ENV: test
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Compile
run: mix compile --warnings-as-errors
- name: Run tests
run: mix test --trace

coverage:
name: Coverage
runs-on: ubuntu-latest
env:
MIX_ENV: test
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-coverage-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Run coverage and push to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: mix coveralls.github || true
- name: Check coverage threshold (70%)
run: mix coveralls

security:
name: Security
runs-on: ubuntu-latest
env:
MIX_ENV: dev
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Install dependencies
run: mix deps.get
- name: Run sobelow
run: mix sobelow --skip --exit-low

dialyzer:
name: Dialyzer
runs-on: ubuntu-latest
env:
MIX_ENV: dev
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- uses: actions/cache@v4
with:
path: |
deps
_build
priv/plts
key: ${{ runner.os }}-dialyzer-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-dialyzer-
- name: Install dependencies
run: mix deps.get
- name: Build PLT
run: mix dialyzer --plt
- name: Run dialyzer
run: mix dialyzer --no-check

docs:
name: Build Docs
runs-on: ubuntu-latest
env:
MIX_ENV: dev
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Install dependencies
run: mix deps.get
- name: Build docs
run: mix docs
- uses: actions/upload-artifact@v4
with:
name: docs
path: doc/

release:
name: Release to Hex.pm
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [lint, test, coverage, security, dialyzer, docs]
runs-on: ubuntu-latest
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Install dependencies
run: mix deps.get
- name: Publish to Hex.pm
run: mix hex.publish --yes
26 changes: 26 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Release

on:
push:
tags: ["v*"]

jobs:
release:
name: Publish to Hex.pm
runs-on: ubuntu-latest
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: "1.18"
otp-version: "27"
- name: Install dependencies
run: mix deps.get
- name: Compile
run: mix compile
- name: Run tests
run: mix test
- name: Publish to Hex.pm
run: mix hex.publish --yes
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ erl_crash.dump
*.beam
/config/*.secret.exs
.elixir_ls/
/priv/plts/*.plt
/priv/plts/*.plt.hash
coveralls.json
*.tar
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Changelog

## v0.1.0 (2026-06-10)

### Added

- `Drone` public API module for connecting, flying, and disconnecting drones
- `Drone.Vehicle` supervised GenServer per drone process
- `Drone.Adapter` behaviour for pluggable drone adapters
- `Drone.Adapters.Sim` simulator adapter (in-process state machine, no hardware needed)
- `Drone.Adapters.Tello` Tello UDP adapter with command encoding and response parsing
- `Drone.Adapters.Tello.Connection` UDP connection handler with centralized defaults
- `Drone.Command` struct with constructors for all Tello SDK commands
- `Drone.Error` error type helpers with `safety/1`, `adapter/1`, `invalid_command/1`
- `Drone.Geometry` shared position math module (`move_delta/3`, `rotate_yaw/3`, `flip_delta/1`)
- `Drone.Safety` pure validation module with altitude, distance, battery, geofence, and allowlist checks
- `Drone.Safety.Policy` struct with default, indoor, and unrestricted presets
- `Drone.Safety.Geofence` circle and polygon geofence support
- `Drone.Telemetry` event helpers emitting `:telemetry` events
- `Drone.Mission` DSL for scripting command sequences
- `Drone.Supervisor` dynamic supervisor for vehicle processes
- `Drone.Application` OTP application starting Registry and Supervisor
- Emergency stop command bypassing all safety checks
- Dry-run mode for validating missions without sending commands
- Command history tracking in simulator and vehicle state
- Flight time simulation in simulator (`query(:time)` returns cumulative motor-on seconds)
- Configurable battery drain simulation
- Configurable failure injection in simulator
- Position tracking (x, y, z, yaw) in simulator and vehicle state
- Command argument validation enforcing Tello SDK ranges (distance 20-500cm, degrees 1-3600, speed 10-100cm/s, hover seconds >0)
- CI/CD pipeline (lint, test matrix, coverage, sobelow, dialyzer, docs, Hex.pm release)

### Changed

- `Drone.Vehicle.child_spec` simplified: `:id` is a constant since DynamicSupervisor ignores it
- `Drone.Adapters.Tello` now references `Connection.default_*()` accessor functions instead of duplicating defaults
- `Drone.Adapters.Sim.query(:time)` returns cumulative flight seconds (not command count), matching real Tello behavior
- `@type drone` narrowed from `atom() | pid()` to `atom()` (pids not supported)
- `Drone.Error.safety/1` used throughout Safety module (was inline tuples)
- Battery stored as `number()` internally in sim, exposed as `trunc(battery)` integer

### Fixed

- Command range validation (F-01): `move/3`, `rotate/3`, `set_speed/2`, `hover/2` reject out-of-range values
- Battery always reported as integer (F-02)
- Graceful `{:error, :not_connected}` for unknown drone names (F-04)
- Tello parser handles negative numbers (F-08)
- `Policy.new(unrestricted: true)` no longer crashes (F-09/F-10)
- `Drone.Vehicle.terminate/2` emits adapter key (`:sim`/`:tello`) not raw module name
- Telemetry metadata bug: terminate/2 was emitting adapter module instead of adapter key
Loading
Loading