15.1.970. 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/whiskerconstants.h"
#include "whisker/whiskerconnectionstate.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);
};