15.1.985. tablet_qt/whisker/whiskermanager.h

/*
    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/>.
*/

#pragma once

/*

GENERAL THREADING APPROACH FOR WHISKER CLIENT
===============================================================================

- QTcpSocket can run via an event-driven system using the readyRead() signal,
  or a blocking system using waitForReadyRead(). The docs warn that
  waitForReadyRead() can fail randomly under Windows, so that means we must
  use readyRead()
  [https://doc.qt.io/qt-6.5/qabstractsocket.html#waitForReadyRead].

- We must presume that the end user will run the task on the GUI thread (which
  is the worst-case scenario; if a separate thread is used, it can do what it
  likes, but if it uses the GUI thread, it mustn't sit there and spin-wait).

- The Whisker side of things mustn't care which thread the user decides to run
  the task on, though. That means that if the task calls a function to send
  data, that data must cross to a socket-owning thread.
  For the main socket, the simplest thing is to use Qt signals/slots, since
  they handle the thread boundary. So that means something like:

    class WhiskerController {
    signals:
        void eventReceived(const QString& event);
    slots:
        void sendToServer(const QString& command);
    };

- Then the tricky bit is the blocking call:

    QString sendImmediateGetReply(const QString& command);

- Remember that there are several ways to use a QThread:
  - let the QThread do its default run(), which runs a Qt event loop, and
    give it (via QObject::moveToThread()) objects that do useful things via
    signals; or
  - override run().
  See https://doc.qt.io/qt-6.5/qthread.html.

- So the simplest way will be to have a WhiskerWorker that's derived from
  QObject, and for WhiskerManager to put that into a new thread.

*/

#include <QObject>
#include <QPointer>
#include <QSize>
#include <QThread>

#include "whisker/whiskerapi.h"
#include "whisker/whiskercallbackhandler.h"
#include "whisker/whiskerconnectionstate.h"
#include "whisker/whiskerconstants.h"

class CamcopsApp;
class WhiskerInboundMessage;
class WhiskerOutboundCommand;
class WhiskerWorker;

class WhiskerManager : public QObject
{
    // High-level object to communicate with a Whisker server, and provide its
    // API. Owned by the GUI thread. (Uses a worker thread for socket
    // communications.)

    Q_OBJECT

public:
    // Constructor.
    WhiskerManager(
        QObject* parent = nullptr, const QString& sysevent_prefix = "sys"
    );

    // Destructor.
    ~WhiskerManager();

    // Send a message via the main socket.
    void sendMain(const QString& command);
    void sendMain(const QStringList& args);
    void sendMain(std::initializer_list<QString> args);

    // Send a message via the immediate socket, ignoring the reply.
    void sendImmediateIgnoreReply(const QString& command);

    // Send a message via the immediate socket, returning the reply.
    WhiskerInboundMessage sendImmediateGetReply(const QString& command);
    QString immResp(const QString& command);
    QString immResp(const QStringList& args);
    QString immResp(std::initializer_list<QString> args);

    // Send a message via the immediate socket, returning "did the reply
    // indicate success?"
    bool immBool(const QString& command, bool ignore_reply = false);
    bool immBool(const QStringList& args, bool ignore_reply = false);
    bool immBool(
        std::initializer_list<QString> args, bool ignore_reply = false
    );

    // Connect to a Whisker server.
    void connectToServer(const QString& host, quint16 main_port);

    // Are we fully connected.
    bool isConnected() const;

    // Are we fully disconnected?
    bool isFullyDisconnected() const;

    // Provide a user alert that we are not connected.
    void alertNotConnected() const;

    // Calls disconnectAllWhiskerSignals(), then emits disconnectFromServer().
    void disconnectServerAndSignals(QObject* receiver);

signals:
    // this -> worker: "please disconnect from the Whisker server".
    void disconnectFromServer();

    // Worker -> this -> world: "Whisker connection state has changed".
    void connectionStateChanged(WhiskerConnectionState state);

    // Worker -> this -> world: "Fully connected to Whisker server."
    void onFullyConnected();

    // "Whisker message received."
    void messageReceived(const WhiskerInboundMessage& msg);

    // "Whisker event received."
    void eventReceived(const WhiskerInboundMessage& msg);

    // "Whisker key event received."
    void keyEventReceived(const WhiskerInboundMessage& msg);

    // "Whisker client-to-client message received."
    void clientMessageReceived(const WhiskerInboundMessage& msg);

    // "Warning received from Whisker."
    void warningReceived(const WhiskerInboundMessage& msg);

    // "Syntax error received from Whisker."
    void syntaxErrorReceived(const WhiskerInboundMessage& msg);

    // "Error received from Whisker."
    void errorReceived(const WhiskerInboundMessage& msg);

