Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UsbSerialDiscovery service based on Javax-Usb #3930

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
14e3349
Supress log output on shutdown
andrewfg Dec 10, 2023
0790943
Revert "Supress log output on shutdown"
andrewfg Dec 10, 2023
68569d3
Merge branch 'openhab:main' into main
andrewfg Dec 12, 2023
69bda66
Merge branch 'main' of https://github.com/andrewfg/openhab-core
andrewfg Dec 18, 2023
2f41474
Merge branch 'openhab:main' into main
andrewfg Dec 21, 2023
4e1f44b
Merge branch 'main' of https://github.com/andrewfg/openhab-core
andrewfg Dec 22, 2023
b073ad6
Split from other PR
andrewfg Dec 18, 2023
e513770
Move product information to external resource
andrewfg Dec 18, 2023
0c0ec4e
Spotless
andrewfg Dec 18, 2023
c466b17
Fix compiler null type warning
andrewfg Dec 18, 2023
d2e5890
Update information provider
andrewfg Dec 18, 2023
1a74415
Typo
andrewfg Dec 18, 2023
5f5f580
JUnit test and refactoring
andrewfg Dec 19, 2023
2f7f05b
Fix JavaDoc
andrewfg Dec 19, 2023
f0e7dd2
Apply spotless after release, resolve bundles (#3953)
holgerfriedrich Dec 22, 2023
da6043b
Merge branch 'openhab:main' into discovery-javax-usb
andrewfg Dec 22, 2023
c821522
Update version
andrewfg Dec 22, 2023
c5de645
Remove local info provider; Add interface infos
andrewfg Dec 25, 2023
6b4aeb0
Suppress on Linux and Windows
andrewfg Dec 27, 2023
afe6af4
Remove test unit; Fix copyright date
andrewfg Jan 7, 2024
02c6243
Merge branch 'openhab:main' into discovery-javax-usb
andrewfg Jan 10, 2024
38d5df4
Install bundles depending on OS
andrewfg Jan 11, 2024
3ab3915
fix merge conflict
andrewfg Jan 21, 2024
a4dc730
Merge branch 'main' into discovery-javax-usb
andrewfg Jan 21, 2024
9eaecac
fix merge conflict (again)
andrewfg Jan 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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 excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" 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,68 @@
<?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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<version>4.2.0-SNAPSHOT</version>
<version>4.3.0-SNAPSHOT</version>

</parent>

<artifactId>org.openhab.core.config.discovery.usbserial.javaxusb</artifactId>

<name>openHAB Core :: Bundles :: Configuration USB-Serial Discovery via 'javax.usb'</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>
<dependency>
<groupId>javax.usb</groupId>
<artifactId>usb-api</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.usb4java</groupId>
<artifactId>usb4java-javax</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>embed-dependencies</id>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<includeTypes>jar</includeTypes>
<includeGroupIds>org.usb4java,javax.usb</includeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<excludeTransitive>false</excludeTransitive>
<type>jar</type>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can exclude the Linux and Windows libraries by adding this:

Suggested change
<type>jar</type>
<excludes>**/linux-*,**/linux-*/*.so,**/win32-*,**/win32-*/*.dll</excludes>
<type>jar</type>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed it only contains darwin-x86-64 libraries so it will not work on Apple silicon based Macs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will not work on Apple silicon based Macs

Nor on Arm based ones .. although TBH I am not sure if any such exist(ed)..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. and I even wonder if MacOS does not (still) have an inner core based on Linux .. I know that it used to

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's based on Unix because Linux didn't even exist yet back then. 🦖

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To summarise: this seems to be a Mac specific issue; neither you nor I have a Mac to test on; so we can't really judge if this PR adds value, or if the existing Discovery component added value prior to this. So the question is what shall we do with this one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wait for the feedback of a maintainer using macOS. IIRC there is also x86 emulation on the Macs using Apple silicon. I'm not familiar with how the emulation works. It could be that it only works well when all code runs on the x86 emulator and not just a single library.

</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* 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.javaxusb.internal;

import java.io.UnsupportedEncodingException;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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 javax.usb.UsbConfiguration;
import javax.usb.UsbDevice;
import javax.usb.UsbDeviceDescriptor;
import javax.usb.UsbDisconnectedException;
import javax.usb.UsbException;
import javax.usb.UsbHostManager;
import javax.usb.UsbHub;
import javax.usb.UsbInterface;

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;

/**
* This is a {@link UsbSerialDiscovery} implementation component that scans the system for USB devices by means of the
* {@link org.usb4java} library implementation of the {@link javax.usb} interface.
* <p>
* It provides USB coverage on non Linux Operating Systems. Linux is already better covered by the scanners in the
* {@link org.openhab.core.config.discovery.usbserial.linuxsysfs} module.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(service = UsbSerialDiscovery.class, name = JavaxUsbSerialDiscovery.SERVICE_NAME)
public class JavaxUsbSerialDiscovery implements UsbSerialDiscovery {

protected static final String SERVICE_NAME = "usb-serial-discovery-javaxusb";

private final Logger logger = LoggerFactory.getLogger(JavaxUsbSerialDiscovery.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 JavaxUsbSerialDiscovery() {
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.stream().forEach(this::announceRemovedDevice);
added.stream().forEach(this::announceAddedDevice);
unchanged.stream().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 for devices that are children of the ROOT hub, and return a set of USB device information.
*
* @return a set of USB device information.
*/
private Set<UsbSerialDeviceInformation> scanAllUsbDevicesInformation() {
try {
return scanChildUsbDeviceInformation(UsbHostManager.getUsbServices().getRootUsbHub());
} catch (SecurityException | UsbException e) {
logger.warn("Error getting USB device information: {}", e.getMessage());
return Set.of();
}
}

/**
* Traverse the USB tree for devices that are children of the given hub, and return a set of USB device information.
*
* @param usbHub the hub whose children are to be found.
* @return a set of USB device information.
*/
private Set<UsbSerialDeviceInformation> scanChildUsbDeviceInformation(UsbHub usbHub) {
Set<UsbSerialDeviceInformation> result = new HashSet<>();

@SuppressWarnings("unchecked")
List<UsbDevice> deviceList = usbHub.getAttachedUsbDevices();

deviceList.forEach(usbDevice -> {
if (usbDevice.isUsbHub()) {
result.addAll(scanChildUsbDeviceInformation((UsbHub) usbDevice));
} else {
UsbDeviceDescriptor d = usbDevice.getUsbDeviceDescriptor();
short vendorId = d.idVendor();
short productId = d.idProduct();

String manufacturer = null;
String product = null;
String serialNumber = null;

/*
* Note: the getString() calls below may fail depending on the Operating System:
* - on Windows if no libusb device driver is installed for the device.
* - on Linux if the user has no write permission on the USB device file.
*/
try {
manufacturer = usbDevice.getString(d.iManufacturer());
product = usbDevice.getString(d.iProduct());
serialNumber = usbDevice.getString(d.iSerialNumber());
} catch (UnsupportedEncodingException | UsbDisconnectedException | UsbException e) {
// ignore because this would be a 'normal' runtime failure
}

String serialPort = "";
int interfaceNumber = 0;
String interfaceDescription = null;

UsbConfiguration configuration = usbDevice.getActiveUsbConfiguration();
if (configuration == null) {
UsbSerialDeviceInformation usbDeviceInfo = new UsbSerialDeviceInformation(vendorId, productId,
serialNumber, manufacturer, product, interfaceNumber, interfaceDescription, serialPort);

result.add(usbDeviceInfo);
logger.trace("Added device: {}", usbDeviceInfo);
} else {
@SuppressWarnings("unchecked")
List<UsbInterface> interfaces = configuration.getUsbInterfaces();
for (UsbInterface ifx : interfaces) {
try {
interfaceDescription = ifx.getInterfaceString();
} catch (UnsupportedEncodingException | UsbDisconnectedException | UsbException e) {
interfaceDescription = null;
}

UsbSerialDeviceInformation usbDeviceInfo = new UsbSerialDeviceInformation(vendorId, productId,
serialNumber, manufacturer, product, interfaceNumber, interfaceDescription, serialPort);

result.add(usbDeviceInfo);
logger.trace("Added device: {}", usbDeviceInfo);
interfaceNumber++;
}
}
}
});

return result;
}

@Override
public synchronized void startBackgroundScanning() {
if (Platform.isWindows() || Platform.isLinux()) {
return;
}
ScheduledFuture<?> scanTask = this.scanTask;
if (scanTask == null || scanTask.isDone()) {
this.scanTask = scheduler.scheduleWithFixedDelay(() -> doSingleScan(), 0, scanInterval.toSeconds(),
TimeUnit.SECONDS);
}
}

@Override
public synchronized void stopBackgroundScanning() {
ScheduledFuture<?> scanTask = this.scanTask;
if (scanTask != null) {
scanTask.cancel(false);
}
this.scanTask = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
javax.usb.services = org.usb4java.javax.Services
1 change: 1 addition & 0 deletions bundles/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<module>org.openhab.core.config.discovery.addon.usb</module>
<module>org.openhab.core.config.discovery.mdns</module>
<module>org.openhab.core.config.discovery.usbserial</module>
<module>org.openhab.core.config.discovery.usbserial.javaxusb</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.upnp</module>
Expand Down
Loading