Turret
Command-line tool for STorM32 gimbal controllers. Uses the RC Commands protocol.
What this is
Turret is an embedded-Linux gimbal manager / targeting middleware: the standards-compliant front-end (MAVLink Gimbal Manager v2 today; the spec calls out multi-source arbitration, capability advertising, and a common command shape) bolted onto cheap or non-standard gimbal controllers (STorM32 RC over serial today). The daemon takes the operator-protocol side seriously — primary/secondary control ownership, ACK shapes, frame-flag conventions, rate→position integration, drift correction — so that every GCS, autopilot, or companion app on the network sees a normal MAVLink gimbal regardless of what's actually moving the motors.
Operator protocols are pluggable
The daemon's core (daemon::state::StateManager, daemon::arbitrator,
daemon::gimbal_handle::GimbalHandle) owns no MAVLink types. Protocol
front-ends — daemon::mavlink_manager for MAVLink, daemon::ipc_server
for the Unix socket — are peers that translate inbound frames to
GimbalCommand, dispatch through the arbitrator, and translate outbound
state back. Adding a new operator protocol (CCSDS, a custom mission-bus
frame, gRPC) means writing a new module alongside mavlink_manager,
not touching the arbitrator, state, or gimbal handle.
Today only MAVLink (+ the local IPC) is implemented. That's a deliberate scope choice, not architectural lock-in: assume MAVLink is one of potentially-several front-ends, but ship one well before the second.
Hardware protocols are pluggable too
The hardware side runs through the gimbal::GimbalDevice trait
(set_attitude, get_attitude, protocol_name). Today's only
implementation is STorM32 RC (protocols::storm32_rc); a different
controller is a new GimbalDevice impl, no other module changes. The
embedded-use docstring on daemon shows the minimum invocation.
Features
- CLI Mode: Direct command-line control
- Daemon Mode: Background service with Unix socket IPC and MAVLink
- Auto-detects STorM32 devices
- Set gimbal angles, pan modes, standby
- Query status and firmware info
- Structured logging with verbosity levels
- Priority-based command arbitration
Installation
Download Binary
Download the latest release from Releases for your platform.
OR:
From source
With a recent Rust installed:
Usage
CLI
The CLI is a thin client. Each subcommand prefers the daemon's IPC
socket (/tmp/turret.sock by default; override with --socket <path>).
When the daemon is running, commands route through it — same arbitration,
calibration, and primary-control as MAVLink/IPC clients see. When no
daemon is running, the CLI opens the device directly for the call.
# Basic commands — work the same way whether or not a daemon is running.
# Pick a specific daemon by socket path (multi-gimbal setups).
# Direct-only commands (no IPC equivalent — daemon must not be holding
# the port). Errors with `Error::DeviceBusy` if a daemon is running.
# Direct-mode device override (only used when daemon socket isn't reachable).
# Verbose tracing.
If a daemon is holding the device and you try a direct-only subcommand
(pitch, roll, yaw, version-str), you'll get
Error::DeviceBusy rather than a misleading "not detected" — stop the
daemon to use them, or use turret set (which routes via IPC) instead.
Daemon mode (background service)
# Start daemon (auto-detects device)
# Or with custom config
Once running, the CLI talks to it automatically (see above). For machine-readable / programmatic use, the IPC socket speaks line-delimited JSON:
|
|
|
|
|
|
# MAVLink on UDP:14550 — pymavlink, QGroundControl, etc.
# Implements Gimbal Manager protocol.
The IPC wire format uses snake_case for both the cmd discriminator
and field names — matches the response shape (firmware_version,
pan_mode, ...) and standard JSON conventions. Send {"cmd":"help"}
for the full machine-readable schema.
Reading attitude after a set
status returns the most recent attitude polled by the daemon's 4 Hz
attitude loop — at most ~250 ms behind the IMU, but more importantly
it doesn't wait for the gimbal to physically settle on the new SP. The
STorM32 motor traverses at roughly 10°/s, so a set 0,0,10 followed
immediately by status will show pitch/yaw still en route. Allow ~1.5
to 2 seconds of motor travel for a 10° swing before reading status if
you need the settled value:
For a verified round-trip across the full working range, use
{"cmd":"selftest"} — it sweeps, waits per-step, samples freshly, and
reports per-axis SP/PV errors.
Yaw calibration
If your gimbal's IMU yaw reading is biased relative to its commanded
zero (typical symptom: set yaw=0 then status returns yaw≠0 by a
fixed amount), calibrate from the daemon — no GUI required:
# Auto-calibrate: daemon centers the gimbal, samples raw IMU yaw,
# stores the bias as the new offset.
|
# → {"status":"ok","data":{"yaw_offset_deg": -4.48, "prior_yaw_offset_deg": 0.0}}
# Or set an explicit value (e.g. from a measurement you took elsewhere):
|
# 0.0 disables the offset:
|
The offset persists to $XDG_STATE_HOME/turret/calibration.toml (default
~/.local/state/turret/calibration.toml) and is auto-loaded on daemon
start. The current offset is included in every status response as
"yaw_offset_deg".
Read-side correction model: commands hit the device exactly as you wrote
them (the gimbal physically goes to the asked-for angle), and the
calibration offset is subtracted from the IMU's report before it reaches
any consumer (IPC status, MAVLink GIMBAL_DEVICE_ATTITUDE_STATUS).
Continuous drift correction
After the boot-time calibration captures the initial bias, the daemon
runs a slow integral controller on every 4 Hz attitude poll: once the
gimbal has been settled at the most recent SP for 2 s, the operator-frame
PV - SP gap is treated as accumulated IMU drift and a tiny slice of
it (capped at 0.05° per call → ~0.2°/s of correction) is added to the
calibration offset. Geometric convergence with a ~50 s time constant
keeps the offset honest over hours, so you don't need to re-run
calibrate_yaw mid-session as the IMU drifts.
The corrected offset is persisted back to the same XDG state file (throttled — at most one disk write per minute, and only after the offset has slid more than 0.05° from the last persisted value).
The corrector assumes the gimbal is at rest in the world between SETs,
which is true for static mounts, parked vehicles, and pan-mode HOLD on
moving vehicles. In actively-yawing flight with pan-mode PAN it can
mistake vehicle motion for IMU drift; if that's your deployment,
disable it with gimbal.yaw_corrector_enabled: false in turret.yaml
(defaults to on).
Future hardware paths
- Encoder feedback on the gimbal motor shafts (a STorM32 firmware
option) would give absolute axis positions independent of the IMU.
From the daemon's perspective the data arrives through the same
IMU1ANGLESchannel and is drift-free for free; no daemon-side change required. - Non-orthogonal axis design (each axis off-vertical so gravity contributes a yaw-axis reference) is a vendor-side choice the STorM32 firmware abstracts; transparent at the host level.
- Cross-IMU comparison with autopilot AHRS for actively-flying
pan-mode-PAN deployments: planned. We already ingest
AUTOPILOT_STATE_FOR_GIMBAL_DEVICE; a future revision will use vehicle yaw as the reference instead ofPV - SP.
Self-test
Verify SP→PV agreement across the gimbal's working range without leaving the daemon — useful both as a post-calibration check and as a quick-and-dirty health probe before a mission:
|
The daemon sweeps (0,0,0) → (15,0,0) → (-15,0,0) → (0,0,15) → (0,0,-15) → (0,0,0), waits 2.5 s after each step, samples
operator-frame attitude, and returns per-sample SP/PV/error plus per-axis
max error and a pass/fail against a 1° tolerance. The whole run takes
~16 s. Refuses with an error if standby is engaged. The original pose is
restored at the end.
A clean run on a calibrated gimbal looks like:
If the gimbal hasn't been calibrated yet, the failing samples make the
bias obvious — that's a good cue to run {"cmd":"calibrate_yaw"}.
Configuration (turret.yaml):
device:
path: auto
ipc:
socket_path: /tmp/turret.sock
mavlink:
enabled: true
transport: udp
bind_addr: 127.0.0.1
udp_port: 14550
sysid: 1
compid: 154
gimbal:
yaw_corrector_enabled: true
Priority System: MAVLink autopilot (highest) > GCS > Unix socket > CLI (lowest)
Daemon behavior:
- 1 Hz
HEARTBEAT(advertisesMAV_TYPE_GIMBALonMAV_COMP_ID_GIMBAL) - 5 Hz
GIMBAL_MANAGER_STATUS, broadcast to every recorded peer - 4 Hz attitude poll →
GIMBAL_DEVICE_ATTITUDE_STATUSbroadcast.COMMS_ERRORflag is set for ~1 s after any failed poll. - 8 consecutive poll failures → automatic reconnect (scan + reopen, exponential backoff to 10 s).
SIGTERM/SIGINTstart a 2 s grace shutdown; in-flight tasks are drained, then aborted; the IPC socket file is removed.- A critical task exiting unexpectedly (IPC, MAVLink, reconnect) tears the daemon down instead of leaving it half-running.
Use as a library
Two consumption modes:
-
Lean driver (
default-features = false): just theStorm32RCdriver and theGimbalDevicetrait. Pulls 4 direct deps (serde / serialport / thiserror / tracing) and no tokio.[] = { = "0.1", = false }let mut g = detect_gimbal?; g.set_attitude?; # Ok:: -
Embedded MAVLink manager: bring your own
dyn GimbalDevice(different protocol, simulator, network-attached gimbal) and let Turret handle MAVLink discovery, primary-control arbitration, hot attitude broadcast, IPC, and bounded shutdown. The default features pull tokio + the daemon stack.See
turret::daemonmodule-level docs for the full embedding example.
Develop
With a recent Rust installed:
# Clone and build
Protocol
Uses STorM32 RC Commands protocol over serial (115200 baud).
Message format: [0xFA][len][cmd][payload][crc16]
Supports: version query, angle setting, status reading, pan modes.
Device Detection
Auto-detects STM32 Virtual COM Port (VID:0x0483, PID:0x5740).
Falls back to common patterns: /dev/ttyACM*, /dev/ttyUSB*, COM*
Troubleshooting
- No device found: Check USB connection and power
- Permission denied: Add user to
dialoutgroup or usesudo - Communication errors: Verify 115200 baud rate and cable quality