15.1.950. tablet_qt/whisker/whiskermanager.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

// ============================================================================
// debugging #defines
// ============================================================================

// #define WHISKERMGR_DEBUG_MESSAGES

// ============================================================================
// #includes
// ============================================================================

#include "whiskermanager.h"
#include <QRegularExpression>
#include "lib/uifunc.h"
#include "whisker/whiskerapi.h"
#include "whisker/whiskerconstants.h"
#include "whisker/whiskerworker.h"
using namespace whiskerapi;
using namespace whiskerconstants;


// ============================================================================
// WhiskerManager
// ============================================================================

WhiskerManager::WhiskerManager(QObject* parent,
                               const QString& sysevent_prefix) :
    QObject(parent),
    m_worker(new WhiskerWorker()),
    m_sysevent_prefix(sysevent_prefix),
    m_sysevent_counter(0)
{
    // As per https://doc.qt.io/qt-6.5/qthread.html:
    m_worker->moveToThread(&m_worker_thread);  // changes thread affinity
    connect(&m_worker_thread, &QThread::finished,
            m_worker, &QObject::deleteLater);  // this is how we ensure deletion of m_worker

    // Our additional signal/slot connections:
    connect(this, &WhiskerManager::internalConnectToServer,
            m_worker, &WhiskerWorker::connectToServer);
    connect(this, &WhiskerManager::disconnectFromServer,
            m_worker, &WhiskerWorker::disconnectFromServer);
    connect(this, &WhiskerManager::internalSend,
            m_worker, &WhiskerWorker::sendToServer);

    connect(m_worker, &WhiskerWorker::connectionStateChanged,
            this, &WhiskerManager::connectionStateChanged);
    connect(m_worker, &WhiskerWorker::onFullyConnected,
            this, &WhiskerManager::onFullyConnected);
    connect(m_worker, &WhiskerWorker::receivedFromServerMainSocket,
            this, &WhiskerManager::internalReceiveFromMainSocket);
    connect(m_worker, &WhiskerWorker::socketError,
            this, &WhiskerManager::onSocketError);

    m_worker_thread.start();
}


WhiskerManager::~WhiskerManager()
{
    m_worker_thread.quit();
    m_worker_thread.wait();
}


void WhiskerManager::connectToServer(const QString& host, quint16 main_port)
{
    emit internalConnectToServer(host, main_port);
}


bool WhiskerManager::isConnected() const
{
    return m_worker->isFullyConnected();
}


bool WhiskerManager::isFullyDisconnected() const
{
    return m_worker->isFullyDisconnected();
}


void WhiskerManager::alertNotConnected() const
{
    uifunc::alert(whiskerconstants::NOT_CONNECTED,
                  whiskerconstants::WHISKER_ALERT_TITLE);
}


void WhiskerManager::sendMain(const QString& command)
{
    WhiskerOutboundCommand cmd(command, false);
    emit internalSend(cmd);
}


void WhiskerManager::sendMain(const QStringList& args)
{
    sendMain(msgFromArgs(args));
}


void WhiskerManager::sendMain(std::initializer_list<QString> args)
{
    sendMain(msgFromArgs(QStringList(args)));
}


void WhiskerManager::sendImmediateIgnoreReply(const QString& command)
{
#ifdef WHISKERMGR_DEBUG_MESSAGES
    qDebug() << "Sending immediate-socket command (for no reply):" << command;
#endif
    WhiskerOutboundCommand cmd(command, true, true);
    emit internalSend(cmd);  // transfer send command to our worker on its socket thread
}


WhiskerInboundMessage WhiskerManager::sendImmediateGetReply(
        const QString& command)
{
#ifdef WHISKERMGR_DEBUG_MESSAGES
    qDebug() << "Sending immediate-socket command:" << command;
#endif
    WhiskerOutboundCommand cmd(command, true, false);
    emit internalSend(cmd);  // transfer send command to our worker on its socket thread
    WhiskerInboundMessage msg = m_worker->getPendingImmediateReply();
#ifdef WHISKERMGR_DEBUG_MESSAGES
        qDebug()
                << "Immediate-socket command" << msg.causalCommand()
                << "-> reply" << msg.message();
#endif
    return msg;
}


QString WhiskerManager::immResp(const QString& command)
{
    const WhiskerInboundMessage reply = sendImmediateGetReply(command);
    return reply.message();
}


QString WhiskerManager::immResp(const QStringList& args)
{
    return immResp(msgFromArgs(args));
}


QString WhiskerManager::immResp(std::initializer_list<QString> args)
{
    return immResp(msgFromArgs(QStringList(args)));
}


bool WhiskerManager::immBool(const QString& command, bool ignore_reply)
{
    if (ignore_reply) {
        sendImmediateIgnoreReply(command);
        return true;
    }
    const WhiskerInboundMessage msg = sendImmediateGetReply(command);
    return msg.immediateReplySucceeded();
}


bool WhiskerManager::immBool(const QStringList& args, bool ignore_reply)
{
    return immBool(msgFromArgs(args), ignore_reply);
}