    // "Ping acknowledgement received from Whisker."
    void pingAckReceived(const WhiskerInboundMessage& msg);

    // this -> worker: "please connect to the Whisker server".
    void internalConnectToServer(const QString& host, quint16 main_port);

    // this -> worker: "send message to the Whisker server".
    void internalSend(const WhiskerOutboundCommand& cmd);

public slots:
    // worker -> this: "Message received from server main socket."
    void internalReceiveFromMainSocket(const WhiskerInboundMessage& msg);

    // Worker -> this -> world: "Whisker socket error has occurred."
    void onSocketError(const QString& msg);

protected:
    // Disconnect all signals from "this" to "receiver".
    void disconnectAllWhiskerSignals(QObject* receiver);

    // Return a new event name for a system event.
    // The name is of the format
    // <m_sysevent_prefix><m_sysevent_counter><suffix>.
    QString getNewSysEvent(const QString& suffix = "");

    // Clear all user-defined Whisker event callbacks.
    void clearAllCallbacks();

    // Send a message to the Whisker server after a delay (using a Whisker
    // timer for that delay).
    // If the event name is not specified, a new system event name is created.
    void sendAfterDelay(
        unsigned int delay_ms, const QString& msg, QString event = ""
    );

    // Call a user function after a delay, via a Whisker timer event.
    // If the event name is not specified, a new system event name is created.
    void callAfterDelay(
        unsigned int delay_ms,
        const WhiskerCallbackDefinition::CallbackFunction& callback,
        QString event = ""
    );

protected:
    QThread m_worker_thread;  // worker thread to talk to sockets
    QPointer<WhiskerWorker> m_worker;  // worker object; lives in worker thread
    QString m_sysevent_prefix;  // prefix for all "system" events
    quint64 m_sysevent_counter;  // counter to make system events unique
    WhiskerCallbackHandler m_internal_callback_handler;  // manages callbacks

    // ========================================================================
    // Whisker API: see http://www.whiskercontrol.com/
    // ========================================================================

public:
    // ------------------------------------------------------------------------
    // Whisker command set: comms, misc
    // ------------------------------------------------------------------------
    bool setTimestamps(bool on, bool ignore_reply = false);
    bool resetClock(bool ignore_reply = false);
    QString getServerVersion();
    float getServerVersionNumeric();
    unsigned int getServerTimeMs();
    int getClientNumber();
    bool permitClientMessages(bool permit, bool ignore_reply = false);
    bool sendToClient(
        int clientNum, const QString& message, bool ignore_reply = false
    );
    bool
        setMediaDirectory(const QString& directory, bool ignore_reply = false);
    bool reportName(const QString& name, bool ignore_reply = false);
    bool reportStatus(const QString& status, bool ignore_reply = false);
    bool reportComment(const QString& comment, bool ignore_reply = false);
    int getNetworkLatencyMs();  // whiskerconstants::FAILURE_INT for failure
    bool ping();
    bool shutdown(bool ignore_reply = false);
    QString authenticateGetChallenge(
        const QString& package, const QString& client_name
    );
    bool authenticateProvideResponse(
        const QString& response, bool ignore_reply = false
    );

    // ------------------------------------------------------------------------
    // Whisker command set: logs
    // ------------------------------------------------------------------------
    bool logOpen(const QString& filename, bool ignore_reply = false);
    bool logSetOptions(
        const whiskerapi::LogOptions& options, bool ignore_reply = false
    );
    bool logPause(bool ignore_reply = false);
    bool logResume(bool ignore_reply = false);
    bool logWrite(const QString& msg, bool ignore_reply = false);
    bool logClose(bool ignore_reply = false);

    // ------------------------------------------------------------------------
    // Whisker command set: timers
    // ------------------------------------------------------------------------
    bool timerSetEvent(
        const QString& event,
        unsigned int duration_ms,
        int reload_count = 0,
        bool ignore_reply = false
    );
    bool timerClearEvent(const QString& event, bool ignore_reply = false);
    bool timerClearAllEvents(bool ignore_reply = false);

