/* Device Connection Manager * * Copyright (c) 2020-2024 The OSCAR Team * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ #include "deviceconnection.h" #include "xmlreplay.h" #include "version.h" #include #include static QString hex(int i) { return QString("0x") + QString::number(i, 16).toUpper(); } // MARK: - // MARK: Device connection manager /* * DeviceRecorder/DeviceReplay subclasses of XmlRecorder/XmlReplay * * Used by DeviceConnectionManager to record its activity, such as * port scanning and connection opening/closing. */ class DeviceRecorder : public XmlRecorder { public: static const QString TAG; DeviceRecorder(class QFile * file) : XmlRecorder(file, DeviceRecorder::TAG) { m_xml->writeAttribute("oscar", getVersion().toString()); } DeviceRecorder(QString & string) : XmlRecorder(string, DeviceRecorder::TAG) { m_xml->writeAttribute("oscar", getVersion().toString()); } }; const QString DeviceRecorder::TAG = "devicereplay"; class DeviceReplay : public XmlReplay { public: DeviceReplay(class QFile * file) : XmlReplay(file, DeviceRecorder::TAG) {} DeviceReplay(QXmlStreamReader & xml) : XmlReplay(xml, DeviceRecorder::TAG) {} }; void DeviceConnectionManager::record(QFile* stream) { if (m_record) { delete m_record; } if (stream) { m_record = new DeviceRecorder(stream); } else { // nullptr turns off recording m_record = nullptr; } } void DeviceConnectionManager::record(QString & string) { if (m_record) { delete m_record; } m_record = new DeviceRecorder(string); } void DeviceConnectionManager::replay(const QString & string) { QXmlStreamReader xml(string); reset(); if (m_replay) { delete m_replay; } m_replay = new DeviceReplay(xml); } void DeviceConnectionManager::replay(QFile* file) { reset(); if (m_replay) { delete m_replay; } if (file) { m_replay = new DeviceReplay(file); } else { // nullptr turns off replay m_replay = nullptr; } } // Return singleton instance of DeviceConnectionManager, creating it if necessary. DeviceConnectionManager & DeviceConnectionManager::getInstance() { static DeviceConnectionManager instance; return instance; } // Protected constructor DeviceConnectionManager::DeviceConnectionManager() : m_record(nullptr), m_replay(nullptr) { } DeviceConnection* DeviceConnectionManager::openConnection(const QString & type, const QString & name) { if (!factories().contains(type)) { qWarning() << "Unknown device connection type:" << type; return nullptr; } if (m_connections.contains(name)) { qWarning() << "connection to" << name << "already open"; return nullptr; } // Recording/replay (if any) is handled by the connection. DeviceConnection* conn = factories()[type](name, m_record, m_replay); if (conn) { if (conn->open()) { m_connections[name] = conn; } else { qWarning().noquote() << "unable to open" << type << "connection to" << name; delete conn; conn = nullptr; } } else { qWarning() << "unable to create" << type << "connection to" << name; } return conn; } // Called by connections to deregister themselves. void DeviceConnectionManager::connectionClosed(DeviceConnection* conn) { Q_ASSERT(conn); const QString & type = conn->type(); const QString & name = conn->name(); if (m_connections.contains(name)) { if (m_connections[name] == conn) { m_connections.remove(name); } else { qWarning() << "connection to" << name << "not created by openConnection!"; } } else { qWarning() << type << "connection to" << name << "missing"; } } // Temporary convenience function for code that still supports only serial ports. SerialPortConnection* DeviceConnectionManager::openSerialPortConnection(const QString & portName) { return dynamic_cast(getInstance().openConnection(SerialPortConnection::TYPE, portName)); } QHash & DeviceConnectionManager::factories() { static QHash s_factories; return s_factories; } bool DeviceConnectionManager::registerClass(const QString & type, DeviceConnection::FactoryMethod factory) { if (factories().contains(type)) { qWarning() << "Connection class already registered for type" << type; return false; } factories()[type] = factory; return true; } // Since there are relatively few connection types, don't bother with a CRTP // parent class. Instead, this macro defines the factory method, and the // subclass will need to declare createInstance() and TYPE manually. #define REGISTER_DEVICECONNECTION(type, T) \ const QString T::TYPE = type; \ const bool T::registered = DeviceConnectionManager::registerClass(T::TYPE, T::createInstance); \ DeviceConnection* T::createInstance(const QString & name, XmlRecorder* record, XmlReplay* replay) { return static_cast(new T(name, record, replay)); } // MARK: - // MARK: Device manager events // See XmlReplayEvent discussion of complex data types above. class GetAvailableSerialPortsEvent : public XmlReplayBase { public: QList m_ports; protected: virtual void write(QXmlStreamWriter & xml) const { xml << m_ports; } virtual void read(QXmlStreamReader & xml) { xml >> m_ports; } }; REGISTER_XMLREPLAYEVENT("getAvailableSerialPorts", GetAvailableSerialPortsEvent); QList DeviceConnectionManager::getAvailableSerialPorts() { XmlReplayLock lock(this, m_replay); GetAvailableSerialPortsEvent event; if (!m_replay) { // Query the actual hardware present. for (auto & info : QSerialPortInfo::availablePorts()) { event.m_ports.append(SerialPortInfo(info)); } } else { auto replayEvent = m_replay->getNextEvent(); if (replayEvent) { event.m_ports = replayEvent->m_ports; } else { // If there are no replay events available, reuse the most recent state. event.m_ports = m_serialPorts; } } m_serialPorts = event.m_ports; event.record(m_record); return event.m_ports; } // MARK: - // MARK: Serial port info /* * This class is both a drop-in replacement for QSerialPortInfo and * supports XML serialization for the GetAvailableSerialPortsEvent above. */ SerialPortInfo::SerialPortInfo(const QSerialPortInfo & other) { if (other.isNull() == false) { m_info["portName"] = other.portName(); m_info["systemLocation"] = other.systemLocation(); m_info["description"] = other.description(); m_info["manufacturer"] = other.manufacturer(); m_info["serialNumber"] = other.serialNumber(); if (other.hasVendorIdentifier()) { m_info["vendorIdentifier"] = other.vendorIdentifier(); } if (other.hasProductIdentifier()) { m_info["productIdentifier"] = other.productIdentifier(); } } } SerialPortInfo::SerialPortInfo(const SerialPortInfo & other) : m_info(other.m_info) { } SerialPortInfo::SerialPortInfo(const QString & data) { QXmlStreamReader xml(data); xml.readNextStartElement(); xml >> *this; } SerialPortInfo::SerialPortInfo() { } // TODO: This method is a temporary wrapper that mimics the QSerialPortInfo interface until we begin refactoring. QList SerialPortInfo::availablePorts() { return DeviceConnectionManager::getInstance().getAvailableSerialPorts(); } QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const SerialPortInfo & info) { xml.writeStartElement("serial"); if (info.isNull() == false) { xml.writeAttribute("portName", info.portName()); xml.writeAttribute("systemLocation", info.systemLocation()); xml.writeAttribute("description", info.description()); xml.writeAttribute("manufacturer", info.manufacturer()); xml.writeAttribute("serialNumber", info.serialNumber()); if (info.hasVendorIdentifier()) { xml.writeAttribute("vendorIdentifier", hex(info.vendorIdentifier())); } if (info.hasProductIdentifier()) { xml.writeAttribute("productIdentifier", hex(info.productIdentifier())); } } xml.writeEndElement(); return xml; } QXmlStreamReader & operator>>(QXmlStreamReader & xml, SerialPortInfo & info) { if (xml.atEnd() == false && xml.isStartElement() && xml.name() == "serial") { for (auto & attribute : xml.attributes()) { QString name = attribute.name().toString(); QString value = attribute.value().toString(); if (name == "vendorIdentifier" || name == "productIdentifier") { bool ok; quint16 id = value.toUInt(&ok, 0); if (ok) { info.m_info[name] = id; } else { qWarning() << "invalid" << name << "value" << value; } } else { info.m_info[name] = value; } } } else { qWarning() << "no tag"; } xml.skipCurrentElement(); return xml; } SerialPortInfo::operator QString() const { QString out; QXmlStreamWriter xml(&out); xml << *this; return out; } bool SerialPortInfo::operator==(const SerialPortInfo & other) const { return m_info == other.m_info; } // MARK: - // MARK: Device connection base classes and events /* * Event recorded in the Device Connection Manager XML stream that indicates * a connection was opened (or attempted). On success, a ConnectionEvent * (see below) will begin the connection's substream. */ class OpenConnectionEvent : public XmlReplayBase { public: OpenConnectionEvent() {} OpenConnectionEvent(const QString & type, const QString & name) { set("type", type); set("name", name); } virtual const QString id() const { return m_values["name"]; } }; REGISTER_XMLREPLAYEVENT("openConnection", OpenConnectionEvent); /* * Event created when a connection is successfully opened, used as the * enclosing tag for the connection substream. */ class ConnectionEvent : public XmlReplayBase { public: ConnectionEvent() { Q_ASSERT(false); } // Implement if we ever support string-based substreams ConnectionEvent(const OpenConnectionEvent & trigger) { copy(trigger); } virtual const QString id() const { QString time = m_time.toString("yyyyMMdd.HHmmss.zzz"); return m_values["name"] + "-" + time; } }; REGISTER_XMLREPLAYEVENT("connection", ConnectionEvent); /* * ConnectionRecorder/ConnectionReplay subclasses of XmlRecorder/XmlReplay * * Used by DeviceConnection subclasses to record their activity, such as * configuration and data sent and received. */ class ConnectionRecorder : public XmlRecorder { public: ConnectionRecorder(XmlRecorder* parent, const ConnectionEvent& event) : XmlRecorder(parent, event.id(), event.tag()) { Q_ASSERT(m_xml); event.writeTag(*m_xml); m_xml->writeAttribute("oscar", getVersion().toString()); } }; class ConnectionReplay : public XmlReplay { public: ConnectionReplay(XmlReplay* parent, const ConnectionEvent& event) : XmlReplay(parent, event.id(), event.tag()) {} }; // Device connection base class DeviceConnection::DeviceConnection(const QString & name, XmlRecorder* record, XmlReplay* replay) : m_name(name), m_record(record), m_replay(replay), m_opened(false) { } DeviceConnection::~DeviceConnection() { } /* * Generic get/set events */ class SetValueEvent : public XmlReplayBase { public: SetValueEvent() {} SetValueEvent(const QString & name, int value) { set(name, value); } virtual const QString id() const { return m_keys.first(); } }; REGISTER_XMLREPLAYEVENT("set", SetValueEvent); class GetValueEvent : public XmlReplayBase { public: GetValueEvent() {} GetValueEvent(const QString & id) { set(id, 0); } virtual const QString id() const { return m_keys.first(); } void setValue(qint64 value) { if (m_keys.isEmpty()) { qWarning() << "setValue: get event missing key"; return; } set(m_keys.first(), value); } QString value() const { if (m_keys.isEmpty()) { qWarning() << "getValue: get event missing key"; return 0; } return get(m_keys.first()); } }; REGISTER_XMLREPLAYEVENT("get", GetValueEvent); /* * Event recorded in the Device Connection Manager XML stream when a * open connection is closed. This is the complement to a successful * OpenConnectionEvent (see above), and does not appear when the connection * failed to open. */ class CloseConnectionEvent : public XmlReplayBase { public: CloseConnectionEvent() {} CloseConnectionEvent(const QString & type, const QString & name) { set("type", type); set("name", name); } virtual const QString id() const { return m_values["name"]; } }; REGISTER_XMLREPLAYEVENT("closeConnection", CloseConnectionEvent); class ClearConnectionEvent : public XmlReplayBase { }; REGISTER_XMLREPLAYEVENT("clear", ClearConnectionEvent); class FlushConnectionEvent : public XmlReplayBase { }; REGISTER_XMLREPLAYEVENT("flush", FlushConnectionEvent); /* * Event representing data received from a device * * The data is stored as hexadecimal data in the XML tag's contents. */ class ReceiveDataEvent : public XmlReplayBase { virtual bool usesData() const { return true; } }; REGISTER_XMLREPLAYEVENT("rx", ReceiveDataEvent); /* * Event representing data sent to a device * * The data is stored as hexadecimal data in the XML tag's contents. * * These events are random-access events (see discussion above), which cause * subsequent event retrieval to begin searching after the transmission * event. * * Since the data sent is used as the ID for these events, we can treat * these like distinct "commands" that that elicit a deterministic response, * which can be replayed independently of other events if desired. * * Of course, for any device that has more complex internal state (where * responses to multiple transmissions of a particular "command" depend * on some intervening event), this reordering will not be accurate. * * But the intent is that some small changes to client code should still * work with existing recordings before requiring creation of new ones. */ class TransmitDataEvent : public XmlReplayBase { virtual bool usesData() const { return true; } public: virtual const QString id() const { return m_data; } virtual bool randomAccess() const { return true; } }; REGISTER_XMLREPLAYEVENT("tx", TransmitDataEvent); /* * Event representing a "readyRead" signal emitted by a physical device. * * These events are marked as "signal" events (see discussion of m_signal * above) so that connections will automatically send them to clients when * the preceding event (API call) is processed. */ class ReadyReadEvent : public XmlReplayBase { public: // Use the connection's slot that receives readyRead signals. ReadyReadEvent() { m_signal = "onReadyRead"; } }; REGISTER_XMLREPLAYEVENT("readyRead", ReadyReadEvent); // MARK: - // MARK: Serial port connection /* * Serial port connection class * * This class wraps calls to an underlying QSerialPort with the logic * necessary to record and replay arbitrary serial port activity. * (or, at least, the serial port functionality currently used by OSCAR). * * Clients obtain a connection instance via DeviceConnectionManager::openConnection() * or openSerialPortConnection (for convenience, if they require a serial port). */ REGISTER_DEVICECONNECTION("serial", SerialPortConnection); SerialPortConnection::SerialPortConnection(const QString & name, XmlRecorder* record, XmlReplay* replay) : DeviceConnection(name, record, replay) { connect(&m_port, SIGNAL(readyRead()), this, SLOT(onReadyRead())); } SerialPortConnection::~SerialPortConnection() { // This will only be false if the connection failed to open immediately after construction. if (m_opened) { close(); DeviceConnectionManager::getInstance().connectionClosed(this); } disconnect(&m_port, SIGNAL(readyRead()), this, SLOT(onReadyRead())); } /* * Try to open the physical port (or replay a previous attempt), returning * false if the port was not opened. * * DeviceConnectionManager::openConnection calls this immediately after * creating a connection instance, and will only return open connections * to clients. */ bool SerialPortConnection::open() { if (m_opened) { qWarning() << "serial connection to" << m_name << "already opened"; return false; } XmlReplayLock lock(this, m_replay); OpenConnectionEvent* replayEvent = nullptr; OpenConnectionEvent event("serial", m_name); if (!m_replay) { // TODO: move this into SerialPortConnection::openDevice() and move // the rest of the logic up to DeviceConnection::open(). m_port.setPortName(m_name); checkResult(m_port.open(QSerialPort::ReadWrite), event); } else { replayEvent = m_replay->getNextEvent(event.id()); if (replayEvent) { event.copyIf(replayEvent); } else { event.set("error", QSerialPort::DeviceNotFoundError); } } event.record(m_record); m_opened = event.ok(); if (m_opened) { // open a connection substream for connection events if (m_record) { ConnectionEvent connEvent(event); m_record = new ConnectionRecorder(m_record, connEvent); } if (m_replay) { Q_ASSERT(replayEvent); ConnectionEvent connEvent(*replayEvent); // we need to use the replay's timestamp to find the referenced substream m_replay = new ConnectionReplay(m_replay, connEvent); } } return event.ok(); } bool SerialPortConnection::setBaudRate(qint32 baudRate, QSerialPort::Directions directions) { XmlReplayLock lock(this, m_replay); SetValueEvent event("baudRate", baudRate); event.set("directions", directions); if (!m_replay) { checkResult(m_port.setBaudRate(baudRate, directions), event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } bool SerialPortConnection::setDataBits(QSerialPort::DataBits dataBits) { XmlReplayLock lock(this, m_replay); SetValueEvent event("setDataBits", dataBits); if (!m_replay) { checkResult(m_port.setDataBits(dataBits), event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } bool SerialPortConnection::setParity(QSerialPort::Parity parity) { XmlReplayLock lock(this, m_replay); SetValueEvent event("setParity", parity); if (!m_replay) { checkResult(m_port.setParity(parity), event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } bool SerialPortConnection::setStopBits(QSerialPort::StopBits stopBits) { XmlReplayLock lock(this, m_replay); SetValueEvent event("setStopBits", stopBits); if (!m_replay) { checkResult(m_port.setStopBits(stopBits), event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } bool SerialPortConnection::setFlowControl(QSerialPort::FlowControl flowControl) { XmlReplayLock lock(this, m_replay); SetValueEvent event("setFlowControl", flowControl); if (!m_replay) { checkResult(m_port.setFlowControl(flowControl), event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } bool SerialPortConnection::clear(QSerialPort::Directions directions) { XmlReplayLock lock(this, m_replay); ClearConnectionEvent event; event.set("directions", directions); if (!m_replay) { checkResult(m_port.clear(directions), event); } else { auto replayEvent = m_replay->getNextEvent(); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } qint64 SerialPortConnection::bytesAvailable() const { XmlReplayLock lock(this, m_replay); GetValueEvent event("bytesAvailable"); qint64 result; if (!m_replay) { result = m_port.bytesAvailable(); event.setValue(result); checkResult(result, event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); bool ok; result = event.value().toLong(&ok); if (!ok) { qWarning() << event.tag() << event.id() << "has bad value"; } } event.record(m_record); return result; } qint64 SerialPortConnection::read(char *data, qint64 maxSize) { XmlReplayLock lock(this, m_replay); qint64 len; ReceiveDataEvent event; if (!m_replay) { len = m_port.read(data, maxSize); if (len > 0) { event.setData(data, len); } event.set("len", len); if (len != maxSize) { event.set("req", maxSize); } checkResult(len, event); } else { auto replayEvent = m_replay->getNextEvent(); event.copyIf(replayEvent); if (!replayEvent) { qWarning() << "reading data past replay"; event.set("len", -1); event.set("error", QSerialPort::ReadError); } bool ok; len = event.get("len").toLong(&ok); if (ok) { if (event.ok()) { if (len != maxSize) { qWarning() << "replay of" << len << "bytes but" << maxSize << "requested"; } if (len > maxSize) { len = maxSize; } QByteArray replayData = event.getData(); memcpy(data, replayData, len); } } else { qWarning() << event << "has bad len"; len = -1; } } event.record(m_record); return len; } qint64 SerialPortConnection::write(const char *data, qint64 maxSize) { XmlReplayLock lock(this, m_replay); qint64 len; TransmitDataEvent event; event.setData(data, maxSize); if (!m_replay) { len = m_port.write(data, maxSize); event.set("len", len); if (len != maxSize) { event.set("req", maxSize); } checkResult(len, event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); event.copyIf(replayEvent); if (!replayEvent) { qWarning() << "writing data past replay"; event.set("len", -1); event.set("error", QSerialPort::WriteError); } bool ok; len = event.get("len").toLong(&ok); // No need to copy any data, since the event already contains it. if (!ok) { qWarning() << event << "has bad len"; len = -1; } } event.record(m_record); return len; } bool SerialPortConnection::flush() { XmlReplayLock lock(this, m_replay); FlushConnectionEvent event; if (!m_replay) { checkResult(m_port.flush(), event); } else { auto replayEvent = m_replay->getNextEvent(); event.copyIf(replayEvent); } event.record(m_record); return event.ok(); } void SerialPortConnection::close() { if (m_opened) { // close event substream first if (m_record) { m_record = m_record->closeSubstream(); } if (m_replay) { m_replay = m_replay->closeSubstream(); } } XmlReplayLock lock(this, m_replay); CloseConnectionEvent event("serial", m_name); // TODO: We'll also need to include a loader ID and stream version number // in the "connection" tag, so that if we ever have to change a loader's download code, // the older replays will still work as expected. // NOTE: This may only be required for downloads rather than all connections. if (!m_replay) { // TODO: move this into SerialPortConnection::closeDevice() and move // the remaining logic up to DeviceConnection::close(). m_port.close(); checkError(event); } else { auto replayEvent = m_replay->getNextEvent(event.id()); if (replayEvent) { event.copyIf(replayEvent); } else { event.set("error", QSerialPort::ResourceError); } } event.record(m_record); } void SerialPortConnection::onReadyRead() { { // Wait until the replay signaler (if any) has released its lock. XmlReplayLock lock(this, m_replay); // This needs to be recorded before the signal below, since the slot may trigger more events. ReadyReadEvent event; event.record(m_record); // Unlocking will queue any subsequent signals. } // Because clients typically leave this as Qt::AutoConnection, the below emit may // execute immediately in this thread, so we have to release the lock before sending // the signal. // Unlike client-called events, We don't need to handle replay differently here, // because the replay will signal this slot just like the serial port. emit readyRead(); } // Check the boolean returned by a serial port call and the port's error status, and update the event accordingly. void SerialPortConnection::checkResult(bool ok, XmlReplayEvent & event) const { QSerialPort::SerialPortError error = m_port.error(); if (ok && error == QSerialPort::NoError) return; event.set("error", error); if (ok) event.set("ok", ok); // we don't expect to see this, but we should know if it happens } // Check the length returned by a serial port call and the port's error status, and update the event accordingly. void SerialPortConnection::checkResult(qint64 len, XmlReplayEvent & event) const { QSerialPort::SerialPortError error = m_port.error(); if (len < 0 || error != QSerialPort::NoError) { event.set("error", error); } } // Check the port's error status, and update the event accordingly. void SerialPortConnection::checkError(XmlReplayEvent & event) const { QSerialPort::SerialPortError error = m_port.error(); if (error != QSerialPort::NoError) { event.set("error", error); } } // MARK: - // MARK: SerialPort legacy class /* * SerialPort drop-in replacement for QSerialPort * * This class mimics the interface of QSerialPort for client code, while * using DeviceConnectionManager to open the SerialPortConnection, allowing * for transparent recording and replay. */ SerialPort::SerialPort() : m_conn(nullptr) { } SerialPort::~SerialPort() { if (m_conn) { close(); } } void SerialPort::setPortName(const QString &name) { m_portName = name; } bool SerialPort::open(QIODevice::OpenMode mode) { Q_ASSERT(!m_conn); Q_ASSERT(mode == QSerialPort::ReadWrite); m_conn = DeviceConnectionManager::openSerialPortConnection(m_portName); if (m_conn) { // Listen for readyRead events from the connection so that we can relay them to the client. connect(m_conn, SIGNAL(readyRead()), this, SLOT(onReadyRead())); } return m_conn != nullptr; } bool SerialPort::setBaudRate(qint32 baudRate, QSerialPort::Directions directions) { Q_ASSERT(m_conn); return m_conn->setBaudRate(baudRate, directions); } bool SerialPort::setDataBits(QSerialPort::DataBits dataBits) { Q_ASSERT(m_conn); return m_conn->setDataBits(dataBits); } bool SerialPort::setParity(QSerialPort::Parity parity) { Q_ASSERT(m_conn); return m_conn->setParity(parity); } bool SerialPort::setStopBits(QSerialPort::StopBits stopBits) { Q_ASSERT(m_conn); return m_conn->setStopBits(stopBits); } bool SerialPort::setFlowControl(QSerialPort::FlowControl flowControl) { Q_ASSERT(m_conn); return m_conn->setFlowControl(flowControl); } bool SerialPort::clear(QSerialPort::Directions directions) { Q_ASSERT(m_conn); return m_conn->clear(directions); } qint64 SerialPort::bytesAvailable() const { Q_ASSERT(m_conn); return m_conn->bytesAvailable(); } qint64 SerialPort::read(char *data, qint64 maxSize) { Q_ASSERT(m_conn); return m_conn->read(data, maxSize); } qint64 SerialPort::write(const char *data, qint64 maxSize) { Q_ASSERT(m_conn); return m_conn->write(data, maxSize); } bool SerialPort::flush() { Q_ASSERT(m_conn); return m_conn->flush(); } void SerialPort::close() { Q_ASSERT(m_conn); disconnect(m_conn, SIGNAL(readyRead()), this, SLOT(onReadyRead())); delete m_conn; // this will close the connection m_conn = nullptr; } void SerialPort::onReadyRead() { // Relay readyRead events from the connection on to the client. emit readyRead(); }