Troubleshooting USB-to-CAN V2 Compact Reopen Failures

Daniel Price8 min read
Industrial NetworkingOther ManufacturerTroubleshooting
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

Problem Details

A USB-to-CAN V2 Compact interface works correctly on first use, but a second open attempt from the same or a new application instance fails until the USB connector is physically unplugged and reinserted. Typical field symptoms:

  • The device enumerates in the operating system and is visible in the vendor device browser, but the application's open call returns a busy/in-use or resource-not-available error.
  • The CAN channel opens but no frames are received, or transmit calls fail, because the controller was left in a stopped or error state by the previous session.
  • The failure appears only after an abnormal exit (debugger stop, unhandled exception, process kill) and not after a clean shutdown — a strong indicator of handle leakage rather than a hardware fault.
  • The behavior is intermittent and not reproducible on every machine, which is normal for reference-counted driver resources: it depends on whether another process still holds a reference.
Scope note: The symptom description below covers the general class of "cannot reopen without replugging" faults on USB CAN interfaces. Confirm the exact API return codes and driver revision behavior against the interface documentation shipped with your driver package before treating any single cause as proven.

Root Cause Analysis

USB CAN interfaces expose a layered resource model. Each layer holds its own reference, and the physical USB replug is effectively a brute-force reset of all of them at once. If the application only closes the topmost object, the lower references survive the process and block the next open.

Layer What it represents Failure if not released
Device / interface handle The enumerated USB adapter as a whole Second open of the same adapter is refused as in use
Controller / control object Exclusive right to configure bit timing and start/stop the CAN controller Reopen succeeds but bit timing cannot be reprogrammed; controller stays in the previous state
Message channel / socket RX and TX FIFOs bound to the controller Channel is reported busy, or a stale FIFO delivers old frames
Driver reference count Per-process references held by the kernel/user-mode driver Count never reaches zero; the adapter is released only on unplug or reboot

The four practical root causes, ranked by how often they explain the symptom:

  1. Incomplete teardown. The application closes the channel but never closes the control object or the device handle, or closes them in the wrong order.
  2. Exception path bypasses cleanup. Cleanup code sits after the main loop instead of in a finally block or an IDisposable/RAII wrapper, so any thrown exception leaks every handle.
  3. Managed wrapper not disposed. In .NET, COM/interop objects that wrap the native handles are left to the garbage collector. Collection is non-deterministic, so the native handle can outlive the logical "close" by seconds or indefinitely if a static field still roots the object.
  4. Second process or leftover instance. A previous debug run, a background service, or a vendor diagnostic/monitor tool still owns the adapter. This is the most common cause of "works on my machine" irreproducibility.

Solution: Deterministic Open/Close Sequence

Structure the driver session so every acquired object has exactly one owner and one guaranteed release path. Open outermost first, close innermost first.

OPEN  (outer -> inner)
  1. enumerate devices -> select by serial number / unique ID
  2. open device handle
  3. open CAN controller (exclusive)
  4. set operating mode + bit timing
  5. open message channel (RX/TX FIFO), activate channel
  6. start controller

CLOSE (inner -> outer)  -- mirror image, always executed
  1. stop controller (or reset controller to init state)
  2. deactivate + close message channel
  3. close CAN controller object
  4. close device handle
  5. release enumeration/selection objects

