MAVPose API
Reference for the public API of the mavpose package: the data layer, the MCP server and the plot assistant. The full guides live in the repository's docs folder.
Quick start #
Load a log, then read frames and series with units attached.
import mavpose
log = mavpose.load("flight.tlog")
log
# <FlightLog 'flight.tlog': mavlink, 38 message types, 2443.8 s>
alt = log.series("GLOBAL_POSITION_INT", "relative_alt")
alt.attrs
# {'unit': 'm', 'msg_type': 'GLOBAL_POSITION_INT'}
log.unknown_counts
# {'GPS_RAW_INT': {'epv': 6338}}
log.to_parquet("telemetry.parquet")
Outputs above come from a real 40-minute ArduPilot flight log. Every frame shares one time_s axis (seconds from log start), scaled fields are in natural units, and "value not provided" markers are NaN.
Installation #
Python 3.10 or newer. The core has no AI dependencies; add extras only if you need them.
# Data layer only: 10 packages, no AI dependencies
pip install mavpose
# Optional extras
# MCP server for AI agents
pip install "mavpose[mcp]"
# plain-English plot assistant
pip install "mavpose[chat]"
Log formats #
load() picks the reader from the file extension. Every format gets the same guarantees.
| Format | Extension | Autopilot | Message names |
|---|---|---|---|
| MAVLink telemetry log | .tlog | ArduPilot, PX4, any MAVLink vehicle | MAVLink names, e.g. GLOBAL_POSITION_INT |
| DataFlash binary / text log | .bin, .log | ArduPilot | DataFlash names, e.g. GPS, BAT |
| ULog | .ulg | PX4 | Topic plus instance, e.g. vehicle_gps_position_0 |
mavpose.load()
#
The main entry point.
Validates the file, parses every message, puts all message types on one time_s axis, converts units and replaces unknown markers. Damaged packets are skipped and counted in log.stats.
| Name | Type | Default | Description |
|---|---|---|---|
| path | str | Required | A MAVLink .tlog, ArduPilot .bin/.log or PX4 .ulg file. |
| convert_units | bool | True | Convert scaled fields to natural units (mm → m, degE7 → deg, mV → V…). Pass False to keep raw stored values, with raw units reported. |
| filter_unknown | bool | True | Replace "value not provided" markers (e.g. current_battery = -1) with NaN. |
FlightLog
FileNotFoundError — the file doesn't exist. FileValidationError — unsupported extension, empty, larger than 200 MB, or a symlink. ValueError — the file can't be parsed as its format.
class FlightLog
#
A parsed flight log: one time-aligned DataFrame per message type, with units. Create one with mavpose.load().
Also: len(log), "ATTITUDE" in log and for msg in log work as you'd expect.
KeyError — the message type or field doesn't exist. The message suggests close matches, e.g. No message type 'GLOBAL_POS'. Did you mean: GLOBAL_POSITION_INT?
Columns are time_s, msg_type and one column per field. Returns a copy, so it is safe to modify.
KeyError — the message type or field doesn't exist. The message suggests close matches, e.g. No message type 'GLOBAL_POS'. Did you mean: GLOBAL_POSITION_INT?
The unit is in series.attrs["unit"] (None if unknown) and the source in series.attrs["msg_type"]. Ready for any pandas or matplotlib workflow.
alt = log.series("GLOBAL_POSITION_INT", "relative_alt")
alt.plot(ylabel=f"altitude ({alt.attrs['unit']})")
KeyError — the message type or field doesn't exist. The message suggests close matches, e.g. No message type 'GLOBAL_POS'. Did you mean: GLOBAL_POSITION_INT?
log.unit("GLOBAL_POSITION_INT", "alt") # 'm'
raw = mavpose.load("flight.tlog", convert_units=False)
raw.unit("GLOBAL_POSITION_INT", "alt") # 'mm'
Fields with no known unit are omitted.
log.unknown_counts
# {'GPS_RAW_INT': {'epv': 6338}} this GPS never reports vertical accuracy
bad_data counts corrupt packets that were skipped; errors counts reader exceptions recovered from. A warning is logged when either is non-zero. Parsing only gives up after 1,000 consecutive read errors.
log.stats
# {'messages': 163851, 'bad_data': 0, 'errors': 0, 'unknown_values': 6338}
float; 0.0 for an empty log.
log.summary(["SYS_STATUS"])["SYS_STATUS"]["columns"]["current_battery"]
# {'dtype': 'float64', 'unit': 'A', 'min': 10.5, 'max': 10.5, 'unknown': 3}
KeyError — a requested message type doesn't exist.
Rows from different message types are told apart by the msg_type column.
The summary() of what was written.
KeyError — a requested message type doesn't exist.
Plain attributes: log.path (str) and log.format (str).
class LogExtractor
#
The extractor load() is built on.
Same options as load(). Does not validate the file; call validate_mavlink_file() first for untrusted input.
FileNotFoundError — the file doesn't exist.
{msg_type: {"count": int, "fields": {name: type}, "units": {name: unit}}}
{msg_type: DataFrame} with columns time_s, msg_type and one per field. Also stored in .frames.
Runs extract_all() first if needed.
Types not present in the log are skipped with a warning.
The summary() of what was written.
raw_units gives the units as stored in the log, before conversion; units gives them after.
Validation #
Input checks, also run by load().
Accepts .tlog, .bin, .log and .ulg. Rejects symlinks, empty files and files over 200 MB. load() calls it for you.
FileNotFoundError — the file doesn't exist. mavpose.FileValidationError (a ValueError) — any other failed check.
MCP server — mavpose-mcp
#
A Model Context Protocol server, so any MCP-capable assistant or agent framework can read your logs with MAVPose's guarantees. Needs [mcp].
mavpose-mcp --root ~/flight-logs
# Pass --root several times for several folders. Without it, the server
# uses $MAVPOSE_MCP_ROOTS (":"-separated; ";" on Windows), else the current folder.
{
"mcpServers": {
"mavpose": {
"command": "mavpose-mcp",
"args": ["--root", "/home/you/flight-logs"]
}
}
}
| Tool | Returns | Writes? |
|---|---|---|
list_logs | Log files in the allowed folders, with format and size | no |
summarize_flight | Duration; altitude, speed, battery and GPS ranges (each with source field and unit); parse stats; unknown counts; vehicle warnings | no |
list_messages | Message types with row counts, field counts and time range | no |
describe_message | One message type's fields: dtype, unit, min, max, unknown count | no |
get_series | Fields over a time window (start_s/end_s) on time_s, with units. At most max_points samples (500 default, 5,000 max); min, max and mean cover every sample | no |
get_log_messages | The vehicle's text log (STATUSTEXT, MSG, PX4 log messages), filterable by severity | no |
export_parquet | Writes message types to a .parquet file inside an allowed folder | yes, never overwrites unless overwrite is true |
The server reads, and export_parquet writes, only inside the --root folders. Paths that resolve outside them (including via ..) and symlinked logs are refused. No tool executes code, and every tool except export_parquet is annotated read-only. Parsed logs are cached (four at a time) and re-read if the file changes.
Plot assistant — mavpose
#
Turns a plain-English request into a plot, with any AI provider. Needs [chat].
pip install "mavpose[chat]"
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, GEMINI_API_KEY, … (or a .env file)
mavpose flight.tlog --model anthropic:<model> -p "Plot altitude and battery"
mavpose flight.ulg --model ollama:llama3.1:8b # local; interactive mode
mavpose next_flight.bin --spec plot.json # redraw, no model
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
| log_file | — | path | Required | MAVLink .tlog, ArduPilot .bin/.log, or PX4 .ulg. |
| --prompt | -p | str | None | Plot request. Omit for interactive mode. |
| --model | -m | str | None | provider:model. Default: MAVPOSE_MODEL, or the provider whose key is set. |
| --spec | — | path | None | Redraw a saved plot.json on this log. No model or key needed. |
| --out-dir | -o | path | log's folder | Folder for the plot, its spec or script, and telemetry.parquet; created if needed. |
| --format | -f | str | png | png, svg or pdf. |
| --dpi | — | int | 150 | Resolution (400 by default with --code); ignored for SVG. |
| --code | — | flag | False | Have the model write a Python script instead of a plot spec. |
| --retries | -r | int | 3 | Attempts per request when the spec or script fails. |
| --verbose | -v | flag | False | Enable DEBUG logging. |
| Provider | Key variable | Notes |
|---|---|---|
openai | OPENAI_API_KEY | |
anthropic | ANTHROPIC_API_KEY | native client |
gemini | GEMINI_API_KEY / GOOGLE_API_KEY | |
openrouter | OPENROUTER_API_KEY | model optional (default z-ai/glm-5.1, or OPENROUTER_MODEL) |
groq, deepseek, mistral, together | GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY, TOGETHER_API_KEY | |
ollama, lmstudio | none | local, localhost:11434 / localhost:1234 |
openai-compatible | MAVPOSE_API_KEY (optional) | needs MAVPOSE_BASE_URL |
Choose with --model provider:model or MAVPOSE_MODEL. Without either, OpenRouter is used if OPENROUTER_API_KEY is set (as in 0.2), otherwise the one provider whose key is set. MAVPOSE_BASE_URL and MAVPOSE_API_KEY override the endpoint and key. Settings can live in a .env file in the folder you run mavpose from.
{
"title": "Climb and battery",
"panels": [
{"series": [{"msg_type": "GLOBAL_POSITION_INT", "field": "relative_alt"}]},
{"series": [{"msg_type": "SYS_STATUS", "field": "voltage_battery"},
{"msg_type": "SYS_STATUS", "field": "current_battery", "axis": "right"}]}
]
}
The model sees the log's message types and the summary() schema, never the raw log, and replies with a plot spec: up to 6 panels sharing the time axis, line / scatter / step, a right-hand axis, colours, x_field for tracks, and a start_s/end_s window. MAVPose validates it against the log, sends errors back to the model, and draws it with units from the log. mavpose.chat.render_spec(spec, frames, units, path) draws specs from Python; mavpose.chat.PlotCreator is the class behind the command; in code mode its run_script() returns a RunResult (success, plot_path, code, error, attempts). In interactive mode the model sees your last three requests.
By default no model-written code runs. With --code, the generated script runs in a separate process with no API keys in its environment, under an audit hook that blocks process spawning, network access and writes outside the output folder, and with CPU, memory and time limits. This is defence in depth, not a security boundary: for untrusted logs or prompts in code mode, run MAVPose in a container too.
API stability #
MAVPose follows Semantic Versioning. These names are covered.
| Name | What it is |
|---|---|
mavpose.load() | Read a log into a FlightLog |
mavpose.FlightLog | All methods and properties documented above |
mavpose.LogExtractor | All methods and properties documented above |
mavpose.validate_mavlink_file(), mavpose.FileValidationError | Input validation |
mavpose.__version__ | Installed version |
The mavpose command and the plot spec format | Plot assistant CLI (needs [chat]); saved plot.json files keep working |
The mavpose-mcp command and its tools | Tool names, arguments and result fields (needs [mcp]) |
The data guarantees are public API too: the shared time_s axis, unit conversion, unknown-marker filtering and message naming. Changing any of them counts as a breaking change.
MAVPose is pre-1.0. Minor releases (0.2 → 0.3) may break things and list each change under "Breaking" in the changelog; patch releases never do. Not public: names starting with _, mavpose.ulog_reader, mavpose.mcp_server, mavpose.chat.* except PlotCreator, and files under mavpose/data/. Full policy: docs/api-stability.md.