mcardupilot 2026.10.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcardupilot-2026.10.3/LICENSE +21 -0
- mcardupilot-2026.10.3/PKG-INFO +214 -0
- mcardupilot-2026.10.3/README.md +190 -0
- mcardupilot-2026.10.3/pyproject.toml +80 -0
- mcardupilot-2026.10.3/src/mcardupilot/__init__.py +8 -0
- mcardupilot-2026.10.3/src/mcardupilot/config.py +151 -0
- mcardupilot-2026.10.3/src/mcardupilot/errors.py +23 -0
- mcardupilot-2026.10.3/src/mcardupilot/frames.py +48 -0
- mcardupilot-2026.10.3/src/mcardupilot/leases.py +234 -0
- mcardupilot-2026.10.3/src/mcardupilot/logcompare.py +188 -0
- mcardupilot-2026.10.3/src/mcardupilot/logread.py +419 -0
- mcardupilot-2026.10.3/src/mcardupilot/logs.py +285 -0
- mcardupilot-2026.10.3/src/mcardupilot/mission.py +154 -0
- mcardupilot-2026.10.3/src/mcardupilot/models.py +277 -0
- mcardupilot-2026.10.3/src/mcardupilot/procs.py +234 -0
- mcardupilot-2026.10.3/src/mcardupilot/provenance.py +63 -0
- mcardupilot-2026.10.3/src/mcardupilot/server.py +60 -0
- mcardupilot-2026.10.3/src/mcardupilot/sessions.py +973 -0
- mcardupilot-2026.10.3/src/mcardupilot/sitl.py +615 -0
- mcardupilot-2026.10.3/src/mcardupilot/state.py +50 -0
- mcardupilot-2026.10.3/src/mcardupilot/steps.py +302 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/__init__.py +23 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/_common.py +43 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/compare_tools.py +47 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/info.py +43 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/lease_tools.py +161 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/log_tools.py +170 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/session_tools.py +472 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/steps_tools.py +130 -0
- mcardupilot-2026.10.3/src/mcardupilot/tools/wind_tools.py +43 -0
- mcardupilot-2026.10.3/src/mcardupilot/turbulence.py +100 -0
- mcardupilot-2026.10.3/src/mcardupilot/wind.py +157 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Malloy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcardupilot
|
|
3
|
+
Version: 2026.10.3
|
|
4
|
+
Summary: ArduPilot SITL instances, flights and logs for LLM agents
|
|
5
|
+
Keywords: ardupilot,sitl,mavlink,mcp,fastmcp
|
|
6
|
+
Author: Ryan Malloy
|
|
7
|
+
Author-email: Ryan Malloy <ryan@supported.systems>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Dist: fastmcp>=4,<5
|
|
18
|
+
Requires-Dist: pymavlink>=2.4.50
|
|
19
|
+
Requires-Dist: pydantic>=2
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Project-URL: Documentation, https://mcardupilot.warehack.ing
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# mcardupilot
|
|
26
|
+
|
|
27
|
+
ArduPilot SITL instances, flights and dataflash logs for LLM agents, as a Python library with
|
|
28
|
+
an MCP server on top of it.
|
|
29
|
+
|
|
30
|
+
Several sessions on one workstation fly SITL at the same time. Each SITL instance number picks
|
|
31
|
+
its ports (MAVLink TCP `5760 + 10n`, the JSON backend `9002 + 10n`), so mcardupilot hands out
|
|
32
|
+
instances from a shared pool through flock leases. A lease names its holder, and the kernel
|
|
33
|
+
drops it when the holder exits, so a crashed run never strands an instance.
|
|
34
|
+
|
|
35
|
+
Status: phases 0 to 4 of [docs/PLAN.md](docs/PLAN.md): settings, leases, the `Sitl` library
|
|
36
|
+
class, missions, log housekeeping, the MCP flight tools, and log reading.
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
The documentation site lives in [docs-site/](docs-site/) (Astro and Starlight), to be published
|
|
41
|
+
at https://mcardupilot.warehack.ing: a tutorial, how-to guides, a tool and settings reference
|
|
42
|
+
generated from the code (`make -C docs-site data`), and explanations of the design.
|
|
43
|
+
|
|
44
|
+
## Flying through the tools
|
|
45
|
+
|
|
46
|
+
`open_sitl` starts a SITL on a leased instance and returns a session id. A thread per session
|
|
47
|
+
owns the MAVLink link and keeps it alive between calls (GCS heartbeat and RC overrides, paced on
|
|
48
|
+
sim time), so the failsafes stay quiet while the agent thinks. A hover hop:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
open_sitl(model="quadplane") # frame defaults from the tree's vehicleinfo.json
|
|
52
|
+
arm(session_id, mode="QLOITER")
|
|
53
|
+
rc_override(session_id, channels={3: 2000}) # climb
|
|
54
|
+
wait_until(session_id, "altitude", 5, then_rc={3: 1500}) # hold at 5 m, applied in the same tick
|
|
55
|
+
set_mode(session_id, "QLAND")
|
|
56
|
+
wait_until(session_id, "disarmed", timeout_s=120)
|
|
57
|
+
close_sitl(session_id) # SITL killed, instance freed, BIN kept
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`wait_until` returns within about 85 s with a reason (`met`, `timeout`, `died`, `cap`) and a
|
|
61
|
+
snapshot whose `seq` goes back in as `since_seq`; long flights are loops of bounded waits.
|
|
62
|
+
|
|
63
|
+
Agent time is sim time: at speedup N every second spent between calls is N sim seconds with the
|
|
64
|
+
last RC still held. So react inside the wait (`then_rc`, `then_mode`, `then_speedup`, applied in
|
|
65
|
+
the tick the condition is met), schedule against the autopilot clock (`sim_time` until
|
|
66
|
+
`t = snapshot.t_sim_s + 15`, not `time`, which counts from the call), and slow down to think
|
|
67
|
+
(`set_speedup(0.05)`: SITL cannot pause, but applies a fractional `SIM_SPEEDUP` live). A wait
|
|
68
|
+
that settles in the server returns the snapshot taken in that tick, a measurement at that moment.
|
|
69
|
+
|
|
70
|
+
A manoeuvre of several steps still costs one round trip per step that way, so `fly_steps` runs
|
|
71
|
+
the whole sequence inside the server. Each step applies its actions (`rc`, `mode`, `speedup`,
|
|
72
|
+
`params`) in the tick it starts and then waits on one `wait_until` condition; the next step
|
|
73
|
+
starts in the tick the previous one settled. The hop above, after `arm`:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
fly_steps(session_id, steps=[
|
|
77
|
+
{"rc": {3: 2000}, "until": "altitude", "value": 5},
|
|
78
|
+
{"rc": {3: 1500}, "until": "sim_time", "value": 15, "relative": True}, # hold 15 s
|
|
79
|
+
{"mode": "QLAND", "until": "disarmed", "timeout_s": 120},
|
|
80
|
+
])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Every step is validated before any runs. The call returns within about 85 s with each settled
|
|
84
|
+
step's reason, start and end sim time and settle-tick snapshot, or with status `running`, which
|
|
85
|
+
`steps_status` follows; `abort_steps` stops it with RC left as last set. A program cannot arm or
|
|
86
|
+
upload a mission, since those need round trips with retries. On an idle-paced session a running
|
|
87
|
+
program keeps the sim at full speed, whether or not a call is waiting on it, and idle applies
|
|
88
|
+
once it ends.
|
|
89
|
+
|
|
90
|
+
Session tools: `open_sitl`, `snapshot`, `get_param`, `set_param`, `set_mode`, `arm`, `disarm`,
|
|
91
|
+
`set_speedup`, `rc_override`, `upload_mission`, `wait_until`, `close_sitl`, `list_sessions`.
|
|
92
|
+
Step tools: `fly_steps`, `steps_status`, `abort_steps`.
|
|
93
|
+
Wind tools: `set_wind`, `wind_history`.
|
|
94
|
+
Log tools: `list_logs`, `log_summary`, `log_fields`, `log_messages`, `log_compare`, `pin_log`.
|
|
95
|
+
Lease tools: `lease_instance`, `release_instance`,
|
|
96
|
+
`kill_instance` (strays only, never another holder's), `list_instances`.
|
|
97
|
+
|
|
98
|
+
## Wind and gusts
|
|
99
|
+
|
|
100
|
+
`open_sitl(wind=...)`, `set_wind` and a step's `wind` action set a mean wind (speed, the
|
|
101
|
+
direction it blows from, an optional updraft) and its gusts. `gusts: "dryden"` runs the
|
|
102
|
+
MIL-F-8785C model in `mcardupilot.turbulence` (exact update) every 0.05 sim s from the current
|
|
103
|
+
height and airspeed and sends SITL the sum as `SIM_WIND_SPD`, `SIM_WIND_DIR` and
|
|
104
|
+
`SIM_WIND_DIR_Z`, so gusts evolve through every wait; `"sitl"` hands turbulence to SITL's own
|
|
105
|
+
`SIM_WIND_TURB`; `"off"` is steady. `scale` strengthens or weakens the gusts, and a `seed`
|
|
106
|
+
repeats them exactly in another flight, since real SITL ticks on an exact 20 Hz sim clock.
|
|
107
|
+
Snapshots show the mean wind and the gust now; `wind_history` returns up to two minutes of it.
|
|
108
|
+
|
|
109
|
+
## Reading logs
|
|
110
|
+
|
|
111
|
+
`log_summary` splits a flight into phases at every flight-mode change, arm and disarm, and (in
|
|
112
|
+
AUTO) mission item, with each phase's duration and averages: airspeed, groundspeed, throttle,
|
|
113
|
+
lift motors (found from the log's own `SERVOn_FUNCTION`), pitch, peak roll, angle of attack,
|
|
114
|
+
current, voltage, peak height. It also returns the firmware, every autopilot text with its
|
|
115
|
+
time, and message counts. `log_fields` returns a time series of any message's columns with
|
|
116
|
+
their units, thinned to fit; `log_messages` returns whole messages; `list_logs` lists the kept
|
|
117
|
+
logs. A log is named by a session id (its live log), a kept log's name, or a path. Reads go
|
|
118
|
+
through pymavlink's per-type index, so a 45 MB log summarizes in about a third of a second.
|
|
119
|
+
|
|
120
|
+
Kept logs are held to `log_budget_gb`: after every close that keeps a log, and once at server
|
|
121
|
+
start, the oldest kept logs past the budget are deleted (the close's `pruned` lists them).
|
|
122
|
+
Only the kept log directory is pruned. A log that is pinned (`pin_log`), written in the last ten
|
|
123
|
+
minutes, or under a running SITL is never taken. `server_info` shows `logs_used_gb` beside the
|
|
124
|
+
budget, and `list_logs` which logs are pinned.
|
|
125
|
+
|
|
126
|
+
Each phase also carries `energy_wh`, BAT volts times amps integrated by the trapezoid rule
|
|
127
|
+
with the samples interpolated to the phase bounds, so the phases add up to the summary's own
|
|
128
|
+
`energy_wh`.
|
|
129
|
+
|
|
130
|
+
`log_compare(a, b)` is for A/B tests (`mcardupilot.logcompare.compare` in the library). It keys
|
|
131
|
+
each flight's phases by mode, armed state and mission item and pairs them by longest common
|
|
132
|
+
subsequence, so an extra phase in one flight is listed on its own instead of shifting every
|
|
133
|
+
later pair. Each pair, and the totals (duration, energy, peak height, peak current), give every
|
|
134
|
+
number as `{a, b, delta, pct}`. It also diffs the parameters (last PARM value per name, with
|
|
135
|
+
ArduPilot's own `STAT_*` and ground-pressure values set apart), the autopilot texts (those that
|
|
136
|
+
differ only in their numbers grouped by a template), and the firmware banners.
|
|
137
|
+
|
|
138
|
+
A MODE record stamped at a log's first timestamp is the mode when the log file opened, not a
|
|
139
|
+
switch at that moment: ArduPilot writes its startup records then.
|
|
140
|
+
|
|
141
|
+
Every Claude session runs its own server on the shared data directory. At start a server kills
|
|
142
|
+
SITLs that a dead server's sessions left behind, judged by the lease: an instance whose lock it
|
|
143
|
+
can take has no live owner.
|
|
144
|
+
|
|
145
|
+
## Run it
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
uv run mcardupilot # stdio MCP server
|
|
149
|
+
claude mcp add mcardupilot-dev -- uv run --directory ~/claude/mcardupilot mcardupilot # the working tree
|
|
150
|
+
claude mcp add mcardupilot -- uvx --from ~/claude/mcardupilot mcardupilot # a built copy
|
|
151
|
+
uv run python -m mcardupilot.leases status # who holds which instance
|
|
152
|
+
uv run python -m mcardupilot.logs prune --root DIR # dry run: oldest logs past the budget
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`uv run --directory` runs the editable install, so a restarted server has every edit. `uvx --from`
|
|
156
|
+
installs a copy, rebuilt whenever `pyproject.toml` or anything under `src/` changes (`cache-keys`).
|
|
157
|
+
mcardupilot is not on PyPI, so plain `uvx mcardupilot` finds nothing until it is published.
|
|
158
|
+
|
|
159
|
+
## Settings
|
|
160
|
+
|
|
161
|
+
Environment variables, or a `.env` in the working directory.
|
|
162
|
+
|
|
163
|
+
| Variable | Default | Meaning |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `MCARDUPILOT_ARDUPILOT` | `~/ardupilot` | ArduPilot tree (read and run only) |
|
|
166
|
+
| `MCARDUPILOT_BINARY` | `<ardupilot>/build/sitl/bin/arduplane` | SITL binary |
|
|
167
|
+
| `MCARDUPILOT_DATA_DIR` | `~/.local/share/mcardupilot` | `leases/`, `sessions/`, `jobs/`, kept logs |
|
|
168
|
+
| `MCARDUPILOT_INSTANCE_POOL` | `40-59` | `"40-59"` or `"40,41,45-47"`; 0, 14 and 15 are refused |
|
|
169
|
+
| `MCARDUPILOT_WAIT_CAP_S` | `85` | longest any waiting tool blocks |
|
|
170
|
+
| `MCARDUPILOT_SESSION_IDLE_S` | `1800` | idle sessions close after this |
|
|
171
|
+
| `MCARDUPILOT_STALL_S` | `30` | a session's SITL whose clock stops this long is dead |
|
|
172
|
+
| `MCARDUPILOT_LOG_BUDGET_GB` | `20` | kept logs are pruned past this |
|
|
173
|
+
| `MCARDUPILOT_TRANSPORT` | `stdio` | `stdio` or `http` |
|
|
174
|
+
| `MCARDUPILOT_HTTP_HOST` / `_PORT` | `127.0.0.1` / `8374` | for `http` |
|
|
175
|
+
|
|
176
|
+
Instance 0 belongs to `autotest.py`, and 14 and 15 land on the desktop containers' VNC ports,
|
|
177
|
+
so the pool validator refuses them.
|
|
178
|
+
|
|
179
|
+
## Library
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from mcardupilot.sitl import Sitl
|
|
183
|
+
|
|
184
|
+
with Sitl(frame="quadplane", defaults=[".../default_params/quadplane.parm"], speedup=10) as s:
|
|
185
|
+
s.arm_on_pad("QLOITER") # leased a free pool instance, launched, waited for home
|
|
186
|
+
s.rc[2] = 1800
|
|
187
|
+
s.wait(6) # sim seconds, on the autopilot's clock
|
|
188
|
+
print(s.height(), s.provenance["ardupilot_sha"])
|
|
189
|
+
# SITL's process group is killed, its ports are free, the lease is released
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`instance=None` (the default) takes the first pool instance whose lock is free and whose ports
|
|
193
|
+
(MAVLink TCP, the JSON backend, and a bridge's own) nothing else holds; `instance=42` asks for one
|
|
194
|
+
and raises `InstanceBusy` naming the holder. A SITL that exits or PANICs raises `SitlDied` after
|
|
195
|
+
being killed. A script that dies without `close()` still kills its SITL at exit; one that is
|
|
196
|
+
SIGKILLed leaves `<run_root>/<n>/sitl.pid`, which `mcardupilot.sitl.kill_instance` reads.
|
|
197
|
+
|
|
198
|
+
`mcardupilot.mission` uploads items given in metres from home (`nav`, `do`, `box_mission`),
|
|
199
|
+
retrying while ArduPilot ignores mission traffic after boot, and reads a mission back.
|
|
200
|
+
`keep_logs="last"` with `log_dest=` moves a flight's BIN out of the instance directory at
|
|
201
|
+
`close()`; `mcardupilot.logs.prune` deletes the oldest logs past `log_budget_gb`, never one that is
|
|
202
|
+
pinned (`logs.pin`), written in the last ten minutes, or under a SITL that is still running.
|
|
203
|
+
|
|
204
|
+
A subclass sets its own state, then calls `start()`, and overrides `tick(kind, msg)` to drive
|
|
205
|
+
time-varying inputs from every pumped message. A `Bridge` (see `sitl.py`) plugs in an external
|
|
206
|
+
physics process on SITL's JSON backend.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
uv run pytest -q # about 20 s on 8 workers, against tests/fake_autopilot.py
|
|
212
|
+
uv run pytest -m sitl # real hops on a stock arduplane (MCARDUPILOT_STOCK_ARDUPILOT)
|
|
213
|
+
uvx ruff check . && uvx ruff format --check .
|
|
214
|
+
```
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# mcardupilot
|
|
2
|
+
|
|
3
|
+
ArduPilot SITL instances, flights and dataflash logs for LLM agents, as a Python library with
|
|
4
|
+
an MCP server on top of it.
|
|
5
|
+
|
|
6
|
+
Several sessions on one workstation fly SITL at the same time. Each SITL instance number picks
|
|
7
|
+
its ports (MAVLink TCP `5760 + 10n`, the JSON backend `9002 + 10n`), so mcardupilot hands out
|
|
8
|
+
instances from a shared pool through flock leases. A lease names its holder, and the kernel
|
|
9
|
+
drops it when the holder exits, so a crashed run never strands an instance.
|
|
10
|
+
|
|
11
|
+
Status: phases 0 to 4 of [docs/PLAN.md](docs/PLAN.md): settings, leases, the `Sitl` library
|
|
12
|
+
class, missions, log housekeeping, the MCP flight tools, and log reading.
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
The documentation site lives in [docs-site/](docs-site/) (Astro and Starlight), to be published
|
|
17
|
+
at https://mcardupilot.warehack.ing: a tutorial, how-to guides, a tool and settings reference
|
|
18
|
+
generated from the code (`make -C docs-site data`), and explanations of the design.
|
|
19
|
+
|
|
20
|
+
## Flying through the tools
|
|
21
|
+
|
|
22
|
+
`open_sitl` starts a SITL on a leased instance and returns a session id. A thread per session
|
|
23
|
+
owns the MAVLink link and keeps it alive between calls (GCS heartbeat and RC overrides, paced on
|
|
24
|
+
sim time), so the failsafes stay quiet while the agent thinks. A hover hop:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
open_sitl(model="quadplane") # frame defaults from the tree's vehicleinfo.json
|
|
28
|
+
arm(session_id, mode="QLOITER")
|
|
29
|
+
rc_override(session_id, channels={3: 2000}) # climb
|
|
30
|
+
wait_until(session_id, "altitude", 5, then_rc={3: 1500}) # hold at 5 m, applied in the same tick
|
|
31
|
+
set_mode(session_id, "QLAND")
|
|
32
|
+
wait_until(session_id, "disarmed", timeout_s=120)
|
|
33
|
+
close_sitl(session_id) # SITL killed, instance freed, BIN kept
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`wait_until` returns within about 85 s with a reason (`met`, `timeout`, `died`, `cap`) and a
|
|
37
|
+
snapshot whose `seq` goes back in as `since_seq`; long flights are loops of bounded waits.
|
|
38
|
+
|
|
39
|
+
Agent time is sim time: at speedup N every second spent between calls is N sim seconds with the
|
|
40
|
+
last RC still held. So react inside the wait (`then_rc`, `then_mode`, `then_speedup`, applied in
|
|
41
|
+
the tick the condition is met), schedule against the autopilot clock (`sim_time` until
|
|
42
|
+
`t = snapshot.t_sim_s + 15`, not `time`, which counts from the call), and slow down to think
|
|
43
|
+
(`set_speedup(0.05)`: SITL cannot pause, but applies a fractional `SIM_SPEEDUP` live). A wait
|
|
44
|
+
that settles in the server returns the snapshot taken in that tick, a measurement at that moment.
|
|
45
|
+
|
|
46
|
+
A manoeuvre of several steps still costs one round trip per step that way, so `fly_steps` runs
|
|
47
|
+
the whole sequence inside the server. Each step applies its actions (`rc`, `mode`, `speedup`,
|
|
48
|
+
`params`) in the tick it starts and then waits on one `wait_until` condition; the next step
|
|
49
|
+
starts in the tick the previous one settled. The hop above, after `arm`:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
fly_steps(session_id, steps=[
|
|
53
|
+
{"rc": {3: 2000}, "until": "altitude", "value": 5},
|
|
54
|
+
{"rc": {3: 1500}, "until": "sim_time", "value": 15, "relative": True}, # hold 15 s
|
|
55
|
+
{"mode": "QLAND", "until": "disarmed", "timeout_s": 120},
|
|
56
|
+
])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every step is validated before any runs. The call returns within about 85 s with each settled
|
|
60
|
+
step's reason, start and end sim time and settle-tick snapshot, or with status `running`, which
|
|
61
|
+
`steps_status` follows; `abort_steps` stops it with RC left as last set. A program cannot arm or
|
|
62
|
+
upload a mission, since those need round trips with retries. On an idle-paced session a running
|
|
63
|
+
program keeps the sim at full speed, whether or not a call is waiting on it, and idle applies
|
|
64
|
+
once it ends.
|
|
65
|
+
|
|
66
|
+
Session tools: `open_sitl`, `snapshot`, `get_param`, `set_param`, `set_mode`, `arm`, `disarm`,
|
|
67
|
+
`set_speedup`, `rc_override`, `upload_mission`, `wait_until`, `close_sitl`, `list_sessions`.
|
|
68
|
+
Step tools: `fly_steps`, `steps_status`, `abort_steps`.
|
|
69
|
+
Wind tools: `set_wind`, `wind_history`.
|
|
70
|
+
Log tools: `list_logs`, `log_summary`, `log_fields`, `log_messages`, `log_compare`, `pin_log`.
|
|
71
|
+
Lease tools: `lease_instance`, `release_instance`,
|
|
72
|
+
`kill_instance` (strays only, never another holder's), `list_instances`.
|
|
73
|
+
|
|
74
|
+
## Wind and gusts
|
|
75
|
+
|
|
76
|
+
`open_sitl(wind=...)`, `set_wind` and a step's `wind` action set a mean wind (speed, the
|
|
77
|
+
direction it blows from, an optional updraft) and its gusts. `gusts: "dryden"` runs the
|
|
78
|
+
MIL-F-8785C model in `mcardupilot.turbulence` (exact update) every 0.05 sim s from the current
|
|
79
|
+
height and airspeed and sends SITL the sum as `SIM_WIND_SPD`, `SIM_WIND_DIR` and
|
|
80
|
+
`SIM_WIND_DIR_Z`, so gusts evolve through every wait; `"sitl"` hands turbulence to SITL's own
|
|
81
|
+
`SIM_WIND_TURB`; `"off"` is steady. `scale` strengthens or weakens the gusts, and a `seed`
|
|
82
|
+
repeats them exactly in another flight, since real SITL ticks on an exact 20 Hz sim clock.
|
|
83
|
+
Snapshots show the mean wind and the gust now; `wind_history` returns up to two minutes of it.
|
|
84
|
+
|
|
85
|
+
## Reading logs
|
|
86
|
+
|
|
87
|
+
`log_summary` splits a flight into phases at every flight-mode change, arm and disarm, and (in
|
|
88
|
+
AUTO) mission item, with each phase's duration and averages: airspeed, groundspeed, throttle,
|
|
89
|
+
lift motors (found from the log's own `SERVOn_FUNCTION`), pitch, peak roll, angle of attack,
|
|
90
|
+
current, voltage, peak height. It also returns the firmware, every autopilot text with its
|
|
91
|
+
time, and message counts. `log_fields` returns a time series of any message's columns with
|
|
92
|
+
their units, thinned to fit; `log_messages` returns whole messages; `list_logs` lists the kept
|
|
93
|
+
logs. A log is named by a session id (its live log), a kept log's name, or a path. Reads go
|
|
94
|
+
through pymavlink's per-type index, so a 45 MB log summarizes in about a third of a second.
|
|
95
|
+
|
|
96
|
+
Kept logs are held to `log_budget_gb`: after every close that keeps a log, and once at server
|
|
97
|
+
start, the oldest kept logs past the budget are deleted (the close's `pruned` lists them).
|
|
98
|
+
Only the kept log directory is pruned. A log that is pinned (`pin_log`), written in the last ten
|
|
99
|
+
minutes, or under a running SITL is never taken. `server_info` shows `logs_used_gb` beside the
|
|
100
|
+
budget, and `list_logs` which logs are pinned.
|
|
101
|
+
|
|
102
|
+
Each phase also carries `energy_wh`, BAT volts times amps integrated by the trapezoid rule
|
|
103
|
+
with the samples interpolated to the phase bounds, so the phases add up to the summary's own
|
|
104
|
+
`energy_wh`.
|
|
105
|
+
|
|
106
|
+
`log_compare(a, b)` is for A/B tests (`mcardupilot.logcompare.compare` in the library). It keys
|
|
107
|
+
each flight's phases by mode, armed state and mission item and pairs them by longest common
|
|
108
|
+
subsequence, so an extra phase in one flight is listed on its own instead of shifting every
|
|
109
|
+
later pair. Each pair, and the totals (duration, energy, peak height, peak current), give every
|
|
110
|
+
number as `{a, b, delta, pct}`. It also diffs the parameters (last PARM value per name, with
|
|
111
|
+
ArduPilot's own `STAT_*` and ground-pressure values set apart), the autopilot texts (those that
|
|
112
|
+
differ only in their numbers grouped by a template), and the firmware banners.
|
|
113
|
+
|
|
114
|
+
A MODE record stamped at a log's first timestamp is the mode when the log file opened, not a
|
|
115
|
+
switch at that moment: ArduPilot writes its startup records then.
|
|
116
|
+
|
|
117
|
+
Every Claude session runs its own server on the shared data directory. At start a server kills
|
|
118
|
+
SITLs that a dead server's sessions left behind, judged by the lease: an instance whose lock it
|
|
119
|
+
can take has no live owner.
|
|
120
|
+
|
|
121
|
+
## Run it
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uv run mcardupilot # stdio MCP server
|
|
125
|
+
claude mcp add mcardupilot-dev -- uv run --directory ~/claude/mcardupilot mcardupilot # the working tree
|
|
126
|
+
claude mcp add mcardupilot -- uvx --from ~/claude/mcardupilot mcardupilot # a built copy
|
|
127
|
+
uv run python -m mcardupilot.leases status # who holds which instance
|
|
128
|
+
uv run python -m mcardupilot.logs prune --root DIR # dry run: oldest logs past the budget
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`uv run --directory` runs the editable install, so a restarted server has every edit. `uvx --from`
|
|
132
|
+
installs a copy, rebuilt whenever `pyproject.toml` or anything under `src/` changes (`cache-keys`).
|
|
133
|
+
mcardupilot is not on PyPI, so plain `uvx mcardupilot` finds nothing until it is published.
|
|
134
|
+
|
|
135
|
+
## Settings
|
|
136
|
+
|
|
137
|
+
Environment variables, or a `.env` in the working directory.
|
|
138
|
+
|
|
139
|
+
| Variable | Default | Meaning |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `MCARDUPILOT_ARDUPILOT` | `~/ardupilot` | ArduPilot tree (read and run only) |
|
|
142
|
+
| `MCARDUPILOT_BINARY` | `<ardupilot>/build/sitl/bin/arduplane` | SITL binary |
|
|
143
|
+
| `MCARDUPILOT_DATA_DIR` | `~/.local/share/mcardupilot` | `leases/`, `sessions/`, `jobs/`, kept logs |
|
|
144
|
+
| `MCARDUPILOT_INSTANCE_POOL` | `40-59` | `"40-59"` or `"40,41,45-47"`; 0, 14 and 15 are refused |
|
|
145
|
+
| `MCARDUPILOT_WAIT_CAP_S` | `85` | longest any waiting tool blocks |
|
|
146
|
+
| `MCARDUPILOT_SESSION_IDLE_S` | `1800` | idle sessions close after this |
|
|
147
|
+
| `MCARDUPILOT_STALL_S` | `30` | a session's SITL whose clock stops this long is dead |
|
|
148
|
+
| `MCARDUPILOT_LOG_BUDGET_GB` | `20` | kept logs are pruned past this |
|
|
149
|
+
| `MCARDUPILOT_TRANSPORT` | `stdio` | `stdio` or `http` |
|
|
150
|
+
| `MCARDUPILOT_HTTP_HOST` / `_PORT` | `127.0.0.1` / `8374` | for `http` |
|
|
151
|
+
|
|
152
|
+
Instance 0 belongs to `autotest.py`, and 14 and 15 land on the desktop containers' VNC ports,
|
|
153
|
+
so the pool validator refuses them.
|
|
154
|
+
|
|
155
|
+
## Library
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from mcardupilot.sitl import Sitl
|
|
159
|
+
|
|
160
|
+
with Sitl(frame="quadplane", defaults=[".../default_params/quadplane.parm"], speedup=10) as s:
|
|
161
|
+
s.arm_on_pad("QLOITER") # leased a free pool instance, launched, waited for home
|
|
162
|
+
s.rc[2] = 1800
|
|
163
|
+
s.wait(6) # sim seconds, on the autopilot's clock
|
|
164
|
+
print(s.height(), s.provenance["ardupilot_sha"])
|
|
165
|
+
# SITL's process group is killed, its ports are free, the lease is released
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`instance=None` (the default) takes the first pool instance whose lock is free and whose ports
|
|
169
|
+
(MAVLink TCP, the JSON backend, and a bridge's own) nothing else holds; `instance=42` asks for one
|
|
170
|
+
and raises `InstanceBusy` naming the holder. A SITL that exits or PANICs raises `SitlDied` after
|
|
171
|
+
being killed. A script that dies without `close()` still kills its SITL at exit; one that is
|
|
172
|
+
SIGKILLed leaves `<run_root>/<n>/sitl.pid`, which `mcardupilot.sitl.kill_instance` reads.
|
|
173
|
+
|
|
174
|
+
`mcardupilot.mission` uploads items given in metres from home (`nav`, `do`, `box_mission`),
|
|
175
|
+
retrying while ArduPilot ignores mission traffic after boot, and reads a mission back.
|
|
176
|
+
`keep_logs="last"` with `log_dest=` moves a flight's BIN out of the instance directory at
|
|
177
|
+
`close()`; `mcardupilot.logs.prune` deletes the oldest logs past `log_budget_gb`, never one that is
|
|
178
|
+
pinned (`logs.pin`), written in the last ten minutes, or under a SITL that is still running.
|
|
179
|
+
|
|
180
|
+
A subclass sets its own state, then calls `start()`, and overrides `tick(kind, msg)` to drive
|
|
181
|
+
time-varying inputs from every pumped message. A `Bridge` (see `sitl.py`) plugs in an external
|
|
182
|
+
physics process on SITL's JSON backend.
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv run pytest -q # about 20 s on 8 workers, against tests/fake_autopilot.py
|
|
188
|
+
uv run pytest -m sitl # real hops on a stock arduplane (MCARDUPILOT_STOCK_ARDUPILOT)
|
|
189
|
+
uvx ruff check . && uvx ruff format --check .
|
|
190
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcardupilot"
|
|
3
|
+
version = "2026.10.03"
|
|
4
|
+
description = "ArduPilot SITL instances, flights and logs for LLM agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Ryan Malloy", email = "ryan@supported.systems" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["ardupilot", "sitl", "mavlink", "mcp", "fastmcp"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"fastmcp>=4,<5",
|
|
24
|
+
"pymavlink>=2.4.50",
|
|
25
|
+
"pydantic>=2",
|
|
26
|
+
"pydantic-settings>=2.15.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Documentation = "https://mcardupilot.warehack.ing"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
mcardupilot = "mcardupilot.server:main"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=9.1.1",
|
|
38
|
+
"pytest-asyncio>=1.4.0",
|
|
39
|
+
"pytest-xdist>=3.8.0",
|
|
40
|
+
"ruff>=0.16.10",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
asyncio_mode = "auto"
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
markers = [
|
|
47
|
+
"sitl: launches a built SITL binary; run with -m sitl",
|
|
48
|
+
"harness: reads a private test harness checkout; run with -m harness",
|
|
49
|
+
]
|
|
50
|
+
# 8 workers: most of a test's time is waiting on a fake autopilot, and conftest gives
|
|
51
|
+
# each worker its own fake instances; -n 0 runs serially
|
|
52
|
+
addopts = "-m 'not sitl and not harness' -n 8"
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py312"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
60
|
+
|
|
61
|
+
[tool.uv]
|
|
62
|
+
# `uvx --from <checkout> mcardupilot` rebuilds when the source changes, not only
|
|
63
|
+
# pyproject.toml (uv's default cache key for a local directory)
|
|
64
|
+
cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.py" }]
|
|
65
|
+
|
|
66
|
+
[tool.uv.build-backend]
|
|
67
|
+
# Operator-private and local files never ship. Tests stay out of the sdist too:
|
|
68
|
+
# fixtures are the first place a local path or a real log would land.
|
|
69
|
+
source-exclude = [
|
|
70
|
+
"CLAUDE.md",
|
|
71
|
+
".env",
|
|
72
|
+
".env.*",
|
|
73
|
+
".mcp.json",
|
|
74
|
+
"tests",
|
|
75
|
+
"docs/agent-threads",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[build-system]
|
|
79
|
+
requires = ["uv_build>=0.11.3,<0.12.0"]
|
|
80
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""mcardupilot: ArduPilot SITL instances, flights and logs for LLM agents."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("mcardupilot")
|
|
7
|
+
except PackageNotFoundError: # running from a checkout without install
|
|
8
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Server settings, read from the environment with the MCARDUPILOT_ prefix."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator, model_validator
|
|
8
|
+
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
|
9
|
+
|
|
10
|
+
# Instances no pool may contain, with the reason the error gives.
|
|
11
|
+
RESERVED_INSTANCES = {
|
|
12
|
+
0: "reserved for autotest.py",
|
|
13
|
+
14: "its ports collide with VNC desktops on 5900 to 5911",
|
|
14
|
+
15: "its ports collide with VNC desktops on 5900 to 5911",
|
|
15
|
+
}
|
|
16
|
+
# SITL's highest base port is the JSON backend at 9002 + 10n (it also uses 9003 + 10n).
|
|
17
|
+
MAX_INSTANCE = (65535 - 9003) // 10
|
|
18
|
+
|
|
19
|
+
_TOKEN = re.compile(r"^(\d+)(?:-(\d+))?$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mavlink_port(instance: int) -> int:
|
|
23
|
+
return 5760 + 10 * instance
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def json_port(instance: int) -> int:
|
|
27
|
+
return 9002 + 10 * instance
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_pool(text: str) -> list[int]:
|
|
31
|
+
"""Read "40-59" or "40,41,45-47" into instance numbers, in the order written."""
|
|
32
|
+
out: list[int] = []
|
|
33
|
+
for token in (t.strip() for t in text.split(",")):
|
|
34
|
+
if not token:
|
|
35
|
+
continue
|
|
36
|
+
m = _TOKEN.match(token)
|
|
37
|
+
if m is None:
|
|
38
|
+
raise ValueError(f"cannot read {token!r} in instance pool {text!r}; use '40-59'")
|
|
39
|
+
lo = int(m.group(1))
|
|
40
|
+
hi = int(m.group(2)) if m.group(2) else lo
|
|
41
|
+
if hi < lo:
|
|
42
|
+
raise ValueError(f"range {token!r} runs backwards")
|
|
43
|
+
out.extend(range(lo, hi + 1))
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_pool(pool: list[int]) -> str:
|
|
48
|
+
"""The inverse of parse_pool: runs of three or more ascending become a-b."""
|
|
49
|
+
parts: list[str] = []
|
|
50
|
+
i = 0
|
|
51
|
+
while i < len(pool):
|
|
52
|
+
j = i
|
|
53
|
+
while j + 1 < len(pool) and pool[j + 1] == pool[j] + 1:
|
|
54
|
+
j += 1
|
|
55
|
+
if j - i >= 2:
|
|
56
|
+
parts.append(f"{pool[i]}-{pool[j]}")
|
|
57
|
+
else:
|
|
58
|
+
parts.extend(str(n) for n in pool[i : j + 1])
|
|
59
|
+
i = j + 1
|
|
60
|
+
return ",".join(parts)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Settings(BaseSettings):
|
|
64
|
+
model_config = SettingsConfigDict(env_prefix="MCARDUPILOT_", env_file=".env", extra="ignore")
|
|
65
|
+
|
|
66
|
+
ardupilot: Path = Field(
|
|
67
|
+
default=Path.home() / "ardupilot", # where ArduPilot's setup guide clones it
|
|
68
|
+
description="ArduPilot tree; read and run only, never built or edited",
|
|
69
|
+
)
|
|
70
|
+
binary: Path | None = Field(
|
|
71
|
+
default=None, description="SITL binary; default <ardupilot>/build/sitl/bin/arduplane"
|
|
72
|
+
)
|
|
73
|
+
data_dir: Path = Field(
|
|
74
|
+
default=Path.home() / ".local" / "share" / "mcardupilot",
|
|
75
|
+
description="lease locks, session run directories, kept logs",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
instance_pool: Annotated[list[int], NoDecode] = Field(
|
|
79
|
+
default_factory=lambda: list(range(40, 60)),
|
|
80
|
+
description='instances this server may lease, "40-59" or "40,41,45-47"',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
wait_cap_s: float = Field(default=85.0, description="longest any waiting tool blocks")
|
|
84
|
+
session_idle_s: float = Field(default=1800.0, description="close a session idle this long")
|
|
85
|
+
reap_interval_s: float = Field(default=30.0, description="how often idle sessions are checked")
|
|
86
|
+
stall_s: float = Field(
|
|
87
|
+
default=30.0, description="a session's SITL whose clock stops this long is dead"
|
|
88
|
+
)
|
|
89
|
+
log_budget_gb: float = Field(
|
|
90
|
+
default=20.0, gt=0, description="kept dataflash logs prune past this"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# transport: stdio for one client; http is the seam for a shared deployment
|
|
94
|
+
transport: Literal["stdio", "http"] = "stdio"
|
|
95
|
+
http_host: str = "127.0.0.1"
|
|
96
|
+
http_port: int = 8374
|
|
97
|
+
|
|
98
|
+
@field_validator("instance_pool", mode="before")
|
|
99
|
+
@classmethod
|
|
100
|
+
def _read_pool(cls, v: object) -> object:
|
|
101
|
+
if isinstance(v, str):
|
|
102
|
+
return parse_pool(v)
|
|
103
|
+
if isinstance(v, int):
|
|
104
|
+
return [v]
|
|
105
|
+
return v
|
|
106
|
+
|
|
107
|
+
@field_validator("instance_pool")
|
|
108
|
+
@classmethod
|
|
109
|
+
def _check_pool(cls, pool: list[int]) -> list[int]:
|
|
110
|
+
for n in pool:
|
|
111
|
+
if n in RESERVED_INSTANCES:
|
|
112
|
+
raise ValueError(f"instance {n} cannot be pooled: {RESERVED_INSTANCES[n]}")
|
|
113
|
+
if not 0 <= n <= MAX_INSTANCE:
|
|
114
|
+
raise ValueError(f"instance {n} is outside 1 to {MAX_INSTANCE}")
|
|
115
|
+
dups = sorted({n for n in pool if pool.count(n) > 1})
|
|
116
|
+
if dups:
|
|
117
|
+
raise ValueError(f"instance pool lists {dups} more than once")
|
|
118
|
+
return pool
|
|
119
|
+
|
|
120
|
+
@field_validator("ardupilot", "binary", "data_dir")
|
|
121
|
+
@classmethod
|
|
122
|
+
def _expand(cls, p: Path | None) -> Path | None:
|
|
123
|
+
return p.expanduser() if p is not None else None
|
|
124
|
+
|
|
125
|
+
@model_validator(mode="after")
|
|
126
|
+
def _default_binary(self) -> "Settings":
|
|
127
|
+
if self.binary is None:
|
|
128
|
+
self.binary = self.ardupilot / "build" / "sitl" / "bin" / "arduplane"
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def sitl_binary(self) -> Path:
|
|
133
|
+
"""binary, typed as always set (the validator fills it)."""
|
|
134
|
+
assert self.binary is not None
|
|
135
|
+
return self.binary
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def leases_dir(self) -> Path:
|
|
139
|
+
return self.data_dir / "leases"
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def sessions_dir(self) -> Path:
|
|
143
|
+
return self.data_dir / "sessions"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def logs_dir(self) -> Path:
|
|
147
|
+
return self.data_dir / "logs"
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def jobs_dir(self) -> Path:
|
|
151
|
+
return self.data_dir / "jobs"
|