bool WhiskerManager::immBool(std::initializer_list<QString> args,
                             bool ignore_reply)
{
    return immBool(msgFromArgs(QStringList(args)), ignore_reply);
}


void WhiskerManager::internalReceiveFromMainSocket(
        const WhiskerInboundMessage& msg)
{
#ifdef WHISKERMGR_DEBUG_MESSAGES
    qDebug() << "Received Whisker main-socket message:" << msg;
#endif

    // Send the message via the general-purpose signal
    emit messageReceived(msg);

    // Send the message to specific-purpose receivers
    if (msg.isEvent()) {
        const bool swallowed = m_internal_callback_handler.processEvent(msg);
        if (!swallowed && !msg.event().startsWith(m_sysevent_prefix)) {
            emit eventReceived(msg);
        }
    } else if (msg.isKeyEvent()) {
        emit keyEventReceived(msg);
    } else if (msg.isClientMessage()) {
        emit clientMessageReceived(msg);
    } else if (msg.isWarning()) {
        qWarning().noquote() << WHISKER_SAYS << msg.message();
        emit warningReceived(msg);
    } else if (msg.isSyntaxError()) {
        qWarning().noquote() << WHISKER_SAYS << msg.message();
        emit syntaxErrorReceived(msg);
    } else if (msg.isError()) {
        qWarning().noquote() << WHISKER_SAYS << msg.message();
        emit errorReceived(msg);
    } else if (msg.isPingAck()) {
        emit pingAckReceived(msg);
    }
}


void WhiskerManager::onSocketError(const QString& msg)
{
    uifunc::alert("Whisker socket error:\n\n" + msg, WHISKER_ALERT_TITLE);
}


// ============================================================================
// Internals for piped events etc.
// ============================================================================

QString WhiskerManager::getNewSysEvent(const QString& suffix)
{
    ++m_sysevent_counter;
    QString event(QString("%1_%2_%3").arg(m_sysevent_prefix,
                                          QString::number(m_sysevent_counter),
                                          suffix));
    return event;
}


void WhiskerManager::clearAllCallbacks()
{
    m_internal_callback_handler.clearCallbacks();
}


void WhiskerManager::sendAfterDelay(unsigned int delay_ms,
                                    const QString& msg,
                                    QString event)
{
    if (event.isEmpty()) {
        event = getNewSysEvent(QString("send_%1").arg(msg));
    }
    timerSetEvent(event, delay_ms, 0, true);
    WhiskerCallbackDefinition::CallbackFunction callback =
            std::bind(&WhiskerManager::sendImmediateIgnoreReply, this, msg);
    m_internal_callback_handler.addSingle(event, callback);
}


void WhiskerManager::callAfterDelay(
        unsigned int delay_ms,
        const WhiskerCallbackDefinition::CallbackFunction& callback,
        QString event)
{
    if (event.isEmpty()) {
        event = getNewSysEvent("callback");
    }
    timerSetEvent(event, delay_ms, 0, true);
    m_internal_callback_handler.addSingle(event, callback);
}


// ============================================================================
// API
// ============================================================================

// ----------------------------------------------------------------------------
// Whisker command set: comms, misc
// ----------------------------------------------------------------------------

bool WhiskerManager::setTimestamps(bool on, bool ignore_reply)
{
    return immBool({CMD_TIMESTAMPS, onVal(on)}, ignore_reply);
}


bool WhiskerManager::resetClock(bool ignore_reply)
{
    return immBool(CMD_RESET_CLOCK, ignore_reply);
}


QString WhiskerManager::getServerVersion()
{
    return immResp(CMD_VERSION);
}


float WhiskerManager::getServerVersionNumeric()
{
    const QString version_str = getServerVersion();
    return version_str.toFloat();
}


unsigned int WhiskerManager::getServerTimeMs()
{
    const QString time_str = immResp(CMD_REQUEST_TIME);
    return time_str.toUInt();
}


int WhiskerManager::getClientNumber()
{
    const QString clientnum_str = immResp(CMD_CLIENT_NUMBER);
    return clientnum_str.toInt();
}


bool WhiskerManager::permitClientMessages(bool permit, bool ignore_reply)
{
    return immBool({CMD_PERMIT_CLIENT_MESSAGES, onVal(permit)}, ignore_reply);
}


bool WhiskerManager::sendToClient(int clientNum, const QString& message,
                                  bool ignore_reply)
{
    return immBool({CMD_SEND_TO_CLIENT, QString::number(clientNum), message},
                   ignore_reply);
}


bool WhiskerManager::setMediaDirectory(const QString& directory,
                                       bool ignore_reply)
{
    return immBool({CMD_SET_MEDIA_DIRECTORY, quote(directory)}, ignore_reply);
}


bool WhiskerManager::reportName(const QString& name, bool ignore_reply)
{
    return immBool({CMD_REPORT_NAME, name}, ignore_reply);
    // quotes not necessary
}


bool WhiskerManager::reportStatus(const QString& status, bool ignore_reply)
{
    return immBool({CMD_REPORT_STATUS, status}, ignore_reply);
    // quotes not necessary
}


