diff --git a/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java b/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java index d898e4e1f7f..da6e87eb38f 100644 --- a/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java +++ b/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java @@ -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"); } diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath new file mode 100644 index 00000000000..58cd399d639 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project new file mode 100644 index 00000000000..ad2f4ec236a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.usbserial.javaxusb + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE new file mode 100644 index 00000000000..6c17d0d8a45 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE @@ -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 + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml new file mode 100644 index 00000000000..59a092e7d41 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.core.config.discovery.usbserial.windowsregistry + + openHAB Core :: Bundles :: Configuration USB-Serial Discovery for Windows + + + + org.openhab.core.bundles + org.openhab.core.config.discovery.usbserial + ${project.version} + + + net.java.dev.jna + jna-platform + 5.13.0 + + + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java new file mode 100644 index 00000000000..5c19cd089cc --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java @@ -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 discoveryListeners = new CopyOnWriteArraySet<>(); + private final Duration scanInterval = Duration.ofSeconds(15); + private final ScheduledExecutorService scheduler; + + private Set 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 scanResult = scanAllUsbDevicesInformation(); + Set added = setDifference(scanResult, lastScanResult); + Set removed = setDifference(lastScanResult, scanResult); + Set unchanged = setDifference(scanResult, added); + + lastScanResult = scanResult; + + removed.forEach(this::announceRemovedDevice); + added.forEach(this::announceAddedDevice); + unchanged.forEach(this::announceAddedDevice); + } + + private Set setDifference(Set set1, Set set2) { + Set 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 scanAllUsbDevicesInformation() { + if (!Platform.isWindows()) { + return Set.of(); + } + + Set 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 values = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE, interfacePath); + + if (logger.isTraceEnabled()) { + for (Entry 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 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; + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 5035de7e1a7..8f6fd77f721 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -40,6 +40,7 @@ org.openhab.core.config.discovery.usbserial org.openhab.core.config.discovery.usbserial.linuxsysfs org.openhab.core.config.discovery.usbserial.ser2net + org.openhab.core.config.discovery.usbserial.windowsregistry org.openhab.core.config.discovery.upnp org.openhab.core.config.dispatch org.openhab.core.config.serial diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 6f25f1e80c2..94d766da68a 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -519,8 +519,19 @@ mvn:org.openhab.core.bundles/org.openhab.core.config.serial/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial/${project.version} - mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/${project.version} + + + req:osgi.native;filter:="(osgi.native.osname=Linux)" + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/${project.version} + + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.ser2net/${project.version} + + + req:osgi.native;filter:="(osgi.native.osname=Windows*)" + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/${project.version} + + mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial.rxtx/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial.rxtx.rfc2217/${project.version}