mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-04 18:20:42 +00:00
1047 lines
30 KiB
C++
1047 lines
30 KiB
C++
/* 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 <QtSerialPort/QSerialPortInfo>
|
|
#include <QDebug>
|
|
|
|
|
|
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<SerialPortConnection*>(getInstance().openConnection(SerialPortConnection::TYPE, portName));
|
|
}
|
|
|
|
|
|
QHash<QString,DeviceConnection::FactoryMethod> & DeviceConnectionManager::factories()
|
|
{
|
|
static QHash<QString,DeviceConnection::FactoryMethod> 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<DeviceConnection*>(new T(name, record, replay)); }
|
|
|
|
|
|
// MARK: -
|
|
// MARK: Device manager events
|
|
|
|
// See XmlReplayEvent discussion of complex data types above.
|
|
class GetAvailableSerialPortsEvent : public XmlReplayBase<GetAvailableSerialPortsEvent>
|
|
{
|
|
public:
|
|
QList<SerialPortInfo> 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<SerialPortInfo> 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<GetAvailableSerialPortsEvent>();
|
|
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> 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 <serial> 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<OpenConnectionEvent>
|
|
{
|
|
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<ConnectionEvent>
|
|
{
|
|
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<SetValueEvent>
|
|
{
|
|
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<GetValueEvent>
|
|
{
|
|
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<CloseConnectionEvent>
|
|
{
|
|
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<ClearConnectionEvent>
|
|
{
|
|
};
|
|
REGISTER_XMLREPLAYEVENT("clear", ClearConnectionEvent);
|
|
|
|
class FlushConnectionEvent : public XmlReplayBase<FlushConnectionEvent>
|
|
{
|
|
};
|
|
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<ReceiveDataEvent>
|
|
{
|
|
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<TransmitDataEvent>
|
|
{
|
|
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<ReadyReadEvent>
|
|
{
|
|
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<OpenConnectionEvent>(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<SetValueEvent>(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<SetValueEvent>(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<SetValueEvent>(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<SetValueEvent>(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<SetValueEvent>(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<ClearConnectionEvent>();
|
|
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<GetValueEvent>(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<ReceiveDataEvent>();
|
|
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<TransmitDataEvent>(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<FlushConnectionEvent>();
|
|
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<CloseConnectionEvent>(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();
|
|
}
|