bool WhiskerManager::reportComment(const QString& comment, bool ignore_reply)
{
    return immBool({CMD_REPORT_COMMENT, comment}, ignore_reply);
    // quotes not necessary
}


int WhiskerManager::getNetworkLatencyMs()
{
    WhiskerInboundMessage reply_ping = sendImmediateGetReply(
                CMD_TEST_NETWORK_LATENCY);
    if (reply_ping.message() != PING) {
        return FAILURE_INT;
    }
    WhiskerInboundMessage reply_latency = sendImmediateGetReply(PING_ACK);
    bool ok;
    const int latency_ms = reply_latency.message().toInt(&ok);
    if (!ok) {
        return FAILURE_INT;
    }
    return latency_ms;
}


bool WhiskerManager::ping()
{
    return immResp(PING) == PING_ACK;
}


bool WhiskerManager::shutdown(bool ignore_reply)
{
    return immBool(CMD_SHUTDOWN, ignore_reply);
}


QString WhiskerManager::authenticateGetChallenge(const QString& package,
                                                 const QString& client_name)
{
    const QString reply = immResp({CMD_AUTHENTICATE, package, client_name});
    const QStringList parts = reply.split(SPACE);
    if (parts.size() != 2 || parts.at(0) != MSG_AUTHENTICATE_CHALLENGE) {
        return "";
    }
    return parts.at(1);
}


bool WhiskerManager::authenticateProvideResponse(const QString& response,
                                                 bool ignore_reply)
{
    return immBool({CMD_AUTHENTICATE_RESPONSE, response}, ignore_reply);
}


// ----------------------------------------------------------------------------
// Whisker command set: logs
// ----------------------------------------------------------------------------

bool WhiskerManager::logOpen(const QString& filename, bool ignore_reply)
{
    return immBool({CMD_LOG_OPEN, quote(filename)}, ignore_reply);
}


bool WhiskerManager::logSetOptions(const LogOptions& options, bool ignore_reply)
{
    return immBool({
        CMD_LOG_SET_OPTIONS,
        FLAG_EVENTS, onVal(options.events),
        FLAG_KEYEVENTS, onVal(options.key_events),
        FLAG_CLIENTCLIENT, onVal(options.client_client),
        FLAG_COMMS, onVal(options.comms),
        FLAG_SIGNATURE, onVal(options.signature),
    }, ignore_reply);
}


bool WhiskerManager::logPause(bool ignore_reply)
{
    return immBool(CMD_LOG_PAUSE, ignore_reply);
}


bool WhiskerManager::logResume(bool ignore_reply)
{
    return immBool(CMD_LOG_RESUME, ignore_reply);
}


bool WhiskerManager::logWrite(const QString& msg, bool ignore_reply)
{
    return immBool({CMD_LOG_WRITE, msg}, ignore_reply);
}


bool WhiskerManager::logClose(bool ignore_reply)
{
    return immBool(CMD_LOG_CLOSE, ignore_reply);
}


// ----------------------------------------------------------------------------
// Whisker command set: timers
// ----------------------------------------------------------------------------

bool WhiskerManager::timerSetEvent(const QString& event,
                                   unsigned int duration_ms,
                                   int reload_count,
                                   bool ignore_reply)
{
    return immBool({
        CMD_TIMER_SET_EVENT,
        QString::number(duration_ms),
        QString::number(reload_count),
        quote(event),
    }, ignore_reply);
}


bool WhiskerManager::timerClearEvent(const QString& event, bool ignore_reply)
{
    return immBool({CMD_TIMER_CLEAR_EVENT, event}, ignore_reply);
}


bool WhiskerManager::timerClearAllEvents(bool ignore_reply)
{
    return immBool(CMD_TIMER_CLEAR_ALL_EVENTS, ignore_reply);
}

// ----------------------------------------------------------------------------
// Whisker command set: claiming, relinquishing
// ----------------------------------------------------------------------------

bool WhiskerManager::claimGroup(const QString& group,
                                const QString& prefix,
                                const QString& suffix)
{
    QStringList args{CMD_CLAIM_GROUP, group};
    if (!prefix.isEmpty()) {
        args.append({FLAG_PREFIX, prefix});
    }
    if (!suffix.isEmpty()) {
        args.append({FLAG_SUFFIX, suffix});
    }
    return immBool(args);
}


