Add playback of serial port scan, along with supporting infrastructure.

This commit is contained in:
sawinglogz 2020-06-11 15:58:34 -04:00
parent efbb967b5c
commit ac1281c1d9
3 changed files with 417 additions and 42 deletions

View File

@ -8,6 +8,8 @@
#include "deviceconnection.h"
#include <QtSerialPort/QSerialPortInfo>
#include <QFile>
#include <QBuffer>
#include <QDateTime>
#include <QDebug>
@ -31,63 +33,393 @@ DeviceConnectionManager::DeviceConnectionManager()
{
}
void DeviceConnectionManager::Record(QXmlStreamWriter* stream)
// MARK: -
class XmlRecord
{
getInstance().m_record = stream;
public:
XmlRecord(class QFile * file);
XmlRecord(QString & string);
~XmlRecord();
inline QXmlStreamWriter & xml() { return *m_xml; }
protected:
QFile* m_file; // nullptr for non-file recordings
QXmlStreamWriter* m_xml;
void prologue();
void epilogue();
};
class XmlReplay
{
public:
XmlReplay(class QFile * file);
XmlReplay(QXmlStreamReader & xml);
~XmlReplay();
template<class T> inline T* getNextEvent();
protected:
void deserialize(QXmlStreamReader & xml);
void deserializeEvents(QXmlStreamReader & xml);
// TODO: maybe the QList should be a QHash on the timestamp?
// Then indices would be iterators over a sorted list of keys.
QHash<QString,QList<class XmlReplayEvent*>> m_events;
QHash<QString,int> m_indices;
class XmlReplayEvent* getNextEvent(const QString & type);
};
class XmlReplayEvent
{
public:
XmlReplayEvent();
virtual ~XmlReplayEvent() = default;
virtual const QString & tag() const = 0;
void record(XmlRecord* xml);
friend QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const XmlReplayEvent & event);
friend QXmlStreamReader & operator>>(QXmlStreamReader & xml, XmlReplayEvent & event);
typedef XmlReplayEvent* (*FactoryMethod)();
static bool registerClass(const QString & tag, FactoryMethod factory);
static XmlReplayEvent* createInstance(const QString & tag);
protected:
static QHash<QString,FactoryMethod> s_factories;
QDateTime m_time;
virtual void write(QXmlStreamWriter & /*xml*/) const {}
virtual void read(QXmlStreamReader & /*xml*/) {}
};
QHash<QString,XmlReplayEvent::FactoryMethod> XmlReplayEvent::s_factories;
XmlRecord::XmlRecord(QFile* stream)
: m_file(stream), m_xml(new QXmlStreamWriter(stream))
{
prologue();
}
void DeviceConnectionManager::Replay(QXmlStreamReader* stream)
XmlRecord::XmlRecord(QString & string)
: m_file(nullptr), m_xml(new QXmlStreamWriter(&string))
{
getInstance().m_replay = stream;
prologue();
}
void DeviceConnectionManager::startEvent(const QString & event)
XmlRecord::~XmlRecord()
{
if (m_record) {
QDateTime now = QDateTime::currentDateTime();
now = now.toOffsetFromUtc(now.offsetFromUtc()); // force display of UTC offset
epilogue();
delete m_xml;
}
void XmlRecord::prologue()
{
Q_ASSERT(m_xml);
m_xml->setAutoFormatting(true);
m_xml->setAutoFormattingIndent(2);
m_xml->writeStartElement("xmlreplay");
m_xml->writeStartElement("events");
}
void XmlRecord::epilogue()
{
Q_ASSERT(m_xml);
m_xml->writeEndElement(); // close events
// TODO: write out any inline connections
m_xml->writeEndElement(); // close xmlreplay
}
XmlReplay::XmlReplay(QFile* file)
{
QXmlStreamReader xml(file);
deserialize(xml);
}
XmlReplay::XmlReplay(QXmlStreamReader & xml)
{
deserialize(xml);
}
XmlReplay::~XmlReplay()
{
for (auto list : m_events.values()) {
for (auto event : list) {
delete event;
}
}
}
void XmlReplay::deserialize(QXmlStreamReader & xml)
{
if (xml.readNextStartElement()) {
if (xml.name() == "xmlreplay") {
while (xml.readNextStartElement()) {
if (xml.name() == "events") {
deserializeEvents(xml);
// else TODO: inline connections
} else {
qWarning() << "unexpected payload in replay XML:" << xml.name();
xml.skipCurrentElement();
}
}
}
}
}
void XmlReplay::deserializeEvents(QXmlStreamReader & xml)
{
while (xml.readNextStartElement()) {
QString name = xml.name().toString();
XmlReplayEvent* event = XmlReplayEvent::createInstance(name);
if (event) {
xml >> *event;
auto & events = m_events[name];
events.append(event);
} else {
xml.skipCurrentElement();
}
}
}
XmlReplayEvent* XmlReplay::getNextEvent(const QString & type)
{
XmlReplayEvent* event = nullptr;
if (m_events.contains(type)) {
auto & events = m_events[type];
int i = m_indices[type];
if (i < events.size()) {
event = events[i];
// TODO: if we're simulating the original timing, return nullptr if we haven't reached this event's time yet;
// otherwise:
m_indices[type] = i + 1;
}
}
return event;
}
template<class T>
T* XmlReplay::getNextEvent()
{
T* event = dynamic_cast<T*>(getNextEvent(T::TAG));
return event;
}
// MARK: -
XmlReplayEvent::XmlReplayEvent()
: m_time(QDateTime::currentDateTime())
{
}
void XmlReplayEvent::record(XmlRecord* writer)
{
// Do nothing if we're not recording.
if (writer != nullptr) {
writer->xml() << *this;
}
}
bool XmlReplayEvent::registerClass(const QString & tag, XmlReplayEvent::FactoryMethod factory)
{
if (s_factories.contains(tag)) {
qWarning() << "Event class already registered for tag" << tag;
return false;
}
s_factories[tag] = factory;
return true;
}
XmlReplayEvent* XmlReplayEvent::createInstance(const QString & tag)
{
XmlReplayEvent* event = nullptr;
XmlReplayEvent::FactoryMethod factory = s_factories.value(tag);
if (factory == nullptr) {
qWarning() << "No event class registered for XML tag" << tag;
} else {
event = factory();
}
return event;
}
QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const XmlReplayEvent & event)
{
QDateTime time = event.m_time.toOffsetFromUtc(event.m_time.offsetFromUtc()); // force display of UTC offset
#if QT_VERSION < QT_VERSION_CHECK(5,9,0)
// TODO: Can we please deprecate support for Qt older than 5.9?
QString timestamp = time.toString(Qt::ISODate);
#else
QString timestamp = time.toString(Qt::ISODateWithMs);
#endif
xml.writeStartElement(event.tag());
xml.writeAttribute("time", timestamp);
event.write(xml);
xml.writeEndElement();
return xml;
}
QXmlStreamReader & operator>>(QXmlStreamReader & xml, XmlReplayEvent & event)
{
Q_ASSERT(xml.isStartElement() && xml.name() == event.tag());
QDateTime time;
if (xml.attributes().hasAttribute("time")) {
#if QT_VERSION < QT_VERSION_CHECK(5,9,0)
// TODO: Can we please deprecate support for Qt older than 5.9?
QString timestamp = now.toString(Qt::ISODate);
time = QDateTime::fromString(xml.attributes().value("time").toString(), Qt::ISODate);
#else
QString timestamp = now.toString(Qt::ISODateWithMs);
time = QDateTime::fromString(xml.attributes().value("time").toString(), Qt::ISODateWithMs);
#endif
m_record->writeStartElement(event);
m_record->writeAttribute("time", timestamp);
} else {
qWarning() << "Missing timestamp in" << xml.name() << "tag, using current time";
time = QDateTime::currentDateTime();
}
event.read(xml);
return xml;
}
#define RECORD(x) if (m_record) { *m_record << (x); }
template<typename T> QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const QList<T> & list)
{
for (auto & item : list) {
xml << item;
}
return xml;
}
void DeviceConnectionManager::endEvent()
template<typename T> QXmlStreamReader & operator>>(QXmlStreamReader & xml, QList<T> & list)
{
list.clear();
while (xml.readNextStartElement()) {
T item;
xml >> item;
list.append(item);
}
return xml;
}
template <typename Derived>
class XmlReplayBase : public XmlReplayEvent
{
public:
static const QString TAG;
static const bool registered;
virtual const QString & tag() const { return TAG; };
static XmlReplayEvent* createInstance()
{
Derived* instance = new Derived();
return static_cast<XmlReplayEvent*>(instance);
}
};
#define REGISTER_XMLREPLAYEVENT(tag, type) \
template<> const QString XmlReplayBase<type>::TAG = tag; \
template<> const bool XmlReplayBase<type>::registered = XmlReplayEvent::registerClass(XmlReplayBase<type>::TAG, XmlReplayBase<type>::createInstance);
// MARK: -
void DeviceConnectionManager::record(QFile* stream)
{
if (m_record) {
m_record->writeEndElement();
delete m_record;
}
if (stream) {
m_record = new XmlRecord(stream);
} else {
// nullptr turns off recording
m_record = nullptr;
}
}
void DeviceConnectionManager::record(QString & string)
{
if (m_record) {
delete m_record;
}
m_record = new XmlRecord(string);
}
void DeviceConnectionManager::replay(const QString & string)
{
QXmlStreamReader xml(string);
reset();
if (m_replay) {
delete m_replay;
}
m_replay = new XmlReplay(xml);
}
void DeviceConnectionManager::replay(QFile* file)
{
reset();
if (m_replay) {
delete m_replay;
}
if (file) {
m_replay = new XmlReplay(file);
} else {
// nullptr turns off replay
m_replay = nullptr;
}
}
// MARK: -
class GetAvailablePortsEvent : public XmlReplayBase<GetAvailablePortsEvent>
{
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("getAvailablePorts", GetAvailablePortsEvent);
QList<SerialPortInfo> DeviceConnectionManager::getAvailablePorts()
{
QList<SerialPortInfo> out;
GetAvailablePortsEvent event;
startEvent("getAvailablePorts");
if (m_replay) {
// TODO
} else {
if (!m_replay) {
for (auto & info : QSerialPortInfo::availablePorts()) {
out.append(SerialPortInfo(info));
event.m_ports.append(SerialPortInfo(info));
}
} else {
auto replayEvent = m_replay->getNextEvent<GetAvailablePortsEvent>();
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;
for (auto & portInfo : out) {
//qDebug().noquote() << portInfo;
RECORD(portInfo);
}
endEvent();
return out;
event.record(m_record);
return event.m_ports;
}
// TODO: Once we start recording/replaying connections, we'll need to include a version number, so that
// if we ever have to change the download code, the older replays will still work as expected.
// MARK: -
SerialPortInfo::SerialPortInfo(const QSerialPortInfo & other)
@ -119,6 +451,10 @@ SerialPortInfo::SerialPortInfo(const QString & data)
xml >> *this;
}
SerialPortInfo::SerialPortInfo()
{
}
// TODO: This is a temporary wrapper until we begin refactoring.
QList<SerialPortInfo> SerialPortInfo::availablePorts()
{
@ -166,7 +502,7 @@ QXmlStreamReader & operator>>(QXmlStreamReader & xml, SerialPortInfo & info)
} else {
qWarning() << "no <serial> tag";
}
xml.readNext();
xml.skipCurrentElement();
return xml;
}
@ -177,3 +513,8 @@ SerialPortInfo::operator QString() const
xml << *this;
return out;
}
bool SerialPortInfo::operator==(const SerialPortInfo & other) const
{
return m_info == other.m_info;
}

