How should I structure a LabVIEW ultrasonic test app?

Brian Holt13 min read
Other ManufacturerOther TopicTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The ultrasonic tank application already has working hardware connections, but motion and acquisition must run asynchronously; the structure needs to let those tasks execute independently while a coordinator makes the test-level decisions. Keep the working hardware code, but stop using the hidden tab control and loosely shared variables as the application’s state-management plan.

Remove hidden tab controls and globals from the data path

Storing values in a hidden tab control and copying them when a front-panel control changes makes the interface responsible for application state. That creates an indirect data path: a task’s behavior depends on which VI writes a control, when the write occurs, and whether another task reads it before the next update. It also makes the wiring difficult to follow because the screen becomes a shared storage area.

Replacing that arrangement with native global variables everywhere does not fix the ownership problem. A global makes a value reachable from many locations, but reachability does not define who may change it, when a change takes effect, or what should happen if two loops update it at once. Unordered reads and writes can produce stale values or race conditions.

Quick fix Why it causes trouble Better choice
Use a hidden control as shared state Application logic depends on UI controls and their update order. Keep runtime state with the loop or module that owns it; send changes as messages.
Replace every hidden control with a global Any caller can change state without a visible command path or clear ownership. Use a queue or event for cross-loop work; use a functional global for a small, deliberately shared state API.
Add local variables to avoid long wires Local-variable reads and writes can hide data dependencies and introduce races. Keep normal dataflow wires for values used within a VI; extract a SubVI when the diagram becomes unwieldy.

A functional global variable can be appropriate for a small amount of shared data when callers use a defined action interface. It is not a substitute for a message path between motion, acquisition, and test coordination. For state used in one loop, use that loop’s state data and an explicit initialization path rather than creating additional shared storage.

Separate the interface from the execution architecture

A tab control organizes screens; it does not define the program’s concurrency or module boundaries. Making every tab its own SubVI is not a requirement, and placing the whole application under one large VI does not make its components easier to coordinate. Decide which tasks need independent execution first, then decide which panels or tabs present those tasks to the operator.

Keep the user-interface loop responsible for front-panel events and display updates. It can send operator requests to a test coordinator and display status that modules send back. The coordinator owns the test sequence. Motion and acquisition modules own their respective hardware operations. Reporting receives completed test data and status rather than reaching into controls to discover whether a scan finished.

This gives each boundary a purpose: the interface translates operator input into requests; the coordinator decides what the application does next; hardware modules execute device work; reporting formats results. A screen can change without changing motion control, and a hardware implementation can change without rewriting tab logic.

Keep the project organized around these responsibilities. Place related VIs and their supporting types together, name them for their function, and make the top-level diagram show the main loops and their communication paths. Avoid hiding consequential behavior in a front-panel property or an unlabelled block of nested structures. A maintainer should be able to trace a request from the operator, through the coordinator, to the owning hardware module and back to a result.

Give each parallel loop one owner

LabVIEW’s dataflow model differs from a text program that runs line by line. A node runs when its input data is available, and independent diagram branches can execute in parallel. For a machine that needs motion and asynchronous acquisition, represent genuinely independent work with separate loops instead of forcing both into one long sequence.

Assign every important state value to one owner. The motion loop owns motion state and device references; the acquisition loop owns acquisition state and its data path; the coordinator owns the test phase and the decision to continue, pause, or abort. The interface owns transient presentation state. Other modules request a change from the owner and receive a status or result rather than writing directly into the owner’s state.

Use the loop’s state data to remember values that belong only to it. An explicit initialization state can load constants and reset state before the loop processes normal requests. Keep independent activities independent: a wire dependency between two otherwise parallel loops makes one wait for the other, while unsynchronized shared writes leave execution order unclear. Use an intentional message or event where coordination is required.

Do not put long hardware operations in the user-interface event loop. If an event handler blocks while waiting for a scan or motion call, the interface can stop processing other events. Send the request to the responsible module and let that module report progress and completion asynchronously. The UI then remains a view of the operation rather than its execution engine.

Move asynchronous commands and data with messages

A producer-consumer design is a practical starting point for separating acquisition work from processing or display work. The producer gathers data; the consumer handles the data at its own pace. The same message-based idea works for commands: one loop posts a request, and the owning loop handles it when ready. LabVIEW’s Producer/Consumer examples can illustrate the basic pattern, but an empty template does not decide the application’s queue ownership, error policy, or data-retention requirements.