bool WhiskerManager::lineClaim(unsigned int line_number, bool output,
                               const QString& alias, ResetState reset_state)
{
    QStringList args{
        CMD_LINE_CLAIM,
        QString::number(line_number),
        output ? FLAG_OUTPUT : FLAG_INPUT,
        LINE_RESET_FLAGS[output ? reset_state : ResetState::Input],
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::lineClaim(const QString& group, const QString& device,
                               bool output, const QString& alias,
                               ResetState reset_state)
{
    QStringList args{
        CMD_LINE_CLAIM,
        group,
        device,
        output ? FLAG_OUTPUT : FLAG_INPUT,
        LINE_RESET_FLAGS[output ? reset_state : ResetState::Input],
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::lineRelinquishAll(bool ignore_reply)
{
    return immBool(CMD_LINE_RELINQUISH_ALL, ignore_reply);
}


bool WhiskerManager::lineSetAlias(unsigned int line_number,
                                  const QString& alias, bool ignore_reply)
{
    return immBool({CMD_LINE_SET_ALIAS, QString::number(line_number), alias},
                   ignore_reply);
}


bool WhiskerManager::lineSetAlias(const QString& existing_alias,
                                  const QString& new_alias, bool ignore_reply)
{
    return immBool({CMD_LINE_SET_ALIAS, existing_alias, new_alias},
                   ignore_reply);
}



bool WhiskerManager::audioClaim(unsigned int device_number,
                                const QString& alias)
{
    QStringList args{
        CMD_AUDIO_CLAIM,
        QString::number(device_number),
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::audioClaim(const QString& group, const QString& device,
                                const QString& alias)
{
    QStringList args{
        CMD_AUDIO_CLAIM,
        group,
        device,
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::audioSetAlias(unsigned int device_number,
                                   const QString& alias,
                                   bool ignore_reply)
{
    return immBool({CMD_AUDIO_SET_ALIAS, QString::number(device_number), alias},
                   ignore_reply);
}


bool WhiskerManager::audioSetAlias(const QString& existing_alias,
                                   const QString& new_alias,
                                   bool ignore_reply)
{
    return immBool({CMD_AUDIO_SET_ALIAS, existing_alias, new_alias},
                   ignore_reply);
}


bool WhiskerManager::audioRelinquishAll(bool ignore_reply)
{
    return immBool(CMD_AUDIO_RELINQUISH_ALL, ignore_reply);
}


bool WhiskerManager::displayClaim(unsigned int display_number,
                                  const QString& alias)
{
    // Autocreating debug views not supported (see C++ WhiskerClientLib).
    QStringList args{
        CMD_DISPLAY_CLAIM,
        QString::number(display_number),
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::displayClaim(const QString& group, const QString& device,
                                  const QString& alias)
{
    // Autocreating debug views not supported (see C++ WhiskerClientLib).
    QStringList args{
        CMD_DISPLAY_CLAIM,
        group,
        device,
    };
    if (!alias.isEmpty()) {
        args.append({FLAG_ALIAS, alias});
    }
    return immBool(args);
}


bool WhiskerManager::displaySetAlias(unsigned int display_number,
                                     const QString& alias,
                                     bool ignore_reply)
{
    return immBool({CMD_DISPLAY_SET_ALIAS, QString::number(display_number), alias},
                   ignore_reply);
}


bool WhiskerManager::displaySetAlias(const QString& existing_alias,
                                     const QString& new_alias,
                                     bool ignore_reply)
{
    return immBool({CMD_DISPLAY_SET_ALIAS, existing_alias, new_alias},
                   ignore_reply);
}


bool WhiskerManager::displayRelinquishAll(bool ignore_reply)
{
    return immBool(CMD_DISPLAY_RELINQUISH_ALL, ignore_reply);
}


bool WhiskerManager::displayCreateDevice(const QString& name,
                                         DisplayCreationOptions options)
{
    QStringList args{
        CMD_DISPLAY_CREATE_DEVICE,
        name,
        FLAG_RESIZE, onVal(options.resize),
        FLAG_DIRECTDRAW, onVal(options.directdraw),
    };
    if (!options.rectangle.isEmpty()) {
        args.append({
            QString::number(options.rectangle.left()),
            QString::number(options.rectangle.top()),
            QString::number(options.rectangle.width()),
            QString::number(options.rectangle.height()),
        });
    }
    if (options.debug_touches) {
        args.append(FLAG_DEBUG_TOUCHES);
    }
    return immBool(args);
}


bool WhiskerManager::displayDeleteDevice(const QString& device,
                                         bool ignore_reply)
{
    return immBool({CMD_DISPLAY_DELETE_DEVICE, device}, ignore_reply);
}


// ----------------------------------------------------------------------------
// Whisker command set: lines
// ----------------------------------------------------------------------------

bool WhiskerManager::lineSetState(const QString& line, bool on,
                                  bool ignore_reply)
{
    return immBool({CMD_LINE_SET_STATE, line, onVal(on)}, ignore_reply);
}


bool WhiskerManager::lineReadState(const QString& line, bool* ok)
{
    WhiskerInboundMessage reply = immResp({CMD_LINE_READ_STATE, line});
    const QString msg = reply.message();
    if (msg == VAL_ON) {
        // Line is on
        if (ok) {
            *ok = true;
        }
        return true;
    }
    if (msg == VAL_OFF) {
        // Line is off
        if (ok) {
            *ok = true;
        }
        return false;
    }
    // Something went wrong
    if (ok) {
        *ok = false;
    }
    return false;
}


bool WhiskerManager::lineSetEvent(const QString& line, const QString& event,
                                  LineEventType event_type, bool ignore_reply)
{
    return immBool({
        CMD_LINE_SET_EVENT, line, LINE_EVENT_TYPES[event_type], quote(event)
    }, ignore_reply);
}


bool WhiskerManager::lineClearEvent(const QString& event, bool ignore_reply)
{
    return immBool({CMD_LINE_CLEAR_EVENT, event}, ignore_reply);
}


bool WhiskerManager::lineClearEventByLine(const QString& line,
                                          LineEventType event_type,
                                          bool ignore_reply)
{
    return immBool({
        CMD_LINE_CLEAR_EVENTS_BY_LINE, line, LINE_EVENT_TYPES[event_type]
    }, ignore_reply);
}


bool WhiskerManager::lineClearAllEvents(bool ignore_reply)
{
    return immBool(CMD_LINE_CLEAR_ALL_EVENTS, ignore_reply);
}


bool WhiskerManager::lineSetSafetyTimer(const QString& line,
                                        unsigned int time_ms,
                                        SafetyState safety_state,
                                        bool ignore_reply)
{
    return immBool({
        CMD_LINE_SET_SAFETY_TIMER, line, QString::number(time_ms),
        LINE_SAFETY_STATES[safety_state],
    }, ignore_reply);
}


bool WhiskerManager::lineClearSafetyTimer(const QString& line,
                                          bool ignore_reply)
{
    return immBool({CMD_LINE_CLEAR_SAFETY_TIMER, line}, ignore_reply);
}


// ----------------------------------------------------------------------------
// Whisker command set: audio
// ----------------------------------------------------------------------------

bool WhiskerManager::audioPlayWav(const QString& device,
                                  const QString& filename,
                                  bool ignore_reply)
{
    return immBool({CMD_AUDIO_PLAY_FILE, device, quote(filename)},
                   ignore_reply);
}


bool WhiskerManager::audioLoadTone(const QString& device,
                                   const QString& sound_name,
                                   unsigned int frequency_hz,
                                   whiskerconstants::ToneType tone_type,
                                   unsigned int duration_ms,
                                   bool ignore_reply)
{
    return immBool({
        CMD_AUDIO_LOAD_TONE,
        device,
        sound_name,
        QString::number(frequency_hz),
        AUDIO_TONE_TYPES[tone_type],
        QString::number(duration_ms),
    }, ignore_reply);
    // 2018-09-04: Whisker docs fixed (optional duration_ms parameter wasn't
    // mentioned).
}


bool WhiskerManager::audioLoadWav(const QString& device,
                                  const QString& sound_name,
                                  const QString& filename,
                                  bool ignore_reply)
{
    return immBool({CMD_AUDIO_LOAD_SOUND, device, sound_name, quote(filename)},
                   ignore_reply);
}


bool WhiskerManager::audioPlaySound(const QString& device,
                                    const QString& sound_name,
                                    bool loop, bool ignore_reply)
{
    QStringList args{CMD_AUDIO_PLAY_SOUND, device, sound_name};
    if (loop) {
        args.append(FLAG_LOOP);
    }
    return immBool(args, ignore_reply);
}


bool WhiskerManager::audioUnloadSound(const QString& device,
                                      const QString& sound_name,
                                      bool ignore_reply)
{
    return immBool({CMD_AUDIO_UNLOAD_SOUND, device, sound_name}, ignore_reply);
}


bool WhiskerManager::audioStopSound(const QString& device,
                                    const QString& sound_name,
                                    bool ignore_reply)
{
    return immBool({CMD_AUDIO_STOP_SOUND, device, sound_name}, ignore_reply);
}


bool WhiskerManager::audioSilenceDevice(const QString& device,
                                        bool ignore_reply)
{
    return immBool({CMD_AUDIO_SILENCE_DEVICE, device}, ignore_reply);
}


bool WhiskerManager::audioUnloadAll(const QString& device, bool ignore_reply)
{
    return immBool({CMD_AUDIO_UNLOAD_ALL, device}, ignore_reply);
}


bool WhiskerManager::audioSetSoundVolume(const QString& device,
                                         const QString& sound_name,
                                         unsigned int volume,
                                         bool ignore_reply)
{
    return immBool({
        CMD_AUDIO_SET_SOUND_VOLUME, device, sound_name, QString::number(volume)
    }, ignore_reply);
}


bool WhiskerManager::audioSilenceAllDevices(bool ignore_reply)
{
    return immBool(CMD_AUDIO_SILENCE_ALL_DEVICES, ignore_reply);
}


unsigned int WhiskerManager::audioGetSoundDurationMs(const QString& device,
                                                     const QString& sound_name,
                                                     bool* ok)
{
    const QString reply = immResp(
        {CMD_AUDIO_GET_SOUND_LENGTH, device, sound_name});
    return reply.toUInt(ok);
}


// ----------------------------------------------------------------------------
// Whisker command set: display: display operations
// ----------------------------------------------------------------------------

QSize WhiskerManager::displayGetSize(const QString& device)
{
    const QString reply = immResp({CMD_DISPLAY_GET_SIZE, device});
    const QStringList parts = reply.split(SPACE);
    if (parts.size() != 3 || parts.at(0) != MSG_SIZE) {
        return QSize();
    }
    bool ok;
    const int width = parts.at(1).toInt(&ok);
    if (!ok) {
        return QSize();
    }
    const int height = parts.at(2).toInt(&ok);
    if (!ok) {
        return QSize();
    }
    return QSize(width, height);
}


bool WhiskerManager::displayScaleDocuments(const QString& device,
                                           bool scale, bool ignore_reply)
{
    return immBool({CMD_DISPLAY_SCALE_DOCUMENTS, device, onVal(scale)},
                   ignore_reply);
}


bool WhiskerManager::displayShowDocument(const QString& device,
                                         const QString& doc,
                                         bool ignore_reply)
{
    return immBool({CMD_DISPLAY_SHOW_DOCUMENT, device, doc}, ignore_reply);
}


bool WhiskerManager::displayBlank(const QString& device, bool ignore_reply)
{
    return immBool({CMD_DISPLAY_BLANK, device}, ignore_reply);
}


// ----------------------------------------------------------------------------
// Whisker command set: display: document operations
// ----------------------------------------------------------------------------

bool WhiskerManager::displayCreateDocument(const QString& doc,
                                           bool ignore_reply)
{
    return immBool({CMD_DISPLAY_CREATE_DOCUMENT, doc}, ignore_reply);
}


bool WhiskerManager::displayDeleteDocument(const QString& doc,
                                           bool ignore_reply)
{
    return immBool({CMD_DISPLAY_DELETE_DOCUMENT, doc}, ignore_reply);
}


bool WhiskerManager::displaySetDocumentSize(const QString& doc,
                                            const QSize& size,
                                            bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_SET_DOCUMENT_SIZE,
        doc,
        QString::number(size.width()),
        QString::number(size.height()),
    }, ignore_reply);
}


bool WhiskerManager::displaySetBackgroundColour(const QString& doc,
                                                const QColor& colour,
                                                bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_SET_BACKGROUND_COLOUR,
        doc,
        rgbFromColour(colour),
    }, ignore_reply);
}


bool WhiskerManager::displayDeleteObject(const QString& doc,
                                         const QString& obj,
                                         bool ignore_reply)
{
    return immBool({CMD_DISPLAY_DELETE_OBJECT, doc, obj}, ignore_reply);
}


bool WhiskerManager::displayAddObject(
        const QString& doc, const QString& obj,
        const DisplayObject& object_definition, bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_ADD_OBJECT,
        doc, obj,
        object_definition.optionString(),
    }, ignore_reply);
}


bool WhiskerManager::displaySetEvent(const QString& doc,
                                     const QString& obj,
                                     DocEventType event_type,
                                     const QString& event,
                                     bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_SET_EVENT,
        doc,
        obj,
        DOC_EVENT_TYPES[event_type],
        quote(event),
    }, ignore_reply);
}


bool WhiskerManager::displayClearEvent(const QString& doc,
                                       const QString& obj,
                                       DocEventType event_type,
                                       bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_CLEAR_EVENT,
        doc,
        obj,
        DOC_EVENT_TYPES[event_type],
    }, ignore_reply);
}


