Hardware-in-the-loop simulation daemon for Pixhawk flight controllers. Download the latest version for your platform to bridge your FC to the browser simulator.
Sign in to get your install command.
Downloading directly? Your operating system will block it first.
The install command on this page avoids this entirely. If you download the binary yourself, run these afterwards:
macOS
chmod +x ~/Downloads/hitl-daemon xattr -d com.apple.quarantine ~/Downloads/hitl-daemon ~/Downloads/hitl-daemon
Ubuntu
chmod +x ~/Downloads/hitl-daemon ~/Downloads/hitl-daemon
Windows
Right-click the file, choose Properties, tick Unblock, then Apply .\hitl-daemon
well as one packed immediately after the header.** This is the first half of a
two-sided change and does nothing on its own; the browser still sends the
unpadded form.
It exists so the browser can eventually use Float32Array.prototype.set --
one memcpy per tile -- where it currently writes DataView.setFloat32 per
sample, 589,824 calls for the nine tiles of a single push. That fast path is
only available when the payload begins at a multiple of four, which the header
does not naturally guarantee.
It lands here first because a daemon is installed on someone's machine and
updates on their schedule. A browser that started padding unconditionally
would take terrain away from every daemon older than the change, so the reader
has to tolerate both forms before the writer picks one.
The forms are told apart by length rather than a flag: padding is 0-3 bytes,
so at most one placement leaves exactly the expected payload behind, and when
the header already ends on a boundary the two are the same offset. A wrong
size is still rejected -- eight trailing bytes is not padding. The padding
bytes are not inspected, since they carry nothing.
Four tests, including that the unpadded form still decodes and that disabling
the padded branch fails the padded one.
Measured against a same-session control arm: one second of simulated sampling
at 400 Hz went from 223.79 us to 109.29 us, and a single IMU sample from
401.45 ns to 206.42 ns. Box-Muller was generating two normal variates per call
and discarding one, and GaussMarkov recomputed alpha and noise_sigma
from a dt that does not change between ticks.
The draw *order* changes as a result, so a seeded run no longer reproduces the
previous sequence value-for-value. The distributions are unchanged.
GaussMarkov::step called out of line six times per IMU sample, and
max_motor_speed_from_voltage likewise — the default lto = false with 16
codegen units gave the optimiser nothing to inline across the crate boundary.
sleep_until(next_tick) rather than sleep(remaining), so scheduling jitter
on one tick no longer pushes the next one late.
Spin accuracy was left at the default deliberately. The review claimed
spin_sleep burned about 5 % of a core; measured, daemon total CPU is 1.4 %,
a same-session A/B of 125 us against 60 us was identical, and the proposed
tuning made max_latency worse — 95 us to 160 us. Reverted; only the deadline
change was kept.
which truncated the physics phase to 0 — a single RK4 step is 264 ns.
1 ms sleep, and reuses one write buffer. A fourth blocking-send site was
fixed at the same time. The inbound half is unchanged and still polls; moving
it requires receiver ownership out of the shared MavlinkIo handle, which was
left rather than attempted blind.
contact.** It was sampled every tick and used on a small fraction of them.
in_waiting > 0 with a 10 ms sleep inside the loop; at 57600 baud roughly 58
bytes arrive during each sleep, so against a streaming port the condition
never became false and the buffer grew at ~5.8 KB/s until the process was
killed. It is the first thing every invocation does through wake_console,
and nsh/CLAUDE.md documents the streaming-port state it hits. Now bounded by
a deadline and a byte cap.
The WebSocket server capped incoming messages at 1 KB, and a restore carries
the whole parameter snapshot — 21 entries of name, value and type, about
1.2 KB. The daemon rejected a message it had asked the browser to send, at
the WebSocket layer, before any handler could log it, and closed the
connection. The interface sat on "Writing your original settings" forever,
over a board nothing had written to.
The limit is now 64 KB: room for a snapshot several times larger than any
board's parameter set, still bounded against a local client making the daemon
buffer without limit. A test builds the real payload and fails if it no
longer fits.