Skip to content

Commit

Permalink
Add ser2net mDNS USB serial discovery
Browse files Browse the repository at this point in the history
* Add support for using multiple UsbSerialDiscovery services
* Add Ser2NetUsbSerialDiscovery that can use mDNS to discover ser2net RFC2217 serial ports
* Use discovered USB ports in SerialConfigOptionProvider

mDNS discovery is supported in ser2net 4.3.0 and newer.
E.g. you can install a ser2net version that provides this using APT in Ubuntu 21.04 and Debian 11.

Example ser2net YAML configuration that allows a serial port to be discovered using mDNS discovery:

%YAML 1.1
---
connection: &con01
  accepter: telnet(rfc2217),tcp,2222
  connector: serialdev,/dev/ttyUSB0
  options:
    mdns: true
    mdns-sysattrs: true
    mdns-name: devicename

Closes openhab#1511

Signed-off-by: Wouter Born <[email protected]>
  • Loading branch information
wborn committed Oct 13, 2021
1 parent cffdcd0 commit 8a27f87
Show file tree
Hide file tree
Showing 14 changed files with 835 additions and 45 deletions.
6 changes: 6 additions & 0 deletions bom/openhab-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial.ser2net</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.upnp</artifactId>
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-11">
<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.ser2net</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 bundles/org.openhab.core.config.discovery.usbserial.ser2net/NOTICE
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 http://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>3.2.0-SNAPSHOT</version>
</parent>

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

<name>openHAB Core :: Bundles :: Configuration USB-Serial Discovery using ser2net mDNS scanning</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.mdns</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2021 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.ser2net.internal;

import java.time.Duration;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;

import org.eclipse.jdt.annotation.NonNullByDefault;
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.io.transport.mdns.MDNSClient;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@link UsbSerialDiscovery} that implements background discovery of RFC2217 by listening to
* ser2net mDNS service events.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
@Component(service = UsbSerialDiscovery.class)
public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDiscovery {

private final Logger logger = LoggerFactory.getLogger(Ser2NetUsbSerialDiscovery.class);

static final String SERVICE_TYPE = "_iostream._tcp.local.";

static final String PROPERTY_PROVIDER = "provider";
static final String PROPERTY_DEVICE_TYPE = "devicetype";
static final String PROPERTY_GENSIO_STACK = "gensiostack";

static final String PROPERTY_VENDOR_ID = "idVendor";
static final String PROPERTY_PRODUCT_ID = "idProduct";

static final String PROPERTY_SERIAL_NUMBER = "serial";
static final String PROPERTY_MANUFACTURER = "manufacturer";
static final String PROPERTY_PRODUCT = "product";

static final String PROPERTY_INTERFACE_NUMBER = "bInterfaceNumber";
static final String PROPERTY_INTERFACE = "interface";

static final String SER2NET = "ser2net";
static final String SERIALUSB = "serialusb";
static final String TELNET_RFC2217_TCP = "telnet(rfc2217),tcp";

static final Duration SINGLE_SCAN_DURATION = Duration.ofSeconds(5);
static final String SERIAL_PORT_NAME_FORMAT = "rfc2217://%s:%s";

private final Set<UsbSerialDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
private final MDNSClient mdnsClient;

private boolean backgroundDiscoveryEnabled = false;

private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>();

@Activate
public Ser2NetUsbSerialDiscovery(final @Reference MDNSClient mdnsClient) {
this.mdnsClient = mdnsClient;
}

@Override
public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) {
discoveryListeners.add(listener);
}

@Override
public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) {
discoveryListeners.remove(listener);
}

@Override
public synchronized void startBackgroundScanning() {
backgroundDiscoveryEnabled = true;
mdnsClient.addServiceListener(SERVICE_TYPE, this);
logger.debug("Started ser2net USB-Serial mDNS background discovery");
}

