mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
Refactor Windows USB discovery (#5092)
* Create User32Ex with some mappings missing in JNA's User32 * Create WindowMessageHandler for subscribing to device change messages The class, when run as a Runnable, creates an invisible window and uses that to listen for device change events for USB devices and serial ports. It listens in a blocking message loop, so it's necessary to call terminate() for the loop to exit and the run() method to exit. Results are sent to subscribing WindowMessageListeners. * Refactor WindowsUsbSerialDiscovery to use a different approach to gathering device information The previous implementation scanned the registry for USB devices on a fixed interval. This implementation changes most of the core logic, using SetupAPI as the primary source of information instead of the registry. That allows it to process less information to get to the information of interest, in addition to only "discovering" devices that are currently connected to the computer. The registry keeps the entries for all devices that have previously been connected as well, which would also be "discovered" with the previous implementation. In addition, this implementation uses WindowMessageHandler to receive device change messages from Windows, making regular scanning unnecessary. If the WindowMessageHandler fails for some reason, the implementation will fall back to scanning on an interval, but this scanning is still using SetupAPI to acquire the data. * Various thread-safety and consistency fixes - UsbAddonFinder: Remove unused Set and fix concurrency behavior. ConcurrentHashMap is unsuitable here because it doesn't allow locking the full map, which is needed during "merging" and when resolving suggested add-ons. - DeltaUsbSerialScanner: Make lastScanResult thread-safe. - Ser2NetUsbSerialDiscovery: Make lastScanResult thread-safe, reduce the scope of locking to reduce contention and lessen how many locks are held at once. Make notifyListeners volatile for thread-safe access, and stop reacting to jmdns serviceAdded events. jmdns first fires serviceAdded when the service has just been discovered, but most of the information hasn't yet been gathered, and then fires serviceResolved once all the data has been registered. The data received in serviceAdded is incomplete and rarely useful, at best it's a "heads-up, this device might be resolved soon". - UsbSerialDiscovery: Correct JavaDoc - UsbSerialDiscoveryService: Minor logging tweaks, only log "Discovered new" the first time the device is "discovered". Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
This commit is contained in:
+49
-47
@@ -15,11 +15,10 @@ package org.openhab.core.config.discovery.addon.usb;
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_NAME_USB;
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_TYPE_USB;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -68,19 +67,17 @@ public class UsbAddonFinder extends BaseAddonFinder implements UsbSerialDiscover
|
||||
public static final Set<String> SUPPORTED_PROPERTIES = Set.of(PRODUCT, MANUFACTURER, CHIP_ID, REMOTE);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UsbAddonFinder.class);
|
||||
private final Set<UsbSerialDiscovery> usbSerialDiscoveries = new CopyOnWriteArraySet<>();
|
||||
private final Map<Long, UsbSerialDeviceInformation> usbDeviceInformations = new ConcurrentHashMap<>();
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private final Map<Long, UsbSerialDeviceInformation> usbDeviceInformations = new HashMap<>();
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
protected void addUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
|
||||
usbSerialDiscoveries.add(usbSerialDiscovery);
|
||||
usbSerialDiscovery.registerDiscoveryListener(this);
|
||||
usbSerialDiscovery.doSingleScan();
|
||||
}
|
||||
|
||||
protected synchronized void removeUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
|
||||
usbSerialDiscovery.unregisterDiscoveryListener(this);
|
||||
usbSerialDiscoveries.remove(usbSerialDiscovery);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,17 +99,19 @@ public class UsbAddonFinder extends BaseAddonFinder implements UsbSerialDiscover
|
||||
}
|
||||
|
||||
logger.trace("Checking candidate: {}", candidate.getUID());
|
||||
for (UsbSerialDeviceInformation device : usbDeviceInformations.values()) {
|
||||
logger.trace("Checking device: {}", device);
|
||||
synchronized (this) {
|
||||
for (UsbSerialDeviceInformation device : usbDeviceInformations.values()) {
|
||||
logger.trace("Checking device: {}", device);
|
||||
|
||||
if (propertyMatches(matchProperties, PRODUCT, device.getProduct())
|
||||
&& propertyMatches(matchProperties, MANUFACTURER, device.getManufacturer())
|
||||
&& propertyMatches(matchProperties, CHIP_ID,
|
||||
getChipId(device.getVendorId(), device.getProductId()))
|
||||
&& propertyMatches(matchProperties, REMOTE, String.valueOf(device.getRemote()))) {
|
||||
result.add(candidate);
|
||||
logger.debug("Suggested add-on found: {}", candidate.getUID());
|
||||
break;
|
||||
if (propertyMatches(matchProperties, PRODUCT, device.getProduct())
|
||||
&& propertyMatches(matchProperties, MANUFACTURER, device.getManufacturer())
|
||||
&& propertyMatches(matchProperties, CHIP_ID,
|
||||
getChipId(device.getVendorId(), device.getProductId()))
|
||||
&& propertyMatches(matchProperties, REMOTE, String.valueOf(device.getRemote()))) {
|
||||
result.add(candidate);
|
||||
logger.debug("Suggested add-on found: {}", candidate.getUID());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,47 +140,50 @@ public class UsbAddonFinder extends BaseAddonFinder implements UsbSerialDiscover
|
||||
/**
|
||||
* Add the discovered USB device information record to our internal map. If there is already an entry in the map
|
||||
* then merge the two sets of data.
|
||||
*
|
||||
*
|
||||
* @param discoveredInfo the newly discovered USB device information.
|
||||
*/
|
||||
@Override
|
||||
public void usbSerialDeviceDiscovered(UsbSerialDeviceInformation discoveredInfo) {
|
||||
UsbSerialDeviceInformation targetInfo = discoveredInfo;
|
||||
UsbSerialDeviceInformation existingInfo = usbDeviceInformations.get(keyOf(targetInfo));
|
||||
synchronized (this) {
|
||||
UsbSerialDeviceInformation existingInfo = usbDeviceInformations.get(keyOf(targetInfo));
|
||||
|
||||
if (existingInfo != null) {
|
||||
boolean isMerging = false;
|
||||
String product = existingInfo.getProduct();
|
||||
if (product != null) {
|
||||
product = discoveredInfo.getProduct();
|
||||
isMerging = true;
|
||||
}
|
||||
String manufacturer = existingInfo.getManufacturer();
|
||||
if (manufacturer != null) {
|
||||
manufacturer = discoveredInfo.getManufacturer();
|
||||
isMerging = true;
|
||||
}
|
||||
String serialNumber = existingInfo.getSerialNumber();
|
||||
if (serialNumber != null) {
|
||||
serialNumber = discoveredInfo.getSerialNumber();
|
||||
isMerging = true;
|
||||
}
|
||||
boolean remote = existingInfo.getRemote();
|
||||
if (remote == discoveredInfo.getRemote()) {
|
||||
isMerging = true;
|
||||
}
|
||||
if (isMerging) {
|
||||
targetInfo = new UsbSerialDeviceInformation(discoveredInfo.getVendorId(), discoveredInfo.getProductId(),
|
||||
serialNumber, manufacturer, product, discoveredInfo.getInterfaceNumber(),
|
||||
discoveredInfo.getInterfaceDescription(), discoveredInfo.getSerialPort()).setRemote(remote);
|
||||
if (existingInfo != null) {
|
||||
boolean isMerging = false;
|
||||
String product = existingInfo.getProduct();
|
||||
if (product != null) {
|
||||
product = discoveredInfo.getProduct();
|
||||
isMerging = true;
|
||||
}
|
||||
String manufacturer = existingInfo.getManufacturer();
|
||||
if (manufacturer != null) {
|
||||
manufacturer = discoveredInfo.getManufacturer();
|
||||
isMerging = true;
|
||||
}
|
||||
String serialNumber = existingInfo.getSerialNumber();
|
||||
if (serialNumber != null) {
|
||||
serialNumber = discoveredInfo.getSerialNumber();
|
||||
isMerging = true;
|
||||
}
|
||||
boolean remote = existingInfo.getRemote();
|
||||
if (remote == discoveredInfo.getRemote()) {
|
||||
isMerging = true;
|
||||
}
|
||||
if (isMerging) {
|
||||
targetInfo = new UsbSerialDeviceInformation(discoveredInfo.getVendorId(),
|
||||
discoveredInfo.getProductId(), serialNumber, manufacturer, product,
|
||||
discoveredInfo.getInterfaceNumber(), discoveredInfo.getInterfaceDescription(),
|
||||
discoveredInfo.getSerialPort()).setRemote(remote);
|
||||
}
|
||||
}
|
||||
|
||||
usbDeviceInformations.put(keyOf(targetInfo), targetInfo);
|
||||
}
|
||||
|
||||
usbDeviceInformations.put(keyOf(targetInfo), targetInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void usbSerialDeviceRemoved(UsbSerialDeviceInformation removedInfo) {
|
||||
public synchronized void usbSerialDeviceRemoved(UsbSerialDeviceInformation removedInfo) {
|
||||
usbDeviceInformations.remove(keyOf(removedInfo));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -29,6 +29,7 @@ import org.openhab.core.config.discovery.usbserial.UsbSerialDeviceInformation;
|
||||
@NonNullByDefault
|
||||
public class DeltaUsbSerialScanner {
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>();
|
||||
|
||||
private final UsbSerialScanner usbSerialScanner;
|
||||
@@ -37,8 +38,8 @@ public class DeltaUsbSerialScanner {
|
||||
this.usbSerialScanner = usbSerialScanner;
|
||||
}
|
||||
|
||||
public Set<UsbSerialDeviceInformation> getLastScanResult() {
|
||||
return lastScanResult;
|
||||
public synchronized Set<UsbSerialDeviceInformation> getLastScanResult() {
|
||||
return Set.copyOf(lastScanResult);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+36
-14
@@ -73,8 +73,9 @@ public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDisc
|
||||
private final Set<UsbSerialDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
|
||||
private final MDNSClient mdnsClient;
|
||||
|
||||
private boolean notifyListeners = false;
|
||||
private volatile boolean notifyListeners = false;
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>();
|
||||
|
||||
@Activate
|
||||
@@ -85,6 +86,10 @@ public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDisc
|
||||
@Override
|
||||
public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) {
|
||||
discoveryListeners.add(listener);
|
||||
Set<UsbSerialDeviceInformation> lastScanResult;
|
||||
synchronized (this) {
|
||||
lastScanResult = Set.copyOf(this.lastScanResult);
|
||||
}
|
||||
for (UsbSerialDeviceInformation deviceInfo : lastScanResult) {
|
||||
listener.usbSerialDeviceDiscovered(deviceInfo);
|
||||
}
|
||||
@@ -110,7 +115,7 @@ public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDisc
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void doSingleScan() {
|
||||
public void doSingleScan() {
|
||||
logger.debug("Starting ser2net USB-Serial mDNS single discovery scan");
|
||||
|
||||
Set<UsbSerialDeviceInformation> scanResult = Stream.of(mdnsClient.list(SERVICE_TYPE, SINGLE_SCAN_DURATION))
|
||||
@@ -118,11 +123,16 @@ public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDisc
|
||||
.flatMap(Optional::stream) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<UsbSerialDeviceInformation> added = setDifference(scanResult, lastScanResult);
|
||||
Set<UsbSerialDeviceInformation> removed = setDifference(lastScanResult, scanResult);
|
||||
Set<UsbSerialDeviceInformation> unchanged = setDifference(scanResult, added);
|
||||
Set<UsbSerialDeviceInformation> added;
|
||||
Set<UsbSerialDeviceInformation> removed;
|
||||
Set<UsbSerialDeviceInformation> unchanged;
|
||||
synchronized (this) {
|
||||
added = setDifference(scanResult, lastScanResult);
|
||||
removed = setDifference(lastScanResult, scanResult);
|
||||
unchanged = setDifference(scanResult, added);
|
||||
|
||||
lastScanResult = scanResult;
|
||||
lastScanResult = scanResult;
|
||||
}
|
||||
|
||||
removed.forEach(this::announceRemovedDevice);
|
||||
added.forEach(this::announceAddedDevice);
|
||||
@@ -151,23 +161,35 @@ public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDisc
|
||||
|
||||
@Override
|
||||
public void serviceAdded(@NonNullByDefault({}) ServiceEvent event) {
|
||||
if (notifyListeners) {
|
||||
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
|
||||
deviceInfo.ifPresent(this::announceAddedDevice);
|
||||
}
|
||||
// The service isn't resolved yet, so don't try to use it
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(@NonNullByDefault({}) ServiceEvent event) {
|
||||
if (notifyListeners) {
|
||||
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
|
||||
deviceInfo.ifPresent(this::announceRemovedDevice);
|
||||
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
|
||||
if (deviceInfo.isPresent()) {
|
||||
UsbSerialDeviceInformation removed = deviceInfo.get();
|
||||
synchronized (this) {
|
||||
lastScanResult.remove(removed);
|
||||
}
|
||||
if (notifyListeners) {
|
||||
announceRemovedDevice(removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(@NonNullByDefault({}) ServiceEvent event) {
|
||||
serviceAdded(event);
|
||||
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
|
||||
if (deviceInfo.isPresent()) {
|
||||
UsbSerialDeviceInformation added = deviceInfo.get();
|
||||
synchronized (this) {
|
||||
lastScanResult.add(added);
|
||||
}
|
||||
if (notifyListeners) {
|
||||
announceAddedDevice(added);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UsbSerialDeviceInformation> createUsbSerialDeviceInformation(ServiceInfo serviceInfo) {
|
||||
|
||||
+2
-2
@@ -202,7 +202,7 @@ public class Ser2NetUsbSerialDiscoveryTest {
|
||||
public void backgroundScanning() {
|
||||
discovery.startBackgroundScanning();
|
||||
|
||||
discovery.serviceAdded(serviceEvent1Mock);
|
||||
discovery.serviceResolved(serviceEvent1Mock);
|
||||
discovery.serviceRemoved(serviceEvent1Mock);
|
||||
discovery.serviceAdded(serviceEvent2Mock);
|
||||
discovery.serviceAdded(invalidServiceEventMock);
|
||||
@@ -215,7 +215,7 @@ public class Ser2NetUsbSerialDiscoveryTest {
|
||||
verify(discoveryListenerMock, times(1)).usbSerialDeviceDiscovered(usb1);
|
||||
verify(discoveryListenerMock, times(1)).usbSerialDeviceRemoved(usb1);
|
||||
|
||||
verify(discoveryListenerMock, times(1)).usbSerialDeviceDiscovered(usb2);
|
||||
verify(discoveryListenerMock, never()).usbSerialDeviceDiscovered(usb2);
|
||||
verify(discoveryListenerMock, never()).usbSerialDeviceRemoved(usb2);
|
||||
|
||||
verify(discoveryListenerMock, times(1)).usbSerialDeviceDiscovered(usb3);
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.usbserial.windowsregistry.internal;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.platform.win32.NTStatus;
|
||||
import com.sun.jna.platform.win32.User32;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.win32.W32APIOptions;
|
||||
|
||||
/**
|
||||
* Extra {@code user32.dll} mappings not defined in {@link User32}.
|
||||
*
|
||||
* @author Ravi Nadahar - Initial contribution.
|
||||
*/
|
||||
public interface User32Ex extends User32 {
|
||||
|
||||
/** The instance. */
|
||||
User32Ex INSTANCE = Native.load("user32", User32Ex.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||
|
||||
int WAIT_OBJECT_0 = NTStatus.STATUS_WAIT_0 + 0;
|
||||
int WAIT_ABANDONED_0 = NTStatus.STATUS_ABANDONED_WAIT_0 + 0;
|
||||
int WAIT_ABANDONED = WAIT_ABANDONED_0;
|
||||
int WAIT_TIMEOUT = 258;
|
||||
int WAIT_FAILED = 0xFFFFFFFF;
|
||||
int MAXIMUM_WAIT_OBJECTS = 64;
|
||||
|
||||
int PM_NOREMOVE = 0;
|
||||
int PM_REMOVE = 1;
|
||||
int PM_NOYIELD = 2;
|
||||
|
||||
/**
|
||||
* A {@code WM_KEYUP}, {@code WM_KEYDOWN}, {@code WM_SYSKEYUP}, or {@code WM_SYSKEYDOWN} message is in the queue.
|
||||
*/
|
||||
int QS_KEY = 0x0001;
|
||||
|
||||
/** A {@code WM_MOUSEMOVE} message is in the queue. */
|
||||
int QS_MOUSEMOVE = 0x0002;
|
||||
|
||||
/** A mouse-button message ({@code WM_LBUTTONUP}, {@code WM_RBUTTONDOWN}, and so on). */
|
||||
int QS_MOUSEBUTTON = 0x0004;
|
||||
|
||||
/**
|
||||
* A posted message (other than those listed here) is in the queue.
|
||||
* <p>
|
||||
* This value is cleared when you call {@link #GetMessage(MSG, HWND, int, int)} or
|
||||
* {@link #PeekMessage(MSG, HWND, int, int, int)}, whether or not you are filtering messages.
|
||||
*
|
||||
* @see #PostMessage(HWND, int, WPARAM, LPARAM)
|
||||
*/
|
||||
int QS_POSTMESSAGE = 0x0008;
|
||||
|
||||
/** A {@code WM_TIMER} message is in the queue. */
|
||||
int QS_TIMER = 0x0010;
|
||||
|
||||
/** A {@code WM_PAINT} message is in the queue. */
|
||||
int QS_PAINT = 0x0020;
|
||||
|
||||
/**
|
||||
* A message sent by another thread or application is in the queue.
|
||||
*
|
||||
* @see #SendMessage(HWND, int, WPARAM, LPARAM)
|
||||
*/
|
||||
int QS_SENDMESSAGE = 0x0040;
|
||||
|
||||
/** A {@code WM_HOTKEY} message is in the queue. */
|
||||
int QS_HOTKEY = 0x0080;
|
||||
|
||||
/**
|
||||
* A posted message (other than those listed here) is in the queue.
|
||||
* <p>
|
||||
* This value is cleared when you call {@link #GetMessage(MSG, HWND, int, int)} or
|
||||
* {@link #PeekMessage(MSG, HWND, int, int, int)} without filtering messages.
|
||||
*
|
||||
* @see #PostMessage(HWND, int, WPARAM, LPARAM)
|
||||
*/
|
||||
int QS_ALLPOSTMESSAGE = 0x0100;
|
||||
|
||||
/** Windows XP and newer: A raw input message is in the queue. For more information, see Raw Input. */
|
||||
int QS_RAWINPUT = 0x0400;
|
||||
|
||||
/** Windows 8 and newer: A touch input message is in the queue. For more information, see Touch Input. */
|
||||
int QS_TOUCH = 0x0800;
|
||||
|
||||
/** Windows 8 and newer: A pointer input message is in the queue. For more information, see Pointer Input. */
|
||||
int QS_POINTER = 0x1000;
|
||||
|
||||
/**
|
||||
* A {@code WM_MOUSEMOVE} message or mouse-button message ({@code WM_LBUTTONUP}, {@code WM_RBUTTONDOWN}, and so on).
|
||||
*/
|
||||
final int QS_MOUSE = (QS_MOUSEMOVE | QS_MOUSEBUTTON);
|
||||
|
||||
/** An input message is in the queue. */
|
||||
final int QS_INPUT = (QS_MOUSE | QS_KEY | QS_RAWINPUT | QS_TOUCH | QS_POINTER);
|
||||
|
||||
/** An input, {@code WM_TIMER}, {@code WM_PAINT}, {@code WM_HOTKEY}, or posted message is in the queue. */
|
||||
final int QS_ALLEVENTS = (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY);
|
||||
|
||||
/** Any message is in the queue. */
|
||||
final int QS_ALLINPUT = (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE);
|
||||
|
||||
/**
|
||||
* Waits until one or all of the specified objects are in the signaled state or the time-out interval elapses.
|
||||
* The objects can include input event objects, which you specify using the dwWakeMask parameter.
|
||||
*
|
||||
* @param nCount The number of object handles in the array pointed to by pHandles. The maximum number of object
|
||||
* handles is {@link #MAXIMUM_WAIT_OBJECTS} minus one. If this parameter has the value zero, then the
|
||||
* function
|
||||
* waits only for an input event.
|
||||
* @param pHandles An array of object handles. The array can contain handles of objects of different types.
|
||||
* It may not contain multiple copies of the same handle.
|
||||
* If one of these handles is closed while the wait is still pending, the function's behavior is
|
||||
* undefined.
|
||||
* The handles must have the {@code SYNCHRONIZE} access right.
|
||||
* @param fWaitAll If this parameter is {@code true} the function returns when the states of all objects in the
|
||||
* {@code pHandles} array have been set to signaled and an input event has been received.
|
||||
* If this parameter is {{@code false}, the function returns when the state of any one of the objects is
|
||||
* set to signaled or an input event has been received. In this case, the return value indicates the
|
||||
* object whose state caused the function to return.
|
||||
* @param dwMilliseconds The time-out interval, in milliseconds. If a nonzero value is specified,
|
||||
* the function waits until the specified objects are signaled or the interval elapses.
|
||||
* If {@code dwMilliseconds} is zero, the function does not enter a wait state if the specified objects
|
||||
* are not signaled;
|
||||
* it always returns immediately. If {{@code dwMilliseconds} is {@link WinBase#INFINITE}, the function
|
||||
* will return only when the specified objects are signaled.
|
||||
* @param dwWakeMask The input types for which an input event object handle will be added to the array of object
|
||||
* handles. This parameter can be any combination of the {@code QS} constants defined in this interface.
|
||||
* @return If the function succeeds, the return value indicates the event that caused the function to return.
|
||||
* It can be one of the following values:
|
||||
* <ul>
|
||||
* <li>{@link #WAIT_OBJECT_0} to ({@link #WAIT_OBJECT_0} + nCount– 1)</li>
|
||||
* <li>{@link #WAIT_OBJECT_0} + nCount</li>
|
||||
* <li>{@link #WAIT_ABANDONED_0} to ({@link #WAIT_ABANDONED_0} + nCount– 1)</li>
|
||||
* <li>{@link #WAIT_TIMEOUT}</li>
|
||||
* <li>{@link #WAIT_FAILED}</li>
|
||||
* </ul>
|
||||
*/
|
||||
int MsgWaitForMultipleObjects(int nCount, HANDLE[] pHandles, boolean fWaitAll, int dwMilliseconds, int dwWakeMask);
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.usbserial.windowsregistry.internal;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.platform.win32.DBT;
|
||||
import com.sun.jna.platform.win32.DBT.DEV_BROADCAST_DEVICEINTERFACE;
|
||||
import com.sun.jna.platform.win32.DBT.DEV_BROADCAST_HDR;
|
||||
import com.sun.jna.platform.win32.DBT.DEV_BROADCAST_PORT;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.Kernel32Util;
|
||||
import com.sun.jna.platform.win32.User32;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.platform.win32.WinDef.HMODULE;
|
||||
import com.sun.jna.platform.win32.WinDef.HWND;
|
||||
import com.sun.jna.platform.win32.WinDef.LPARAM;
|
||||
import com.sun.jna.platform.win32.WinDef.LRESULT;
|
||||
import com.sun.jna.platform.win32.WinDef.WPARAM;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
import com.sun.jna.platform.win32.WinUser;
|
||||
import com.sun.jna.platform.win32.WinUser.HDEVNOTIFY;
|
||||
import com.sun.jna.platform.win32.WinUser.MSG;
|
||||
import com.sun.jna.platform.win32.WinUser.WNDCLASSEX;
|
||||
import com.sun.jna.platform.win32.WinUser.WindowProc;
|
||||
|
||||
/**
|
||||
* This class, when run as a {@link Runnable}, creates an invisible window and uses that to listen for device change
|
||||
* events for USB devices and serial ports. It listens in a blocking message loop, so it's necessary to call
|
||||
* {@link #terminate()} for the loop to exit and the {@link #run()} method to exit.
|
||||
* <p>
|
||||
* The results of the device change events are delivered to subscribing {@link WindowMessageListener}s.
|
||||
*
|
||||
* @author Ravi Nadahar - Initial contribution.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WindowMessageHandler implements Runnable, WindowProc {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WindowMessageHandler.class);
|
||||
|
||||
private final AtomicInteger threadCounter = new AtomicInteger(0);
|
||||
|
||||
private final Set<WindowMessageListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
/** A Windows event that can be used to stop the message loop */
|
||||
private final HANDLE terminateEvent = Kernel32.INSTANCE.CreateEvent(null, false, false, null);
|
||||
|
||||
/**
|
||||
* Registers a {@link WindowMessageListener} that will receive USB device and serial port events.
|
||||
*
|
||||
* @param listener the {@link WindowMessageListener} to register.
|
||||
* @return {@code true} if the listener was added, {@code false} if it was already registered.
|
||||
*/
|
||||
public boolean addListener(WindowMessageListener listener) {
|
||||
return listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a {@link WindowMessageListener} so that it will no longer receive USB device and serial port events.
|
||||
*
|
||||
* @param listener the {@link WindowMessageListener} to unregister.
|
||||
* @return {@code true} if the listener was removed, {@code false} if it wasn't registered.
|
||||
*/
|
||||
public boolean removeListener(WindowMessageListener listener) {
|
||||
return listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread currentThread = Thread.currentThread();
|
||||
String threadName = currentThread.getName();
|
||||
currentThread.setName("OH-window-message-handler");
|
||||
|
||||
// Create and register window class
|
||||
String windowClass = "OHMessageHandlerWindowClass";
|
||||
User32Ex user32 = User32Ex.INSTANCE;
|
||||
HMODULE hInst = Kernel32.INSTANCE.GetModuleHandle(null);
|
||||
if (hInst == null) {
|
||||
logger.debug("Failed to get module handle, aborting message window creation");
|
||||
notifyTerminate();
|
||||
currentThread.setName(threadName);
|
||||
return;
|
||||
}
|
||||
WNDCLASSEX wClass = new WNDCLASSEX();
|
||||
wClass.hInstance = hInst;
|
||||
wClass.lpfnWndProc = WindowMessageHandler.this;
|
||||
wClass.lpszClassName = windowClass;
|
||||
if (user32.RegisterClassEx(wClass).intValue() == 0) {
|
||||
logger.debug("Failed to register window class, aborting message window creation");
|
||||
notifyTerminate();
|
||||
currentThread.setName(threadName);
|
||||
return;
|
||||
}
|
||||
|
||||
HWND hWnd = null;
|
||||
HDEVNOTIFY hDevNotify = null;
|
||||
try {
|
||||
// Parent can't be the recommended HWND_MESSAGE, because WM_DEVICECHANGE is a broadcast message,
|
||||
// which aren't sent to message-only windows.
|
||||
hWnd = user32.CreateWindowEx(User32.WS_EX_TOPMOST, windowClass,
|
||||
"OH helper window, used only to receive window events", 0, 0, 0, 0, 0, null, null, hInst, null);
|
||||
if (hWnd == null) {
|
||||
logger.debug("Failed to create window, aborting message window creation");
|
||||
return;
|
||||
}
|
||||
|
||||
DEV_BROADCAST_DEVICEINTERFACE notificationFilter = new DEV_BROADCAST_DEVICEINTERFACE();
|
||||
notificationFilter.dbcc_size = notificationFilter.size();
|
||||
notificationFilter.dbcc_devicetype = DBT.DBT_DEVTYP_DEVICEINTERFACE;
|
||||
notificationFilter.dbcc_classguid = DBT.GUID_DEVINTERFACE_USB_DEVICE;
|
||||
|
||||
hDevNotify = user32.RegisterDeviceNotification(hWnd, notificationFilter,
|
||||
User32.DEVICE_NOTIFY_WINDOW_HANDLE);
|
||||
if (hDevNotify == null) {
|
||||
logger.debug("Failed to register for device notification, terminating message window");
|
||||
return;
|
||||
}
|
||||
|
||||
MSG msg = new MSG();
|
||||
HANDLE[] handles = new HANDLE[] { terminateEvent };
|
||||
boolean running = true;
|
||||
while (running) {
|
||||
switch (user32.MsgWaitForMultipleObjects(handles.length, handles, false, WinBase.INFINITE,
|
||||
User32Ex.QS_ALLINPUT)) {
|
||||
case User32Ex.WAIT_OBJECT_0:
|
||||
// terminateEvent was triggered, terminate
|
||||
logger.debug("Terminate event received, terminating message loop");
|
||||
running = false;
|
||||
user32.PostQuitMessage(0);
|
||||
break;
|
||||
case User32Ex.WAIT_OBJECT_0 + 1:
|
||||
// A window message has been queued, process the message queue
|
||||
while (user32.PeekMessage(msg, hWnd, 0, 0, User32Ex.PM_REMOVE)) {
|
||||
if (msg.message == WinUser.WM_QUIT) {
|
||||
user32.PostQuitMessage(msg.wParam.intValue());
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
user32.TranslateMessage(msg);
|
||||
user32.DispatchMessage(msg);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// This should not happen, something is very wrong
|
||||
int lastError = Native.getLastError();
|
||||
logger.warn(
|
||||
"An error ({}) occurred while waiting for a window message, terminating message loop: {}",
|
||||
lastError, Kernel32Util.formatMessage(lastError));
|
||||
running = false;
|
||||
user32.PostQuitMessage(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (hDevNotify != null) {
|
||||
user32.UnregisterDeviceNotification(hDevNotify);
|
||||
}
|
||||
user32.UnregisterClass(windowClass, hInst);
|
||||
if (hWnd != null) {
|
||||
user32.DestroyWindow(hWnd);
|
||||
}
|
||||
|
||||
notifyTerminate();
|
||||
currentThread.setName(threadName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the event loop (the {@link #run()} method) that it should terminate.
|
||||
*/
|
||||
public void terminate() {
|
||||
Kernel32.INSTANCE.SetEvent(terminateEvent);
|
||||
}
|
||||
|
||||
private void notifyTerminate() {
|
||||
Set<WindowMessageListener> listeners = Set.copyOf(this.listeners);
|
||||
if (!listeners.isEmpty()) {
|
||||
createNotificationThread(() -> {
|
||||
for (WindowMessageListener listener : listeners) {
|
||||
listener.serviceTerminated();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private Thread createNotificationThread(Runnable runnable) {
|
||||
return new Thread(runnable, "OH-window-message-notifier-" + threadCounter.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNullByDefault({})
|
||||
public LRESULT callback(HWND hWnd, int uMsg, WPARAM wParam, LPARAM lParam) {
|
||||
switch (uMsg) {
|
||||
case WinUser.WM_CREATE:
|
||||
logger.trace("Window message handler created");
|
||||
return new LRESULT(0);
|
||||
case WinUser.WM_DESTROY:
|
||||
logger.trace("Window message handler destroyed");
|
||||
User32Ex.INSTANCE.PostQuitMessage(0);
|
||||
return new LRESULT(0);
|
||||
case WinUser.WM_DEVICECHANGE: {
|
||||
LRESULT lResult = onDeviceChange(wParam, lParam);
|
||||
return lResult != null ? lResult : User32Ex.INSTANCE.DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
default:
|
||||
return User32Ex.INSTANCE.DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LRESULT onDeviceChange(WPARAM wParam, LPARAM lParam) {
|
||||
switch (wParam.intValue()) {
|
||||
case DBT.DBT_DEVICEARRIVAL:
|
||||
return onDeviceAddedOrRemoved(lParam, true);
|
||||
case DBT.DBT_DEVICEREMOVECOMPLETE:
|
||||
return onDeviceAddedOrRemoved(lParam, false);
|
||||
case DBT.DBT_DEVNODES_CHANGED:
|
||||
// LRESULT(1) aka TRUE means that the message was processed. This message is non-specific
|
||||
// (basically means "something changed"), so we don't want to take any action.
|
||||
return new LRESULT(1);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LRESULT onDeviceAddedOrRemoved(LPARAM lParam, boolean added) {
|
||||
DEV_BROADCAST_HDR bhdr = new DEV_BROADCAST_HDR(lParam.longValue());
|
||||
Set<WindowMessageListener> listeners;
|
||||
switch (bhdr.dbch_devicetype) {
|
||||
case DBT.DBT_DEVTYP_DEVICEINTERFACE:
|
||||
DEV_BROADCAST_DEVICEINTERFACE bdif = new DEV_BROADCAST_DEVICEINTERFACE(bhdr.getPointer());
|
||||
listeners = Set.copyOf(this.listeners);
|
||||
if (!listeners.isEmpty()) {
|
||||
createNotificationThread(() -> {
|
||||
for (WindowMessageListener listener : listeners) {
|
||||
if (added) {
|
||||
listener.deviceAdded(bdif.getDbcc_name());
|
||||
} else {
|
||||
listener.deviceRemoved(bdif.getDbcc_name());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
break;
|
||||
case DBT.DBT_DEVTYP_PORT:
|
||||
DEV_BROADCAST_PORT bpt = new DEV_BROADCAST_PORT(bhdr.getPointer());
|
||||
listeners = Set.copyOf(this.listeners);
|
||||
if (!listeners.isEmpty()) {
|
||||
createNotificationThread(() -> {
|
||||
for (WindowMessageListener listener : listeners) {
|
||||
if (added) {
|
||||
listener.portAdded(bpt.getDbcpName());
|
||||
} else {
|
||||
listener.portRemoved(bpt.getDbcpName());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
break;
|
||||
// Don't process the remaining types
|
||||
case DBT.DBT_DEVTYP_HANDLE:
|
||||
case DBT.DBT_DEVTYP_OEM:
|
||||
case DBT.DBT_DEVTYP_VOLUME:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// LRESULT(1) aka TRUE means that the message was processed.
|
||||
return new LRESULT(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener that listens for {@link WindowMessageHandler} events.
|
||||
*
|
||||
* @author Ravi Nadahar - Initial contribution.
|
||||
*/
|
||||
public interface WindowMessageListener {
|
||||
|
||||
/**
|
||||
* A USB device was added.
|
||||
*
|
||||
* @param devicePath the device path of the added device.
|
||||
*/
|
||||
void deviceAdded(String devicePath);
|
||||
|
||||
/**
|
||||
* A USB device was removed.
|
||||
*
|
||||
* @param devicePath the device path of the removed device.
|
||||
*/
|
||||
void deviceRemoved(String devicePath);
|
||||
|
||||
/**
|
||||
* A serial port was added.
|
||||
*
|
||||
* @param portName the name of the port, e.g. {@code COM3}.
|
||||
*/
|
||||
void portAdded(String portName);
|
||||
|
||||
/**
|
||||
* A serial port was removed.
|
||||
*
|
||||
* @param portName the name of the port, e.g. {@code COM3}.
|
||||
*/
|
||||
void portRemoved(String portName);
|
||||
|
||||
/**
|
||||
* {@link WindowMessageHandler} was terminated, no more events will be sent.
|
||||
*/
|
||||
void serviceTerminated();
|
||||
}
|
||||
}
|
||||
+465
-154
@@ -12,18 +12,23 @@
|
||||
*/
|
||||
package org.openhab.core.config.discovery.usbserial.windowsregistry.internal;
|
||||
|
||||
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
|
||||
import static java.lang.Long.parseLong;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -31,55 +36,131 @@ import org.openhab.core.common.ThreadFactoryBuilder;
|
||||
import org.openhab.core.config.discovery.usbserial.UsbSerialDeviceInformation;
|
||||
import org.openhab.core.config.discovery.usbserial.UsbSerialDiscovery;
|
||||
import org.openhab.core.config.discovery.usbserial.UsbSerialDiscoveryListener;
|
||||
import org.openhab.core.config.discovery.usbserial.windowsregistry.internal.WindowMessageHandler.WindowMessageListener;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Platform;
|
||||
import com.sun.jna.platform.win32.Advapi32;
|
||||
import com.sun.jna.platform.win32.Advapi32Util;
|
||||
import com.sun.jna.platform.win32.Guid.GUID;
|
||||
import com.sun.jna.platform.win32.Kernel32Util;
|
||||
import com.sun.jna.platform.win32.SetupApi;
|
||||
import com.sun.jna.platform.win32.SetupApi.SP_DEVICE_INTERFACE_DATA;
|
||||
import com.sun.jna.platform.win32.SetupApi.SP_DEVINFO_DATA;
|
||||
import com.sun.jna.platform.win32.Win32Exception;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.platform.win32.WinError;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
/**
|
||||
* This is a {@link UsbSerialDiscovery} implementation component for Windows.
|
||||
* It parses the Windows registry for USB device entries.
|
||||
* This is a {@link UsbSerialDiscovery} implementation component for Windows. It uses the Windows API to query for and
|
||||
* be notified of USB devices. It will attempt to subscribe to device change notifications by creating an invisible
|
||||
* window for that subscribes to changes. If that fails, it will fall back to interval scanning.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Ravi Nadahar - Refactor to use SetupApi
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = UsbSerialDiscovery.class, name = WindowsUsbSerialDiscovery.SERVICE_NAME)
|
||||
public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery {
|
||||
@Component(service = UsbSerialDiscovery.class, name = WindowsUsbSerialDiscovery.SERVICE_NAME, configurationPid = "discovery.usbserial.windows")
|
||||
public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery, WindowMessageListener {
|
||||
|
||||
protected static final String SERVICE_NAME = "usb-serial-discovery-windows";
|
||||
public static final String SCAN_INTERVAL_PROPERTY = "scanInterval";
|
||||
public static final int DEFAULT_SCAN_INTERVAL_SECONDS = 15;
|
||||
private static final String DEVICE_PATH_PATTERN = "^\\\\\\\\\\?\\\\usb#vid_(?<vid>[0-9a-f]{4})&pid_(?<pid>[0-9a-f]{4})(?:&mi_(?<mi>[0-9a-f]{2}))?#(?<id>.*?)(?:#(?<guid>\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}))$";
|
||||
|
||||
private final Pattern devicePathPattern = Pattern.compile(DEVICE_PATH_PATTERN);
|
||||
|
||||
private record DevicePathData(int vendorId, int productId, String id, int interfaceNumber) {
|
||||
}
|
||||
|
||||
private static final boolean IS_64_BIT = Platform.is64Bit();
|
||||
private static final int ERROR_NO_SUCH_DEVINST = 0xe000020b;
|
||||
|
||||
// registry accessor strings
|
||||
private static final String USB_REGISTRY_ROOT = "SYSTEM\\CurrentControlSet\\Enum\\USB";
|
||||
private static final String BACKSLASH = "\\";
|
||||
private static final String PREFIX_PID = "PID_";
|
||||
private static final String PREFIX_VID = "VID_";
|
||||
private static final String PREFIX_HEX = "0x";
|
||||
private static final String SPLIT_IDS = "&";
|
||||
private static final String SPLIT_VALUES = ";";
|
||||
private static final String KEY_MANUFACTURER = "Mfg";
|
||||
private static final String KEY_PRODUCT = "DeviceDesc";
|
||||
private static final String KEY_DEVICE_PARAMETERS = "Device Parameters";
|
||||
private static final String KEY_SERIAL_PORT = "PortName";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WindowsUsbSerialDiscovery.class);
|
||||
private final Set<UsbSerialDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
|
||||
private final Duration scanInterval = Duration.ofSeconds(15);
|
||||
private volatile Duration scanInterval = Duration.ofSeconds(DEFAULT_SCAN_INTERVAL_SECONDS);
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>();
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private @Nullable ScheduledFuture<?> scanTask;
|
||||
|
||||
// All access must be guarded by "this"
|
||||
private @Nullable WindowMessageHandler windowMessageHandler;
|
||||
|
||||
/** Indicated that listening for device changes using window messages failed */
|
||||
private volatile boolean windowMessageFailed;
|
||||
|
||||
@Activate
|
||||
public WindowsUsbSerialDiscovery() {
|
||||
public WindowsUsbSerialDiscovery(Map<String, Object> config) {
|
||||
Object value = config.get(SCAN_INTERVAL_PROPERTY);
|
||||
if (value instanceof String s) {
|
||||
try {
|
||||
scanInterval = Duration.ofSeconds(parseLong(s));
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid configuration value for '{}': {}", SCAN_INTERVAL_PROPERTY, s);
|
||||
}
|
||||
} else if (value instanceof Number n) {
|
||||
scanInterval = Duration.ofSeconds(n.longValue());
|
||||
}
|
||||
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(
|
||||
ThreadFactoryBuilder.create().withName(SERVICE_NAME).withDaemonThreads(true).build());
|
||||
}
|
||||
|
||||
@Modified
|
||||
protected void modified(Map<String, Object> config) {
|
||||
Object value = config.get(SCAN_INTERVAL_PROPERTY);
|
||||
Duration newScanInterval = null;
|
||||
if (value instanceof String s) {
|
||||
try {
|
||||
newScanInterval = Duration.ofSeconds(parseLong(s));
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid configuration value for '{}': {}", SCAN_INTERVAL_PROPERTY, s);
|
||||
}
|
||||
} else if (value instanceof Number n) {
|
||||
newScanInterval = Duration.ofSeconds(n.longValue());
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (!Objects.equals(newScanInterval, scanInterval)) {
|
||||
if (newScanInterval == null) {
|
||||
scanInterval = Duration.ofSeconds(DEFAULT_SCAN_INTERVAL_SECONDS);
|
||||
} else {
|
||||
scanInterval = newScanInterval;
|
||||
}
|
||||
if (scanTask != null) {
|
||||
stopBackgroundScanning();
|
||||
startBackgroundScanning();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
synchronized (this) {
|
||||
stopBackgroundScanning();
|
||||
lastScanResult.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) {
|
||||
for (UsbSerialDiscoveryListener listener : discoveryListeners) {
|
||||
listener.usbSerialDeviceDiscovered(deviceInfo);
|
||||
@@ -92,20 +173,24 @@ public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
stopBackgroundScanning();
|
||||
lastScanResult.clear();
|
||||
@Override
|
||||
public void doSingleScan() {
|
||||
doSingleScanInternal(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void doSingleScan() {
|
||||
Set<UsbSerialDeviceInformation> scanResult = scanAllUsbDevicesInformation();
|
||||
Set<UsbSerialDeviceInformation> added = setDifference(scanResult, lastScanResult);
|
||||
Set<UsbSerialDeviceInformation> removed = setDifference(lastScanResult, scanResult);
|
||||
Set<UsbSerialDeviceInformation> unchanged = setDifference(scanResult, added);
|
||||
protected void doSingleScanInternal(boolean includeExisting) {
|
||||
Set<UsbSerialDeviceInformation> scanResult;
|
||||
Set<UsbSerialDeviceInformation> added;
|
||||
Set<UsbSerialDeviceInformation> removed;
|
||||
Set<UsbSerialDeviceInformation> unchanged;
|
||||
synchronized (this) {
|
||||
scanResult = gatherUsbDevicesInformation();
|
||||
added = setDifference(scanResult, lastScanResult);
|
||||
removed = setDifference(lastScanResult, scanResult);
|
||||
unchanged = includeExisting ? setDifference(scanResult, added) : Set.of();
|
||||
|
||||
lastScanResult = scanResult;
|
||||
lastScanResult = scanResult;
|
||||
}
|
||||
|
||||
removed.forEach(this::announceRemovedDevice);
|
||||
added.forEach(this::announceAddedDevice);
|
||||
@@ -121,6 +206,10 @@ public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery {
|
||||
@Override
|
||||
public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) {
|
||||
discoveryListeners.add(listener);
|
||||
Set<UsbSerialDeviceInformation> lastScanResult;
|
||||
synchronized (this) {
|
||||
lastScanResult = Set.copyOf(this.lastScanResult);
|
||||
}
|
||||
for (UsbSerialDeviceInformation deviceInfo : lastScanResult) {
|
||||
listener.usbSerialDeviceDiscovered(deviceInfo);
|
||||
}
|
||||
@@ -132,161 +221,383 @@ public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the USB tree in Windows registry and return a set of USB device information.
|
||||
* Traverse Windows USB devices and return a set of USB device information.
|
||||
*
|
||||
* @return a set of USB device information.
|
||||
*/
|
||||
public Set<UsbSerialDeviceInformation> scanAllUsbDevicesInformation() {
|
||||
public Set<UsbSerialDeviceInformation> gatherUsbDevicesInformation() {
|
||||
if (!Platform.isWindows()) {
|
||||
return new HashSet<>();
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
GUID GUID_DEVINTERFACE_USB_DEVICE = new GUID("A5DCBF10-6530-11D2-901F-00C04FB951ED");
|
||||
int SPDRP_FRIENDLYNAME = 0x0000000C;
|
||||
int SPDRP_MFG = 0x0000000B;
|
||||
|
||||
SetupApi apiInst = SetupApi.INSTANCE;
|
||||
|
||||
Set<UsbSerialDeviceInformation> result = new HashSet<>();
|
||||
String[] deviceKeys;
|
||||
try {
|
||||
deviceKeys = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, USB_REGISTRY_ROOT);
|
||||
} catch (Win32Exception e) {
|
||||
logger.debug("registryGetKeys failed for {}", USB_REGISTRY_ROOT, e);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (String deviceKey : deviceKeys) {
|
||||
logger.trace("{}", deviceKey);
|
||||
|
||||
if (!deviceKey.startsWith(PREFIX_VID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] ids = deviceKey.split(SPLIT_IDS);
|
||||
if (ids.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ids[1].startsWith(PREFIX_PID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int vendorId;
|
||||
int productId;
|
||||
HANDLE deviceInfoSet = apiInst.SetupDiGetClassDevs(GUID_DEVINTERFACE_USB_DEVICE, null, null,
|
||||
SetupApi.DIGCF_DEVICEINTERFACE | SetupApi.DIGCF_PRESENT);
|
||||
String serialPort;
|
||||
int lastError;
|
||||
if (!WinBase.INVALID_HANDLE_VALUE.equals(deviceInfoSet)) {
|
||||
try {
|
||||
vendorId = Integer.decode(PREFIX_HEX + ids[0].substring(4));
|
||||
productId = Integer.decode(PREFIX_HEX + ids[1].substring(4));
|
||||
} catch (NumberFormatException e) {
|
||||
continue;
|
||||
}
|
||||
SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA();
|
||||
SP_DEVICE_INTERFACE_DATA deviceInterfaceData = new SP_DEVICE_INTERFACE_DATA();
|
||||
|
||||
String serialNumber = ids.length > 2 ? ids[2] : null;
|
||||
int devIdx = 0;
|
||||
int intIdx;
|
||||
while (apiInst.SetupDiEnumDeviceInfo(deviceInfoSet, devIdx, deviceInfoData)) {
|
||||
|
||||
String devicePath = USB_REGISTRY_ROOT + BACKSLASH + deviceKey;
|
||||
String[] interfaceNames;
|
||||
try {
|
||||
interfaceNames = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, devicePath);
|
||||
} catch (Win32Exception e) {
|
||||
logger.debug("registryGetKeys failed for {}", devicePath, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
int interfaceId = 0;
|
||||
for (String interfaceName : interfaceNames) {
|
||||
logger.trace(" interfaceId:{}, interfaceName:{}", interfaceId, interfaceName);
|
||||
|
||||
String interfacePath = devicePath + BACKSLASH + interfaceName;
|
||||
TreeMap<String, Object> values;
|
||||
try {
|
||||
values = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE, interfacePath);
|
||||
} catch (Win32Exception e) {
|
||||
logger.debug("registryGetValues failed for {}", interfacePath, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
for (Entry<String, Object> value : values.entrySet()) {
|
||||
logger.trace(" {}={}", value.getKey(), value.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
String manufacturer;
|
||||
Object manufacturerValue = values.get(KEY_MANUFACTURER);
|
||||
if (manufacturerValue instanceof String manufacturerString) {
|
||||
String[] manufacturerData = manufacturerString.split(SPLIT_VALUES);
|
||||
if (manufacturerData.length < 2) {
|
||||
continue;
|
||||
}
|
||||
manufacturer = manufacturerData[1];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
String product;
|
||||
Object productValue = values.get(KEY_PRODUCT);
|
||||
if (productValue instanceof String productString) {
|
||||
String[] productData = productString.split(SPLIT_VALUES);
|
||||
if (productData.length < 2) {
|
||||
continue;
|
||||
}
|
||||
product = productData[1];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
String serialPort = "";
|
||||
String[] interfaceSubKeys;
|
||||
try {
|
||||
interfaceSubKeys = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, interfacePath);
|
||||
} catch (Win32Exception e) {
|
||||
logger.debug("registryGetKeys failed for {}", interfacePath, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String interfaceSubKey : interfaceSubKeys) {
|
||||
if (!KEY_DEVICE_PARAMETERS.equals(interfaceSubKey)) {
|
||||
continue;
|
||||
}
|
||||
String deviceParametersPath = interfacePath + BACKSLASH + interfaceSubKey;
|
||||
TreeMap<String, Object> deviceParameterValues;
|
||||
String name;
|
||||
String friendlyName;
|
||||
String mfg;
|
||||
try {
|
||||
deviceParameterValues = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE,
|
||||
deviceParametersPath);
|
||||
name = getDeviceRegistryPropertyString(deviceInfoSet, SetupApi.SPDRP_DEVICEDESC,
|
||||
deviceInfoData);
|
||||
friendlyName = getDeviceRegistryPropertyString(deviceInfoSet, SPDRP_FRIENDLYNAME,
|
||||
deviceInfoData);
|
||||
mfg = getDeviceRegistryPropertyString(deviceInfoSet, SPDRP_MFG, deviceInfoData);
|
||||
} catch (Win32Exception e) {
|
||||
logger.debug("registryGetValues failed for {}", deviceParametersPath, e);
|
||||
logger.warn("Failed to get USB device property: {}", e.getMessage());
|
||||
continue;
|
||||
}
|
||||
Object serialPortValue = deviceParameterValues.get(KEY_SERIAL_PORT);
|
||||
if (serialPortValue instanceof String serialPortString) {
|
||||
serialPort = serialPortString;
|
||||
|
||||
intIdx = 0;
|
||||
while (apiInst.SetupDiEnumDeviceInterfaces(deviceInfoSet, deviceInfoData.getPointer(),
|
||||
GUID_DEVINTERFACE_USB_DEVICE, intIdx, deviceInterfaceData)) {
|
||||
List<String> devicePaths;
|
||||
try {
|
||||
devicePaths = getDeviceInterfaceDetails(deviceInfoSet, deviceInterfaceData, null);
|
||||
} catch (Win32Exception e) {
|
||||
logger.warn("Failed to get USB device interface details for \"{}\": {}", name,
|
||||
e.getMessage());
|
||||
continue;
|
||||
}
|
||||
DevicePathData data;
|
||||
for (String devicePath : devicePaths) {
|
||||
data = parseDevicePath(devicePath);
|
||||
if (data != null) {
|
||||
WinReg.HKEY hKey = apiInst.SetupDiOpenDevRegKey(deviceInfoSet, deviceInfoData,
|
||||
SetupApi.DICS_FLAG_GLOBAL, 0, SetupApi.DIREG_DEV, WinNT.KEY_READ);
|
||||
if (hKey != WinBase.INVALID_HANDLE_VALUE) {
|
||||
try {
|
||||
serialPort = Advapi32Util.registryGetStringValue(hKey, KEY_SERIAL_PORT);
|
||||
} catch (Win32Exception e) {
|
||||
if (e.getErrorCode() != WinError.ERROR_FILE_NOT_FOUND) {
|
||||
logger.debug("Failed to read serial port for USB device \"{}\": {} {}",
|
||||
name, e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
serialPort = "";
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Failed to read serial port for USB device \"{}\": {} {}", name,
|
||||
e.getClass().getSimpleName(), e.getMessage());
|
||||
serialPort = "";
|
||||
} finally {
|
||||
Advapi32.INSTANCE.RegCloseKey(hKey);
|
||||
}
|
||||
} else {
|
||||
serialPort = "";
|
||||
}
|
||||
|
||||
UsbSerialDeviceInformation usbSerialDeviceInformation = new UsbSerialDeviceInformation(
|
||||
data.vendorId, data.productId, data.id, mfg,
|
||||
friendlyName == null || friendlyName.isBlank() ? name : friendlyName,
|
||||
data.interfaceNumber, data.id, serialPort);
|
||||
logger.debug("Parsed {}", usbSerialDeviceInformation);
|
||||
result.add(usbSerialDeviceInformation);
|
||||
}
|
||||
}
|
||||
intIdx++;
|
||||
}
|
||||
break;
|
||||
lastError = Native.getLastError();
|
||||
if (lastError != WinError.ERROR_NO_MORE_ITEMS) {
|
||||
logger.warn("Unexpected error while iterating USB device interfaces: {}",
|
||||
Kernel32Util.formatMessage(lastError));
|
||||
}
|
||||
devIdx++;
|
||||
}
|
||||
|
||||
UsbSerialDeviceInformation usbSerialDeviceInformation = new UsbSerialDeviceInformation(vendorId,
|
||||
productId, serialNumber, manufacturer, product, interfaceId, interfaceName, serialPort);
|
||||
|
||||
logger.debug("Add {}", usbSerialDeviceInformation);
|
||||
result.add(usbSerialDeviceInformation);
|
||||
|
||||
interfaceId++;
|
||||
lastError = Native.getLastError();
|
||||
if (lastError != WinError.ERROR_NO_MORE_ITEMS) {
|
||||
logger.warn("Unexpected error while iterating USB devices: {}",
|
||||
Kernel32Util.formatMessage(lastError));
|
||||
}
|
||||
} finally {
|
||||
apiInst.SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
}
|
||||
} else {
|
||||
lastError = Native.getLastError();
|
||||
logger.warn("Unable to enumerate USB devices: {}", Kernel32Util.formatMessage(lastError));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the specified device registry property using {@code SetupDiGetDeviceRegistryProperty} and returns
|
||||
* the result as a {@link String}. Might fail in an unpredictable way if the property value isn't a valid string.
|
||||
*
|
||||
* @param deviceInfoSet the handle to the {@code DeviceInfoSet} to read from.
|
||||
* @param property the code for the property to retrieve.
|
||||
* @param deviceInfoData the {@code DeviceInfoData} that identifies the element to retrieve the property from.
|
||||
* @return The resulting {@link String}.
|
||||
*
|
||||
* @throws Win32Exception If {@code SetupDiGetDeviceRegistryProperty} returns an unexpected status.
|
||||
*/
|
||||
@Nullable
|
||||
protected String getDeviceRegistryPropertyString(HANDLE deviceInfoSet, int property,
|
||||
SP_DEVINFO_DATA deviceInfoData) {
|
||||
Memory buffer = getDeviceRegistryProperty(deviceInfoSet, property, deviceInfoData);
|
||||
return buffer == null ? null : buffer.getWideString(0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the specified device registry property using {@code SetupDiGetDeviceRegistryProperty} and returns
|
||||
* the result as a raw {@link Memory} buffer. The reason is that various properties can have different types/
|
||||
* data structures.
|
||||
*
|
||||
* @param deviceInfoSet the handle to the {@code DeviceInfoSet} to read from.
|
||||
* @param property the code for the property to retrieve.
|
||||
* @param deviceInfoData the {@code DeviceInfoData} that identifies the element to retrieve the property from.
|
||||
* @return The resulting {@link Memory} buffer.
|
||||
*
|
||||
* @throws Win32Exception If {@code SetupDiGetDeviceRegistryProperty} returns an unexpected status.
|
||||
*/
|
||||
@Nullable
|
||||
protected Memory getDeviceRegistryProperty(HANDLE deviceInfoSet, int property, SP_DEVINFO_DATA deviceInfoData) {
|
||||
SetupApi apiInst = SetupApi.INSTANCE;
|
||||
IntByReference size = new IntByReference();
|
||||
int lastError;
|
||||
if (!apiInst.SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, null, null, 0, size)
|
||||
&& (lastError = Native.getLastError()) != WinError.ERROR_INSUFFICIENT_BUFFER) {
|
||||
if (lastError == WinError.ERROR_INVALID_DATA || lastError == ERROR_NO_SUCH_DEVINST) {
|
||||
return null;
|
||||
}
|
||||
throw new Win32Exception(lastError);
|
||||
}
|
||||
int sizeValue = size.getValue();
|
||||
if (sizeValue == 0) {
|
||||
return null;
|
||||
}
|
||||
Memory buffer = new Memory(sizeValue);
|
||||
if (!apiInst.SetupDiGetDeviceRegistryProperty(deviceInfoSet, deviceInfoData, property, null, buffer, sizeValue,
|
||||
null)) {
|
||||
lastError = Native.getLastError();
|
||||
if (lastError == WinError.ERROR_INVALID_DATA) {
|
||||
return null;
|
||||
}
|
||||
throw new Win32Exception(lastError);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the details for the specified device interface using {@code SetupDiGetDeviceInterfaceDetail} and
|
||||
* returns the result as a {@link List} of {@link String}s.
|
||||
*
|
||||
* @param deviceInfoSet the handle to the {@code DeviceInfoSet} to read from.
|
||||
* @param deviceInterfaceData the {@code DeviceInterfaceData} from which to retrieve the details.
|
||||
* @param deviceInfoData [out] the optional {@link SP_DEVINFO_DATA} structure that will be populated with
|
||||
* {@code DeviceInfoData} about the device that supports the requested interface. The structure must
|
||||
* first have been initialized with {@code DeviceInfoData.cbSize} to {@code sizeof(SP_DEVINFO_DATA)}.
|
||||
* @return The resulting {@link List} of {@link String}s.
|
||||
*
|
||||
* @throws Win32Exception If {@code SetupDiGetDeviceInterfaceDetail} returns an unexpected status.
|
||||
*/
|
||||
protected List<String> getDeviceInterfaceDetails(HANDLE deviceInfoSet, SP_DEVICE_INTERFACE_DATA deviceInterfaceData,
|
||||
@Nullable SP_DEVINFO_DATA deviceInfoData) {
|
||||
SetupApi apiInst = SetupApi.INSTANCE;
|
||||
IntByReference size = new IntByReference();
|
||||
int lastError;
|
||||
if (!apiInst.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, deviceInterfaceData, null, 0, size, deviceInfoData)
|
||||
&& (lastError = Native.getLastError()) != WinError.ERROR_INSUFFICIENT_BUFFER) {
|
||||
if (lastError == WinError.ERROR_INVALID_DATA) {
|
||||
return List.of();
|
||||
}
|
||||
throw new Win32Exception(lastError);
|
||||
}
|
||||
int sizeValue = size.getValue();
|
||||
if (sizeValue == 0) {
|
||||
return List.of();
|
||||
}
|
||||
Memory result = new Memory(sizeValue);
|
||||
|
||||
/*
|
||||
* The DWORD (uint) must contain the "size of the structure", which is only logical for those that
|
||||
* know how C compilers handle padding (64-bit pads where 32-bit doesn't).
|
||||
*
|
||||
* The 32-bit value represents: sizeOf(DWORD) + sizeOf(UTF16 char) = 4 + 2
|
||||
* The 64-bit value represents: sizeOf(DWORD) + sizeOf(UTF16 char) + padding = 4 + 2 + 2
|
||||
*
|
||||
* See https://stackoverflow.com/a/10729517 for further details.
|
||||
*/
|
||||
result.setInt(0L, IS_64_BIT ? 8 : 6);
|
||||
if (!apiInst.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, deviceInterfaceData, result, sizeValue, null,
|
||||
deviceInfoData)) {
|
||||
lastError = Native.getLastError();
|
||||
if (lastError == WinError.ERROR_INVALID_DATA) {
|
||||
return List.of();
|
||||
}
|
||||
throw new Win32Exception(lastError);
|
||||
}
|
||||
return readRegMultiSz(result, 4L);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code DevicePath} is a Windows concept that has a certain syntax. This method attempts to parse a USB
|
||||
* {@code DevicePath} and extract available data.
|
||||
*
|
||||
* @param devicePath the Windows USB {@code DevicePath} to parse.
|
||||
* @return The resulting {@link DevicePathData}.
|
||||
*/
|
||||
@Nullable
|
||||
protected DevicePathData parseDevicePath(String devicePath) {
|
||||
Matcher m = devicePathPattern.matcher(devicePath.toLowerCase(Locale.ROOT));
|
||||
if (m.find()) {
|
||||
try {
|
||||
int vendorId = Integer.valueOf(m.group("vid"), 16);
|
||||
int productId = Integer.valueOf(m.group("pid"), 16);
|
||||
String s = m.group("mi");
|
||||
int interfaceNumber = s == null || s.isBlank() ? 0 : Integer.valueOf(s, 16);
|
||||
s = m.group("id");
|
||||
return new DevicePathData(vendorId, productId, s, interfaceNumber);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Unable to parse USB device data idVendor: {}, idProduct {} or interface number {}: {}",
|
||||
m.group("vid"), m.group("pid"), m.group("mi"), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void startBackgroundScanning() {
|
||||
public void startBackgroundScanning() {
|
||||
if (Platform.isWindows()) {
|
||||
ScheduledFuture<?> scanTask = this.scanTask;
|
||||
if (scanTask == null || scanTask.isDone()) {
|
||||
this.scanTask = scheduler.scheduleWithFixedDelay(this::doSingleScan, 0, scanInterval.toSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
boolean initScan = false;
|
||||
synchronized (this) {
|
||||
ScheduledFuture<?> scanTask = this.scanTask;
|
||||
WindowMessageHandler messageHandler = this.windowMessageHandler;
|
||||
if (windowMessageFailed) {
|
||||
if (messageHandler != null) {
|
||||
messageHandler.removeListener(this);
|
||||
// Should not be necessary, but it doesn't hurt to make sure
|
||||
messageHandler.terminate();
|
||||
this.windowMessageHandler = null;
|
||||
}
|
||||
if (scanTask == null || scanTask.isDone()) {
|
||||
this.scanTask = scheduler.scheduleWithFixedDelay(() -> {
|
||||
doSingleScanInternal(false);
|
||||
}, 0, scanInterval.toSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
} else {
|
||||
if (scanTask != null) {
|
||||
scanTask.cancel(true);
|
||||
this.scanTask = null;
|
||||
}
|
||||
if (messageHandler == null) {
|
||||
messageHandler = new WindowMessageHandler();
|
||||
messageHandler.addListener(this);
|
||||
this.windowMessageHandler = messageHandler;
|
||||
scheduler.submit(messageHandler);
|
||||
initScan = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (initScan) {
|
||||
doSingleScanInternal(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stopBackgroundScanning() {
|
||||
WindowMessageHandler messageHandler = this.windowMessageHandler;
|
||||
if (messageHandler != null) {
|
||||
messageHandler.removeListener(this);
|
||||
messageHandler.terminate();
|
||||
this.windowMessageHandler = null;
|
||||
}
|
||||
ScheduledFuture<?> scanTask = this.scanTask;
|
||||
if (scanTask != null) {
|
||||
scanTask.cancel(false);
|
||||
scanTask.cancel(true);
|
||||
this.scanTask = null;
|
||||
}
|
||||
this.scanTask = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(String devicePath) {
|
||||
logger.debug("New USB device discovered: {}", devicePath);
|
||||
doSingleScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(String devicePath) {
|
||||
logger.debug("USB device removed: {}", devicePath);
|
||||
doSingleScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void portAdded(String portName) {
|
||||
logger.debug("New serial port discovered: {}", portName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void portRemoved(String portName) {
|
||||
logger.debug("Serial port removed: {}", portName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceTerminated() {
|
||||
logger.debug("Listening for window messages failed, falling back to interval scanning");
|
||||
windowMessageFailed = true;
|
||||
synchronized (this) {
|
||||
if (windowMessageHandler != null) {
|
||||
startBackgroundScanning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a {@link Memory} buffer containing a {@code RegMultiSz} value into a list of strings. The result of using
|
||||
* this method on a buffer than doesn't contain a {@code RegMultiSz} value is unpredictable.
|
||||
*
|
||||
* @param buffer the buffer to parse.
|
||||
* @param offset the offset for where to start parsing.
|
||||
* @return The resulting {@link List} of {@link String}s.
|
||||
*
|
||||
* @throws IllegalArgumentException If the offset is invalid.
|
||||
*/
|
||||
public static List<String> readRegMultiSz(Memory buffer, long offset) {
|
||||
long bufferSize = buffer.size();
|
||||
if (offset < 0L || offset >= bufferSize) {
|
||||
throw new IllegalArgumentException("Invalid offset " + offset + " for buffer of size " + bufferSize);
|
||||
}
|
||||
int size = (int) (bufferSize - offset) / 2;
|
||||
if (size == 0) {
|
||||
return List.of();
|
||||
}
|
||||
return readRegMultiSz(buffer.getCharArray(offset, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a char array containing a {@code RegMultiSz} value into a list of strings.
|
||||
*
|
||||
* @param chars the char array.
|
||||
* @return The resulting {@link List} of {@link String}s.
|
||||
*/
|
||||
public static List<String> readRegMultiSz(char[] chars) {
|
||||
List<String> result = new ArrayList<>();
|
||||
int start = 0;
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
if (chars[i] != 0) {
|
||||
continue;
|
||||
}
|
||||
if (start < i) {
|
||||
result.add(String.valueOf(chars, start, i - start));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -42,9 +42,8 @@ public interface UsbSerialDiscovery {
|
||||
void stopBackgroundScanning();
|
||||
|
||||
/**
|
||||
* Registers an {@link UsbSerialDiscoveryListener} that is then notified about discovered serial ports and USB,
|
||||
* including those already found during previous scan.
|
||||
* devices.
|
||||
* Registers an {@link UsbSerialDiscoveryListener} that is then notified about discovered USB serial ports.
|
||||
* Previously found devices will be notified during registration.
|
||||
*/
|
||||
void registerDiscoveryListener(UsbSerialDiscoveryListener listener);
|
||||
|
||||
|
||||
+4
-3
@@ -149,8 +149,9 @@ public class UsbSerialDiscoveryService extends AbstractDiscoveryService implemen
|
||||
|
||||
@Override
|
||||
public void usbSerialDeviceDiscovered(UsbSerialDeviceInformation usbSerialDeviceInformation) {
|
||||
logger.debug("Discovered new USB-Serial device: {}", usbSerialDeviceInformation);
|
||||
previouslyDiscovered.add(usbSerialDeviceInformation);
|
||||
if (previouslyDiscovered.add(usbSerialDeviceInformation)) {
|
||||
logger.debug("Discovered new USB-Serial device: {}", usbSerialDeviceInformation);
|
||||
}
|
||||
for (UsbSerialDiscoveryParticipant participant : discoveryParticipants) {
|
||||
DiscoveryResult result = participant.createResult(usbSerialDeviceInformation);
|
||||
if (result != null) {
|
||||
@@ -161,7 +162,7 @@ public class UsbSerialDiscoveryService extends AbstractDiscoveryService implemen
|
||||
|
||||
@Override
|
||||
public void usbSerialDeviceRemoved(UsbSerialDeviceInformation usbSerialDeviceInformation) {
|
||||
logger.debug("Discovered removed USB-Serial device: {}", usbSerialDeviceInformation);
|
||||
logger.debug("Discovered removal of USB-Serial device: {}", usbSerialDeviceInformation);
|
||||
previouslyDiscovered.remove(usbSerialDeviceInformation);
|
||||
for (UsbSerialDiscoveryParticipant participant : discoveryParticipants) {
|
||||
ThingUID thingUID = participant.getThingUID(usbSerialDeviceInformation);
|
||||
|
||||
Reference in New Issue
Block a user