1 Commits

Author SHA1 Message Date
Storm
19b7efec05 chore: implemented test video function
If there is no qi session, the webcam of the device is used to send video.
2026-01-22 11:36:34 +01:00
38 changed files with 224 additions and 692 deletions

View File

@@ -7,3 +7,4 @@ sphinx
sphinx_rtd_theme sphinx_rtd_theme
pre-commit pre-commit
python-dotenv python-dotenv
opencv-python==4.1.2.30

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from robot_interface.utils.get_config import get_config from robot_interface.utils.get_config import get_config

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that we can log texts with Unicode characters from __future__ import unicode_literals # So that we can log texts with Unicode characters
import logging import logging
from threading import Thread from threading import Thread

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that `logging` can use Unicode characters in names from __future__ import unicode_literals # So that `logging` can use Unicode characters in names
import threading import threading
import logging import logging
@@ -84,23 +77,16 @@ class AudioSender(SocketBase):
chunk = audio_settings.chunk_size # 320 at 16000 Hz is 20ms, 512 is required for Silero-VAD chunk = audio_settings.chunk_size # 320 at 16000 Hz is 20ms, 512 is required for Silero-VAD
# Docs say this only raises an error if neither `input` nor `output` is True # Docs say this only raises an error if neither `input` nor `output` is True
def open_stream(): stream = self.audio.open(
return self.audio.open( format=pyaudio.paFloat32,
format=pyaudio.paFloat32, channels=audio_settings.channels,
channels=audio_settings.channels, rate=audio_settings.sample_rate,
rate=audio_settings.sample_rate, input=True,
input=True, input_device_index=self.microphone["index"],
input_device_index=self.microphone["index"], frames_per_buffer=chunk,
frames_per_buffer=chunk, )
)
stream = None
try: try:
# Test in case exit_event was set while waiting
if not state.exit_event.is_set():
stream = open_stream()
while not state.exit_event.is_set(): while not state.exit_event.is_set():
data = stream.read(chunk) data = stream.read(chunk)
if (state.is_speaking): continue # Do not send audio while the robot is speaking if (state.is_speaking): continue # Do not send audio while the robot is speaking
@@ -108,9 +94,5 @@ class AudioSender(SocketBase):
except IOError as e: except IOError as e:
logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e) logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e)
finally: finally:
if stream: stream.stop_stream()
try: stream.close()
stream.stop_stream()
stream.close()
except IOError:
pass # Ignore errors on closing

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
class GestureTags: class GestureTags:
tags = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any", tags = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any",
"assuage", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank", "assuage", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank",

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import zmq import zmq
from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.endpoints.receiver_base import ReceiverBase
@@ -79,30 +72,6 @@ class MainReceiver(ReceiverBase):
return {"endpoint": "negotiate/error", "data": "The requested endpoint is not implemented."} return {"endpoint": "negotiate/error", "data": "The requested endpoint is not implemented."}
@staticmethod
def _handle_pause(message):
"""
Handle a pause request. Pauses or resumes the video and audio streams.
:param message: The pause request message.
:type message: dict
:return: A response dictionary indicating success.
:rtype: dict[str, str]
"""
if message.get("data"):
if state.active_event.is_set():
state.active_event.clear()
return {"endpoint": "pause", "data": "Streams paused."}
else:
return {"endpoint": "pause", "data": "Streams are already paused."}
else:
if not state.active_event.is_set():
state.active_event.set()
return {"endpoint": "pause", "data": "Streams resumed."}
else:
return {"endpoint": "pause", "data": "Streams are already running."}
def handle_message(self, message): def handle_message(self, message):
""" """
Main entry point for handling incoming messages. Main entry point for handling incoming messages.
@@ -119,7 +88,5 @@ class MainReceiver(ReceiverBase):
return self._handle_ping(message) return self._handle_ping(message)
elif message["endpoint"].startswith("negotiate"): elif message["endpoint"].startswith("negotiate"):
return self._handle_negotiation(message) return self._handle_negotiation(message)
elif message["endpoint"] == "pause":
return self._handle_pause(message)
return {"endpoint": "error", "data": "The requested endpoint is not supported."} return {"endpoint": "error", "data": "The requested endpoint is not supported."}

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from abc import ABCMeta from abc import ABCMeta
import zmq import zmq