bool WhiskerManager::displaySetObjectEventTransparency(
        const QString& doc, const QString& obj,
        bool transparent, bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_SET_OBJ_EVENT_TRANSPARENCY,
        doc,
        obj,
        onVal(transparent),
    }, ignore_reply);
}


bool WhiskerManager::displayEventCoords(bool on, bool ignore_reply)
{
    return immBool({CMD_DISPLAY_EVENT_COORDS, onVal(on)}, ignore_reply);
}


bool WhiskerManager::displayBringToFront(const QString& doc,
                                         const QString& obj,
                                         bool ignore_reply)
{
    return immBool({CMD_DISPLAY_BRING_TO_FRONT, doc, obj}, ignore_reply);
}


bool WhiskerManager::displaySendToBack(const QString& doc,
                                       const QString& obj,
                                       bool ignore_reply)
{
    return immBool({CMD_DISPLAY_BRING_TO_FRONT, doc, obj}, ignore_reply);
}


bool WhiskerManager::displayKeyboardEvents(const QString& doc,
                                           KeyEventType key_event_type,
                                           bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_KEYBOARD_EVENTS,
        doc,
        KEY_EVENT_TYPES[key_event_type],
    }, ignore_reply);
}


bool WhiskerManager::displayCacheChanges(const QString& doc,
                                         bool ignore_reply)
{
    return immBool({CMD_DISPLAY_CACHE_CHANGES, doc}, ignore_reply);
}


