-
-
Notifications
You must be signed in to change notification settings - Fork 428
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UsbSerialDiscovery service based on Windows registry (#3934)
Signed-off-by: Andrew Fiddian-Green <[email protected]>
- Loading branch information
Showing
8 changed files
with
373 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<classpath> | ||
<classpathentry kind="src" output="target/classes" path="src/main/java"> | ||
<attributes> | ||
<attribute name="optional" value="true"/> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="annotationpath" value="target/dependency"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="annotationpath" value="target/dependency"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="src" output="target/test-classes" path="src/test/java"> | ||
<attributes> | ||
<attribute name="optional" value="true"/> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="test" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="output" path="target/classes"/> | ||
</classpath> |
23 changes: 23 additions & 0 deletions
23
bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<projectDescription> | ||
<name>org.openhab.core.config.discovery.usbserial.javaxusb</name> | ||
<comment></comment> | ||
<projects> | ||
</projects> | ||
<buildSpec> | ||
<buildCommand> | ||
<name>org.eclipse.jdt.core.javabuilder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
<buildCommand> | ||
<name>org.eclipse.m2e.core.maven2Builder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
</buildSpec> | ||
<natures> | ||
<nature>org.eclipse.jdt.core.javanature</nature> | ||
<nature>org.eclipse.m2e.core.maven2Nature</nature> | ||
</natures> | ||
</projectDescription> |
14 changes: 14 additions & 0 deletions
14
bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
This content is produced and maintained by the openHAB project. | ||
|
||
* Project home: https://www.openhab.org | ||
|
||
== Declared Project Licenses | ||
|
||
This program and the accompanying materials are made available under the terms | ||
of the Eclipse Public License 2.0 which is available at | ||
https://www.eclipse.org/legal/epl-2.0/. | ||
|
||
== Source Code | ||
|
||
https://github.com/openhab/openhab-core | ||
|
30 changes: 30 additions & 0 deletions
30
bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
|
||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>org.openhab.core.bundles</groupId> | ||
<artifactId>org.openhab.core.reactor.bundles</artifactId> | ||
<version>4.2.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>org.openhab.core.config.discovery.usbserial.windowsregistry</artifactId> | ||
|
||
<name>openHAB Core :: Bundles :: Configuration USB-Serial Discovery for Windows</name> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.openhab.core.bundles</groupId> | ||
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>net.java.dev.jna</groupId> | ||
<artifactId>jna-platform</artifactId> | ||
<version>5.13.0</version> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
261 changes: 261 additions & 0 deletions
261
...b/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
/** | ||
* Copyright (c) 2010-2024 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 static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; | ||
|
||
import java.time.Duration; | ||
import java.util.HashSet; | ||
import java.util.Map.Entry; | ||
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 org.eclipse.jdt.annotation.NonNullByDefault; | ||
import org.eclipse.jdt.annotation.Nullable; | ||
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.osgi.service.component.annotations.Activate; | ||
import org.osgi.service.component.annotations.Component; | ||
import org.osgi.service.component.annotations.Deactivate; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.sun.jna.Platform; | ||
import com.sun.jna.platform.win32.Advapi32Util; | ||
|
||
/** | ||
* This is a {@link UsbSerialDiscovery} implementation component for Windows. | ||
* It parses the Windows registry for USB device entries. | ||
* | ||
* @author Andrew Fiddian-Green - Initial contribution | ||
*/ | ||
@NonNullByDefault | ||
@Component(service = UsbSerialDiscovery.class, name = WindowsUsbSerialDiscovery.SERVICE_NAME) | ||
public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery { | ||
|
||
protected static final String SERVICE_NAME = "usb-serial-discovery-windows"; | ||
|
||
// 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 final ScheduledExecutorService scheduler; | ||
|
||
private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>(); | ||
private @Nullable ScheduledFuture<?> scanTask; | ||
|
||
@Activate | ||
public WindowsUsbSerialDiscovery() { | ||
scheduler = Executors.newSingleThreadScheduledExecutor( | ||
ThreadFactoryBuilder.create().withName(SERVICE_NAME).withDaemonThreads(true).build()); | ||
} | ||
|
||
private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) { | ||
for (UsbSerialDiscoveryListener listener : discoveryListeners) { | ||
listener.usbSerialDeviceDiscovered(deviceInfo); | ||
} | ||
} | ||
|
||
private void announceRemovedDevice(UsbSerialDeviceInformation deviceInfo) { | ||
for (UsbSerialDiscoveryListener listener : discoveryListeners) { | ||
listener.usbSerialDeviceRemoved(deviceInfo); | ||
} | ||
} | ||
|
||
@Deactivate | ||
public void deactivate() { | ||
stopBackgroundScanning(); | ||
lastScanResult.clear(); | ||
} | ||
|
||
@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); | ||
|
||
lastScanResult = scanResult; | ||
|
||
removed.forEach(this::announceRemovedDevice); | ||
added.forEach(this::announceAddedDevice); | ||
unchanged.forEach(this::announceAddedDevice); | ||
} | ||
|
||
private <T> Set<T> setDifference(Set<T> set1, Set<T> set2) { | ||
Set<T> result = new HashSet<>(set1); | ||
result.removeAll(set2); | ||
return result; | ||
} | ||
|
||
@Override | ||
public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) { | ||
discoveryListeners.add(listener); | ||
for (UsbSerialDeviceInformation deviceInfo : lastScanResult) { | ||
listener.usbSerialDeviceDiscovered(deviceInfo); | ||
} | ||
} | ||
|
||
@Override | ||
public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) { | ||
discoveryListeners.remove(listener); | ||
} | ||
|
||
/** | ||
* Traverse the USB tree in Windows registry and return a set of USB device information. | ||
* | ||
* @return a set of USB device information. | ||
*/ | ||
public Set<UsbSerialDeviceInformation> scanAllUsbDevicesInformation() { | ||
if (!Platform.isWindows()) { | ||
return Set.of(); | ||
} | ||
|
||
Set<UsbSerialDeviceInformation> result = new HashSet<>(); | ||
String[] deviceKeys = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, USB_REGISTRY_ROOT); | ||
|
||
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; | ||
try { | ||
vendorId = Integer.decode(PREFIX_HEX + ids[0].substring(4)); | ||
productId = Integer.decode(PREFIX_HEX + ids[1].substring(4)); | ||
} catch (NumberFormatException e) { | ||
continue; | ||
} | ||
|
||
String serialNumber = ids.length > 2 ? ids[2] : null; | ||
|
||
String devicePath = USB_REGISTRY_ROOT + BACKSLASH + deviceKey; | ||
String[] interfaceNames = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, devicePath); | ||
|
||
int interfaceId = 0; | ||
for (String interfaceName : interfaceNames) { | ||
logger.trace(" interfaceId:{}, interfaceName:{}", interfaceId, interfaceName); | ||
|
||
String interfacePath = devicePath + BACKSLASH + interfaceName; | ||
TreeMap<String, Object> values = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE, interfacePath); | ||
|
||
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 = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, interfacePath); | ||
|
||
for (String interfaceSubKey : interfaceSubKeys) { | ||
if (!KEY_DEVICE_PARAMETERS.equals(interfaceSubKey)) { | ||
continue; | ||
} | ||
String deviceParametersPath = interfacePath + BACKSLASH + interfaceSubKey; | ||
TreeMap<String, Object> deviceParameterValues = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE, | ||
deviceParametersPath); | ||
Object serialPortValue = deviceParameterValues.get(KEY_SERIAL_PORT); | ||
if (serialPortValue instanceof String serialPortString) { | ||
serialPort = serialPortString; | ||
} | ||
break; | ||
} | ||
|
||
UsbSerialDeviceInformation usbSerialDeviceInformation = new UsbSerialDeviceInformation(vendorId, | ||
productId, serialNumber, manufacturer, product, interfaceId, interfaceName, serialPort); | ||
|
||
logger.debug("Add {}", usbSerialDeviceInformation); | ||
result.add(usbSerialDeviceInformation); | ||
|
||
interfaceId++; | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
@Override | ||
public synchronized 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); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public synchronized void stopBackgroundScanning() { | ||
ScheduledFuture<?> scanTask = this.scanTask; | ||
if (scanTask != null) { | ||
scanTask.cancel(false); | ||
} | ||
this.scanTask = null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.