View File

@@ -1,15 +1,6 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import struct
import zmq import zmq
import threading import threading
import logging import logging
import numpy as np
import cv2 import cv2
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase
@@ -29,7 +20,7 @@ class VideoSender(SocketBase):
""" """
def __init__(self, zmq_context, port=settings.agent_settings.video_sender_port): def __init__(self, zmq_context, port=settings.agent_settings.video_sender_port):
super(VideoSender, self).__init__("video") super(VideoSender, self).__init__("video")
self.create_socket(zmq_context, zmq.PUB, port, [(zmq.SNDHWM,3)]) self.create_socket(zmq_context, zmq.PUB, port, [(zmq.CONFLATE,1)])
def start_video_rcv(self): def start_video_rcv(self):
""" """
@@ -38,8 +29,7 @@ class VideoSender(SocketBase):
Will not start if no qi session is available. Will not start if no qi session is available.
""" """
if not state.qi_session: if not state.qi_session:
logging.info("No Qi session available. Not starting video loop.") logging.info("No Qi session available. Starting video from webcam.")
logging.info("Starting test video stream from local webcam.")
thread = threading.Thread(target=self.test_video_stream) thread = threading.Thread(target=self.test_video_stream)
thread.start() thread.start()
return return
@@ -55,9 +45,27 @@ class VideoSender(SocketBase):
thread = threading.Thread(target=self.video_rcv_loop, args=(video, vid_stream_name)) thread = threading.Thread(target=self.video_rcv_loop, args=(video, vid_stream_name))
thread.start() thread.start()
def video_rcv_loop(self, vid_service, vid_stream_name):
"""
The main loop of retrieving video images from the robot.
:param vid_service: The video service object that the active Qi session is connected to.
:type vid_service: Object (Qi service object)
:param vid_stream_name: The name of a camera subscription on the video service object vid_service
:type vid_stream_name: str
"""
while not state.exit_event.is_set():
try:
img = vid_service.getImageRemote(vid_stream_name)
#Possibly limit images sent if queuing issues arise
self.socket.send(img[settings.video_config.image_buffer])
except:
logging.warn("Failed to retrieve video image from robot.")
def test_video_stream(self): def test_video_stream(self):
""" """
Test function to send video from a local webcam instead of the robot. Test function to send video from local webcam instead of the robot.
""" """
cap = cv2.VideoCapture(0) cap = cv2.VideoCapture(0)
if not cap.isOpened(): if not cap.isOpened():
@@ -71,48 +79,12 @@ class VideoSender(SocketBase):
logging.warning("Failed to read frame from webcam.") logging.warning("Failed to read frame from webcam.")
continue continue
if cv2.waitKey(1) & 0xFF == ord('q'): # << Add this: Updates the window cv2.waitKey(1)
break
height, width, channels = frame.shape small_frame = cv2.resize(frame, (320, 240), interpolation=cv2.INTER_AREA)
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 70]
_, buffer = cv2.imencode('.jpg', small_frame, encode_param)
pixel_data = frame.tobytes() self.socket.send(buffer.tobytes())
width_bytes = struct.pack('<I', width)
height_bytes = struct.pack('<I', height)
self.socket.send_multipart([width_bytes, height_bytes, pixel_data])
cap.release() cap.release()
def video_rcv_loop(self, vid_service, vid_stream_name):
"""
The main loop of retrieving video images from the robot.
Sends the image data over the ZMQ socket in 3 parts: image width, image height and raw image bytes.
:param vid_service: The video service object that the active Qi session is connected to.
:type vid_service: Object (Qi service object)
:param vid_stream_name: The name of a camera subscription on the video service object vid_service
:type vid_stream_name: str
"""
try:
while not state.exit_event.is_set():
try:
img = vid_service.getImageRemote(vid_stream_name)
if img is not None:
image_bytes = img[6]
width = img[0]
height = img[1]
width_bytes = struct.pack('<I', width)
height_bytes = struct.pack('<I', height)
self.socket.send_multipart([width_bytes, height_bytes, image_bytes])
except:
logging.warn("Failed to retrieve video image from robot.")
except KeyboardInterrupt:
logging.info("Video receiving loop interrupted by user.")
finally:
vid_service.unsubscribe(vid_stream_name)
logging.info("Unsubscribed from video stream.")

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
from robot_interface.endpoints.audio_sender import AudioSender from robot_interface.endpoints.audio_sender import AudioSender
@@ -91,7 +84,6 @@ def main():
context = zmq.Context() context = zmq.Context()
state.initialize() state.initialize()
state.active_event.set()
try: try:
main_loop(context) main_loop(context)

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
import signal import signal
import threading import threading
@@ -56,8 +49,6 @@ class State(object):
signal.signal(signal.SIGINT, handle_exit) signal.signal(signal.SIGINT, handle_exit)
signal.signal(signal.SIGTERM, handle_exit) signal.signal(signal.SIGTERM, handle_exit)
self.active_event = threading.Event()
self.qi_session = get_qi_session() self.qi_session = get_qi_session()
self.is_initialized = True self.is_initialized = True

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that `print` can print Unicode characters in names from __future__ import unicode_literals # So that `print` can print Unicode characters in names
import logging import logging
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import time import time

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that we can format strings with Unicode characters from __future__ import unicode_literals # So that we can format strings with Unicode characters
import random import random
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from mock import patch, MagicMock from mock import patch, MagicMock
import pytest import pytest

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from mock import patch, mock from mock import patch, mock
from robot_interface.core.config import Settings from robot_interface.core.config import Settings

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pyaudio import pyaudio
import pytest import pytest

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import sys import sys
import mock import mock

