AdvancedControllerRemoteCPP/lib/controllerlink.cpp

280 lines
8.3 KiB
C++

// controllerlink.cpp - RemoteClient in its own thread, results as signals (see controllerlink.h)
#include "controllerlink.h"
#include <QElapsedTimer>
#include <QMetaObject>
#include <QMutex>
#include <QMutexLocker>
#include <QQueue>
#include <QThread>
#include <QWaitCondition>
#include <utility>
namespace AdvCtrl {
// Owns the serial port: everything below run() happens in this thread. Results go to the
// ControllerLink as queued calls, so they arrive in the thread of the ControllerLink.
class LinkThread : public QThread
{
public:
LinkThread(ControllerLink *link, const QString &portName, int baudRate)
: m_link(link), m_portName(portName), m_baudRate(baudRate)
{
}
void post(const QString &commandLine)
{
QMutexLocker lock(&m_mutex);
m_queue.enqueue(commandLine);
m_wake.wakeOne();
}
void stop()
{
requestInterruption();
QMutexLocker lock(&m_mutex);
m_wake.wakeOne();
}
protected:
void run() override;
private:
bool takeCommand(QString *commandLine, int waitMs);
void dropCommands(const QString &why);
template <typename Function>
void toLink(Function &&function)
{
QMetaObject::invokeMethod(m_link, std::forward<Function>(function), Qt::QueuedConnection);
}
void reportError(const QString &message)
{
ControllerLink *const link = m_link;
toLink([link, message] { emit link->error(message); });
}
ControllerLink *const m_link;
const QString m_portName;
const int m_baudRate;
QMutex m_mutex;
QWaitCondition m_wake;
QQueue<QString> m_queue;
};
void LinkThread::run()
{
ControllerLink *const link = m_link;
RemoteClient rc;
if (!rc.open(m_portName, m_baudRate)) {
reportError(QStringLiteral("cannot open %1: %2").arg(m_portName, rc.errorString()));
toLink([link] { link->handleClosed(); });
return;
}
bool detected = false;
bool ledsSupported = true;
int failures = 0; // consecutive requests without an answer
QElapsedTimer clock;
clock.start();
qint64 nextLeds = 0;
qint64 nextDetect = 0;
while (!isInterruptionRequested() && !rc.portFailed()) {
if (!detected) {
dropCommands(QStringLiteral("controller not connected"));
if (clock.elapsed() >= nextDetect) {
nextDetect = clock.elapsed() + ControllerLink::DetectRetryMs;
if (rc.detect()) {
detected = true;
failures = 0;
Fields ident;
bool known = false;
for (int i = 0; i < 3 && !known; ++i) { // ID; a late PONG may come first
known = rc.identify(&ident);
if (!known)
reportError(QStringLiteral("ID: ") + rc.lastReply().toString());
if (rc.portFailed() || isInterruptionRequested())
break;
}
const QString unknown = QStringLiteral("?");
const QString firmware = known ? ident.value(QStringLiteral("FW"), unknown) : unknown;
const QString cameras = known ? ident.value(QStringLiteral("CAMS"), unknown) : unknown;
const QString uid = known ? ident.value(QStringLiteral("UID"), unknown) : unknown;
toLink([link, firmware, cameras, uid] { link->handleDetected(firmware, cameras, uid); });
} else if (!rc.portFailed() && !isInterruptionRequested()) {
toLink([link] { link->handleNotDetected(); });
}
}
QThread::msleep(50);
continue;
}
QString commandLine;
if (takeCommand(&commandLine, 50)) {
const Reply r = rc.command(commandLine);
QString result;
if (r.status == Reply::Ok || r.status == Reply::DeviceError) {
failures = 0; // ERR is an answer, not a link failure
result = r.toString();
} else {
++failures;
result = QStringLiteral("!! ") + r.text;
}
toLink([link, commandLine, result] { emit link->reply(commandLine, result); });
} else if (clock.elapsed() >= nextLeds) {
nextLeds = clock.elapsed() + ControllerLink::LedsPeriodMs;
if (ledsSupported) {
quint32 mask = 0;
if (rc.leds(&mask)) {
failures = 0;
toLink([link, mask] { emit link->leds(mask); });
} else if (rc.lastReply().status == Reply::DeviceError) {
ledsSupported = false;
reportError(QStringLiteral("LEDS not supported by this firmware (%1) - firmware update required")
.arg(rc.lastReply().text));
} else {
++failures;
reportError(rc.lastReply().text);
}
} else if (rc.ping() || rc.lastReply().status == Reply::Ok
|| rc.lastReply().status == Reply::DeviceError) {
failures = 0; // old firmware: keep the link check alive
} else {
++failures;
reportError(rc.lastReply().text);
}
}
if (failures >= ControllerLink::LostAfter) {
detected = false; // back to detection, retried every 2 s
nextDetect = 0;
toLink([link] { link->handleLost(); });
}
}
if (rc.portFailed()) // e.g. the USB adapter was unplugged
reportError(m_portName + QStringLiteral(": ") + rc.errorString());
rc.close();
toLink([link] { link->handleClosed(); });
}
bool LinkThread::takeCommand(QString *commandLine, int waitMs)
{
QMutexLocker lock(&m_mutex);
if (m_queue.isEmpty() && !isInterruptionRequested())
m_wake.wait(&m_mutex, waitMs);
if (m_queue.isEmpty())
return false;
*commandLine = m_queue.dequeue();
return true;
}
void LinkThread::dropCommands(const QString &why)
{
QQueue<QString> dropped;
{
QMutexLocker lock(&m_mutex);
dropped.swap(m_queue);
}
ControllerLink *const link = m_link;
const QString result = QStringLiteral("!! ") + why;
for (const QString &commandLine : std::as_const(dropped))
toLink([link, commandLine, result] { emit link->reply(commandLine, result); });
}
// ------------------------------------------------------------------ ControllerLink
ControllerLink::ControllerLink(QObject *parent)
: QObject(parent)
{
}
ControllerLink::~ControllerLink()
{
if (m_thread) {
m_thread->stop();
m_thread->wait();
delete m_thread;
}
}
ControllerLink::State ControllerLink::state() const
{
return m_state;
}
QString ControllerLink::portName() const
{
return m_portName;
}
void ControllerLink::open(const QString &portName, int baudRate)
{
if (m_thread) // already open: close() first
return;
m_portName = portName;
m_thread = new LinkThread(this, portName, baudRate);
m_thread->start();
setState(NoLink);
}
void ControllerLink::close()
{
if (m_thread)
m_thread->stop();
}
void ControllerLink::send(const QString &commandLine)
{
if (m_thread)
m_thread->post(commandLine);
}
void ControllerLink::setState(State state)
{
if (state == m_state)
return;
m_state = state;
emit stateChanged(state);
}
void ControllerLink::handleDetected(const QString &firmware, const QString &cameras, const QString &uid)
{
if (!m_thread)
return;
setState(Link);
emit detected(firmware, cameras, uid);
}
void ControllerLink::handleNotDetected()
{
if (!m_thread)
return;
setState(NoLink);
emit notDetected();
}
void ControllerLink::handleLost()
{
if (!m_thread)
return;
setState(NoLink);
emit lost();
}
void ControllerLink::handleClosed()
{
if (m_thread) {
m_thread->wait();
delete m_thread;
m_thread = nullptr;
}
m_portName.clear();
setState(Closed);
emit closed();
}
} // namespace AdvCtrl