com.diozero.api.SerialDevice Maven / Gradle / Ivy
The newest version!
package com.diozero.api;
/*-
* #%L
* Organisation: diozero
* Project: diozero - Core
* Filename: SerialDevice.java
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
* Copyright (C) 2016 - 2024 diozero
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.tinylog.Logger;
import com.diozero.internal.spi.InternalSerialDeviceInterface;
import com.diozero.sbc.DeviceFactoryHelper;
/**
* Serial device. The SerialDevice represents serial devices connected via USB
* or via the serial RX/TX pins on the GPIO header.
*
* On the Raspberry Pi, to use the serial RX/TX pins on the GPIO header, the
* serial interface must be enabled and the login shell must be disabled. The
* device file name for the serial RX/TX pins is /dev/serial0. See
*
* Raspberry Pi UART configuration for additional detail.
*
*/
public class SerialDevice implements SerialConstants, SerialDeviceInterface {
/**
* Provides descriptive information for a connected serial device. The
* information can be provided by the manufacturer of the device or by the UART
* used by the device.
*
* Often the information can be used to identify specific devices connected to a
* serial port. If two identical devices are connected to serial ports, they
* cannot be differentiated using this information.
*
*
* The following fields are supported:
*
*
* - deviceName
* - generally a subset of deviceFile, e.g., ttyACM0
* - deviceFile
* - the file system name for a serial device, e.g., /dev/ttyACM0 /dev/ttyS0,
* /dev/ttyAMA0
* - description
* - human readable, and theoretically unique, text that identifies the device
* attached to a serial port, e.g., Pololu A-Star 32U4; can be generic, e.g.,
* Physical Port
* - manufacturer
* - human readable text that identifies the manufacturer of the device
* attached to a serial port, e.g., Pololu Corporation; can be null
* - driverName
* - the name of the device driver, e.g., usb:cdc_acm, bcm2835-aux-uart,
* uart-pl011
* - usbVendorId
* - a theoretically unique number identifying the vendor, e.g., 1ffb; can be
* null
* - usbProductId
* - theoretically unique number identifying the product, e.g., 2300; can be
* null
*
*/
public static class DeviceInfo {
private String deviceName;
private String deviceFile;
private String description;
private String manufacturer;
private String driverName;
private String usbVendorId;
private String usbProductId;
public DeviceInfo(String deviceName, String deviceFile, String description, String manufacturer,
String driverName, String usbVendorId, String usbProductId) {
this.deviceName = deviceName;
this.deviceFile = deviceFile;
this.description = description;
this.manufacturer = manufacturer;
this.driverName = driverName;
this.usbVendorId = usbVendorId;
this.usbProductId = usbProductId;
}
public String getDeviceName() {
return deviceName;
}
public String getDeviceFile() {
return deviceFile;
}
public String getDescription() {
return description;
}
public String getManufacturer() {
return manufacturer;
}
public String getDriverName() {
return driverName;
}
public String getUsbVendorId() {
return usbVendorId;
}
public String getUsbProductId() {
return usbProductId;
}
}
/**
* Attempt to discover the locally attached serial devices using Linux device
* tree.
*
* @return A list of locally attached devices
*/
public static List getLocalSerialDevices() {
/*-
* On Linux:
* > cd /sys/devices
* > find . -name \*tty\*
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty/ttyUSB0
* ./platform/soc/fe201000.serial/tty
* ./platform/soc/fe201000.serial/tty/ttyAMA0
* ./virtual/tty
* ./virtual/tty/tty58
* ...
*
* > ls -l /sys/devices/platform/soc/fe201000.serial/driver
* /sys/devices/platform/soc/fe201000.serial/driver -> ../../../../bus/amba/drivers/uart-pl011
*
* > find . -name product
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/product
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/product
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/product
* ./platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb2/product
*
* For USB connected devices, the main files are under /sys/devices/platform/scb//.../usbx
* Search for a directory that starts with "tty" and is more than 3 characters in length
* Standard is 7 characters I think - ttyYYYN where YYY is something like USB|ACM|AMA and N is the instance number
* E.g. usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0
* Then navigate up two levels, e.g. usb1/1-1/1-1.2
* Important files in this directory:
* product (e.g. "USB2.0-Serial")
* idVendor (e.g. "1a86")
* idProduct (e.g. "7523")
*
* Look up idVendor and idProduct in the USB id database /var/lib/usbutils/usb.ids
* vendorId "1a86" == "QinHeng Electronics"
* For this vendorId, productId "7523" == "CH340 serial converter"
*
* Also:
* /dev/ttyACM0 (Abstract Control Module), e.g.:
* Pololu_A-Star_32U4 : Pololu_Corporation : Pololu_Corporation_Pololu_A-Star_32U4
* USB_Roboclaw_2x15A : 03eb : 03eb_USB_Roboclaw_2x15A
* /dev/ttyS0 - 9-pin serial connector
*/
List serial_devices = new ArrayList<>();
Path device_root = Paths.get("/sys/devices/platform");
if (device_root.toFile().exists()) {
lookForSerialDevices(device_root, serial_devices);
}
return serial_devices;
}
private static void lookForSerialDevices(Path path, List serialDevices) {
try {
// Ignore hidden files, symbolic links and "virtual"
Files.list(path).filter(p -> p.toFile().isDirectory()).filter(p -> !p.toFile().isHidden())
.filter(p -> !Files.isSymbolicLink(p)).filter(p -> !p.getFileName().toString().equals("virtual"))
.forEach(p -> {
if (isSerialDevice(p.getFileName().toString())) {
try {
serialDevices.add(getDeviceInfo(p));
} catch (IOException e) {
Logger.error(e, "Error: {}", e);
}
} else {
// Continue searching in sub-directories
lookForSerialDevices(p, serialDevices);
}
});
} catch (IOException e) {
Logger.error(e, "Error looking for serial devices: {}", e.getMessage());
}
}
private static boolean isSerialDevice(String fileName) {
return fileName != null && fileName.length() > 3 && (fileName.startsWith("tty") || fileName.startsWith("rfc"));
}
private static DeviceInfo getDeviceInfo(Path p) throws IOException {
String device_name = p.getFileName().toString();
Logger.debug("Found device with path {}", p);
String description = null, driver_name = null, manufacturer = null, usb_vendor_id = null, usb_product_id = null;
// Look for a product description file
// For ttyUSBx devices
Path device_root = p.getParent().getParent();
Path product_file = device_root.resolve("product");
if (!product_file.toFile().exists()) {
// For ttyACMx devices
device_root = p.getParent().getParent().getParent();
product_file = device_root.resolve("product");
}
if (product_file.toFile().exists()) {
Logger.debug("Processing device {} in {}", device_name, device_root);
// I believe this means that this is a USB-connected device
description = Files.lines(product_file).findFirst().orElse(null);
// GEt the manufacturer
Path manufacturer_path = device_root.resolve("manufacturer");
if (manufacturer_path.toFile().exists()) {
manufacturer = Files.lines(manufacturer_path).findFirst().orElse(null);
}
// Get the driver name, e.g. "usb-serial:ch341-uart"
Path drivers_path = p.resolve("driver").resolve("module").resolve("drivers");
if (!drivers_path.toFile().exists()) {
drivers_path = p.resolve("device").resolve("driver").resolve("module").resolve("drivers");
}
if (drivers_path.toFile().exists()) {
driver_name = Files.list(drivers_path).filter(path -> path.toFile().isDirectory())
.filter(path -> !path.toFile().isHidden()).map(path -> path.getFileName().toString())
.findFirst().orElse(null);
}
// Get the USB vendor and product identifiers
usb_vendor_id = Files.lines(device_root.resolve("idVendor")).findFirst().orElse(null);
usb_product_id = Files.lines(device_root.resolve("idProduct")).findFirst().orElse(null);
// Sometimes the manufacturer isn't set - default it to the USB vendor id as per
// udevadm
// Example: Vendor=QinHeng Electronics, Product=HL-340 USB-Serial adapter
if (manufacturer == null) {
manufacturer = usb_vendor_id;
}
} else {
// Must be a physical (or emulated) port
description = "Physical Port";
Path driver_path = p.resolve("device").resolve("driver");
if (driver_path.toFile().exists()) {
driver_name = Paths.get(driver_path.toFile().getCanonicalPath()).getFileName().toString();
}
}
return new DeviceInfo(device_name, "/dev/" + device_name, description, manufacturer, driver_name, usb_vendor_id,
usb_product_id);
}
/**
* Serial device builder. Default values:
*
* - baud: {@link SerialConstants#DEFAULT_BAUD}
* - dataBits: {@link SerialConstants#DEFAULT_DATA_BITS}
* - stopBits: {@link SerialConstants#DEFAULT_STOP_BITS}
* - parity: {@link SerialConstants#DEFAULT_PARITY}
*
*
* The termios
* man page provide more detail on these options. Note that the serial
* device is opened in non-canonical mode so that "input is available
* immediately (without the user having to type a line-delimiter
* character)".
*/
public static class Builder {
private String deviceFilename;
private int baud = DEFAULT_BAUD;
private DataBits dataBits = DEFAULT_DATA_BITS;
private StopBits stopBits = DEFAULT_STOP_BITS;
private Parity parity = DEFAULT_PARITY;
private boolean readBlocking = DEFAULT_READ_BLOCKING;
private int minReadChars = DEFAULT_MIN_READ_CHARS;
private int readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS;
protected Builder(String deviceFilename) {
this.deviceFilename = deviceFilename;
}
public Builder setBaud(int baud) {
this.baud = baud;
return this;
}
public Builder setDataBits(DataBits dataBits) {
this.dataBits = dataBits;
return this;
}
public Builder setStopBits(StopBits stopBits) {
this.stopBits = stopBits;
return this;
}
public Builder setParity(Parity parity) {
this.parity = parity;
return this;
}
/*-
public Builder setReadBlocking(boolean readBlocking) {
this.readBlocking = readBlocking;
return this;
}
public Builder setMinReadChars(int minReadChars) {
this.minReadChars = minReadChars;
return this;
}
public Builder setReadTimeoutMillis(int readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
return this;
}
*/
public SerialDevice build() {
return new SerialDevice(deviceFilename, baud, dataBits, stopBits, parity, readBlocking, minReadChars,
readTimeoutMillis);
}
}
public static Builder builder(String deviceFilename) {
return new Builder(deviceFilename);
}
private InternalSerialDeviceInterface delegate;
private String deviceFilename;
/**
* Create a new serial device using default values for
* {@link SerialConstants#DEFAULT_BAUD baud},
* {@link SerialConstants#DEFAULT_DATA_BITS data bits},
* {@link SerialConstants#DEFAULT_STOP_BITS stop bits},
* {@link SerialConstants#DEFAULT_PARITY parity},
* {@link SerialConstants#DEFAULT_READ_BLOCKING read blocking},
* {@link SerialConstants#DEFAULT_MIN_READ_CHARS min read chars} and
* {@link SerialConstants#DEFAULT_READ_TIMEOUT_MILLIS read timeout}
*
* @param deviceFilename The O/S file name for the device, e.g. /dev/ttyACM0
* @throws RuntimeIOException If an I/O error occurs
*/
public SerialDevice(String deviceFilename) throws RuntimeIOException {
this(deviceFilename, DEFAULT_BAUD, DEFAULT_DATA_BITS, DEFAULT_STOP_BITS, DEFAULT_PARITY, DEFAULT_READ_BLOCKING,
DEFAULT_MIN_READ_CHARS, DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* Create a new serial device using default values for
* {@link SerialConstants#DEFAULT_READ_BLOCKING read blocking},
* {@link SerialConstants#DEFAULT_MIN_READ_CHARS min read chars} and
* {@link SerialConstants#DEFAULT_READ_TIMEOUT_MILLIS read timeout}
*
* @param deviceFilename The O/S file name for the device, e.g. /dev/ttyACM0
* @param baud Baud rate, see {@link SerialConstants SerialConstants}
* for valid baud rate values
* @param dataBits Number of {@link SerialConstants.DataBits data bits}
* @param stopBits Number of {@link SerialConstants.StopBits stop bits}
* @param parity Device error detection {@link SerialConstants.Parity
* parity}
* @throws RuntimeIOException If an I/O error occurs
*/
public SerialDevice(String deviceFilename, int baud, DataBits dataBits, StopBits stopBits, Parity parity)
throws RuntimeIOException {
this(deviceFilename, baud, dataBits, stopBits, parity, DEFAULT_READ_BLOCKING, DEFAULT_MIN_READ_CHARS,
DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* Create a new serial device. Package private - note that readBlocking,
* minReadChars and readTimeoutMillis are fixed as per the default values.
*
* @param deviceFilename The O/S file name for the device, e.g. /dev/ttyACM0
* @param baud Baud rate, see {@link SerialConstants
* SerialConstants} for valid baud rate values
* @param dataBits Number of {@link SerialConstants.DataBits data bits}
* @param stopBits Number of {@link SerialConstants.StopBits stop bits}
* @param parity Device error detection {@link SerialConstants.Parity
* parity}
* @param readBlocking Should all read operations block until data is
* available?
* @param minReadChars Minimum number of characters to read (note actually
* an unsigned char hence max value is 255)
* @param readTimeoutMillis The read timeout value in milliseconds (note
* converted to tenths of a second as an unsigned char)
* @throws RuntimeIOException If an I/O error occurs
*/
SerialDevice(String deviceFilename, int baud, DataBits dataBits, StopBits stopBits, Parity parity,
boolean readBlocking, int minReadChars, int readTimeoutMillis) throws RuntimeIOException {
delegate = DeviceFactoryHelper.getNativeDeviceFactory().provisionSerialDevice(deviceFilename, baud, dataBits,
stopBits, parity, readBlocking, minReadChars, readTimeoutMillis);
this.deviceFilename = deviceFilename;
}
/**
* Get the device filename
*
* @return the device filename, e.g. /dev/ttyUSB0
*/
public String getDeviceFilename() {
return deviceFilename;
}
/**
* {@inheritDoc}
*/
@Override
public void close() {
Logger.trace("close()");
if (delegate.isOpen()) {
delegate.close();
}
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws RuntimeIOException {
return delegate.read();
}
/**
* {@inheritDoc}
*/
@Override
public byte readByte() throws RuntimeIOException {
return delegate.readByte();
}
/**
* {@inheritDoc}
*/
@Override
public void writeByte(byte bVal) throws RuntimeIOException {
delegate.writeByte(bVal);
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] buffer) throws RuntimeIOException {
return delegate.read(buffer);
}
/**
* {@inheritDoc}
*/
@Override
public void write(byte... buffer) throws RuntimeIOException {
delegate.write(buffer);
}
/**
* {@inheritDoc}
*/
@Override
public int bytesAvailable() throws RuntimeIOException {
return delegate.bytesAvailable();
}
}
/*-
* Parked text
* readBlocking: {@link SerialConstants#DEFAULT_READ_BLOCKING}
* minReadChars: {@link SerialConstants#DEFAULT_MIN_READ_CHARS}
* readTimeoutMillis: {@link SerialConstants#DEFAULT_READ_TIMEOUT_MILLIS}
*
* Translating the information on the termios man page to diozero:
*
* The settings of minReadChars and readTimeoutMillis determine the circumstances in which a read(2) completes;
* there are four distinct cases:
*
* - 1) minReadChars == 0, readTimeoutMillis == 0 (polling read)
* - If data is available, read(2) returns immediately, with the lesser of the number of bytes available,
* or the number of bytes requested. If no data is available, read(2) returns 0.
*
* - 2) minReadChars > 0, readTimeoutMillis == 0 (blocking read)
* - read(2) blocks until minReadChars bytes are available, and returns up to the number of bytes requested.
*
* - 3) minReadChars == 0, readTimeoutMillis > 0 (read with timeout)
* - TIME specifies the limit for a timer in millis (converted to tenths of a second).
* The timer is started when read(2) is called. read(2) returns either when at least one byte of data is
* available, or when the timer expires. If the timer expires without any input becoming available, read(2)
* returns 0. If data is already available at the time of the call to read(2), the call behaves as though
* the data was received immediately after the call.
*
* - 4) minReadChars > 0, readTimeoutMillis > 0 (read with interbyte timeout)
* - readTimeoutMillis specifies the limit for a timer in tenths of a second. Once an initial byte of input
* becomes available, the timer is restarted after each further byte is received.
* read(2) returns when any of the following conditions are met:
*
* - minReadChars bytes have been received.
* - The interbyte timer expires.
* - The number of bytes requested by read(2) has been received. (POSIX does not specify this termination
* condition, and on some other implementations read(2) does not return in this case.)
*
*
*
*
* Because the timer is started only after the initial byte becomes available, at least one byte will be read.
* If data is already available at the time of the call to read(2), the call behaves as though the data was
* received immediately after the call.
*/