Measuring a Rocket Motor with an NI USB-6009
In 2008 I built a load cell amplifier to capture the thrust curve of a rocket motor. It was a PIC with a 10-bit analogue-to-digital converter and an INA125 instrumentation amplifier, it enumerated as a COM port, and the data ended up in HyperTerminal and from there, by copy and paste, in an Excel sheet. Two years later the Static Test Controller added chamber pressure and break wires to the same idea, and we used it for the Ballistic Evaluation Motor tests .
The measurement hasn’t changed since. A motor pushes on a load cell, a pressure transducer watches the combustion chamber, break wires burn through one by one, and all of it has to be sampled fast enough and written somewhere safe.
What changed is that this time I didn’t build the front end. We had an NI USB-6009 lying around — 8 analogue inputs at 14 bits, 12 digital I/O lines, 48 kS/s, over USB. It is not a new device by any measure; National Instruments has been selling it since the mid-2000s, it is roughly contemporary with the load cell amplifier it replaces, and nobody would choose it today over a modern DAQ if they were starting with an empty bench and a budget. But it was in a drawer, it is a properly engineered instrument with a real driver behind it, and 14 bits across 8 channels is comfortably more than the thrust and pressure measurements on this stand need. Using what you already have is a perfectly good reason.
That moves where the interesting work sits. In 2008 the interesting part was designing the board; here the board is a given, and the interesting part becomes the software wrapped around it — and in particular everything that has to be true before you connect a live igniter.
A test day, in the order it happens#
The application is a Python program built on nidaqmx. It has three modes, and
they map to the three things you actually do on a test day.
Calibrate#
Before any measurement device is used for measurements, it gets calibrated. Otherwise you have volts, and volts are not newtons or bar — you don’t know what the recorded numbers correspond to in real life. Calibration mode reads the configured channels, filters them with a moving average, and puts the values on screen. Nothing is written to disk.
That has a second job on a test stand: setting the amplifier gain. In BEM test 2 the thrust trace ran into the top of the measuring range, because the gain on the Static Test Controller had been set too high for the thrust the motor turned out to produce. That’s a mistake you can only correct before the test, and only if you can see live numbers while you turn the potentiometer. So calibration mode is a large, slow, filtered readout you use with a set of known weights on the load cell and a screwdriver in your hand.
ni_usb6009_logger --device Dev1 --channels ai0,ai1,ai2 --calibrate ^
--calib-window 5 --calib-sample-rate 100 --rate 1 --term RSELog#
Logging is the measurement itself. The analogue channels — thrust, chamber
pressure, the igniter current sense — are hardware-timed at the configured sample
rate. The digital lines are the break wires: four of them, wired to port0/line0:3,
burning through in sequence as the flame front travels up the grain, so the burn
rate can be derived from the time between them rather than from the much cruder
interval between ignition and burnout.
The break wires come with a caveat that is pure USB-6009. Its digital input is
static — it is not hardware-timed the way the analogue input is. The logger
therefore snapshots the digital lines once per analogue chunk and repeats that
snapshot across the rows of the chunk. The effective time resolution of a break wire
is set by the chunk size, not by the sample rate, so if you want tighter break-wire
timing you lower --chunk rather than raising --rate. It is worth knowing which
of your numbers is precise and which one is a snapshot.
Rows are written as timestamp, sample_index, ai…, di… to CSV or XLSX. The
timestamps are dated back by the chunk’s acquisition duration, so a row’s time is
when its sample was acquired, not when the USB read returned — at 1000 Hz with 1000
samples per chunk, the naive version is a full second late.
Ignite#
And then you fire the motor. Buzzer warning, relay pulse, current-sense failsafes, always logging.
One shunt, three measurements#
The ignition circuit is the part I like most, because a single cheap component answers three different questions depending on the state of the relay.
A 1 Ω shunt resistor sits between the igniter and the ignition supply ground. A 47 kΩ bias resistor runs from the supply to the igniter/shunt junction, and one analogue input measures across the shunt. That’s it.
| Relay | Current through the shunt | Question it answers |
|---|---|---|
| Open | ~0.25 mA (bias only, at 12 V) | Is the igniter connected and intact? |
| Open | ≥ 5 mA | Is something driving the igniter that shouldn’t be? |
| Closed | 3–6 A | Did the igniter actually get current? |
With the relay open, the bias resistor pushes a quarter of a milliamp through the igniter — far too little to do anything, enough to prove the igniter is there and its bridge wire is unbroken. That’s the continuity check. If that same measurement shows more current than it should while the relay is still open, something is wrong in the wiring or the supply, and the software inhibits: that’s the leak check. When the relay closes, several amps flow and drop 3 to 6 volts across the shunt, which is the fire confirm — proof that the igniter received current, as opposed to the relay merely having been told to close.
The rule the whole arrangement rests on: the USB-6009 never carries igniter current. It measures a voltage across a resistor and nothing else. The relay coil is driven through a transistor or a ULN2803 with a flyback diode across it, the ignition supply is fused, and the DAQ is on the other side of the shunt from anything energetic.
The ignition sequence#
Ignition is a state machine, and the order of its steps is the design:
- ARM. The operator confirms a dialog that says what is about to happen. The session starts by forcing the digital output lines LOW — before anything else, not as an afterthought.
- Arming. The buzzer sounds for the configured warning time while the shunt current is monitored live, with the measured milliamps on screen and status LEDs for continuity and leak. ABORT is available throughout.
- Inhibit. If there is leak current, or if continuity is never established, the buzzer stops and the FIRE button is never offered at all. There is no override. The reason goes in the log.
- Stabilize and log. If arming passed, logging starts, and after the stabilize time the state becomes FIRE_PENDING — only now does FIRE become available.
- FIRE. The operator holds the button for two seconds, with a progress bar filling; releasing early cancels. The core then energizes the relay for the pulse time and checks the current.
- ABORT, at any moment. Closing the window counts as an abort: the window stays open behind a “stopping safely” indicator until the worker has really finished and the exit path has forced the lines LOW again.
Two details are dictated by this particular hardware and are worth spelling out.
While ignition is enabled, the analogue stream is read in ~20 ms sub-blocks instead of one chunk per iteration. Without that, the relay would only open when the current chunk read returned, which at 1 kHz with 1000-sample chunks means the pulse length is quantised to a second and an abort arrives whenever the DAQ feels like it. The relay must open on the time you configured, not on the buffer’s schedule.
And the USB-6009 has a single analogue-input timing engine. You cannot run a second AI task beside the acquisition task — the driver refuses it. So the recommended configuration includes the current-sense channel in the normal channel list, and the fire-confirm value is taken as the peak of the running task’s own samples over the chunk covering the pulse. The hardware limitation shapes how the user is told to configure the tool, which is usually how it goes with a constraint you can’t engineer away.
ni_usb6009_logger --device Dev1 --channels ai0,ai2 --rate 1000 --term RSE ^
--ignite --buzzer-line port1/line0 --igniter-line port1/line1 ^
--igniter-sense-ai ai2 --shunt-ohms 1.0 ^
--continuity-min-ma 0.2 --leak-max-ma 5 --fire-confirm-ma 300One core, two front ends#
I started with a command-line tool, because that’s what I wanted for myself. The person who ends up operating a static test stand is not necessarily the person who wrote the software, though, and a stand full of propellant is a poor place to explain virtual environments. So there is now a Windows GUI as well.
The two share everything that matters:
CLI adapter ─┐
├─▶ core (Qt-free): config + validation · Reporter callbacks ·
GUI (PySide6)─┘ LoggingSession · CalibrationSession · writers · daq.py ─▶ NI-DAQmxdaq.pyis the only file in the project that importsnidaqmx. Everything above it can be exercised without a driver.- The core reports through a
Reportercallback interface, so the CLI printing a status line and the GUI lighting an LED are the same event delivered to different sinks. - All safety logic lives in the core. Continuity, leak and fire-confirm thresholds are evaluated there, not in the user interface, so the command line and the GUI give exactly the same protection. The GUI’s contribution is the hold-to-fire gate, which grants permission — it does not implement the check.
- The GUI runs sessions in a worker
QThreadand receives data over queued Qt signals. Widgets never touch the DAQ. - The CLI was rewritten as a thin adapter over that core, with flags, defaults, console output and exit codes unchanged — verified against golden tests captured before the refactor, so the tool that already worked kept behaving identically.
There is also a fake backend (NI_USB6009_FAKE=1), which models the device
closely enough to include the single-timing-engine reservation. The whole
application runs on a machine with no driver and no hardware, which is what makes it
possible to test a Windows DAQ application in CI on Linux — and to rehearse the
ignition sequence at a desk, as often as you like, without a relay in sight.
Decisions that come from the test field#
A handful of behaviours exist purely because of where this software is used.
The output file is chosen first. You cannot start a run, and you cannot arm, until you have said where the data goes. There is no path through the program that produces an unnamed buffer you lose when something goes wrong.
Every run dual-writes a recovery file. Alongside your chosen output, a recovery
CSV is written and flushed every chunk. It is always CSV, never XLSX — an Excel file
only reaches the disk when it is closed, so it cannot serve as a crash copy. A clean
finish renames it to …_recovery_OK.csv; anything else leaves the marker off, so an
interrupted run is visibly interrupted rather than silently short. A Recovery tab
lists them with status and size and can restore any of them.
A hot unplug mid-run is a supported case, not a crash. The read raises, the session stops safely with the digital lines forced LOW, and a dialog in plain language explains what happened and names both files. The data captured so far is on disk. More generally the rule is that the operator never sees a traceback.
Memory stays flat. The live plot appends into fixed-capacity ring buffers, capped at 500 000 points per channel with repaints throttled to about 10 Hz, so a long static test costs no more memory than a short one.
Settings persist completely — including the ignition thresholds, even if the last thing you ran was a plain logging session — so the stand comes back up configured the way you left it. And the Windows build is a PyInstaller bundle inside an Inno Setup installer, with an optional silent NI-DAQmx driver component and a post-install self-test, so installation is Next-Next-Finish.
Where it stands#
Honestly: not finished. The installer, the silent driver install and the relay dry runs on real hardware still have to be done on the Windows machine at the stand. Continuous leak monitoring after arming was deliberately left out for now — it is not required by the specification, and it is a decision I want to make deliberately before first live use rather than discover in the field. No motor has been fired with this system yet.
The next thing that happens is a dry run with a dummy load, which is also the only sensible way to meet ignition software for the first time.
Resources#
- ni_usb6009_logger on GitHub (MIT)
- NI USB-6009 product page
- Load cell amplifier — the 2008 predecessor
- Static Test Controller — the 2010 integrated version
- Ballistic Evaluation Motor test 2 — a real test with that system
- ESP32 Wireless Rocket Launch Controller and its architecture deep dive — the flight-line counterpart to this stand