bool WhiskerManager::displayShowChanges(const QString& doc,
                                        bool ignore_reply)
{
    return immBool({CMD_DISPLAY_SHOW_CHANGES, doc}, ignore_reply);
}


QSize WhiskerManager::displayGetDocumentSize(const QString& doc)
{
    const QString reply = immResp({CMD_DISPLAY_GET_DOCUMENT_SIZE, doc});
    const QStringList parts = reply.split(SPACE);
    if (parts.size() != 3 || parts.at(0) != MSG_SIZE) {
        return QSize();
    }
    bool ok;
    const int width = parts.at(1).toInt(&ok);
    if (!ok) {
        return QSize();
    }
    const int height = parts.at(2).toInt(&ok);
    if (!ok) {
        return QSize();
    }
    return QSize(width, height);
}


QRect WhiskerManager::displayGetObjectExtent(const QString& doc,
                                             const QString& obj)
{
    const QString reply = immResp({CMD_DISPLAY_GET_OBJECT_EXTENT, doc, obj});
    const QStringList parts = reply.split(SPACE);
    if (parts.size() != 5 || parts.at(0) != MSG_EXTENT) {
        return QRect();
    }
    bool ok;
    const int left = parts.at(1).toInt(&ok);
    if (!ok) {
        return QRect();
    }
    const int right = parts.at(2).toInt(&ok);
    if (!ok) {
        return QRect();
    }
    const int top = parts.at(3).toInt(&ok);
    if (!ok) {
        return QRect();
    }
    const int bottom = parts.at(4).toInt(&ok);
    if (!ok) {
        return QRect();
    }
    const int width = right - left;
    const int height = bottom - top;
    return QRect(left, top, width, height);
    // The Whisker coordinate system has its origin at the TOP LEFT, with
    // positive x to the right, and positive y down.
    // This is the same as the default Qt coordinate system.
}


