Compare commits
3 Commits
feat/ci-cd
...
feat/robot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912af8d821 | ||
|
|
017dbfaa28 | ||
|
|
9ff1d9a4d3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -220,3 +220,5 @@ __marimo__/
|
||||
# Docs
|
||||
docs/*
|
||||
!docs/conf.py
|
||||
!docs/installation/
|
||||
!docs/installation/**
|
||||
|
||||
106
README.md
106
README.md
@@ -8,90 +8,21 @@ This is an implementation for the Pepper robot, using the Pepper SDK and Python
|
||||
|
||||
## Installation
|
||||
|
||||
### Linux (or WSL)
|
||||
- [Linux](./docs/installation/linux.md)
|
||||
- [macOS](./docs/installation/macos.md)
|
||||
- [Windows](./docs/installation/windows.md)
|
||||
|
||||
Start off by installing [Pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and walk through the steps outlined there (be sure to also add it to PATH). Also install the [Python build requirements](https://github.com/pyenv/pyenv/wiki#suggested-build-environment). Afterwards, install Python 2.7 and activate it for your current shell:
|
||||
|
||||
|
||||
### Git Hooks
|
||||
|
||||
To activate automatic linting, formatting, branch name checks and commit message checks, run (after installing requirements):
|
||||
|
||||
```bash
|
||||
pyenv install 2.7
|
||||
pyenv shell 2.7
|
||||
pre-commit install
|
||||
pre-commit install --hook-type commit-msg
|
||||
```
|
||||
|
||||
You can check that this worked by typing
|
||||
|
||||
```bash
|
||||
python -V
|
||||
```
|
||||
|
||||
Which should return `Python 2.7.18`.
|
||||
|
||||
Next, `cd` into this repository and create (and activate) a virtual environment:
|
||||
|
||||
```bash
|
||||
cd <path to project>/
|
||||
python -m pip install virtualenv
|
||||
python -m virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
We depend on PortAudio for the `pyaudio` package, so install it with:
|
||||
|
||||
```bash
|
||||
sudo apt install -y portaudio19-dev
|
||||
```
|
||||
|
||||
On WSL, also install:
|
||||
|
||||
```bash
|
||||
sudo apt install -y libasound2-plugins
|
||||
```
|
||||
|
||||
Install the required packages with
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Now we need to install the NaoQi SDK into our virtual environment, which we need to do manually. Begin by downloading the SDK:
|
||||
|
||||
```bash
|
||||
wget https://community-static.aldebaran.com/resources/2.5.10/Python%20SDK/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
```
|
||||
|
||||
Next, move into the `site-packages` directory and extract the file you just downloaded:
|
||||
|
||||
```bash
|
||||
cd .venv/lib/python2.7/site-packages/
|
||||
tar xvfz <path to SDK>/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
rm <path to SDK>/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
```
|
||||
|
||||
Lastly, we need to inform our virtual environment where to find our newly installed package:
|
||||
|
||||
```bash
|
||||
echo <path to project>/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.5.7.1-linux64/lib/python2.7/site-packages/ > pynaoqi-python2.7.pth
|
||||
```
|
||||
|
||||
That's it! Verify that it works with
|
||||
|
||||
```bash
|
||||
python -c "import qi; print(qi)"
|
||||
```
|
||||
|
||||
You should now be able to run this project.
|
||||
|
||||
### macOS
|
||||
|
||||
Similar to Linux, but don't bother installing `pyenv` as it won't be able to install Python 2 on Apple Silicon. Instead, install Python 2.7.18 from the [Python website](https://www.python.org/downloads/release/python-2718/).
|
||||
|
||||
Create the virtual environment as described above in the Linux section. Stop at the point where it shows you how to download the NaoQi SDK. Instead, use:
|
||||
|
||||
```shell
|
||||
curl -OL https://community-static.aldebaran.com/resources/2.5.10/Python%20SDK/pynaoqi-python2.7-2.5.7.1-mac64.tar.gz
|
||||
```
|
||||
|
||||
Then resume the steps from above.
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
@@ -134,23 +65,6 @@ For coverage, add `--cov=robot_interface` as an argument to `pytest`.
|
||||
|
||||
|
||||
|
||||
## Git Hooks
|
||||
|
||||
To activate automatic linting, formatting, branch name checks and commit message checks, run (after installing requirements):
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
pre-commit install --hook-type commit-msg
|
||||
```
|
||||
|
||||
You might get an error along the lines of `Can't install pre-commit with core.hooksPath` set. To fix this, simply unset the hooksPath by running:
|
||||
|
||||
```bash
|
||||
git config --local --unset core.hooksPath
|
||||
```
|
||||
|
||||
Then run the pre-commit install commands again.
|
||||
|
||||
## Documentation
|
||||
Generate documentation web pages using:
|
||||
|
||||
|
||||
75
docs/installation/linux.md
Normal file
75
docs/installation/linux.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Installation
|
||||
|
||||
Of the Pepper Robot Interface on Linux (or WSL).
|
||||
|
||||
Start off by installing [Pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and walk through the steps outlined there (be sure to also add it to PATH). Also install the [Python build requirements](https://github.com/pyenv/pyenv/wiki#suggested-build-environment). Afterwards, install Python 2.7 and activate it for your current shell:
|
||||
|
||||
```bash
|
||||
pyenv install 2.7
|
||||
pyenv shell 2.7
|
||||
```
|
||||
|
||||
You can check that this worked by typing
|
||||
|
||||
```bash
|
||||
python -V
|
||||
```
|
||||
|
||||
Which should return `Python 2.7.18`.
|
||||
|
||||
Next, `cd` into this repository and create (and activate) a virtual environment:
|
||||
|
||||
```bash
|
||||
cd <path to project>/
|
||||
python -m pip install virtualenv
|
||||
python -m virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
We depend on PortAudio for the `pyaudio` package, so install it with:
|
||||
|
||||
```bash
|
||||
sudo apt install -y portaudio19-dev
|
||||
```
|
||||
|
||||
On WSL, also install:
|
||||
|
||||
```bash
|
||||
sudo apt install -y libasound2-plugins
|
||||
```
|
||||
|
||||
Install the required packages with
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Now we need to install the NaoQi SDK into our virtual environment, which we need to do manually. Begin by downloading the SDK:
|
||||
|
||||
```bash
|
||||
wget https://community-static.aldebaran.com/resources/2.5.10/Python%20SDK/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
```
|
||||
|
||||
Next, move into the `site-packages` directory and extract the file you just downloaded:
|
||||
|
||||
```bash
|
||||
cd .venv/lib/python2.7/site-packages/
|
||||
tar xvfz <path to SDK>/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
rm <path to SDK>/pynaoqi-python2.7-2.5.7.1-linux64.tar.gz
|
||||
```
|
||||
|
||||
Lastly, we need to inform our virtual environment where to find our newly installed package:
|
||||
|
||||
```bash
|
||||
echo <path to project>/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.5.7.1-linux64/lib/python2.7/site-packages/ > pynaoqi-python2.7.pth
|
||||
```
|
||||
|
||||
That's it! Verify that it works with
|
||||
|
||||
```bash
|
||||
python -c "import qi; print(qi)"
|
||||
```
|
||||
|
||||
You should now be able to run this project.
|
||||
|
||||
See the README for how to run.
|
||||
106
docs/installation/macos.md
Normal file
106
docs/installation/macos.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Installation
|
||||
|
||||
Of the Pepper Robot Interface on macOS.
|
||||
|
||||
|
||||
|
||||
## Python 2.7
|
||||
|
||||
Install Python 2.7.18 from the [Python website](https://www.python.org/downloads/release/python-2718/).
|
||||
|
||||
Check that it worked by executing
|
||||
|
||||
```shell
|
||||
python2 -V
|
||||
```
|
||||
|
||||
Which should return Python 2.7.18.
|
||||
|
||||
|
||||
|
||||
## Virtual Environment
|
||||
|
||||
Next, cd into this repository and create (and activate) a virtual environment:
|
||||
|
||||
```shell
|
||||
cd /path/to/project/
|
||||
python2 -m pip install virtualenv
|
||||
python2 -m virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
We depend on PortAudio for the `pyaudio` package. If on Intel, run `brew install portaudio`. If on Apple Silicon, compile manually using the steps described in [the YouTrack article](https://utrechtuniversity.youtrack.cloud/articles/N25B-A-22/Install-PyAudio-for-Python-2-on-Apple-Silicon).
|
||||
|
||||
Then install the required Python packages with
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
|
||||
## NaoQi SDK
|
||||
|
||||
We need to manually install the NaoQi SDK into our virtual environment. There are two options:
|
||||
|
||||
1. Install a newer version (2.8) which will make running easier, but compatibility is uncertain.
|
||||
2. Install the version expected by the robot (2.5). This will complicate running slightly.
|
||||
|
||||
### Option 1
|
||||
|
||||
Download the SDK from [twirre.io](https://twirre.io/files/pynaoqi-python2.7-2.8.6.23-mac64-20191127_144231.tar.gz), or find one on the Aldebaran website, or an archived version on Web Archive.
|
||||
|
||||
Extract it to `/path/to/project/.venv/lib/python2.7/site-packages/`.
|
||||
|
||||
We need to inform our virtual environment where to find our newly installed package:
|
||||
|
||||
```bash
|
||||
echo "/path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.8.6.23-mac64-20191127_144231/lib/python2.7/site-packages/" > /path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7.pth
|
||||
```
|
||||
|
||||
Now continue with [verifying](#verifying).
|
||||
|
||||
### Option 2
|
||||
|
||||
This method of installation requires setting the `DYLD_LIBRARY_PATH` environment variable before running. How will be explained.
|
||||
|
||||
Download the SDK from [twirre.io](https://twirre.io/files/pynaoqi-2.5.7.1-mac64-deps.tar.gz). This is a modified version of the one from Aldebaran, this one including required Choregraphe dependencies.
|
||||
|
||||
Extract it to `/path/to/project/.venv/lib/python2.7/site-packages/`.
|
||||
|
||||
We need to inform our virtual environment where to find our newly installed package:
|
||||
|
||||
```shell
|
||||
echo "/path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.5.7.1-mac64/lib/python2.7/site-packages/" > /path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7.pth
|
||||
```
|
||||
|
||||
Now, anytime before running you need to set the `DYLD_LIBRARY_PATH` environment variable.
|
||||
|
||||
```shell
|
||||
export DYLD_LIBRARY_PATH="/path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.5.7.1-mac64/choregraphe_lib:${DYLD_LIBRARY_PATH}"
|
||||
```
|
||||
|
||||
You may want to simplify environment activation with a script `activate.sh` like:
|
||||
|
||||
```shell
|
||||
#!/bin/zsh
|
||||
|
||||
export DYLD_LIBRARY_PATH="/path/to/project/.venv/lib/python2.7/site-packages/pynaoqi-python2.7-2.8.6.23-mac64-20191127_144231/choregraphe_lib:${DYLD_LIBRARY_PATH}"
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
[Verify](#verifying) if it works.
|
||||
|
||||
|
||||
|
||||
## Verifying
|
||||
|
||||
Verify that the NaoQI SDK installation works with
|
||||
|
||||
```bash
|
||||
python -c "import qi; print(qi)"
|
||||
```
|
||||
|
||||
If so, you should now be able to run this project.
|
||||
|
||||
See the README for how to run.
|
||||
44
docs/installation/windows.md
Normal file
44
docs/installation/windows.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Installation
|
||||
|
||||
Of the Pepper Robot Interface on Windows.
|
||||
|
||||
Install Python 2.7.18 from [the Python website](https://www.python.org/downloads/release/python-2718/), choose the x86-64 installer (at the bottom of the page).
|
||||
|
||||
To see if it worked:
|
||||
|
||||
```shell
|
||||
py -2 -V
|
||||
```
|
||||
|
||||
Which should return `Python 2.7.18`.
|
||||
|
||||
Next, `cd` into this repository and create (and activate) a virtual environment:
|
||||
|
||||
```bash
|
||||
cd <path to project>/
|
||||
py -2 -m pip install virtualenv
|
||||
py -2 -m virtualenv .venv
|
||||
.\.venv\Scripts\activate
|
||||
```
|
||||
|
||||
Install the required packages with
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Now we need to install the NaoQi SDK into our virtual environment, which we need to do manually. Download the SDK from [Aldebaran](https://community-static.aldebaran.com/resources/2.5.5/sdk-python/pynaoqi-python2.7-2.5.5.5-win32-vs2013.zip), [Web Archive](https://web.archive.org/web/20240120111043/https://community-static.aldebaran.com/resources/2.5.5/sdk-python/pynaoqi-python2.7-2.5.5.5-win32-vs2013.zip) or [twirre.io](https://twirre.io/files/pynaoqi-python2.7-2.8.6.23-win64-vs2015-20191127_152649.zip).
|
||||
|
||||
Extract to `.\.venv\Lib\site-packages`.
|
||||
|
||||
Create a file `.venv\Lib\site-packages\pynaoqi-python2.7.pth`, put the full path of `pynaoqi-python2.7-2.8.6.23-win64-vs2015-20191127_152649\lib\python2.7\Lib\site-packages` in there.
|
||||
|
||||
Test if it worked by running:
|
||||
|
||||
```bash
|
||||
python -c "import qi; print(qi)"
|
||||
```
|
||||
|
||||
You should now be able to run this project.
|
||||
|
||||
See the README for how to run.
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import unicode_literals # So that we can log texts with Unicode characters
|
||||
import logging
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
import Queue
|
||||
import zmq
|
||||
|
||||
from robot_interface.endpoints.receiver_base import ReceiverBase
|
||||
@@ -27,6 +30,9 @@ class ActuationReceiver(ReceiverBase):
|
||||
self.create_socket(zmq_context, zmq.SUB, port)
|
||||
self.socket.setsockopt_string(zmq.SUBSCRIBE, u"") # Causes block if given in options
|
||||
self._tts_service = None
|
||||
self._message_queue = Queue.Queue()
|
||||
self.message_thread = Thread(target=self._handle_messages)
|
||||
self.message_thread.start()
|
||||
|
||||
def _handle_speech(self, message):
|
||||
"""
|
||||
@@ -53,8 +59,25 @@ class ActuationReceiver(ReceiverBase):
|
||||
if not self._tts_service:
|
||||
self._tts_service = state.qi_session.service("ALTextToSpeech")
|
||||
|
||||
# Returns instantly. Messages received while speaking will be queued.
|
||||
qi.async(self._tts_service.say, text)
|
||||
if (message.get("is_priority")):
|
||||
# Bypass queue and speak immediately
|
||||
self.clear_queue()
|
||||
self._message_queue.put(text)
|
||||
logging.debug("Force speaking immediately: {}".format(text))
|
||||
else:
|
||||
self._message_queue.put(text)
|
||||
|
||||
def clear_queue(self):
|
||||
"""
|
||||
Safely drains all pending messages from the queue.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
# Remove items one by one without waiting
|
||||
self._message_queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
pass
|
||||
logging.info("Message queue cleared.")
|
||||
|
||||
def handle_message(self, message):
|
||||
"""
|
||||
@@ -65,3 +88,18 @@ class ActuationReceiver(ReceiverBase):
|
||||
"""
|
||||
if message["endpoint"] == "actuate/speech":
|
||||
self._handle_speech(message)
|
||||
|
||||
def _handle_messages(self):
|
||||
while not state.exit_event.is_set():
|
||||
try:
|
||||
text = self._message_queue.get(timeout=0.1)
|
||||
if not state.is_speaking: print("Started speaking.")
|
||||
state.is_speaking = True
|
||||
self._tts_service.say(text)
|
||||
except Queue.Empty:
|
||||
if state.is_speaking: print("Finished speaking.")
|
||||
state.is_speaking = False
|
||||
except RuntimeError:
|
||||
logging.warn("Lost connection to Pepper. Please check if you're connected to the local WiFi and restart this application.")
|
||||
state.exit_event.set()
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class State(object):
|
||||
self.exit_event = None
|
||||
self.sockets = []
|
||||
self.qi_session = None
|
||||
self.is_speaking = False
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import mock
|
||||
import pytest
|
||||
import zmq
|
||||
|
||||
import Queue
|
||||
from robot_interface.endpoints.actuation_receiver import ActuationReceiver
|
||||
|
||||
|
||||
@@ -18,47 +18,107 @@ def zmq_context():
|
||||
context = zmq.Context()
|
||||
yield context
|
||||
|
||||
def test_force_speech_clears_queue(mocker):
|
||||
"""
|
||||
Tests that a force speech message clears the existing queue
|
||||
and places the high-priority message at the front.
|
||||
"""
|
||||
mocker.patch("threading.Thread")
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
mock_qi = mock.Mock()
|
||||
sys.modules["qi"] = mock_qi
|
||||
|
||||
def test_handle_unimplemented_endpoint(zmq_context):
|
||||
mock_tts_service = mock.Mock()
|
||||
mock_state.qi_session.service.return_value = mock_tts_service
|
||||
|
||||
# Use Mock Context
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
receiver._message_queue.put("old_message_1")
|
||||
receiver._message_queue.put("old_message_2")
|
||||
|
||||
assert receiver._message_queue.qsize() == 2
|
||||
|
||||
force_msg = {
|
||||
"endpoint": "actuate/speech",
|
||||
"data": "Emergency Notification",
|
||||
"is_priority": True,
|
||||
}
|
||||
receiver.handle_message(force_msg)
|
||||
|
||||
assert receiver._message_queue.qsize() == 1
|
||||
queued_item = receiver._message_queue.get()
|
||||
assert queued_item == "Emergency Notification"
|
||||
|
||||
def test_handle_unimplemented_endpoint(mocker):
|
||||
"""
|
||||
Tests that the ``ActuationReceiver.handle_message`` method can
|
||||
handle an unknown or unimplemented endpoint without raising an error.
|
||||
Tests handling of unknown endpoints.
|
||||
"""
|
||||
receiver = ActuationReceiver(zmq_context)
|
||||
# Should not error
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# Use Mock Context
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
receiver.handle_message({
|
||||
"endpoint": "some_endpoint_that_definitely_does_not_exist",
|
||||
"data": None,
|
||||
})
|
||||
|
||||
|
||||
def test_speech_message_no_data(zmq_context, mocker):
|
||||
def test_speech_message_no_data(mocker):
|
||||
"""
|
||||
Tests that the message handler logs a warning when a speech actuation
|
||||
request (`actuate/speech`) is received but contains empty string data.
|
||||
Tests that if the message data is empty, the receiver returns immediately
|
||||
WITHOUT attempting to access the global robot state or session.
|
||||
"""
|
||||
mock_warn = mocker.patch("logging.warn")
|
||||
# 1. Prevent background threads from running
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# 2. Mock the global state object
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
receiver = ActuationReceiver(zmq_context)
|
||||
# 3. Create a PropertyMock to track whenever 'qi_session' is accessed
|
||||
# We attach it to the class type of the mock so it acts like a real property
|
||||
mock_session_prop = mock.PropertyMock(return_value=None)
|
||||
type(mock_state).qi_session = mock_session_prop
|
||||
|
||||
# 4. Initialize Receiver (Mocking the context to avoid ZMQ errors)
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
# 5. Send empty data
|
||||
receiver.handle_message({"endpoint": "actuate/speech", "data": ""})
|
||||
|
||||
mock_warn.assert_called_with(mock.ANY)
|
||||
# 6. Assertion:
|
||||
# Because the code does `if not text: return` BEFORE `if not state.qi_session`,
|
||||
# the state property should NEVER be read.
|
||||
mock_session_prop.assert_not_called()
|
||||
|
||||
|
||||
def test_speech_message_invalid_data(zmq_context, mocker):
|
||||
def test_speech_message_invalid_data(mocker):
|
||||
"""
|
||||
Tests that the message handler logs a warning when a speech actuation
|
||||
request (`actuate/speech`) is received with data that is not a string (e.g., a boolean).
|
||||
Tests that if the message data is not a string, the function returns.
|
||||
:param mocker: Description
|
||||
"""
|
||||
mock_warn = mocker.patch("logging.warn")
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
receiver = ActuationReceiver(zmq_context)
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
mock_session_prop = mock.PropertyMock(return_value=None)
|
||||
type(mock_state).qi_session = mock_session_prop
|
||||
|
||||
# Use Mock Context
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
receiver.handle_message({"endpoint": "actuate/speech", "data": True})
|
||||
|
||||
mock_warn.assert_called_with(mock.ANY)
|
||||
# Because the code does `if not text: return` BEFORE `if not state.qi_session`,
|
||||
# the state property should NEVER be read.
|
||||
mock_session_prop.assert_not_called()
|
||||
|
||||
|
||||
def test_speech_no_qi(zmq_context, mocker):
|
||||
def test_speech_no_qi(mocker):
|
||||
"""
|
||||
Tests the actuation receiver's behavior when processing a speech request
|
||||
but the global state does not have an active QI session.
|
||||
@@ -68,16 +128,21 @@ def test_speech_no_qi(zmq_context, mocker):
|
||||
mock_qi_session = mock.PropertyMock(return_value=None)
|
||||
type(mock_state).qi_session = mock_qi_session
|
||||
|
||||
receiver = ActuationReceiver(zmq_context)
|
||||
mock_tts_service = mock.Mock()
|
||||
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
receiver._tts_service = mock_tts_service
|
||||
|
||||
receiver._handle_speech({"endpoint": "actuate/speech", "data": "Some message to speak."})
|
||||
|
||||
mock_qi_session.assert_called()
|
||||
receiver._tts_service.assert_not_called()
|
||||
|
||||
|
||||
def test_speech(zmq_context, mocker):
|
||||
def test_speech(mocker):
|
||||
"""
|
||||
Tests the core speech actuation functionality by mocking the QI TextToSpeech
|
||||
service and verifying that it is called correctly.
|
||||
service and verifying that the received message is put into the queue.
|
||||
"""
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
@@ -88,13 +153,179 @@ def test_speech(zmq_context, mocker):
|
||||
mock_state.qi_session = mock.Mock()
|
||||
mock_state.qi_session.service.return_value = mock_tts_service
|
||||
|
||||
receiver = ActuationReceiver(zmq_context)
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
receiver._tts_service = None
|
||||
receiver._handle_speech({"endpoint": "actuate/speech", "data": "Some message to speak."})
|
||||
|
||||
mock_state.qi_session.service.assert_called_once_with("ALTextToSpeech")
|
||||
assert receiver._message_queue.qsize() == 1
|
||||
|
||||
mock_qi.async.assert_called_once()
|
||||
call_args = mock_qi.async.call_args[0]
|
||||
assert call_args[0] == mock_tts_service.say
|
||||
assert call_args[1] == "Some message to speak."
|
||||
queued_item = receiver._message_queue.get()
|
||||
assert queued_item == "Some message to speak."
|
||||
|
||||
def test_speech_priority(mocker):
|
||||
"""
|
||||
Tests that a priority speech message is handled correctly by clearing the queue
|
||||
and placing the priority message at the front.
|
||||
"""
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
mock_qi = mock.Mock()
|
||||
sys.modules["qi"] = mock_qi
|
||||
|
||||
mock_tts_service = mock.Mock()
|
||||
mock_state.qi_session = mock.Mock()
|
||||
mock_state.qi_session.service.return_value = mock_tts_service
|
||||
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
receiver._message_queue.put("old_message_1")
|
||||
receiver._message_queue.put("old_message_2")
|
||||
|
||||
assert receiver._message_queue.qsize() == 2
|
||||
|
||||
priority_msg = {
|
||||
"endpoint": "actuate/speech",
|
||||
"data": "Urgent Message",
|
||||
"is_priority": True,
|
||||
}
|
||||
receiver._handle_speech(priority_msg)
|
||||
|
||||
assert receiver._message_queue.qsize() == 1
|
||||
queued_item = receiver._message_queue.get()
|
||||
assert queued_item == "Urgent Message"
|
||||
|
||||
def test_handle_messages_loop(mocker):
|
||||
"""
|
||||
Tests the background consumer loop (_handle_messages) processing an item.
|
||||
Runs SYNCHRONOUSLY to ensure coverage tools pick up the lines.
|
||||
"""
|
||||
# Patch Thread so the real background thread NEVER starts automatically
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# Mock state
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
# Setup initial speaking state to False (covers "Started speaking" print)
|
||||
mock_state.is_speaking = False
|
||||
|
||||
# Mock the TextToSpeech service
|
||||
mock_tts_service = mock.Mock()
|
||||
mock_state.qi_session.service.return_value = mock_tts_service
|
||||
|
||||
# Initialize receiver (Thread is patched, so no thread starts)
|
||||
# Use Mock Context to avoid ZMQ errors
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
# Manually inject service (since lazy loading might handle it, but this is safer)
|
||||
receiver._tts_service = mock_tts_service
|
||||
|
||||
# This ensures the while loop iterates exactly once
|
||||
mock_state.exit_event.is_set.side_effect = [False, True]
|
||||
|
||||
# Put an item in the queue
|
||||
receiver._message_queue.put("Hello World")
|
||||
|
||||
# RUN MANUALLY in the main thread
|
||||
# This executes the code: while -> try -> get -> if print -> speaking=True -> say
|
||||
receiver._handle_messages()
|
||||
|
||||
# Assertions
|
||||
assert receiver._message_queue.empty()
|
||||
mock_tts_service.say.assert_called_with("Hello World")
|
||||
assert mock_state.is_speaking is True
|
||||
|
||||
|
||||
def test_handle_messages_queue_empty(mocker):
|
||||
"""
|
||||
Tests the Queue.Empty exception handler in the consumer loop.
|
||||
This covers the logic that resets 'state.is_speaking' to False.
|
||||
"""
|
||||
# Prevent the real background thread from starting
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# Mock the state object
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
# Setup 'is_speaking' property mock
|
||||
# We set return_value=True so the code enters the 'if state.is_speaking:' block.
|
||||
# We use PropertyMock to track when this attribute is set.
|
||||
type(mock_state).is_speaking = True
|
||||
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
# This ensures the while loop body runs exactly once for our test
|
||||
mock_state.exit_event.is_set.side_effect = [False, True]
|
||||
|
||||
# Force get() to raise Queue.Empty immediately (simulate timeout)
|
||||
# We patch the 'get' method on the specific queue instance of our receiver
|
||||
#mocker.patch.object(receiver._message_queue, 'get', side_effect=Queue.Empty)
|
||||
|
||||
# Run the loop logic manually (synchronously)
|
||||
receiver._handle_messages()
|
||||
|
||||
# Final Assertion: Verify is_speaking was set to False
|
||||
# The code execution order is: read (returns True) -> print -> set (to False)
|
||||
# assert_called_with checks the arguments of the LAST call, which is the setter.
|
||||
assert mock_state.is_speaking is False
|
||||
|
||||
|
||||
def test_handle_messages_runtime_error(mocker):
|
||||
"""
|
||||
Tests the RuntimeError exception handler (e.g. lost WiFi connection).
|
||||
Uses a Mock ZMQ context to avoid 'Address already in use' errors.
|
||||
"""
|
||||
# Patch Thread so we don't accidentally spawn real threads
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# Mock the state and logging
|
||||
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
|
||||
|
||||
# Use a MOCK ZMQ context.
|
||||
# This prevents the receiver from trying to bind to a real TCP port.
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
|
||||
# Initialize receiver with the mock context
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
mock_state.exit_event.is_set.side_effect = [False, True]
|
||||
|
||||
receiver._message_queue.put("Test Message")
|
||||
|
||||
# Setup: ...BUT the service raises RuntimeError when asked to speak
|
||||
mock_tts = mock.Mock()
|
||||
mock_tts.say.side_effect = RuntimeError("Connection lost")
|
||||
receiver._tts_service = mock_tts
|
||||
|
||||
# Run the loop logic manually
|
||||
receiver._handle_messages()
|
||||
|
||||
# Assertions
|
||||
assert mock_state.exit_event.is_set.called
|
||||
|
||||
def test_clear_queue(mocker):
|
||||
"""
|
||||
Tests that the clear_queue method properly drains all items from the message queue.
|
||||
"""
|
||||
mocker.patch("threading.Thread")
|
||||
|
||||
# Use Mock Context
|
||||
mock_zmq_ctx = mock.Mock()
|
||||
receiver = ActuationReceiver(mock_zmq_ctx)
|
||||
|
||||
# Populate the queue with multiple items
|
||||
receiver._message_queue.put("msg1")
|
||||
receiver._message_queue.put("msg2")
|
||||
receiver._message_queue.put("msg3")
|
||||
|
||||
assert receiver._message_queue.qsize() == 3
|
||||
|
||||
# Clear the queue
|
||||
receiver.clear_queue()
|
||||
|
||||
# Assert the queue is empty
|
||||
assert receiver._message_queue.qsize() == 0
|
||||
Reference in New Issue
Block a user