Implementation rules that make the sequence survive faults:

  1. Wrap the whole session. Put the close sequence in a finally block (C/C++: goto-cleanup or RAII destructors; C#: using / Dispose(); Python: context manager) so an exception cannot skip it.
  2. Make close idempotent. Null each handle after release and guard every release with a validity check. Double-close on a stale handle can itself throw and abort the remainder of the cleanup.
  3. Never rely on the garbage collector. In .NET, call Dispose() explicitly on every interface wrapper, then clear static/singleton references. Add GC.Collect(); GC.WaitForPendingFinalizers(); only as a diagnostic to prove a leak — not as a production fix.
  4. Reset the controller before closing. Stopping or resetting the controller returns it to init state so the next session starts from a known configuration instead of inheriting a bus-off or error-passive condition.
  5. Handle Ctrl+C / service stop / process exit. Register a console handler, AppDomain.ProcessExit, or a signal handler that runs the same teardown. Terminating a process from Task Manager or stopping a debugger cannot run managed cleanup at all — use "Stop and detach" semantics during development and always exit the app through its normal shutdown path.
  6. Select the adapter by serial number, not index. Index-based selection changes when other USB devices enumerate, which produces an unrelated "cannot open" error that looks identical to a leaked handle.

Recovery Without Unplugging

Use this escalation ladder before touching the USB connector. Each step is cheaper and less disruptive than the next.

Step Action Clears
1 Close and reopen the channel and controller inside the running application Bus-off / error-passive state, stale FIFO contents
2 Exit the application through its normal shutdown path; confirm the process is gone in Task Manager Handles held by the process
3 Close any vendor diagnostic, monitor, or configuration tool, plus any background service that opens the adapter Second-owner conflicts
4 Disable then re-enable the adapter in Windows Device Manager Driver-level reference count — equivalent to a replug without physical access
5 Physical unplug/replug or reboot Everything, including kernel-level state
Field workaround: Step 4 is the practical substitute for a replug on panel-mounted or remote hardware. If the adapter recovers at step 4 but not at step 2, the leak is inside your application, not in the driver.

Verification

Prove the fix with a repeatable test rather than a single restart.

  1. Loop test. Run open → configure → transmit/receive a few frames → close, 100 times in a single process with no delay between iterations. Any failure after iteration 1 means a handle is leaking per cycle.
  2. Restart test. Start and cleanly exit the application 20 times. The adapter must open on every run without a replug.
  3. Abort test. Kill the process mid-session, then start it again. Note the result: if the adapter is still unavailable, add or verify the process-exit handler; if it recovers, your cleanup path is correct and the earlier fault was a normal-exit leak.
  4. Handle count. In Windows, watch the process Handles column in Task Manager (enable it in Details view) or use a handle-inspection utility across loop iterations. A monotonically rising count during the loop test localizes the leak to a specific object.
  5. Bus-level confirmation. After reopen, verify the controller is actually running by checking that received frames increment and that the controller status is error-active, not bus-off. A reopen that silently produces zero traffic is a configuration leak (bit timing not reapplied), not an open failure.
  6. Two-process test. Start a second instance while the first holds the adapter. The expected result is a clean, catchable "in use" error — confirm your code reports it instead of crashing, so the field diagnosis is unambiguous.

Hardening for Production

  • Single-owner architecture. Let exactly one process own the adapter and expose CAN data to other software through a local IPC/OPC UA/TCP layer. This removes the entire class of multi-process contention.
  • Reconnect state machine. On open failure, retry with backoff (for example 1 s, 2 s, 5 s, then hold at 10 s) and re-enumerate devices on each attempt instead of caching a stale device object.
  • Log the exact return code. Record the numeric error value and the API call that produced it. "Cannot open device" without a code is not diagnosable; the code distinguishes device-in-use from device-not-found from access-denied.
  • Separate transient CAN errors from open failures. Bus-off recovery, error counters, and cable/termination faults must not trigger a full device close/open cycle — reset the controller instead.
  • Pin the driver version. Record the installed driver/VCI package version alongside the adapter serial number in your commissioning documentation, and re-run the loop test after any driver update.
  • Escalation path. If the loop test still fails with correct teardown, raise a case with the vendor support portal at https://support.hms-networks.com/hc/en-us, attaching the failing return codes, driver version, adapter serial number, OS build, and a minimal reproducer. Vendor programming examples are distributed through that portal rather than through public channels.

Frequently Asked Questions

Why do I have to unplug the USB-to-CAN adapter before my program can open it again?

A previous session left a device, controller, or channel handle open, so the driver's reference count never reached zero. Unplugging forces the driver to tear down all references. Closing every object in reverse order of opening, inside a finally block, removes the need to replug.

How do I release a USB CAN interface without physical access to the connector?

Disable and re-enable the adapter in Windows Device Manager. That performs a driver-level teardown equivalent to a replug and can be scripted or done over a remote desktop session.

Is calling Dispose() enough in .NET, or do I need GC.Collect()?

Explicit Dispose() on every interface wrapper, plus clearing any static reference that roots the object, is sufficient and deterministic. Use GC.Collect() only as a temporary diagnostic to confirm a missing Dispose — if it fixes the problem, you have a leak to fix properly.

The adapter opens after a restart but receives no CAN frames. What is wrong?

The controller was left in a stopped, bus-off, or previously configured state. Reset the controller to init state, reapply bit timing and operating mode, activate the message channel, then start the controller — do not assume settings persist from the last session.

How do I prove my cleanup code actually works?

Run a 100-iteration open/close loop in one process and a 20-cycle application restart test. Watch the process handle count during the loop; a rising count identifies the leaking object, and a flat count with 100 successful iterations confirms correct teardown.

Back to blog