To read a single voltage from a piece of measurement hardware, an engineer used to leave their MATLAB workflow, drop into C, write boilerplate, compile, debug, come back. Every measurement. Every time.
The work that mattered wasn’t the reduction. It was deciding what each of those three lines should say — the function names, the argument order, whether you configured by position or by name. We tested two shapes with 11 engineers across 5 industries, and let the data decide what shipped.
Not every interface lives on a screen.
Some you type, one word at a time.
Illustrative — actual C workflows varied by hardware and team.
Considered for the read function
Tested with 11 engineers across 5 industries. The chosen name was the closest match to the C library they already knew.
NI-DAQmx is the language MATLAB speaks to measurement hardware. This is the story of how we picked the words — and shipped them in MATLAB R2026a.
My Role
Lead UX researcher. Designed the comparative study, partnered with the developer to build two prototype APIs, ran the unmoderated study, recommended which design to ship.
Methods
Two prototype APIs · 11-participant unmoderated remote study · structured task script · paired ease-of-use rating · qualitative pain-point coding · mitigation matrix.
Scale
11 participants across 5 industries · 24 distinct pain points across both APIs · 1 ship/no-ship decision tied to evidence.
Outcome
Function-based API picked over class-based by a 0.72-point ease-of-use margin (3.27 vs. 2.55, on a 5-point scale). Shipped as calldaqlib in MATLAB R2026a.
~6 WEEKS
Scope
API-design partnership
Build prototypes
function vs class
Run study
11 engineers, 5 industries
Synthesize
24 → 4 mitigations
Ship
MATLAB R2026a
I led the research — but framed this study as an API-design partnership from day one, not a traditional usability test handed over to engineering. I designed the comparative protocol, helped scope the two prototype APIs with the developer (so they were genuinely comparable, not one obvious winner and one strawman), wrote the task script, ran the 11-participant unmoderated study, and turned the findings into a mitigation matrix the developer could implement against.
The decision to ship the function-based design was tied to evidence; the recommendation memo cited the participant data, not my opinion.
An API — application programming interface — is the set of commands a programmer uses to drive a piece of software or hardware. If a piece of measurement hardware (a DAQ device that reads voltages, temperatures, vibrations) is the engine, the API is the steering wheel the programmer uses to drive that engine.
Tiller
one stick · simple
Yoke
two handles · complex
Round wheel
familiar
The same engine can be driven by different steering wheels. Each one is technically capable of steering the engine, but they feel utterly different in the hands of the driver — and they make different mistakes more or less likely.
That’s API design. Two APIs that do the same thing, with different shapes, will produce different errors and different speeds of getting started. Picking which one to ship is a UX decision — the kind that should be tested with the people who’ll drive it, not argued in a hallway.
Both prototypes were thin MATLAB wrappers around the existing NI-DAQmx C library. The library was the engine; the wrapper was the steering wheel. The question was which wheel.
Vocab · 1
Wrapper
A thin layer of MATLAB code that calls into a lower-level C library — so engineers stay in MATLAB instead of dropping into C.
Vocab · 2
Function-based shape
One dispatcher function: daqeval("DAQmxXxx", …). Each C call becomes one MATLAB call.
Vocab · 3
Class-based shape
Build an options object, then apply it: niConfigOptions(…); setconfig(…). Each C call becomes two MATLAB calls.
Vocab · 4
Name-value pair
MATLAB’s argument syntax: Name=value. Great for optional args; awkward when required args dominate.
Function-based — one dispatcher function, daqeval, that takes the C function name as a string plus the original arguments. Each C call becomes one MATLAB call. Compact; you have to know the C function name.
args3 = daqeval(daqobject, "DAQmx<FunctionName>", args1, args2)Class-based — helper classes (niConfigOptions, niPropOptions) that build a configuration object from name-value pairs, then verbs (setconfig, setprop, getprop, resetprop) to apply or query against the DAQ object. Each C call becomes two MATLAB calls. More verbose; parameter names are visible without knowing the C signature.
configObj = niConfigOptions("<FunctionName>", Arg1=value1, Arg2=value2);
setconfig(daqobject, configObj);Same engine, two steering wheels. Picking which one to ship is a UX decision.
NI-DAQmx C Library
The shared engine underneath both MATLAB wrappers
Function-based
daqeval
1 call per C function · compact · direct
Class-based
niConfigOptions
2 calls per C function · structured · verbose
MATLAB Engineer
Same task, different experience with each wrapper API
Same engine (NI-DAQmx C library), two steering wheels — which wrapper works better for the driver?
Artifact · Steering-wheel analogy · NI-DAQmx API design
A UI is the furniture in a room. If it doesn’t work, you rearrange it. Bold ideas are welcome, mistakes are reversible, and you ship a v2.
This wasn’t that. What we design sits behind the wall — like the electrical outlets every appliance plugs into. Engineers across automotive, aerospace, biomedical, and academic labs build on top of it: test rigs, production lines, research scripts, regulated workflows. Change the shape of the outlet, and every plug in every house stops working overnight.
UI · REVERSIBLE FURNITURE
ship · watch · rearrange · iterate
API · FIXED INFRASTRUCTURE
LOCKEDship once · live with it for a decade
That changes the standard. With a UI, you ship, watch usage, iterate. With this kind of work, you ship once and live with it. A name we’d regret six months in is a name we’d live with for ten years — because every script ever written against it depends on it staying exactly the same. There’s no quiet redesign. There’s no “we cleaned up the look.” A customer whose script errored out yesterday doesn’t get a chance to admire the new design — they get a phone call from their team saying nothing works.
Two candidate shapes came to the whiteboard. Both worked, both had real engineers in the room defending them, and there was no data to break the tie — only conviction. So before anything was locked in, I scoped a comparative study. Eleven engineers across five industries ran the same workflow against each shape, and the data — not the loudest voice in the room — decided which one we’d live with for the next decade.
Three phases, sequential, ~6 weeks end to end. Each phase fed the next.
Phase 1
0
prototype APIs
Phase 2
0
participants × 5 industries
Phase 3
0
high-priority themes
~6 WEEKS · SEQUENTIAL
Phase 1 — Build comparable prototypes
~2 weeks 2 prototype APIs · 1 shared task surface function-based vs. class-based
I worked with the developer to define a single representative workflow that both APIs had to support: list available DAQ devices, create a task, add an analog input voltage channel, configure the sampling rate, acquire 1 second of data, clean up. Same task; two different shapes. We held the surface area constant so the study was testing the API style, not feature parity.
Phase 2 — Unmoderated comparative study
11 participants · 5 industries automotive · R&D · academia · aerospace · biomedical randomized order
Each participant completed the same workflow twice — once with each API. I randomized the order across participants to control for learning effects. After each attempt, participants rated the API on a 5-point ease-of-use scale and answered three open-ended questions about what worked and what didn't.
Phase 3 — Synthesize and recommend
24 pain points across both APIs mitigation matrix 1 ship/no-ship recommendation
The headline number — function-based 3.27, class-based 2.55 — was decisive on its own, but the qualitative coding was where the design recommendations lived. I tagged every observed pain point against the API style that produced it, then built a mitigation matrix: for each pain point, what specific change would address it.
Two prototype wrappers, one C library underneath. To make the comparison fair, every participant ran the same NI-DAQmx C call through each wrapper. The example below uses DAQmxCfgChangeDetectionTiming — a configuration call that in raw C looks like:
Same call, two wrappers:
% Function-based — one MATLAB call mirrors one C call
daqeval(dq, "DAQmxCfgChangeDetectionTiming", ...
"Dev1/port0/line0:7", "Dev1/port0/line0:7", ...
daq.ni.NIDAQmx.DAQmx_Val_ContSamps, 4);% Class-based — build options, then apply
changeDetectionTiming = niConfigOptions("ChangeDetectionTiming", ...
RisingEdgeChan="Dev1/port0/line0:7", ...
FallingEdgeChan="Dev1/port0/line0:7", ...
SampleMode=daq.ni.NIDAQmx.DAQmx_Val_ContSamps, ...
sampsPerChan=4);
setconfig(dq, changeDetectionTiming);Ease-of-use rating
Distinct pain points coded
0.72
point margin
29%
advantage on a 5-pt scale
5 / 5
industries where function-based led
“Most participants who articulated a preference for the class-based shape during the post-task interview had still rated it lower in the moment. Stated preference and observed performance pulled in different directions. The decision came down to observed performance.”
Four high-priority insights, each tied to a specific pain point and a specific recommendation. Same pattern as always: observation → insight → recommendation → what shipped.
Insight 1
Observation
When participants passed an invalid argument to a function (e.g., a wrong terminal config enum), the error was the raw NI-DAQmx error code — something like Error -200077: Requested value is not a supported value for this property. Two participants spent 5+ minutes hunting through documentation to figure out which property the error referred to.
Insight
The error pointed at the symptom (a value isn’t supported) but not the location (which argument, in which call). Engineers don’t read error codes — they read error messages. The message has to do the work of pointing them to the line they should fix.
Recommendation
Wrap the underlying error so MATLAB users see the function name, the argument name, and the list of valid values — not just the raw code. For the function-based API this is a one-line wrapper around each call; for the class-based API it’s harder because the error chain runs through more layers.
What Shipped
calldaqlib in R2026a includes wrapped errors with valid-values hints for the most common enum arguments.
>> daqeval(dq, "DAQmxCfgChangeDetectionTiming", ..., "rising_edge"); Error using DAQmx DAQmxError: -200077 Requested value is not a supported value for this property. Property: DAQmx_ChangeDetect_DI_RisingEdgePhysicalChans Status Code: -200077
>> daqeval(dq, "DAQmxCfgChangeDetectionTiming", ..., "rising_edge"); Error in DAQmxCfgChangeDetectionTiming › activeEdge: "rising_edge" is not a valid value. Valid values: "Rising", "Falling". Did you mean "Rising"?
Insight 2
Observation
Participants used tab completion as their primary way to discover what was possible. With the function-based wrapper, typing "DAQmx inside a daqeval call surfaced every available C function via tab completion on the string literal — they could scan and pick. With the class-based wrapper, tab-completing setconfig(dq, surfaced the verb but not the things it could operate on — you had to build the niConfigOptions object first before anything was visible. Several participants typed setconfig(dq, and stalled.
% Tab on the string literal surfaces the C namespace
daqeval(dq, "DAQmxCfg|" % cursor here → list appears% setconfig is visible; its argument is not
setconfig(dq, | % cursor here → blank
configObj = niConfigOptions("..."); % must build this firstInsight
Tab completion is the modern engineer’s reading tool. APIs that surface their full surface area through tab completion feel learnable; APIs that hide functionality behind name-value pairs feel mysterious. This is a structural difference between the two design styles, not a bug to be fixed at the margin.
Recommendation
Pick the API style that benefits more from tab completion as a discovery surface. For the function-based API, every function is discoverable by typing a prefix. For the class-based API, name-value pairs hide options.
What Shipped
The function-based design — calldaqlib exposes the C function namespace directly, fully tab-completable.
Tab on the string literal surfaces the full C namespace.
Verb is visible; its argument isn’t. Several participants stalled here.
Insight 3
Observation
The class-based wrapper used name-value pairs as its primary configuration mechanism — not just for channel config, but for every configuration call: niConfigOptions("ChangeDetectionTiming", RisingEdgeChan=…, FallingEdgeChan=…, SampleMode=…, sampsPerChan=…). Three participants wrote the entire options object as one line, then split it, then commented pairs out in a “what’s actually required here?” loop. The C signature requires all four arguments; the MATLAB wrapper made them all look optional. Two participants asked, “Can I just pass the C-style positional list?”
% Which of these are required? The MATLAB syntax doesn't tell you.
configObj = niConfigOptions("ChangeDetectionTiming", ...
RisingEdgeChan="Dev1/port0/line0:7", ...
% FallingEdgeChan="Dev1/port0/line0:7", ... <- commented out, errors
SampleMode=daq.ni.NIDAQmx.DAQmx_Val_ContSamps, ...
sampsPerChan=4);Insight
Name-value pairs are great for optional arguments. They’re awkward when the required arguments outnumber the optional ones — at that point, what looked like an ergonomic API turns into a configuration file that happens to live inside parentheses.
Recommendation
Either constrain name-value pairs to optional arguments only (and use positional arguments for required ones), or pick a design where the structure of required arguments is positionally clear. The function-based shape makes this explicit; the class-based shape hides it.
What Shipped
Function-based design. Documentation makes required arguments positional and explicit.
Every argument is wrapped in Name=value. The MATLAB syntax that signals “optional” is doing all of the talking — even for the three arguments that the C signature requires.
Required arguments stay positional. Optional arguments use Name=value. The shape of the call signals which is which — without docs, without guessing.
Insight 4
Observation
Across both APIs, participants reached for the documentation only after they were stuck. When they did, the reference docs (function signatures, parameter lists) were the least useful resource. Almost every participant explicitly searched for an “example” — a complete, runnable script that did the workflow they were trying to do.
Insight
Reference documentation is for engineers who already know what they’re doing. For onboarding to a new API, a runnable example is the only thing that matters. This isn’t an API-design finding so much as an API-onboarding finding — but it’s actionable in the same release.
Recommendation
Ship at least one complete, runnable example for every common workflow. Link to the example from the reference docs, not the other way around.
What Shipped
The R2026a docs page for calldaqlib opens with a complete worked example before the reference table.
The R2026a docs lead with the runnable workflow. Reference tables sit underneath — discoverable when needed, not the front door.
The function-based low-level API shipped in MATLAB R2026a as calldaqlib. You can read the public documentation at mathworks.com/help/daq/calldaqlib.html.
The shipped design carries forward all four mitigations from the study:
Wrapped errors
Tab-complete namespace
Required = positional
Example-first docs
One thing the function-based wrapper preserves that’s worth naming: engineers can paste a C example from the NI-DAQmx documentation, change the call wrapper, and have working MATLAB code. The class-based wrapper would have required them to learn a parallel taxonomy — which class wraps which family of calls — before they could move. Choosing the wrapper shape that kept the C documentation usable was a strategic call, not just an ergonomics one. NI-DAQmx has thousands of pages of C documentation; we couldn’t out-document it, so we made our wrapper transparent to it.
A small but real signal of the decision’s correctness: the support tickets that have come in since calldaqlib shipped have been about hardware-specific edge cases (the C library’s territory), not about the API shape. The API shape is no longer the friction; the hardware quirks are. That’s the right kind of friction for a low-level API to have.
The artifacts the case-study summary doesn’t show — for readers who want to see the craft underneath the outcome.
Prototype reference docOne of the two prototype reference docs I authored to brief participants. Function-based + class-based companion. Same C library; different MATLAB wrappers.
Affinity-mapping boardSynthesis from 24 raw pain points to 4 high-priority themes. Color-coded by API style; clusters were cross-validated with a second coder.
Unmoderated study instrumentOne page from the study. Each participant ran the same workflow twice — once per API — with order randomized.
ONE
I’d run a shorter, earlier prototype-fidelity check before committing to the comparative design. Several pain points in the class-based prototype were implementation details rather than design-style consequences — a separate, faster round at lower fidelity (“does this naming feel right?”, “are these enum names readable?”) would have stripped the noise out before the formal study and let the head-to-head be a cleaner comparison of style, not implementation polish.
TWO
I’d capture stated preference more carefully, even when observed performance contradicts it. The class-based shape had a real constituency in the post-task interviews — a subset of participants who liked the structure but performed worse with it. Their preference is a signal worth understanding, not a result to discard. In retrospect I would have followed up with two of them to dig into the gap between what they liked and what worked for them. That gap is where the next API design lives.