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.
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:
- Incomplete teardown. The application closes the channel but never closes the control object or the device handle, or closes them in the wrong order.
-
Exception path bypasses cleanup. Cleanup code sits after the main loop instead of in a
finallyblock or anIDisposable/RAII wrapper, so any thrown exception leaks every handle. - 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.
- 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:
-
Wrap the whole session. Put the close sequence in a
finallyblock (C/C++: goto-cleanup or RAII destructors; C#:using/Dispose(); Python: context manager) so an exception cannot skip it. - 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.
-
Never rely on the garbage collector. In .NET, call
Dispose()explicitly on every interface wrapper, then clear static/singleton references. AddGC.Collect(); GC.WaitForPendingFinalizers();only as a diagnostic to prove a leak — not as a production fix. - 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.
-
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. - 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 |
Verification
Prove the fix with a repeatable test rather than a single restart.
- 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.
- Restart test. Start and cleanly exit the application 20 times. The adapter must open on every run without a replug.
- 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.
- 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.
- 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.
- 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.