View File

@ -15,6 +15,7 @@
#include <QtSerialPort/QSerialPort>
#include <QHash>
#include <QVariant>
#include <QDateTime>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
@ -24,10 +25,15 @@ class DeviceConnectionManager : public QObject
private:
DeviceConnectionManager();
QXmlStreamWriter* m_record;
QXmlStreamReader* m_replay;
void startEvent(const QString & event);
void endEvent();
class XmlRecord* m_record;
class XmlReplay* m_replay;
QList<class SerialPortInfo> replayAvailablePorts();
QList<SerialPortInfo> m_serialPorts;
void reset() { // clear state
m_serialPorts.clear();
}
public:
static DeviceConnectionManager & getInstance();
@ -36,8 +42,10 @@ public:
// TODO: method to start a polling thread that maintains the list of ports
// TODO: emit signal when new port is detected
static void Record(QXmlStreamWriter* stream);
static void Replay(QXmlStreamReader* stream);
void record(class QFile* stream);
void record(QString & string);
void replay(class QFile* stream);
void replay(const QString & string);
};
@ -57,6 +65,7 @@ public:
static QList<SerialPortInfo> availablePorts();
SerialPortInfo(const SerialPortInfo & other);
SerialPortInfo(const QString & data);
SerialPortInfo();
inline QString portName() const { return m_info["portName"].toString(); }
inline QString systemLocation() const { return m_info["systemLocation"].toString(); }
@ -75,6 +84,7 @@ public:
operator QString() const;
friend QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const SerialPortInfo & info);
friend QXmlStreamReader & operator>>(QXmlStreamReader & xml, SerialPortInfo & info);
bool operator==(const SerialPortInfo & other) const;
protected:
SerialPortInfo(const class QSerialPortInfo & other);