bool WhiskerManager::displaySetBackgroundEvent(const QString& doc,
                                               DocEventType event_type,
                                               const QString& event,
                                               bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_SET_BACKGROUND_EVENT,
        doc,
        DOC_EVENT_TYPES[event_type],
        quote(event),
    }, ignore_reply);
}


bool WhiskerManager::displayClearBackgroundEvent(const QString& doc,
                                                 DocEventType event_type,
                                                 bool ignore_reply)
{
    return immBool({
        CMD_DISPLAY_CLEAR_BACKGROUND_EVENT,
        doc,
        DOC_EVENT_TYPES[event_type],
    }, ignore_reply);
}

// ----------------------------------------------------------------------------
// Whisker command set: display: specific object creation
// ----------------------------------------------------------------------------
// ... all superseded by calls to displayAddObject().


// ----------------------------------------------------------------------------
// Whisker command set: display: video extras
// ----------------------------------------------------------------------------

bool WhiskerManager::displaySetAudioDevice(const QString& display_device,
                                           const QString& audio_device,
                                           bool ignore_reply)
{
    // Devices may be specified as numbers or names.
    return immBool(
        {CMD_DISPLAY_SET_AUDIO_DEVICE, display_device, audio_device},
        ignore_reply
    );
}


bool WhiskerManager::videoPlay(const QString& doc, const QString& video,
                               bool ignore_reply)
{
    return immBool({CMD_VIDEO_PLAY, doc, video}, ignore_reply);
}


bool WhiskerManager::videoPause(const QString& doc, const QString& video,
                                bool ignore_reply)
{
    return immBool({CMD_VIDEO_PAUSE, doc, video}, ignore_reply);
}


bool WhiskerManager::videoStop(const QString& doc, const QString& video,
                               bool ignore_reply)
{
    return immBool({CMD_VIDEO_STOP, doc, video}, ignore_reply);
}


bool WhiskerManager::videoTimestamps(bool on, bool ignore_reply)
{
    return immBool({CMD_VIDEO_TIMESTAMPS, onVal(on)}, ignore_reply);
}


unsigned int WhiskerManager::videoGetTimeMs(const QString& doc,
                                            const QString& video,
                                            bool* ok)
{
    const QString reply = immResp({CMD_VIDEO_GET_TIME, doc, video});
    const QStringList parts = reply.split(SPACE);
    const unsigned int failure = 0;
    if (parts.size() != 2 || parts.at(0) != MSG_VIDEO_TIME) {
        if (ok) {
            *ok = false;
        }
        return failure;
    }
    return parts.at(1).toUInt(ok);
}


unsigned int WhiskerManager::videoGetDurationMs(const QString& doc,
                                                const QString& video,
                                                bool* ok)
{
    const QString reply = immResp({CMD_VIDEO_GET_DURATION, doc, video});
    const QStringList parts = reply.split(SPACE);
    const unsigned int failure = 0;
    if (parts.size() != 2 || parts.at(0) != MSG_DURATION) {
        if (ok) {
            *ok = false;
        }
        return failure;
    }
    return parts.at(1).toUInt(ok);
}


bool WhiskerManager::videoSeekRelative(const QString& doc,
                                       const QString& video,
                                       int relative_time_ms,
                                       bool ignore_reply)
{
    return immBool({
        CMD_VIDEO_SEEK_RELATIVE, doc, video, QString::number(relative_time_ms)
    }, ignore_reply);
}


bool WhiskerManager::videoSeekAbsolute(const QString& doc,
                                       const QString& video,
                                       unsigned int absolute_time_ms,
                                       bool ignore_reply)
{
    return immBool({
        CMD_VIDEO_SEEK_ABSOLUTE, doc, video, QString::number(absolute_time_ms)
    }, ignore_reply);
}