@Override
public synchronized void stopBackgroundScanning() {
backgroundDiscoveryEnabled = false;
mdnsClient.removeServiceListener(SERVICE_TYPE, this);
logger.debug("Stopped ser2net USB-Serial mDNS background discovery");
}

@Override
public synchronized void doSingleScan() {
logger.debug("Starting ser2net USB-Serial mDNS single discovery scan");

Set<UsbSerialDeviceInformation> scanResult = Stream.of(mdnsClient.list(SERVICE_TYPE, SINGLE_SCAN_DURATION))
.map(this::createUsbSerialDeviceInformation) //
.filter(Optional::isPresent) //
.map(Optional::get) //
.collect(Collectors.toSet());

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);

logger.debug("Completed ser2net USB-Serial mDNS single discovery scan");
}

private <T> Set<T> setDifference(Set<T> set1, Set<T> set2) {
Set<T> result = new HashSet<>(set1);
result.removeAll(set2);
return Set.copyOf(result);
}

private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) {
for (UsbSerialDiscoveryListener listener : discoveryListeners) {
listener.usbSerialDeviceDiscovered(deviceInfo);
}
}

private void announceRemovedDevice(UsbSerialDeviceInformation deviceInfo) {
for (UsbSerialDiscoveryListener listener : discoveryListeners) {
listener.usbSerialDeviceRemoved(deviceInfo);
}
}

@Override
public void serviceAdded(@NonNullByDefault({}) ServiceEvent event) {
if (backgroundDiscoveryEnabled) {
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
deviceInfo.ifPresent(this::announceAddedDevice);
}
}

@Override
public void serviceRemoved(@NonNullByDefault({}) ServiceEvent event) {
if (backgroundDiscoveryEnabled) {
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
deviceInfo.ifPresent(this::announceRemovedDevice);
}
}

@Override
public void serviceResolved(@NonNullByDefault({}) ServiceEvent event) {
serviceAdded(event);
}

private Optional<UsbSerialDeviceInformation> createUsbSerialDeviceInformation(ServiceInfo serviceInfo) {
String provider = serviceInfo.getPropertyString(PROPERTY_PROVIDER);
String deviceType = serviceInfo.getPropertyString(PROPERTY_DEVICE_TYPE);
String gensioStack = serviceInfo.getPropertyString(PROPERTY_GENSIO_STACK);

// Check ser2net specific properties when present
if (SER2NET.equals(provider) && (deviceType != null && !SERIALUSB.equals(deviceType))
|| (gensioStack != null && !TELNET_RFC2217_TCP.equals(gensioStack))) {
logger.debug("Skipping creation of UsbSerialDeviceInformation based on {}", serviceInfo);
return Optional.empty();
}

try {
int vendorId = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_VENDOR_ID), 16);
int productId = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_PRODUCT_ID), 16);

String serialNumber = serviceInfo.getPropertyString(PROPERTY_SERIAL_NUMBER);
String manufacturer = serviceInfo.getPropertyString(PROPERTY_MANUFACTURER);
String product = serviceInfo.getPropertyString(PROPERTY_PRODUCT);

int interfaceNumber = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_INTERFACE_NUMBER), 16);
String interfaceDescription = serviceInfo.getPropertyString(PROPERTY_INTERFACE);

String serialPortName = String.format(SERIAL_PORT_NAME_FORMAT, serviceInfo.getHostAddresses()[0],
serviceInfo.getPort());

UsbSerialDeviceInformation deviceInfo = new UsbSerialDeviceInformation(vendorId, productId, serialNumber,
manufacturer, product, interfaceNumber, interfaceDescription, serialPortName);
logger.debug("Created {} based on {}", deviceInfo, serviceInfo);
return Optional.of(deviceInfo);
} catch (NumberFormatException e) {
logger.debug("Failed to create UsbSerialDeviceInformation based on {}", serviceInfo, e);
return Optional.empty();
}
}
}
Loading

0 comments on commit 8a27f87

Please sign in to comment.