diff --git a/oscar/SleepLib/deviceconnection.cpp b/oscar/SleepLib/deviceconnection.cpp index c5d9e0f5..48153d6f 100644 --- a/oscar/SleepLib/deviceconnection.cpp +++ b/oscar/SleepLib/deviceconnection.cpp @@ -8,6 +8,8 @@ #include "deviceconnection.h" #include +#include +#include #include #include @@ -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 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> m_events; + QHash 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 s_factories; + + QDateTime m_time; + + virtual void write(QXmlStreamWriter & /*xml*/) const {} + virtual void read(QXmlStreamReader & /*xml*/) {} +}; +QHash 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 +T* XmlReplay::getNextEvent() +{ + T* event = dynamic_cast(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 QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const QList & list) +{ + for (auto & item : list) { + xml << item; + } + return xml; +} -void DeviceConnectionManager::endEvent() +template QXmlStreamReader & operator>>(QXmlStreamReader & xml, QList & list) +{ + list.clear(); + while (xml.readNextStartElement()) { + T item; + xml >> item; + list.append(item); + } + return xml; +} + +template +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(instance); + } +}; + +#define REGISTER_XMLREPLAYEVENT(tag, type) \ +template<> const QString XmlReplayBase::TAG = tag; \ +template<> const bool XmlReplayBase::registered = XmlReplayEvent::registerClass(XmlReplayBase::TAG, XmlReplayBase::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 +{ +public: + QList 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 DeviceConnectionManager::getAvailablePorts() { - QList 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(); + 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::availablePorts() { @@ -166,7 +502,7 @@ QXmlStreamReader & operator>>(QXmlStreamReader & xml, SerialPortInfo & info) } else { qWarning() << "no 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; +} diff --git a/oscar/SleepLib/deviceconnection.h b/oscar/SleepLib/deviceconnection.h index ab4c919d..24a441e7 100644 --- a/oscar/SleepLib/deviceconnection.h +++ b/oscar/SleepLib/deviceconnection.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -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 replayAvailablePorts(); + QList 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 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); diff --git a/oscar/tests/deviceconnectiontests.cpp b/oscar/tests/deviceconnectiontests.cpp index f63db987..1d1aeb71 100644 --- a/oscar/tests/deviceconnectiontests.cpp +++ b/oscar/tests/deviceconnectiontests.cpp @@ -9,6 +9,8 @@ #include "deviceconnectiontests.h" #include "SleepLib/deviceconnection.h" +#include + 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(); }