View File

@@ -1,10 +1,4 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import os import os
import mock import mock
@@ -39,6 +33,7 @@ def test_no_microphone(zmq_context, mocker):
sender.start() sender.start()
assert sender.thread is None assert sender.thread is None
mock_info_logger.assert_called()
sender.wait_until_done() # Should return early because we didn't start a thread sender.wait_until_done() # Should return early because we didn't start a thread
@@ -78,7 +73,7 @@ def test_sending_audio(mocker):
mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L} mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L}
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state") mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.exit_event.is_set.side_effect = [False, False, True] mock_state.exit_event.is_set.side_effect = [False, True]
mock_zmq_context = mock.Mock() mock_zmq_context = mock.Mock()
send_socket = mock.Mock() send_socket = mock.Mock()
@@ -98,217 +93,6 @@ def test_sending_audio(mocker):
send_socket.assert_called() send_socket.assert_called()
# SENDING PAUSE RESUME?
def test_stream_initial_wait_exit(mocker):
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L}
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.exit_event.is_set.return_value = True
mock_state.active_event.is_set.return_value = False
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender._stream()
mock_pyaudio_instance.open.assert_not_called()
def test_stream_pause_and_resume(mocker):
mock_stream = mock.Mock()
mock_stream.read.return_value = b"data"
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.return_value = mock_stream
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = False
mock_state.exit_event.is_set.side_effect = [False, False, False, True]
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.socket = mock.Mock()
sender._stream()
assert mock_pyaudio_instance.open.call_count == 2
assert mock_stream.close.call_count == 2
assert mock_stream.stop_stream.call_count == 2
assert mock_state.active_event.wait.called
def test_stream_exit_during_pause(mocker):
mock_stream = mock.Mock()
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.return_value = mock_stream
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = False
mock_state.exit_event.is_set.side_effect = [False, False, True]
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender._stream()
assert mock_pyaudio_instance.open.call_count == 1
assert mock_stream.close.call_count == 1
def test_stream_read_error_recovery(mocker):
stream_fail = mock.Mock()
stream_fail.read.side_effect = IOError("Overflow")
stream_ok = mock.Mock()
stream_ok.read.return_value = b"data"
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.side_effect = [stream_fail, stream_ok]
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = True
mock_state.exit_event.is_set.side_effect = [False, False, False, True]
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.socket = mock.Mock()
sender._stream()
stream_fail.close.assert_called()
assert mock_pyaudio_instance.open.call_count == 2
sender.socket.send.assert_called_with(b"data")
def test_stream_fatal_error(mocker):
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.side_effect = IOError("Fatal error")
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = True
mock_state.exit_event.is_set.return_value = False
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender._stream()
def test_wait_until_done(mocker):
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.wait_until_done()
mock_thread = mocker.Mock()
sender.thread = mock_thread
sender.wait_until_done()
mock_thread.join.assert_called_once()
assert sender.thread is None
def test_stream_pause_close_error(mocker):
"""
Tests that an IOError during stream closure (when pausing) is ignored,
covering the 'pass' statement in the pause logic.
"""
mock_stream = mock.Mock()
mock_stream.read.return_value = b"data"
# Raise IOError when stopping the stream during pause
mock_stream.stop_stream.side_effect = IOError("Failed to stop")
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.return_value = mock_stream
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
# 1. First False triggers the pause block
# 2. Second True resumes the loop
mock_state.active_event.is_set.side_effect = [False, True]
mock_state.exit_event.is_set.side_effect = [False, False, False, True]
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.socket = mock.Mock()
sender._stream()
# Verification: The error should be swallowed, and the stream should re-open
assert mock_stream.stop_stream.called
assert mock_pyaudio_instance.open.call_count == 2
def test_stream_finally_close_error(mocker):
"""
Tests that an IOError during stream closure in the finally block is ignored,
covering the 'pass' statement in the finally logic.
"""
mock_stream = mock.Mock()
mock_stream.read.return_value = b"data"
# Raise IOError when stopping the stream at exit
mock_stream.stop_stream.side_effect = IOError("Cleanup failed")
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
mock_pyaudio_instance.open.return_value = mock_stream
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = True
mock_state.exit_event.is_set.side_effect = [False, False, True]
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.socket = mock.Mock()
# Run
sender._stream()
# Assert: Should finish without raising exception despite the IOError in finally
assert mock_stream.stop_stream.called
def test_stream_recovery_failure(mocker):
"""
Tests the case where recovering from a read error (re-opening stream) also fails.
This ensures the outer try-except catches exceptions from the inner except block.
"""
mock_stream_initial = mock.Mock()
# Trigger the read error logic
mock_stream_initial.read.side_effect = IOError("Read failed")
mock_pyaudio_cls = mocker.patch("robot_interface.endpoints.audio_sender.pyaudio.PyAudio")
mock_pyaudio_instance = mock_pyaudio_cls.return_value
# First open works, Second open (recovery) fails fatally
mock_pyaudio_instance.open.side_effect = [
mock_stream_initial,
IOError("Recovery failed")
]
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.active_event.is_set.return_value = True
mock_state.exit_event.is_set.return_value = False
mock_logger = mocker.patch("robot_interface.endpoints.audio_sender.logger")
mock_zmq_context = mock.Mock()
sender = AudioSender(mock_zmq_context)
sender.socket = mock.Mock()
sender._stream()
# Assert we hit the outer error log
mock_logger.error.assert_called()
def test_no_sending_if_speaking(mocker): def test_no_sending_if_speaking(mocker):
""" """

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from robot_interface.utils.get_config import get_config from robot_interface.utils.get_config import get_config

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pytest import pytest
import threading import threading
import zmq import zmq

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import pytest import pytest
import zmq import zmq