    // ------------------------------------------------------------------------
    // Whisker command set: claiming, relinquishing
    // ------------------------------------------------------------------------
    bool claimGroup(
        const QString& group,
        const QString& prefix = "",
        const QString& suffix = ""
    );
    bool lineClaim(
        unsigned int line_number,
        bool output,
        const QString& alias = "",
        whiskerconstants::ResetState reset_state
        = whiskerconstants::ResetState::Leave
    );
    bool lineClaim(
        const QString& group,
        const QString& device,
        bool output,
        const QString& alias = "",
        whiskerconstants::ResetState reset_state
        = whiskerconstants::ResetState::Leave
    );
    bool lineRelinquishAll(bool ignore_reply = false);
    bool lineSetAlias(
        unsigned int line_number,
        const QString& alias,
        bool ignore_reply = false
    );
    bool lineSetAlias(
        const QString& existing_alias,
        const QString& new_alias,
        bool ignore_reply = false
    );
    bool audioClaim(unsigned int device_number, const QString& alias = "");
    bool audioClaim(
        const QString& group, const QString& device, const QString& alias = ""
    );
    bool audioSetAlias(
        unsigned int device_number,
        const QString& alias,
        bool ignore_reply = false
    );
    bool audioSetAlias(
        const QString& existing_alias,
        const QString& new_alias,
        bool ignore_reply = false
    );
    bool audioRelinquishAll(bool ignore_reply = false);
    bool displayClaim(unsigned int display_number, const QString& alias = "");
    bool displayClaim(
        const QString& group, const QString& device, const QString& alias = ""
    );
    bool displaySetAlias(
        unsigned int display_number,
        const QString& alias,
        bool ignore_reply = false
    );
    bool displaySetAlias(
        const QString& existing_alias,
        const QString& new_alias,
        bool ignore_reply = false
    );
    bool displayRelinquishAll(bool ignore_reply = false);
    bool displayCreateDevice(
        const QString& name, whiskerapi::DisplayCreationOptions options
    );
    bool displayDeleteDevice(const QString& device, bool ignore_reply = false);

    // ------------------------------------------------------------------------
    // Whisker command set: lines
    // ------------------------------------------------------------------------
    bool lineSetState(const QString& line, bool on, bool ignore_reply = false);
    bool lineReadState(const QString& line, bool* ok = nullptr);
    bool lineSetEvent(
        const QString& line,
        const QString& event,
        whiskerconstants::LineEventType event_type
        = whiskerconstants::LineEventType::On,
        bool ignore_reply = false
    );
    bool lineClearEvent(const QString& event, bool ignore_reply);
    bool lineClearEventByLine(
        const QString& line,
        whiskerconstants::LineEventType event_type,
        bool ignore_reply = false
    );
    bool lineClearAllEvents(bool ignore_reply = false);
    bool lineSetSafetyTimer(
        const QString& line,
        unsigned int time_ms,
        whiskerconstants::SafetyState safety_state,
        bool ignore_reply = false
    );
    bool lineClearSafetyTimer(const QString& line, bool ignore_reply = false);

    // ------------------------------------------------------------------------
    // Whisker command set: audio
    // ------------------------------------------------------------------------
    bool audioPlayWav(
        const QString& device,
        const QString& filename,
        bool ignore_reply = false
    );
    bool audioLoadTone(
        const QString& device,
        const QString& sound_name,
        unsigned int frequency_hz,
        whiskerconstants::ToneType tone_type,
        unsigned int duration_ms,
        bool ignore_reply = false
    );
    bool audioLoadWav(
        const QString& device,
        const QString& sound_name,
        const QString& filename,
        bool ignore_reply = false
    );
    bool audioPlaySound(
        const QString& device,
        const QString& sound_name,
        bool loop = false,
        bool ignore_reply = false
    );
    bool audioUnloadSound(
        const QString& device,
        const QString& sound_name,
        bool ignore_reply = false
    );
    bool audioStopSound(
        const QString& device,
        const QString& sound_name,
        bool ignore_reply = false
    );
    bool audioSilenceDevice(const QString& device, bool ignore_reply = false);
    bool audioUnloadAll(const QString& device, bool ignore_reply = false);
    bool audioSetSoundVolume(
        const QString& device,
        const QString& sound_name,
        unsigned int volume,
        bool ignore_reply = false
    );
    bool audioSilenceAllDevices(bool ignore_reply = false);
    unsigned int audioGetSoundDurationMs(
        const QString& device, const QString& sound_name, bool* ok = nullptr
    );

    // ------------------------------------------------------------------------
    // Whisker command set: display: display operations
    // ------------------------------------------------------------------------
    QSize displayGetSize(const QString& device);
    bool displayScaleDocuments(
        const QString& device, bool scale = true, bool ignore_reply = false
    );
    bool displayShowDocument(
        const QString& device, const QString& doc, bool ignore_reply = false
    );
    bool displayBlank(const QString& device, bool ignore_reply = false);

