Skip to content

Commit

Permalink
UsbSerialDiscovery service based on Windows registry (#3934)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Fiddian-Green <[email protected]>
  • Loading branch information
andrewfg authored Jan 11, 2024
1 parent 6b2182d commit 9cb4b9e
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ public synchronized void doSingleScan() {

lastScanResult = scanResult;

removed.stream().forEach(this::announceRemovedDevice);
added.stream().forEach(this::announceAddedDevice);
unchanged.stream().forEach(this::announceAddedDevice);
removed.forEach(this::announceRemovedDevice);
added.forEach(this::announceAddedDevice);
unchanged.forEach(this::announceAddedDevice);

logger.debug("Completed ser2net USB-Serial mDNS single discovery scan");
}
Expand Down
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>
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>
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

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>
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;
}
}
1 change: 1 addition & 0 deletions bundles/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<module>org.openhab.core.config.discovery.usbserial</module>
<module>org.openhab.core.config.discovery.usbserial.linuxsysfs</module>
<module>org.openhab.core.config.discovery.usbserial.ser2net</module>
<module>org.openhab.core.config.discovery.usbserial.windowsregistry</module>
<module>org.openhab.core.config.discovery.upnp</module>
<module>org.openhab.core.config.dispatch</module>
<module>org.openhab.core.config.serial</module>
Expand Down
Loading

0 comments on commit 9cb4b9e

Please sign in to comment.