Define messages around work the receiver can perform: start or stop a requested operation, return a completion result, report a fault, or deliver acquired data. Keep command messages distinct from measurement payloads when their rates or handling requirements differ. A consumer that formats a report should not hold up time-sensitive acquisition simply because both operations share an unexamined queue.

Choose a queue policy from the test requirements. If every acquired item must be retained, determine the queue backlog from measured producer rate and the longest consumer delay: items per second × delay in seconds = backlog items. This gives the backlog needed to absorb that delay under those assumptions; it is not a complete queue capacity specification. Add capacity based on measured operating conditions, and define what the producer does if the queue fills. Never silently discard data that the test requires for a valid result.

Use dynamic user events when event-style notification fits the design; use queues when the receiver must process ordered work items. Avoid sending the same event through several mechanisms without a clear reason. Whichever transport you choose, document who creates and releases it, which loop reads it, and how shutdown drains or rejects pending work. That lifecycle prevents stale commands from being acted on during a later test.

Let a coordinator decide how test errors propagate

Separate error handling by responsibility. A motion error belongs first to the motion module; an acquisition error belongs first to acquisition. Each module should preserve the underlying driver error details, report the fault and relevant operation to the coordinator, and execute its defined equipment response. The coordinator then applies the test policy instead of relying on a shared global error value or one error wire that links all parallel activities.

Reported condition Owning module action Coordinator decision
Motion operation fails Stop issuing dependent motion commands, report the failure, and carry out the motion response defined for the equipment. Decide whether the test can pause or must become incomplete; request any required acquisition action explicitly.
Acquisition fails Report the fault and identify affected data; do not present incomplete data as a completed scan. Decide whether motion must stop or hold based on the test and equipment requirements.
Reporting fails after a scan Report a report-generation failure without relabelling the scan as a successful report. Preserve the scan result and identify the reporting task that needs recovery.

A LabVIEW error wire creates a data dependency, so wiring one error chain through independent loops can serialize operations that were supposed to run in parallel. Keep each module’s internal error flow local. Send a fault notification to the coordinator, which makes an explicit decision and sends a corresponding command if another module must act.

Independence does not mean ignoring a failed peer. Define which failures invalidate a scan, which permit a controlled pause, and which require the application to request a broader stop. Do not invent one universal response: motion and acquisition can have different operational consequences. Record enough context to diagnose the fault, and make reset or retry an explicit transition rather than clearing the error silently.

Isolate motion and acquisition behind hardware modules

Put device-specific calls behind a hardware abstraction layer (HAL) or equivalent module boundary. The coordinator should request a motion or acquisition operation using application-level inputs, not manage driver references and device-specific details itself. The module owning a hardware connection should also own its initialization, normal operation, error reporting, and release.

For motion, expose the operations the test sequence needs and return completion or fault status. For acquisition, expose the start, progress, completion, and data result needed by the scan workflow. Keep the interface narrow: a module should not expose every driver call or every internal variable simply because another VI might use it later. Excessive access widens the scope of changes and makes it unclear which code can affect the hardware.

Translate device-specific responses at the boundary without throwing away useful diagnostic detail. The coordinator needs a meaningful outcome for its test decision; a maintainer also needs the original error information to investigate a driver or hardware fault. Keep those two purposes together in the module response rather than flattening every failure into a generic Boolean.

This boundary also supports staged testing. If the hardware calls can be separated cleanly, exercise the coordinator with controlled module responses before connecting a full test run. Then verify each real module against its hardware interface before relying on the end-to-end sequence. Do not claim a simulated test proves the physical system’s behavior; it checks application logic and communication paths.

Package repeated behavior as SubVIs

A SubVI is a reusable unit with defined inputs and outputs, much like a function in a text-based language. Use one when a diagram implements a coherent operation that deserves a name, has a clear interface, or is repeated. Examples in this application might include a self-contained report-formatting operation or a data-validation operation, provided each has explicit inputs and results.

Do not make a SubVI merely because a screen exists, and do not use it to conceal an unclear dependency. Choose the connector inputs and outputs so callers provide what the operation needs and receive what it produces. If a SubVI reaches into globals or UI controls for undeclared data, its visible interface no longer explains its behavior. A caller should be able to understand the dependency from the connector and the documentation.

