Using Nt Serial Port
Finding, configuring and opening a serial port, reading and writing it, handling errors, and getting permission to use it on Linux.
Link Nt::SerialPort (or Nt6::SerialPort from an installed package) and include <QSerialPort> and <QSerialPortInfo>:
find_package(Nt6 6 REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Nt6::SerialPort)
Finding a Port
QSerialPortInfo::availablePorts() lists the serial ports of the machine, with a description, the manufacturer, the serial number and, for USB devices, the vendor and product identifiers:
const QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : ports) {
QString line = info.portName() + "\t" + info.systemLocation() + "\t" + info.description();
if (info.hasVendorIdentifier() && info.hasProductIdentifier())
line += "\t" + QString::number(info.vendorIdentifier(), 16).rightJustified(4, QChar(u'0')) + ":" +
QString::number(info.productIdentifier(), 16).rightJustified(4, QChar(u'0'));
std::printf("%s\n", qPrintable(line));
}
A QSerialPort can be made from a QSerialPortInfo, from a name such as ttyUSB0, or from a full path such as /dev/ttyS0.
Settings
Choose the baud rate, data bits, parity, stop bits and flow control. Both ends of the line must agree. A new port uses 9600 baud, 8 data bits, no parity, one stop bit and no flow control.
QSerialPort port(positional.first());
bool ok = false;
const qint32 baud = parser.value("baud").toInt(&ok);
if (!ok || baud <= 0) {
std::fprintf(stderr, "serialterm: --baud must be a positive number\n");
return 2;
}
QSerialPort::DataBits dataBits{};
QSerialPort::Parity parity{};
QSerialPort::StopBits stopBits{};
QSerialPort::FlowControl flow{};
if (!choose<QSerialPort::DataBits>("data-bits", parser.value("data-bits"),
{{"5", QSerialPort::Data5},
{"6", QSerialPort::Data6},
{"7", QSerialPort::Data7},
{"8", QSerialPort::Data8}},
&dataBits) ||
!choose<QSerialPort::Parity>("parity", parser.value("parity"),
{{"none", QSerialPort::NoParity},
{"even", QSerialPort::EvenParity},
{"odd", QSerialPort::OddParity},
{"space", QSerialPort::SpaceParity},
{"mark", QSerialPort::MarkParity}},
&parity) ||
!choose<QSerialPort::StopBits>("stop-bits", parser.value("stop-bits"),
{{"1", QSerialPort::OneStop}, {"2", QSerialPort::TwoStop}}, &stopBits) ||
!choose<QSerialPort::FlowControl>("flow", parser.value("flow"),
{{"none", QSerialPort::NoFlowControl},
{"hardware", QSerialPort::HardwareControl},
{"software", QSerialPort::SoftwareControl}},
&flow))
return 2;
port.setBaudRate(baud);
port.setDataBits(dataBits);
port.setParity(parity);
port.setStopBits(stopBits);
port.setFlowControl(flow);
Settings made before open() take effect when the port opens, and settings made later take effect at once. If the driver refuses a value while the port is open, the setter returns false and sets error(); while the port is closed, open() fails instead. A baud rate that is not one of QSerialPortInfo::standardBaudRates() is passed to the driver as it is, which most USB adapters accept. The device is set back to what it was before when the port is closed.
Opening the Port
open() fails if the device cannot be opened. errorString() says why, and error() gives the reason as a code:
if (!port.open(QIODevice::ReadWrite)) {
std::fprintf(stderr, "serialterm: cannot open %s: %s\n", qPrintable(port.portName()),
qPrintable(port.errorString()));
return 1;
}
Error | Usual cause |
|---|---|
| The device does not exist, for example the adapter is not plugged in. |
| The user may not open the device (see Permission to Open a Port), or another process already has it open. |
| The port is already open in this object, or the file is not a terminal device. |
A port is opened exclusively, so two programs cannot read the same device by accident.
Reading and Writing
A QSerialPort is a QIODevice. Reads and writes never block: write() queues the data and readyRead() is emitted when data has arrived, so a program with an event loop needs only to connect to it:
QObject::connect(&port, &QIODevice::readyRead, [&port] {
const QByteArray data = port.readAll();
std::fwrite(data.constData(), 1, static_cast<std::size_t>(data.size()), stdout);
std::fflush(stdout);
});
QSocketNotifier input(STDIN_FILENO, QSocketNotifier::Read);
QObject::connect(&input, &QSocketNotifier::activated, [&] {
char buffer[512];
const ssize_t n = ::read(STDIN_FILENO, buffer, sizeof buffer);
if (n > 0) {
port.write(buffer, n);
return;
}
input.setEnabled(false);
if (!quitTimer.isActive())
quitTimer.start(500);
});
A program without an event loop can wait instead, with waitForReadyRead() and waitForBytesWritten(). Both run the port's own I/O while they wait. Use canReadLine() and readLine() for protocols made of lines, and setReadBufferSize() to cap the memory used by data nobody has read yet.
flush() writes as much of the queued data as the device will take without blocking, and clear() discards what is buffered in one or both directions. The control lines are available through setDataTerminalReady(), setRequestToSend(), pinoutSignals() and setBreakEnabled().
Handling Errors
Errors are reported by errorOccurred(). It is emitted for every error, including the ones that make a function return false.
QObject::connect(&port, &QSerialPort::errorOccurred, [&](QSerialPort::SerialPortError error) {
std::fprintf(stderr, "serialterm: %s\n", qPrintable(port.errorString()));
// A device that disappears, for example an unplugged USB adapter, cannot recover.
if (error == QSerialPort::ResourceError) {
exitCode = 1;
QCoreApplication::quit();
}
});
ResourceError needs special care. It means the device went away, for example because a USB adapter was unplugged. The port stays open but idle; data that arrived earlier can still be read. Call close(), and call open() again when the device is back.
A wait that runs out of time sets TimeoutError, which is not a failure of the port: it can be tried again.
Permission to Open a Port
On Linux the serial devices belong to a group, so a user must be a member of it. The group is dialout on Debian, Ubuntu, Fedora and Enterprise Linux, and uucp on Arch Linux. Add the user, then log in again:
sudo usermod -aG dialout $USER
To give access to one device only, or to set a different group, add a udev rule such as the following to /etc/udev/rules.d/99-serial.rules:
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="dialout", MODE="0660", SYMLINK+="myadapter"
If open() reports PermissionError although the permissions are right, another process has the port. On a desktop this is often ModemManager, which probes new USB serial adapters; stop it or add a udev rule that sets ENV{ID_MM_DEVICE_IGNORE}="1".
Testing Without Hardware
A pair of connected pseudo-terminals stands in for a device. With socat:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
socat prints the names of the two ends, such as /dev/pts/3 and /dev/pts/4. Open one with the program under test and the other with a terminal program, or with serialterm from the examples.
The serialterm Example
examples/serialport/serialterm.cpp is a small terminal built from the snippets above. It is built with the other examples, and takes the same settings as options:
serialterm --list
serialterm ttyUSB0 --baud 115200 --parity even
printf 'AT\r\n' | serialterm ttyUSB0 --time 2
See also QSerialPort and QSerialPortInfo.