View File

@ -9,6 +9,8 @@
#include "deviceconnectiontests.h"
#include "SleepLib/deviceconnection.h"
#include <QTemporaryFile>
void DeviceConnectionTests::testSerialPortInfoSerialization()
{
QString serialized;
@ -54,13 +56,35 @@ void DeviceConnectionTests::testSerialPortInfoSerialization()
void DeviceConnectionTests::testSerialPortScanning()
{
QString string;
QXmlStreamWriter xml(&string);
xml.setAutoFormatting(true);
DeviceConnectionManager::Record(&xml);
SerialPortInfo::availablePorts();
SerialPortInfo::availablePorts();
DeviceConnectionManager::Record(nullptr);
DeviceConnectionManager & devices = DeviceConnectionManager::getInstance();
devices.record(string);
auto list1 = SerialPortInfo::availablePorts();
auto list2 = SerialPortInfo::availablePorts();
devices.record(nullptr);
// string now contains the recorded XML.
qDebug().noquote() << string;
devices.replay(string);
Q_ASSERT(list1 == SerialPortInfo::availablePorts());
Q_ASSERT(list2 == SerialPortInfo::availablePorts());
Q_ASSERT(list2 == SerialPortInfo::availablePorts()); // replaying past the recording should return the final state
devices.replay(nullptr); // turn off replay
auto list3 = SerialPortInfo::availablePorts();
// Test file-based recording/playback
QTemporaryFile recording;
Q_ASSERT(recording.open());
devices.record(&recording);
list1 = SerialPortInfo::availablePorts();
list2 = SerialPortInfo::availablePorts();
devices.record(nullptr);
recording.seek(0);
devices.replay(&recording);
Q_ASSERT(list1 == SerialPortInfo::availablePorts());
Q_ASSERT(list2 == SerialPortInfo::availablePorts());
Q_ASSERT(list2 == SerialPortInfo::availablePorts()); // replaying past the recording should return the final state
devices.replay(nullptr); // turn off replay
list3 = SerialPortInfo::availablePorts();
}