View File

@@ -1,10 +1,4 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import pytest import pytest

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import sys import sys
# Import module under test # Import module under test

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pytest import pytest
from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.endpoints.receiver_base import ReceiverBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import zmq import zmq
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import threading import threading
import signal import signal
import pytest import pytest

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import time import time
import mock import mock

View File

@@ -1,171 +1,216 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import pytest import pytest
import zmq import zmq
from robot_interface.endpoints.video_sender import VideoSender from robot_interface.endpoints.video_sender import VideoSender
from robot_interface.state import state
from robot_interface.core.config import settings
@pytest.fixture @pytest.fixture
def zmq_context(): def zmq_context():
""" """Provide a ZMQ context."""
Yields a real ZMQ context for socket creation. yield zmq.Context()
"""
context = zmq.Context()
yield context
context.term()
def test_init_defaults(zmq_context, mocker):
"""
Test initialization of the VideoSender.
"""
# We patch settings to ensure valid port access inside the class logic,
# although the default arg is evaluated at import time.
mocker.patch("robot_interface.endpoints.video_sender.settings")
mock_zmq = mock.Mock() def _patch_basics(mocker):
sender = VideoSender(mock_zmq) """Common patches: prevent real threads, port binds, and state errors."""
mocker.patch("robot_interface.endpoints.socket_base.zmq.Socket.bind")
mocker.patch("robot_interface.endpoints.video_sender.threading.Thread")
mocker.patch.object(state, "is_initialized", True)
# Verify socket type is PUB
assert sender.identifier == "video"
def test_start_no_qi_session(mocker): def _patch_exit_event(mocker):
""" """Make exit_event stop the loop after one iteration."""
Test that the loop does not start if no Qi session is available. fake_event = mock.Mock()
""" fake_event.is_set.side_effect = [False, True]
# Mock state to return None for qi_session mocker.patch.object(state, "exit_event", fake_event)
mock_state = mocker.patch("robot_interface.endpoints.video_sender.state")
mock_state.qi_session = None
mock_threading = mocker.patch("robot_interface.endpoints.video_sender.threading")
mock_zmq = mock.Mock() def test_no_qi_session(zmq_context, mocker):
sender = VideoSender(mock_zmq) """Video loop should not start without a qi_session."""
_patch_basics(mocker)
mocker.patch.object(state, "qi_session", None)
sender = VideoSender(zmq_context)
sender.start_video_rcv() sender.start_video_rcv()
# Assertions assert not hasattr(sender, "thread")
mock_threading.Thread.assert_not_called()
def test_start_success(mocker):
""" def test_video_streaming(zmq_context, mocker):
Test successful startup of the video receiver thread. """VideoSender should send retrieved image data."""
""" _patch_basics(mocker)
# Mock the Qi Session and Service _patch_exit_event(mocker)
mock_state = mocker.patch("robot_interface.endpoints.video_sender.state")
mock_session = mock.Mock() # Pepper's image buffer lives at index 6
mock_state.qi_session = mock_session mocker.patch.object(settings.video_config, "image_buffer", 6)
mock_video_service = mock.Mock() mock_video_service = mock.Mock()
mock_session.service.return_value = mock_video_service mock_video_service.getImageRemote.return_value = [None]*6 + ["fake_img"]
mock_video_service.subscribeCamera.return_value = "test_subscriber_id"
# Mock Settings fake_session = mock.Mock()
mock_settings = mocker.patch("robot_interface.endpoints.video_sender.settings") fake_session.service.return_value = mock_video_service
mock_settings.video_config.camera_index = 0 mocker.patch.object(state, "qi_session", fake_session)
mock_settings.video_config.resolution = 2
mock_settings.video_config.color_space = 11
mock_settings.video_config.fps = 30
mock_settings.video_config.stream_name = "test_stream"
mock_threading = mocker.patch("robot_interface.endpoints.video_sender.threading") mocker.patch.object(
fake_session.service("ALVideoDevice"),
"subscribeCamera",
return_value="stream_name"
)
sender = VideoSender(zmq_context)
send_socket = mock.Mock()
sender.socket.send = send_socket
# Run
mock_zmq = mock.Mock()
sender = VideoSender(mock_zmq)
sender.start_video_rcv() sender.start_video_rcv()
sender.video_rcv_loop(mock_video_service, "stream_name")
send_socket.assert_called_with("fake_img")
def test_video_receive_error(zmq_context, mocker):
"""Errors retrieving images should not call send()."""
_patch_basics(mocker)
_patch_exit_event(mocker)
mock_video_service = mock.Mock()
mock_video_service.getImageRemote.side_effect = Exception("boom")
fake_session = mock.Mock()
fake_session.service.return_value = mock_video_service
mocker.patch.object(state, "qi_session", fake_session)
mocker.patch.object(
fake_session.service("ALVideoDevice"),
"subscribeCamera",
return_value="stream_name"
)
sender = VideoSender(zmq_context)
send_socket = mock.Mock()
sender.socket.send = send_socket
sender.start_video_rcv()
sender.video_rcv_loop(mock_video_service, "stream_name")
send_socket.assert_not_called()
def test_video_stream_camera_fail(zmq_context, mocker):
"""
Test that the function logs an error and returns early if
the webcam cannot be opened.
"""
_patch_basics(mocker)
# Mock cv2 and logging
mock_cv2 = mocker.patch("robot_interface.endpoints.video_sender.cv2")
mock_logging = mocker.patch("robot_interface.endpoints.video_sender.logging")
# Setup the mock capture to fail isOpened()
mock_cap = mock.Mock()
mock_cap.isOpened.return_value = False
mock_cv2.VideoCapture.return_value = mock_cap
sender = VideoSender(zmq_context)
sender.test_video_stream()
# Assertions # Assertions
mock_session.service.assert_called_with("ALVideoDevice") mock_cv2.VideoCapture.assert_called_with(0)
mock_video_service.subscribeCamera.assert_called_with("test_stream", 0, 2, 11, 30)
mock_threading.Thread.assert_called_once() # Ensure the loop was never entered and cleanup didn't happen
# Verify arguments passed to the thread target assert not mock_cap.read.called
call_args = mock_threading.Thread.call_args[1] assert not mock_cap.release.called
assert call_args["target"] == sender.video_rcv_loop
assert call_args["args"] == (mock_video_service, "test_subscriber_id")
# Ensure thread was started
mock_threading.Thread.return_value.start.assert_called_once()
def test_video_loop_happy_path(mocker): def test_video_stream_read_fail(zmq_context, mocker):
""" """
Test the main loop: Wait -> Get Image -> Send -> Repeat/Exit. Test that the function logs a warning and continues the loop
if a specific frame fails to read.
""" """
# Mock settings for image buffer index _patch_basics(mocker)
mock_settings = mocker.patch("robot_interface.endpoints.video_sender.settings") _patch_exit_event(mocker) # Run loop exactly once
mock_settings.video_config.image_buffer = 6
# Mock Video Service to return a fake image structure mock_cv2 = mocker.patch("robot_interface.endpoints.video_sender.cv2")
# Standard NaoQi image is a list, binary data is usually at index 6 mock_logging = mocker.patch("robot_interface.endpoints.video_sender.logging")
fake_image_data = b"binary_jpeg_data"
fake_image_list = [0] * 7
fake_image_list[6] = fake_image_data
mock_service = mock.Mock() # Setup capture to open successfully, but fail the read()
mock_service.getImageRemote.return_value = fake_image_list mock_cap = mock.Mock()
mock_cap.isOpened.return_value = True
# Return (False, None) simulating a failed frame read
mock_cap.read.return_value = (False, None)
mock_cv2.VideoCapture.return_value = mock_cap
# Mock Events: sender = VideoSender(zmq_context)
# exit_event: False (start), False (loop once), True (break) # Mock the socket to ensure nothing is sent
mock_state = mocker.patch("robot_interface.endpoints.video_sender.state")
mock_state.exit_event.is_set.side_effect = [False, False, True]
# Run
mock_zmq = mock.Mock()
sender = VideoSender(mock_zmq)
sender.socket = mock.Mock() # Mock the socket to verify send
sender.video_rcv_loop(mock_service, "sub_id")
# Assertions
mock_state.active_event.wait.assert_called()
mock_service.getImageRemote.assert_called_with("sub_id")
sender.socket.send.assert_called_with(fake_image_data)
def test_video_loop_exit_during_wait(zmq_context, mocker):
"""
Test that the loop breaks immediately if exit_event is set while waiting.
"""
mock_service = mock.Mock()
mock_state = mocker.patch("robot_interface.endpoints.video_sender.state")
# 1. Loop check: False (enter loop)
# 2. Wait happens (mock returns instantly)
# 3. Post-wait check: True (break)
mock_state.exit_event.is_set.side_effect = [False, True]
mock_zqm = mock.Mock()
sender = VideoSender(mock_zqm)
sender.video_rcv_loop(mock_service, "sub_id")
# Assert we never tried to get an image
mock_service.getImageRemote.assert_not_called()
def test_video_loop_exception_handling(zmq_context, mocker):
"""
Test that exceptions during image retrieval are caught and logged,
and do not crash the thread.
"""
mock_settings = mocker.patch("robot_interface.endpoints.video_sender.settings")
mock_service = mock.Mock()
# First call raises Exception, Second call works (if we allowed it, but we exit)
mock_service.getImageRemote.side_effect = Exception("Camera disconnected")
mock_state = mocker.patch("robot_interface.endpoints.video_sender.state")
# Loop runs once then exits
mock_state.exit_event.is_set.side_effect = [False, False, True]
mock_zmq = mock.Mock()
sender = VideoSender(mock_zmq)
sender.socket = mock.Mock() sender.socket = mock.Mock()
sender.video_rcv_loop(mock_service, "sub_id") sender.test_video_stream()
# Ensure we skipped the processing steps
assert not mock_cv2.resize.called
assert not sender.socket.send.called
# Ensure cleanup happened at the end
mock_cap.release.assert_called_once()
def test_video_stream_success(zmq_context, mocker):
"""
Test the happy path: Frame read -> Resize -> Encode -> Send.
"""
_patch_basics(mocker)
_patch_exit_event(mocker) # Run loop exactly once
mock_cv2 = mocker.patch("robot_interface.endpoints.video_sender.cv2")
# Setup constants usually found in cv2
mock_cv2.IMWRITE_JPEG_QUALITY = 1
mock_cv2.INTER_AREA = 2
# Setup capture to work perfectly
mock_cap = mock.Mock()
mock_cap.isOpened.return_value = True
fake_frame = "original_frame_data"
mock_cap.read.return_value = (True, fake_frame)
mock_cv2.VideoCapture.return_value = mock_cap
# Setup Resize and Encode
mock_cv2.resize.return_value = "small_frame_data"
# Mock buffer behavior
mock_buffer = mock.Mock()
mock_buffer.tobytes.return_value = b"encoded_bytes"
# imencode returns (retval, buffer)
mock_cv2.imencode.return_value = (True, mock_buffer)
sender = VideoSender(zmq_context)
sender.socket = mock.Mock()
sender.test_video_stream()
# Assertions # Assertions
# Ensure loop didn't crash; it should have completed the iteration and checked exit_event # 1. Check waitKey (the 1ms delay)
assert mock_state.exit_event.is_set.call_count >= 2 mock_cv2.waitKey.assert_called_with(1)
# 2. Check Resize logic
mock_cv2.resize.assert_called_with(
fake_frame,
(320, 240),
interpolation=mock_cv2.INTER_AREA
)
# 3. Check Encode logic
mock_cv2.imencode.assert_called_with(
'.jpg',
"small_frame_data",
[mock_cv2.IMWRITE_JPEG_QUALITY, 70]
)
# 4. Check Socket Send
sender.socket.send.assert_called_with(b"encoded_bytes")
# 5. Check Cleanup
mock_cap.release.assert_called_once()