    // ------------------------------------------------------------------------
    // Whisker command set: display: document operations
    // ------------------------------------------------------------------------
    bool displayCreateDocument(const QString& doc, bool ignore_reply = false);
    bool displayDeleteDocument(const QString& doc, bool ignore_reply = false);
    bool displaySetDocumentSize(
        const QString& doc, const QSize& size, bool ignore_reply = false
    );
    bool displaySetBackgroundColour(
        const QString& doc, const QColor& colour, bool ignore_reply = false
    );
    bool displayDeleteObject(
        const QString& doc, const QString& obj, bool ignore_reply = false
    );
    bool displayAddObject(
        const QString& doc,
        const QString& obj,
        const whiskerapi::DisplayObject& object_definition,
        bool ignore_reply = false
    );
    // ... can be used with any derived class too, e.g. TextObject
    bool displaySetEvent(
        const QString& doc,
        const QString& obj,
        whiskerconstants::DocEventType event_type,
        const QString& event,
        bool ignore_reply = false
    );
    bool displayClearEvent(
        const QString& doc,
        const QString& obj,
        whiskerconstants::DocEventType event_type,
        bool ignore_reply = false
    );
    bool displaySetObjectEventTransparency(
        const QString& doc,
        const QString& obj,
        bool transparent,
        bool ignore_reply = false
    );
    bool displayEventCoords(bool on, bool ignore_reply = false);
    bool displayBringToFront(
        const QString& doc, const QString& obj, bool ignore_reply = false
    );
    bool displaySendToBack(
        const QString& doc, const QString& obj, bool ignore_reply = false
    );
    bool displayKeyboardEvents(
        const QString& doc,
        whiskerconstants::KeyEventType key_event_type
        = whiskerconstants::KeyEventType::Down,
        bool ignore_reply = false
    );
    bool displayCacheChanges(const QString& doc, bool ignore_reply = false);
    bool displayShowChanges(const QString& doc, bool ignore_reply = false);
    QSize displayGetDocumentSize(const QString& doc);
    QRect displayGetObjectExtent(const QString& doc, const QString& obj);
    bool displaySetBackgroundEvent(
        const QString& doc,
        whiskerconstants::DocEventType event_type,
        const QString& event,
        bool ignore_reply = false
    );
    bool displayClearBackgroundEvent(
        const QString& doc,
        whiskerconstants::DocEventType event_type,
        bool ignore_reply = false
    );

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

    // ------------------------------------------------------------------------
    // Whisker command set: display: video extras
    // ------------------------------------------------------------------------
    bool displaySetAudioDevice(
        const QString& display_device,
        const QString& audio_device,
        bool ignore_reply = false
    );
    bool videoPlay(
        const QString& doc, const QString& video, bool ignore_reply = false
    );
    bool videoPause(
        const QString& doc, const QString& video, bool ignore_reply = false
    );
    bool videoStop(
        const QString& doc, const QString& video, bool ignore_reply = false
    );
    bool videoTimestamps(bool on, bool ignore_reply = false);
    unsigned int videoGetTimeMs(
        const QString& doc, const QString& video, bool* ok = nullptr
    );
    unsigned int videoGetDurationMs(
        const QString& doc, const QString& video, bool* ok = nullptr
    );
    bool videoSeekRelative(
        const QString& doc,
        const QString& video,
        int relative_time_ms,
        bool ignore_reply = false
    );
    bool videoSeekAbsolute(
        const QString& doc,
        const QString& video,
        unsigned int absolute_time_ms,
        bool ignore_reply = false
    );
    bool videoSetVolume(
        const QString& doc,
        const QString& video,
        unsigned int volume,
        bool ignore_reply = false
    );

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

    // Shorthand for lineSetState(line, true, ignore_reply).
    bool lineOn(const QString& line, bool ignore_reply = false);

    // Shorthand for lineSetState(line, false, ignore_reply).
    bool lineOff(const QString& line, bool ignore_reply = false);

    // Broadcast to all other Whisker clients. Shorthand for
    // sendToClient(VAL_BROADCAST_TO_ALL_CLIENTS, message, ignore_reply).
    bool broadcast(const QString& message, bool ignore_reply = false);

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

    // "Flash" a digital output line "count" times, where the "on" phase lasts
    // on_ms and the "off" phase lasts off_ms.
    // - Flip on_at_rest for a line that is reversed (on by default and you are
    //   flashing it "off").
    // - Returns the total estimated time, in ms.
    unsigned int flashLinePulses(
        const QString& line,
        unsigned int count,
        unsigned int on_ms,
        unsigned int off_ms,
        bool on_at_rest = false
    );

protected:
    // Worker function for flashLinePulses().
    void flashLinePulsesOn(
        const QString& line,
        unsigned int count,
        unsigned int on_ms,
        unsigned int off_ms,
        bool on_at_rest
    );

    // Worker function for flashLinePulses().
    void flashLinePulsesOff(
        const QString& line,
        unsigned int count,
        unsigned int on_ms,
        unsigned int off_ms,
        bool on_at_rest
    );
};