Refactor incrementally. First identify duplicated wiring or a repeated, named operation; extract it; then check that the call sites still pass the intended values and propagate errors. Keep the top-level diagram at the level of application coordination, not low-level device detail. This makes the structure easier to review without forcing every small wire segment into another VI.

Choose a framework that fits the required concurrency

The application requirements already include parallel loops and asynchronous processing, so a single simple state machine should not be expected to own every operation. It can still serve as the coordinator. Keep the concurrency in modules with clear communication rather than adding framework features without a need.

Pattern Useful role Tradeoff to check
Producer/Consumer Separates data production from processing and demonstrates asynchronous loop communication. You must define queue ownership, data-retention behavior, shutdown, and error responses.
JKI State Machine Provides a structured state-machine approach for a simpler sequence or coordinator. It does not by itself define the architecture for independent hardware modules and asynchronous work.
DQMH or a Queued Message Handler Provides a modular message-oriented approach when independent modules and parallel work are central. Adopt one framework consistently and learn its conventions; do not mix several patterns casually.

For this system, a reasonable direction is a coordinator state machine plus independently owned motion and acquisition modules, using a message pattern consistently. DQMH is one option to evaluate for those modules; a Producer/Consumer or Queued Message Handler design can also be appropriate if it meets the project’s maintainability needs. The choice is less important than assigning state ownership, defining module interfaces, and deciding how faults cross boundaries.

Follow the project’s required LabVIEW toolchain rather than rewriting the application in another language just because that language is more familiar. Interoperability with another language is a separate design decision: use it only for a well-isolated component with an agreed interface and deployment plan, not as a substitute for understanding the LabVIEW architecture.

Verify completion and fault recovery with a test matrix

Before changing the full application, write down the expected sequence from initialization through test completion and reporting. Include the normal path and each module’s failure path. The test coordinator should have a defined response to start, progress, completion, stop, and fault notifications; a diagram that only works when every call succeeds is not ready for a production scan.

  1. Check initialization and idle behavior. Confirm that each module initializes and owns its hardware references, the interface displays the actual module status, and the coordinator does not issue test work before required modules report ready.
  2. Run a normal scan. Confirm that the interface remains responsive, motion and acquisition proceed without accidental wire dependencies, acquired data reaches its consumer, and reporting starts only from the intended completed-test result.
  3. Exercise each fault path. Use a controlled test condition or module test harness to make motion and acquisition report failures independently. Confirm that the right module reports the fault, the coordinator applies the written policy, and a failed or partial scan cannot appear as a valid completed result.
  4. Check recovery and shutdown. Verify that reset or retry clears only the state intended by the design, that pending messages from the previous test cannot start work in the next one, and that each module releases its resources through its own shutdown path.

Record state transitions, module status, and full error details during these tests. If queue growth is possible, measure production and consumption rates under the real scan workflow and compare the worst observed consumer delay with the backlog calculation. If results are incomplete, identify whether the failure occurred in acquisition, coordination, or report generation; do not use a successful report operation as proof that the scan data itself is valid.

FAQ: LabVIEW architecture for the ultrasonic tank

Can I use native global variables to share scan state?

Use them sparingly, not as the default communication path. Give each state value an owner; send cross-loop commands and results through a defined queue or event, and use a functional global only for small shared state with a clear action interface.

Does every tab in the user interface need its own SubVI?

No. A tab control is a presentation choice, not a module boundary. Create a SubVI for cohesive reusable behavior with explicit inputs and outputs, not automatically for every screen.

Can a motion error stop the whole test application?

The coordinator should decide from the test policy. The motion module reports the fault and performs its defined equipment response; the coordinator then requests the required acquisition or test action instead of relying on a global error value.

Is DQMH required for parallel acquisition and motion?

No. DQMH is one modular option; Producer/Consumer or a Queued Message Handler can also support asynchronous work. Select one approach the team can maintain and define ownership, communication, and error handling consistently.

When should I stop and contact official LabVIEW support?

Stop production deployment if motion or acquisition faults cannot be isolated, a partial scan can be reported as complete, or the application repeatedly loses control of its hardware communication. Bring the project, module-level error details, and a reproducible test case to NI support or a qualified LabVIEW architect.

Back to blog