bool WhiskerManager::videoSetVolume(const QString& doc, const QString& video,
                                    unsigned int volume, bool ignore_reply)
{
    return immBool({
        CMD_VIDEO_SET_VOLUME, doc, video, QString::number(volume)
    }, ignore_reply);
}


// ----------------------------------------------------------------------------
// Shortcuts to Whisker commands
// ----------------------------------------------------------------------------

bool WhiskerManager::lineOn(const QString& line, bool ignore_reply)
{
    return lineSetState(line, true, ignore_reply);
}


bool WhiskerManager::lineOff(const QString& line, bool ignore_reply)
{
    return lineSetState(line, false, ignore_reply);
}


bool WhiskerManager::broadcast(const QString& message, bool ignore_reply)
{
    return sendToClient(VAL_BROADCAST_TO_ALL_CLIENTS, message, ignore_reply);
}


// ----------------------------------------------------------------------------
// Line flashing
// ----------------------------------------------------------------------------

unsigned int WhiskerManager::flashLinePulses(const QString& line,
                                             unsigned int count,
                                             unsigned int on_ms,
                                             unsigned int off_ms,
                                             bool on_at_rest)
{
    // Returns the total estimated time.
    //
    // This method uses Whisker timers in a ping-pong fashion.
    // ALTERNATIVES:
    // - use Whisker and line up the events in advance
    //   ... but a risk if the user specifies very rapid oscillation that
    //       exceeds the network bandwidth, or something; better to be slow
    //       than to garbage up the sequence.
    // - use Qt QTimer calls internally
    //   ... definitely a possibility, but we built Whisker to be particularly
    //       aggressive about accurate timing; it's a tradeoff between that and
    //       network delays; a toss-up here.

    if (count == 0) {
        qWarning() << Q_FUNC_INFO << "count == 0; daft";
        return 0;
    }

    if (on_at_rest) {
        // Assumed to be currently at rest = on.
        // For 4 flashes:
        // OFF .. ON .... OFF .. ON .... OFF .. ON .... OFF .. ON
        //                                                     | time stops
        flashLinePulsesOff(line, count, on_ms, off_ms, on_at_rest);
        return count * off_ms + (count - 1) * on_ms;
    }

    // Assumed to be currently at rest = off.
    // For 4 flashes:
    // ON .... OFF .. ON .... OFF .. ON .... OFF .. ON .... OFF
    //                                                      | time stops
    flashLinePulsesOn(line, count, on_ms, off_ms, on_at_rest);
    return count * on_ms + (count - 1) * off_ms;
}


void WhiskerManager::flashLinePulsesOn(const QString& line,
                                       unsigned int count,
                                       unsigned int on_ms,
                                       unsigned int off_ms,
                                       bool on_at_rest)
{
    lineOn(line);
    if (on_at_rest) {  // cycle complete
        --count;
        if (count <= 0) {
            return;
        }
    }
    WhiskerCallbackDefinition::CallbackFunction callback =
            std::bind(&WhiskerManager::flashLinePulsesOff, this,
                      line, count, on_ms, off_ms, on_at_rest);
    callAfterDelay(on_ms, callback);
}


void WhiskerManager::flashLinePulsesOff(const QString& line,
                                        unsigned int count,
                                        unsigned int on_ms,
                                        unsigned int off_ms,
                                        bool on_at_rest)
{
    lineOff(line);
    if (!on_at_rest) {  // cycle complete
        --count;
        if (count <= 0) {
            return;
        }
    }
    WhiskerCallbackDefinition::CallbackFunction callback =
            std::bind(&WhiskerManager::flashLinePulsesOn, this,
                      line, count, on_ms, off_ms, on_at_rest);
    callAfterDelay(off_ms, callback);
}


void WhiskerManager::disconnectServerAndSignals(QObject* receiver)
{
    disconnectAllWhiskerSignals(receiver);
    emit disconnectFromServer();
}


void WhiskerManager::disconnectAllWhiskerSignals(QObject* receiver)
{
    // Boilerplate function to disconnect all signals coming from this
    // (WhiskerManager) object to any of the receiver's slots.

    disconnect(receiver, nullptr);

    /*

    Internally, this is the sequence:

    [qobject.h]

    inline bool disconnect(const QObject *receiver, const char *member = nullptr) const
        { return disconnect(this, nullptr, receiver, member); }

    [qobject.cpp]

    bool QObject::disconnect(const QObject *sender, const char *signal,
                             const QObject *receiver, const char *method)
    {
        // ...
            if (!method) {
                res |= QMetaObjectPrivate::disconnect(sender, signal_index, smeta, receiver, -1, 0);
                // i.e. method_index == -1
        // ...
    }

    bool QMetaObjectPrivate::disconnect(...)
    {
        // ... calls disconnectHelper(), passes along method_index
    }

    bool QMetaObjectPrivate::disconnectHelper(QObjectPrivate::Connection *c,
                                              const QObject *receiver, int method_index, void **slot,
                                              QMutex *senderMutex, DisconnectType disconnectType)
    {
        while (c) {
            if (c->receiver
                && (receiver == 0 || (c->receiver == receiver
                               && (method_index < 0 ...
        // ...
    }

    */
}