#!/usr/bin/env python
"""
whisker/qtclient.py
===============================================================================
Copyright © 2011-2020 Rudolf Cardinal (rudolf@pobox.com).
This file is part of the Whisker Python client library.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
**Multithreaded framework for Whisker Python clients using Qt.**
- Created: late 2016
- Last update: 20 Sep 2018
- Note funny bug: data sometimes sent twice.
- Looks like it might be this:
http://www.qtcentre.org/threads/29462-QTcpSocket-sends-data-twice-with-flush()
- Attempted solution:
- change ``QTcpSocket()`` to ``QTcpSocket(parent=self)``, in case the
socket wasn't getting moved between threads properly -- didn't fix
- disable ``flush()`` -- didn't fix.
- send function is only being called once, according to log
- adding thread ID information to the log shows that ``whisker_controller``
events are coming from two threads...
- Anyway, bug was this:
- http://stackoverflow.com/questions/34125065
- https://bugreports.qt.io/browse/PYSIDE-249
- Source:
- http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/kernel/qobject.h?h=5.4
- http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/kernel/qobject.cpp?h=5.4
""" # noqa
import logging
from enum import Enum
from typing import Optional
import arrow
from cardinal_pythonlib.regexfunc import CompiledRegexMemory
# noinspection PyPackageRequirements
from PyQt5.QtCore import (
QByteArray,
QObject,
Qt,
QThread,
pyqtSignal,
pyqtSlot,
)
# noinspection PyPackageRequirements
from PyQt5.QtNetwork import (
QAbstractSocket,
QTcpSocket,
)
from whisker.api import (
CLIENT_MESSAGE_REGEX,
CODE_REGEX,
ENCODING,
EOL,
EOL_LEN,
ERROR_REGEX,
EVENT_REGEX,
IMMPORT_REGEX,
KEY_EVENT_REGEX,
msg_from_args,
PING,
PING_ACK,
split_timestamp,
SYNTAX_ERROR_REGEX,
WARNING_REGEX,
WhiskerApi,
)
from whisker.constants import DEFAULT_PORT
from whisker.qt import exit_on_exception, StatusMixin
log = logging.getLogger(__name__)
INFINITE_WAIT = -1
[docs]class ThreadOwnerState(Enum):
STOPPED = 0
STARTING = 1
RUNNING = 2
STOPPING = 3
[docs]def is_socket_connected(socket: QAbstractSocket) -> bool:
"""
Is the Qt socket connected?
"""
return socket and socket.state() == QAbstractSocket.ConnectedState
[docs]def disable_nagle(socket: QAbstractSocket) -> None:
"""
Disable the Nagle algorithm on the Qt socket. (This makes it send any
outbound data immediately, rather than waiting and packaging it up with
potential subsequent data. So this function chooses a low-latency mode
over a high-efficiency mode.)
"""
# http://doc.qt.io/qt-5/qabstractsocket.html#SocketOption-enum
socket.setSocketOption(QAbstractSocket.LowDelayOption, 1)
[docs]def get_socket_error(socket: QAbstractSocket) -> str:
"""
Returns a textual description of the last error on the specified Qt
socket.
"""
return "{}: {}".format(socket.error(), socket.errorString())
[docs]def quote(msg: str) -> str:
"""
Return its argument suitably quoted for transmission to Whisker.
- Whisker has quite a primitive quoting system...
- Check with strings that actually include quotes.
"""
return '"{}"'.format(msg)
# =============================================================================
# Object to supervise all Whisker functions
# =============================================================================
[docs]class WhiskerOwner(QObject, StatusMixin): # GUI thread
"""
Object to own and manage communication with a Whisker server.
This object is owned by the GUI thread.
It devolves work to two other threads:
(A) **main socket listener** (:class:`WhiskerMainSocketListener`, as the
instance ``self.mainsock``, in the thread ``self.mainsockthread``);
(B) **task** (:class:`WhiskerQtTask`, as the instance ``self.task`` in the
thread ``self.taskthread``) + **immediate socket blocking handler**
(:class:`WhiskerController`, as the instance ``self.controller`` in the
thread ``self.taskthread``).
References to "main" here just refers to the main socket (as opposed to the
immediate socket), not the thread that's doing most of the processing.
Signals of relevance to users:
- ``connected()``
- ``disconnected()``
- ``finished()``
- ``message_received(str, arrow.Arrow, int)``
- ``event_received(str, arrow.Arrow, int)``
- ``pingack_received(arrow.Arrow, int)``
"""
# Outwards, to world/task:
connected = pyqtSignal()
disconnected = pyqtSignal()
finished = pyqtSignal()
message_received = pyqtSignal(str, arrow.Arrow, int)
event_received = pyqtSignal(str, arrow.Arrow, int)
pingack_received = pyqtSignal(arrow.Arrow, int)
# Inwards, to possessions:
controller_finish_requested = pyqtSignal()
mainsock_finish_requested = pyqtSignal()
ping_requested = pyqtSignal()
# And don't forget the signals inherited from StatusMixin.
# noinspection PyUnresolvedReferences
def __init__(self,
task: 'WhiskerQtTask', # forward reference for type hint
server: str,
main_port: int = DEFAULT_PORT,
parent: QObject = None,
connect_timeout_ms: int = 5000,
read_timeout_ms: int = 500,
name: str = "whisker_owner",
sysevent_prefix: str = 'sys_',
**kwargs) -> None:
"""
Args:
task: instance of something derived from :class:`WhiskerQtTask`
server: host name or IP address of Whisker server
main_port: main port number for Whisker server
parent: optional parent :`QObject`
connect_timeout_ms: time to wait for successful connection (ms)
before considering it a failure
read_timeout_ms: time to wait for each read (primary effect is to
determine the application's responsivity when asked to finish;
see :class:`WhiskerMainSocketListener`)
name: (name for :class:`StatusMixin`)
sysevent_prefix: default system event prefix (for
:class:`WhiskerController`)
kwargs: parameters to superclass
"""
super().__init__(parent=parent, name=name, logger=log, **kwargs)
self.state = ThreadOwnerState.STOPPED
self.is_connected = False
self.mainsockthread = QThread(self)
self.mainsock = WhiskerMainSocketListener(
server,
main_port,
connect_timeout_ms=connect_timeout_ms,
read_timeout_ms=read_timeout_ms,
parent=None) # must be None as it'll go to a different thread
self.mainsock.moveToThread(self.mainsockthread)
self.taskthread = QThread(self)
self.controller = WhiskerController(server,
sysevent_prefix=sysevent_prefix)
self.controller.moveToThread(self.taskthread)
self.task = task
# debug_object(self)
# debug_thread(self.taskthread)
# debug_object(self.controller)
# debug_object(task)
self.task.moveToThread(self.taskthread)
# debug_object(self.controller)
# debug_object(task)
self.task.set_controller(self.controller)
# Connect object and thread start/stop events
# ... start sequence
self.taskthread.started.connect(self.task.thread_started)
self.mainsockthread.started.connect(self.mainsock.start)
# ... stop
self.mainsock_finish_requested.connect(self.mainsock.stop,
type=Qt.DirectConnection) # NB!
self.mainsock.finished.connect(self.mainsockthread.quit)
self.mainsockthread.finished.connect(self._mainsockthread_finished)
self.controller_finish_requested.connect(self.task.stop)
self.task.finished.connect(self.controller.task_finished)
self.controller.finished.connect(self.taskthread.quit)
self.taskthread.finished.connect(self._taskthread_finished)
# Status
self.mainsock.error_sent.connect(self.error_sent)
self.mainsock.status_sent.connect(self.status_sent)
self.controller.error_sent.connect(self.error)
self.controller.status_sent.connect(self.status_sent)
self.task.error_sent.connect(self.error)
self.task.status_sent.connect(self.status_sent)
# Network communication
self.mainsock.line_received.connect(self.controller.main_received)
self.controller.connected.connect(self._on_connect)
self.controller.connected.connect(self.task.on_connect)
self.controller.message_received.connect(self.message_received) # different thread # noqa
self.controller.event_received.connect(self.event_received) # different thread # noqa
self.controller.event_received.connect(self.task.on_event) # same thread # noqa
self.controller.key_event_received.connect(self.task.on_key_event) # same thread # noqa
self.controller.client_message_received.connect(self.task.on_client_message) # same thread # noqa
self.controller.pingack_received.connect(self.pingack_received) # different thread # noqa
self.controller.warning_received.connect(self.task.on_warning) # same thread # noqa
self.controller.error_received.connect(self.task.on_error) # same thread # noqa
self.controller.syntax_error_received.connect(
self.task.on_syntax_error) # same thread # noqa
# Abort events
self.mainsock.disconnected.connect(self._on_disconnect)
self.controller.disconnected.connect(self._on_disconnect)
# Other
self.ping_requested.connect(self.controller.ping)
# -------------------------------------------------------------------------
# General state control
# -------------------------------------------------------------------------
[docs] def is_running(self) -> bool:
"""
Are any of our worker threads starting/running/stopping?
"""
running = self.state != ThreadOwnerState.STOPPED
self.debug("is_running: {} (state: {})".format(running,
self.state.name))
return running
def _set_state(self, state: ThreadOwnerState) -> None:
"""
Internal function to set the thread state flag.
"""
self.debug("state: {} -> {}".format(self.state, state))
self.state = state
[docs] def report_status(self) -> None:
"""
Print current thread/server state to the log.
"""
self.status("state: {}".format(self.state))
self.status("connected to server: {}".format(self.is_connected))
# -------------------------------------------------------------------------
# Starting
# -------------------------------------------------------------------------
[docs] def start(self) -> None:
"""
Start our worker threads.
"""
self.debug("WhiskerOwner: start")
if self.state != ThreadOwnerState.STOPPED:
self.error("Can't start: state is: {}".format(self.state.name))
return
self.taskthread.start()
self.mainsockthread.start()
self._set_state(ThreadOwnerState.RUNNING)
# -------------------------------------------------------------------------
# Stopping
# -------------------------------------------------------------------------
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def _on_disconnect(self) -> None:
"""
Slot called when the Whisker server disconnects.
"""
self.debug("WhiskerOwner: on_disconnect")
self.is_connected = False
self.disconnected.emit()
if self.state == ThreadOwnerState.STOPPING:
return
self.stop()
[docs] def stop(self) -> None:
"""
Called by the GUI when we want to stop.
"""
self.info("Stop requested [previous state: {}]".format(
self.state.name))
if self.state == ThreadOwnerState.STOPPED:
self.error("Can't stop: was already stopped")
return
self._set_state(ThreadOwnerState.STOPPING)
self.controller_finish_requested.emit() # -> self.task.stop
self.mainsock_finish_requested.emit() # -> self.mainsock.stop
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def _mainsockthread_finished(self) -> None:
"""
Slot called when our main-socket thread has finished.
"""
self.debug("stop: main socket thread stopped")
self._check_everything_finished()
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def _taskthread_finished(self) -> None:
"""
Slot called when our task thread has finished.
"""
self.debug("stop: task thread stopped")
self._check_everything_finished()
def _check_everything_finished(self) -> None:
"""
Have all out threads finished? If so, emit the ``finished`` signal.
"""
if self.mainsockthread.isRunning() or self.taskthread.isRunning():
return
self._set_state(ThreadOwnerState.STOPPED)
self.finished.emit()
# -------------------------------------------------------------------------
# Other
# -------------------------------------------------------------------------
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def _on_connect(self) -> None:
"""
Slot called when we are fully connected to the Whisker server.
"""
self.status("Fully connected to Whisker server")
self.is_connected = True
self.connected.emit()
[docs] def ping(self) -> None:
"""
Pings the Whisker server.
"""
if not self.is_connected:
self.warning("Won't ping: not connected")
return
self.ping_requested.emit()
# =============================================================================
# Main socket listener
# =============================================================================
[docs]class WhiskerMainSocketListener(QObject, StatusMixin):
"""
Whisker thread (A) (see :class:`WhiskerOwner`).
Listens to the main socket.
Signals:
- ``finished()``
- ``disconnected()``
- ``line_received(msg: str, timestamp: arrow.Arrow)``
"""
finished = pyqtSignal()
disconnected = pyqtSignal()
line_received = pyqtSignal(str, arrow.Arrow)
def __init__(self,
server: str,
port: int,
parent: QObject = None,
connect_timeout_ms: int = 5000,
read_timeout_ms: int = 100,
name: str = "whisker_mainsocket",
**kwargs) -> None:
"""
Args:
server: host name or IP address of Whisker server
port: main port number for Whisker server
parent: optional parent :`QObject`
connect_timeout_ms: time to wait for successful connection (ms)
before considering it a failure
read_timeout_ms: time to wait for each read (primary effect is to
determine the application's responsivity when asked to finish;
see :class:`WhiskerMainSocketListener`)
name: (name for :class:`StatusMixin`)
kwargs: parameters to superclass
"""
super().__init__(parent=parent, name=name, logger=log, **kwargs)
self.server = server
self.port = port
self.connect_timeout_ms = connect_timeout_ms
self.read_timeout_ms = read_timeout_ms
self.finish_requested = False
self.residual = ''
self.socket = None
self.running = False
# Don't create the socket immediately; we're going to be moved to
# another thread.
# noinspection PyArgumentList
@pyqtSlot()
def start(self) -> None:
"""
- Connect to the Whisker server via the main socket.
- Enter a blocking loop waiting for input and periodically checking
for a "please finish" signal (with a frequency determined by
:attr:`read_timeout_ms`.
- Eventually call :func:`WhiskerMainSocketListener.finish`.
"""
# Must be separate from __init__, or signals won't be connected yet.
self.finish_requested = False
self.status("Connecting to {}:{} with timeout {} ms".format(
self.server, self.port, self.connect_timeout_ms))
self.socket = QTcpSocket(self)
# noinspection PyUnresolvedReferences
self.socket.disconnected.connect(self.disconnected)
self.socket.connectToHost(self.server, self.port)
if not self.socket.waitForConnected(self.connect_timeout_ms):
errmsg = "Socket error {}".format(get_socket_error(self.socket))
self.error(errmsg)
self._finish()
return
self.info("Connected to {}:{}".format(self.server, self.port))
disable_nagle(self.socket)
# Main blocking loop
self.running = True
while not self.finish_requested:
# self.debug("ping")
if self.socket.waitForReadyRead(self.read_timeout_ms):
# data is now ready
data = self.socket.readAll() # type: QByteArray
# log.critical(repr(data))
# log.critical(repr(type(data))) # <class 'PyQt5.QtCore.QByteArray'> under PyQt5 # noqa
# for PySide:
# - readAll() returns a QByteArray; bytes() fails; str() is OK
# strdata = str(data)
# for PyQt5:
# - readAll() returns a QByteArray again;
# - however, str(data) looks like "b'Info: ...\\n'"
strdata = data.data().decode(ENCODING) # this works
# log.critical(repr(strdata))
self._process_data(strdata)
self.running = False
self.info("WhiskerMainSocketListener: main event loop complete")
self._finish()
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def stop(self) -> None:
"""
Stop (by requesting a finish and waiting for the blocking loop in
:func:`WhiskerMainSocketListener.start` to notice.
"""
self.debug("WhiskerMainSocketListener: stop")
if not self.running:
self.error(
"WhiskerMainSocketListener: stop requested, but not running")
self.finish_requested = True
def _sendline_mainsock(self, msg: str) -> None:
"""
Send an EOL-terminated message to the server via the main socket.
"""
if not is_socket_connected(self.socket):
self.error("Can't send through a closed socket")
return
self.debug("Sending to server (MAIN): {}".format(msg))
final_str = msg + EOL
data_bytes = final_str.encode(ENCODING)
self.socket.write(data_bytes)
self.socket.flush()
def _finish(self) -> None:
"""
Clean up and emit the ``finished`` signal.
"""
if is_socket_connected(self.socket):
self.socket.close()
self.info("WhiskerMainSocketListener: finished")
self.finished.emit()
def _process_data(self, data: str) -> None:
"""
Adds the incoming data to any stored residual, splits it into lines,
and sends each line out via the ``line_received`` signal (on to the
receiver).
"""
self.debug("incoming: {}".format(repr(data)))
timestamp = arrow.now()
data = self.residual + data
fragments = data.split(EOL)
lines = fragments[:-1]
self.residual = fragments[-1]
for line in lines:
self.debug("incoming line: {}".format(line))
if line == PING:
self._sendline_mainsock(PING_ACK)
self.status("Ping received from server")
return
self.line_received.emit(line, timestamp)
# =============================================================================
# Object to talk to task and to immediate socket
# =============================================================================
[docs]class WhiskerController(QObject, StatusMixin, WhiskerApi):
"""
Controls the Whisker immediate socket.
- Encapsulates the Whisker API.
- Lives in Whisker thread (B) (see :class:`WhiskerOwner`).
- Emits signals that can be processed by the Whisker task (derived from
:class:`WhiskerQtTask`).
Signals:
- ``finished()``
- ``connected()``
- ``disconnected()``
- ``message_received(str, arrow.Arrow, int)``
- ``event_received(str, arrow.Arrow, int)``
- ``key_event_received(str, arrow.Arrow, int)``
- ``client_message_received(int, str, arrow.Arrow, int)``
- ``warning_received(str, arrow.Arrow, int)``
- ``syntax_error_received(str, arrow.Arrow, int)``
- ``error_received(str, arrow.Arrow, int)``
- ``pingack_received(arrow.Arrow, int)``
"""
finished = pyqtSignal()
connected = pyqtSignal()
disconnected = pyqtSignal()
message_received = pyqtSignal(str, arrow.Arrow, int)
event_received = pyqtSignal(str, arrow.Arrow, int)
key_event_received = pyqtSignal(str, arrow.Arrow, int)
client_message_received = pyqtSignal(int, str, arrow.Arrow, int)
warning_received = pyqtSignal(str, arrow.Arrow, int)
syntax_error_received = pyqtSignal(str, arrow.Arrow, int)
error_received = pyqtSignal(str, arrow.Arrow, int)
pingack_received = pyqtSignal(arrow.Arrow, int)
def __init__(self,
server: str,
parent: QObject = None,
connect_timeout_ms: int = 5000,
read_timeout_ms: int = 500,
name: str = "whisker_controller",
sysevent_prefix: str = "sys_",
**kwargs) -> None:
"""
Args:
server: host name or IP address of Whisker server
parent: optional parent :`QObject`
connect_timeout_ms: time to wait for successful connection (ms)
before considering it a failure
read_timeout_ms: time to wait for each read (primary effect is to
determine the application's responsivity when asked to finish;
see :class:`WhiskerMainSocketListener`)
name: (name for :class:`StatusMixin`)
sysevent_prefix: default system event prefix (for
:class:`WhiskerController`)
kwargs: parameters to superclass
"""
super().__init__(
# For QObject:
parent=parent,
# For StatusMixin:
name=name,
logger=log,
# For WhiskerApi:
whisker_immsend_get_reply_fn=self._get_immsock_response,
sysevent_prefix=sysevent_prefix,
# Anyone else?
**kwargs
)
self.server = server
self.connect_timeout_ms = connect_timeout_ms
self.read_timeout_ms = read_timeout_ms
self.immport = None # type: Optional[int]
self.code = None # type: Optional[str]
self.immsocket = None # type: Optional[QTcpSocket]
self.residual = ''
# noinspection PyArgumentList
@pyqtSlot(str, arrow.Arrow)
@exit_on_exception
def main_received(self, msg: str, timestamp: arrow.Arrow) -> None:
"""
Slot to process a message that has come in from the main socket.
Args:
msg: raw message
timestamp: server timestamp
"""
gre = CompiledRegexMemory()
# self.debug("main_received: {}".format(msg))
# 0. Ping has already been dealt with.
# 1. Deal with immediate socket connection internally.
if gre.search(IMMPORT_REGEX, msg):
self.immport = int(gre.group(1))
return
if gre.search(CODE_REGEX, msg):
code = gre.group(1)
self.immsocket = QTcpSocket(self)
# noinspection PyUnresolvedReferences
self.immsocket.disconnected.connect(self.disconnected)
self.debug(
"Connecting immediate socket to {}:{} with timeout {}".format(
self.server, self.immport, self.connect_timeout_ms))
self.immsocket.connectToHost(self.server, self.immport)
if not self.immsocket.waitForConnected(self.connect_timeout_ms):
errmsg = "Immediate socket error {}".format(
get_socket_error(self.immsocket))
self.error(errmsg)
self.finish()
self.debug("Connected immediate socket to "
"{}:{}".format(self.server, self.immport))
disable_nagle(self.immsocket)
self.command("Link {}".format(code))
self.connected.emit()
return
# 2. Get timestamp.
(msg, whisker_timestamp) = split_timestamp(msg)
# 3. Send the message to a general-purpose receiver
self.message_received.emit(msg, timestamp, whisker_timestamp)
# 4. Send the message to specific-purpose receivers.
if gre.search(EVENT_REGEX, msg):
event = gre.group(1)
if self.process_backend_event(event):
return
self.event_received.emit(event, timestamp, whisker_timestamp)
elif gre.search(KEY_EVENT_REGEX, msg):
key = gre.group(1)
self.key_event_received.emit(key, timestamp, whisker_timestamp)
elif gre.search(CLIENT_MESSAGE_REGEX, msg):
source_client_num = int(gre.group(1))
client_msg = gre.group(2)
self.client_message_received.emit(source_client_num, client_msg,
timestamp, whisker_timestamp)
elif WARNING_REGEX.match(msg):
self.warning_received.emit(msg, timestamp, whisker_timestamp)
elif SYNTAX_ERROR_REGEX.match(msg):
self.syntax_error_received.emit(msg, timestamp, whisker_timestamp)
elif ERROR_REGEX.match(msg):
self.error_received.emit(msg, timestamp, whisker_timestamp)
elif msg == PING_ACK:
self.pingack_received.emit(timestamp, whisker_timestamp)
# noinspection PyArgumentList
@pyqtSlot()
@exit_on_exception
def task_finished(self) -> None:
"""
Slot called when the task says that it's finished.
"""
self.debug("Task reports that it is finished")
self._close_immsocket()
self.finished.emit()
def _sendline_immsock(self, *args) -> None:
msg = msg_from_args(*args)
self.debug("Sending to server (IMM): {}".format(msg))
final_str = msg + EOL
data_bytes = final_str.encode(ENCODING)
self.immsocket.write(data_bytes)
self.immsocket.waitForBytesWritten(INFINITE_WAIT)
# http://doc.qt.io/qt-4.8/qabstractsocket.html
self.immsocket.flush()
def _getline_immsock(self) -> str:
"""
Get one line from the socket, and returns it. Blocking.
We must also respond to the possibility that the socket has been
forcibly closed.
"""
data = self.residual
while EOL not in data and is_socket_connected(self.immsocket):
# get more data from socket
# self.debug("WAITING FOR DATA")
self.immsocket.waitForReadyRead(INFINITE_WAIT)
# self.debug("DATA READY. READING IT.")
newdata_bytearray = self.immsocket.readAll() # type: QByteArray
newdata_str = newdata_bytearray.data().decode(ENCODING)
data += newdata_str
# self.debug("DATA: {}".format(repr(data)))
# self.debug("DATA COMPLETE")
if EOL in data:
eol_index = data.index(EOL)
line = data[:eol_index]
self.residual = data[eol_index + EOL_LEN:]
else:
line = '' # socket is closed
self.residual = data # probably blank!
self.debug("Reply from server (IMM): {}".format(line))
return line
def _get_immsock_response(self, *args) -> Optional[str]:
"""
Builds its arguments into a string, sends it as a command to the
Whisker server via the immediate socket, waits for a reply, and
returns it. (Returns ``None`` if the socket isn't connected.)
"""
if not self.is_connected():
self.error("Not connected")
return None
self._sendline_immsock(*args)
reply = self._getline_immsock()
return reply
[docs] def is_connected(self) -> bool:
"""
Is our immediate socket connected?
(If the immediate socket is running, the main socket should be too.)
"""
return is_socket_connected(self.immsocket)
def _close_immsocket(self) -> None:
"""
Close the immediate socket.
"""
if is_socket_connected(self.immsocket):
self.immsocket.close()
[docs] def ping(self) -> None:
"""
Ping the server.
Override :func:`WhiskerApi.ping` so we can emit a signal on success.
"""
# override WhiskerApi.ping() so we can emit a signal on success
reply, whisker_timestamp = self._immresp_with_timestamp(PING)
if reply == PING_ACK:
timestamp = arrow.now()
self.pingack_received.emit(timestamp, whisker_timestamp)
# =============================================================================
# Object from which Whisker tasks should be subclassed
# =============================================================================
[docs]class WhiskerQtTask(QObject, StatusMixin):
"""
Base class for building a Whisker task itself. You should derive from
this.
- Lives in Whisker thread (B) (see :class:`WhiskerOwner`).
Signals:
- ``finished()``
"""
finished = pyqtSignal() # emit from stop() function when all done
def __init__(self, parent: QObject = None,
name: str = "whisker_task", **kwargs) -> None:
"""
Args:
parent: optional parent :`QObject`
name: (name for :class:`StatusMixin`)
kwargs: parameters to superclass
"""
super().__init__(name=name, logger=log, parent=parent, **kwargs)
self.whisker = None
[docs] def set_controller(self, controller: WhiskerController) -> None:
"""
Called by :class:`WhiskerOwner`. No need to override.
"""
self.whisker = controller
# noinspection PyMethodMayBeStatic,PyArgumentList
@pyqtSlot()
def thread_started(self) -> None:
"""
Slot called from the :func:`WhiskerOwner.taskthread.started` signal,
which is called indirectly by :func:`WhiskerOwner.start`.
- Use this for additional setup if required.
- No need to override in simple situations.
"""
pass
# noinspection PyArgumentList
@pyqtSlot()
def stop(self) -> None:
"""
Called by the :class:`WhiskerOwner` when we should stop.
- When we've done what we need to, we should emit the ``finished``
signal.
- No need to override in simple situations.
NOTE: if you think this function is not being called, the likely reason
is not a Qt signal/slot failure, but that the thread is BUSY, e.g.
with its immediate socket. (It'll be busy via the derived class, not
via the code here, which does no waiting!)
"""
self.info("WhiskerQtTask: stopping")
self.finished.emit()
[docs] @exit_on_exception
def on_connect(self) -> None:
"""
The :class:`WhiskerOwner` makes this slot get called when the
:class:`WhiskerController` is connected.
You should override this.
"""
self.warning("on_connect: YOU SHOULD OVERRIDE THIS")
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(str, arrow.Arrow, int)
@exit_on_exception
def on_event(self, event: str, timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
General Whisker events come here.
Args:
event: incoming event
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
# You should override this
msg = "SHOULD BE OVERRIDDEN. EVENT: {}".format(event)
self.status(msg)
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(str, arrow.Arrow, int)
@exit_on_exception
def on_key_event(self, key_event: str, timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
Keyboard events come here.
Args:
key_event: incoming event
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
msg = "SHOULD BE OVERRIDDEN. KEY EVENT: {}".format(key_event)
self.status(msg)
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(int, str, arrow.Arrow, int)
@exit_on_exception
def on_client_message(self,
source_client_num: int,
client_msg: str,
timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
Client messages (from other clients) come here.
Args:
source_client_num: client number of sender
client_msg: message
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
msg = "SHOULD BE OVERRIDDEN. CLIENT MESSAGE FROM CLIENT {}: {}".format(
source_client_num, repr(client_msg))
self.status(msg)
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(str, arrow.Arrow, int)
@exit_on_exception
def on_warning(self, msg: str, timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
Warnings from the server come here.
Args:
msg: incoming event
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
self.warning(msg)
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(str, arrow.Arrow, int)
@exit_on_exception
def on_error(self, msg: str, timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
Error reports from the server come here.
Args:
msg: incoming event
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
self.error(msg)
# noinspection PyUnusedLocal,PyArgumentList
@pyqtSlot(str, arrow.Arrow, int)
@exit_on_exception
def on_syntax_error(self, msg: str, timestamp: arrow.Arrow,
whisker_timestamp_ms: int) -> None:
"""
Syntax error reports from the server come here.
Args:
msg: incoming event
timestamp: time of receipt by client
whisker_timestamp_ms: server's timestamp (ms)
"""
self.error(msg)