15.1.969. 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 ...
        // ...
    }

    */
}