chore: applied all feedback

close: N25B-298
This commit is contained in:
Pim Hutting
2025-11-22 11:45:32 +01:00
parent 051f904576
commit c53307530b
12 changed files with 53 additions and 212 deletions

View File

@@ -5,23 +5,14 @@ class AgentSettings(object):
"""
Agent port configuration.
:param actuating_receiver_port: Port for receiving actuation commands.
:type actuating_receiver_port: int
:param main_receiver_port: Port for receiving main messages.
:type main_receiver_port: int
:param video_sender_port: Port used for sending video frames.
:type video_sender_port: int
:param audio_sender_port: Port used for sending audio data.
:type audio_sender_port: int
:ivar actuating_receiver_port: Port for receiving actuation commands.
:type actuating_receiver_port: int
:vartype actuating_receiver_port: int
:ivar main_receiver_port: Port for receiving main messages.
:type main_receiver_port: int
:vartype main_receiver_port: int
:ivar video_sender_port: Port used for sending video frames.
:type video_sender_port: int
:vartype video_sender_port: int
:ivar audio_sender_port: Port used for sending audio data.
:type audio_sender_port: int
:vartype audio_sender_port: int
"""
def __init__(
self,
@@ -40,31 +31,18 @@ class VideoConfig(object):
"""
Video configuration constants.
:param camera_index: Index of the camera to use.
:type camera_index: int
:param resolution: Video resolution mode.
:type resolution: int
:param color_space: Color space identifier.
:type color_space: int
:param fps: Frames per second of the video stream.
:type fps: int
:param stream_name: Name of the video stream.
:type stream_name: str
:param image_buffer: Internal buffer size for video frames.
:type image_buffer: int
:ivar camera_index: Index of the camera used.
:type camera_index: int
:vartype camera_index: int
:ivar resolution: Video resolution mode.
:type resolution: int
:vartype resolution: int
:ivar color_space: Color space identifier.
:type color_space: int
:vartype color_space: int
:ivar fps: Frames per second of the video stream.
:type fps: int
:vartype fps: int
:ivar stream_name: Name of the video stream.
:type stream_name: str
:vartype stream_name: str
:ivar image_buffer: Internal buffer size for video frames.
:type image_buffer: int
:vartype image_buffer: int
"""
def __init__(
self,
@@ -87,19 +65,12 @@ class AudioConfig(object):
"""
Audio configuration constants.
:param sample_rate: Audio sampling rate in Hz.
:type sample_rate: int
:param chunk_size: Size of audio chunks to capture/process.
:type chunk_size: int
:param channels: Number of audio channels.
:type channels: int
:ivar sample_rate: Audio sampling rate in Hz.
:type sample_rate: int
:vartype sample_rate: int
:ivar chunk_size: Size of audio chunks to capture/process.
:type chunk_size: int
:vartype chunk_size: int
:ivar channels: Number of audio channels.
:type channels: int
:vartype channels: int
"""
def __init__(self, sample_rate=16000, chunk_size=512, channels=1):
self.sample_rate = sample_rate
@@ -111,15 +82,10 @@ class MainConfig(object):
"""
Main system configuration.
:param poll_timeout_ms: Timeout for polling events, in milliseconds.
:type poll_timeout_ms: int
:param max_handler_time_ms: Maximum allowed handler time, in milliseconds.
:type max_handler_time_ms: int
:ivar poll_timeout_ms: Timeout for polling events, in milliseconds.
:type poll_timeout_ms: int
:vartype poll_timeout_ms: int
:ivar max_handler_time_ms: Maximum allowed handler time, in milliseconds.
:type max_handler_time_ms: int
:vartype max_handler_time_ms: int
"""
def __init__(self, poll_timeout_ms=100, max_handler_time_ms=50):
self.poll_timeout_ms = poll_timeout_ms
@@ -130,23 +96,14 @@ class Settings(object):
"""
Global settings container.
:param agent_settings: Agent settings instance or None for defaults.
:type agent_settings: AgentSettings | None
:param video_config: VideoConfig instance or None for defaults.
:type video_config: VideoConfig | None
:param audio_config: AudioConfig instance or None for defaults.
:type audio_config: AudioConfig | None
:param main_config: MainConfig instance or None for defaults.
:type main_config: MainConfig | None
:ivar agent_settings: Agent-related port configuration.
:type agent_settings: AgentSettings
:vartype agent_settings: AgentSettings
:ivar video_config: Video stream configuration.
:type video_config: VideoConfig
:vartype video_config: VideoConfig
:ivar audio_config: Audio stream configuration.
:type audio_config: AudioConfig
:vartype audio_config: AudioConfig
:ivar main_config: Main system-level configuration.
:type main_config: MainConfig
:vartype main_config: MainConfig
"""
def __init__(self, agent_settings=None, video_config=None, audio_config=None, main_config=None):
self.agent_settings = agent_settings or AgentSettings()

View File

@@ -20,7 +20,7 @@ class ActuationReceiver(ReceiverBase):
:type port: int
:ivar _tts_service: The text-to-speech service object from the Qi session.
:vartype _tts_service: ssl.SSLSession | None
:vartype _tts_service: qi.Session | None
"""
def __init__(self, zmq_context, port=settings.agent_settings.actuating_receiver_port):

View File

@@ -24,13 +24,13 @@ class AudioSender(SocketBase):
:type port: int
:ivar thread: Thread used for sending audio.
:type thread: threading.Thread | None
:vartype thread: threading.Thread | None
:ivar audio: PyAudio instance.
:type audio: pyaudio.PyAudio | None
:vartype audio: pyaudio.PyAudio | None
:ivar microphone: Selected microphone information.
:type microphone: dict | None
:vartype microphone: dict | None
"""
def __init__(self, zmq_context, port=settings.agent_settings.audio_sender_port):
super(AudioSender, self).__init__(str("audio")) # Convert future's unicode_literal to str
@@ -51,7 +51,6 @@ class AudioSender(SocketBase):
Will not start if no microphone is available.
"""
if not self.microphone:
logger.info("Not listening: no microphone available.")
return

View File

@@ -7,17 +7,20 @@ class SocketBase(object):
"""
Base class for endpoints associated with a ZeroMQ socket.
:ivar identifier: The identifier of the endpoint.
:param identifier: The identifier of the endpoint.
:type identifier: str
:ivar identifier: The identifier of the endpoint.
:vartype identifier: str
:ivar port: The port used by the socket, set by `create_socket`.
:type port: int | None
:vartype port: int | None
:ivar socket: The ZeroMQ socket object, set by `create_socket`.
:type socket: zmq.Socket | None
:vartype socket: zmq.Socket | None
:ivar bound: Whether the socket is bound or connected, set by `create_socket`.
:type bound: bool | None
:vartype bound: bool | None
"""
__metaclass__ = ABCMeta
@@ -43,8 +46,7 @@ class SocketBase(object):
:param port: The port to use.
:type port: int
:param options: A list of options to be set on the socket. The list contains tuples where the first element contains the option
and the second the value, for example (zmq.CONFLATE, 1).
:param options: A list of tuples where the first element contains the option and the second the value.
:type options: list[tuple[int, int]]
:param bind: Whether to bind the socket or connect to it.
@@ -73,7 +75,7 @@ class SocketBase(object):
Description of the endpoint. Used for negotiation.
:return: A dictionary with the following keys: id, port, bind. See API specification at:
https://utrechtuniversity.youtrack.cloud/articles/N25B-A-14/RI-CB-Communication#negotiation
https://utrechtuniversity.youtrack.cloud/articles/N25B-A-14/RI-CB-Communication#negotiation
:rtype: dict
"""
return {

View File

@@ -24,7 +24,7 @@ class VideoSender(SocketBase):
"""
Prepares arguments for retrieving video images from Pepper and starts video loop on a separate thread.
Will not start of no qi session is available.
Will not start if no qi session is available.
"""
if not state.qi_session:
logging.info("No Qi session available. Not starting video loop.")

View File

@@ -14,16 +14,16 @@ class State(object):
detect this via the `exit_event` property being set.
:ivar is_initialized: Flag indicating whether the state setup (exit handlers, QI session) has completed.
:type is_initialized: bool
:vartype is_initialized: bool
:ivar exit_event: A thread event used to signal all threads that the program is shutting down.
:type exit_event: threading.Event | None
:vartype exit_event: threading.Event | None
:ivar sockets: A list of ZeroMQ socket wrappers (`SocketBase`) that need to be closed during deinitialization.
:type sockets: List[SocketBase]
:vartype sockets: List[SocketBase]
:ivar qi_session: The QI session object used for interaction with the robot/platform services.
:type qi_session: None | ssl.SSLSession
:vartype qi_session: None | qi.Session
"""
def __init__(self):
self.is_initialized = False

View File

@@ -7,13 +7,22 @@ class TimeBlock(object):
limit, or if no limit is given, the callback will be called with the time that the block took.
:param callback: The callback function that is called when the block of code is over,
unless the code block did not exceed the time limit.
unless the code block did not exceed the time limit.
:type callback: Callable[[float], None]
:param limit_ms: The number of milliseconds the block of code is allowed to take. If it
exceeds this time, or if it's None, the callback function will be called with the time the
block took.
exceeds this time, or if it's None, the callback function will be called with the time the
block took.
:type limit_ms: int | None
:ivar limit_ms: The number of milliseconds the block of code is allowed to take.
:vartype limit_ms: float | None
:ivar callback: The callback function that is called when the block of code is over.
:vartype callback: Callable[[float], None]
ivar start: The start time of the block, set when entering the context.
:vartype start: float | None
"""
def __init__(self, callback, limit_ms=None):
self.limit_ms = float(limit_ms) if limit_ms is not None else None

View File

@@ -23,9 +23,6 @@ class MicrophoneUtils(object):
The result must contain at least "index", as this is used to identify the microphone,
and "name" for logging. It must have one or more channels (`maxInputChannels`),
and a default sample rate of at least 16000 Hz.
:param pyaudio_instance: A mocked or real PyAudio instance used to query microphone information.
:type pyaudio_instance: PyAudio
"""
result = choose_mic_default(pyaudio_instance)
assert "index" in result
@@ -47,12 +44,6 @@ class MicrophoneUtils(object):
Tests the robustness of the interactive selection when the user first enters
a non-integer value, ensuring the system prompts again without error and accepts
a valid integer on the second attempt.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
microphones = get_microphones(pyaudio_instance)
target_microphone = next(microphones)
@@ -74,12 +65,6 @@ class MicrophoneUtils(object):
"""
Tests that the interactive selection method prevents the user from entering
a negative integer as a microphone index.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
microphones = get_microphones(pyaudio_instance)
target_microphone = next(microphones)
@@ -101,12 +86,6 @@ class MicrophoneUtils(object):
"""
Tests that the interactive selection method prevents the user from entering
an index that exceeds the total number of available microphones.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
real_count = len(list(get_microphones(pyaudio_instance)))
mock_input = mocker.patch("__builtin__.raw_input", side_effect=[str(real_count), "0"])
@@ -126,12 +105,6 @@ class MicrophoneUtils(object):
Tests the core interactive functionality by simulating the selection of a
random valid microphone index and verifying that the correct microphone
information is returned.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
microphones = list(get_microphones(pyaudio_instance))
random_index = random.randrange(len(microphones))
@@ -143,6 +116,9 @@ class MicrophoneUtils(object):
assert result["index"] == microphones[random_index]["index"]
def test_choose_mic_no_arguments(self, pyaudio_instance, mocker):
"""
Tests `choose_mic_arguments` when no command-line arguments are provided,
"""
mocker.patch.object(sys, "argv", [])
result = choose_mic_arguments(pyaudio_instance)
@@ -153,12 +129,6 @@ class MicrophoneUtils(object):
"""
Tests `choose_mic_arguments` when the microphone name is passed as a separate
argument.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
for mic in get_microphones(pyaudio_instance):
mocker.patch.object(sys, "argv", ["--microphone", mic["name"]])
@@ -172,12 +142,6 @@ class MicrophoneUtils(object):
"""
Tests `choose_mic_arguments` when the microphone name is passed using an
equals sign (`--microphone=NAME`).
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
for mic in get_microphones(pyaudio_instance):
mocker.patch.object(sys, "argv", ["--microphone={}".format(mic["name"])])
@@ -191,12 +155,6 @@ class MicrophoneUtils(object):
"""
Tests `choose_mic_arguments` when a non-existent microphone name is passed
via command-line arguments, expecting the function to return None.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mocker.patch.object(sys, "argv", ["--microphone", "Surely this microphone doesn't exist"])
@@ -208,12 +166,6 @@ class MicrophoneUtils(object):
"""
Tests `choose_mic` function when a valid microphone is
specified via command-line arguments.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mic = next(get_microphones(pyaudio_instance))
mocker.patch.object(sys, "argv", ["--microphone", mic["name"]])
@@ -228,12 +180,6 @@ class MicrophoneUtils(object):
Tests `choose_mic` function when no command-line arguments
are provided, verifying that the function falls back correctly to the
system's default microphone selection.
:param pyaudio_instance: A mocked or real PyAudio instance.
:type pyaudio_instance: PyAudio
:param mocker: The fixture used for mocking built-in functions and system objects.
:type mocker: pytest_mock.plugin.MockerFixture
"""
default_mic = choose_mic_default(pyaudio_instance)
mocker.patch.object(sys, "argv", [])

View File

@@ -23,9 +23,6 @@ def test_handle_unimplemented_endpoint(zmq_context):
"""
Tests that the ``ActuationReceiver.handle_message`` method can
handle an unknown or unimplemented endpoint without raising an error.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = ActuationReceiver(zmq_context)
# Should not error
@@ -39,12 +36,6 @@ def test_speech_message_no_data(zmq_context, mocker):
"""
Tests that the message handler logs a warning when a speech actuation
request (`actuate/speech`) is received but contains empty string data.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch `logging.warn`.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_warn = mocker.patch("logging.warn")
@@ -58,12 +49,6 @@ def test_speech_message_invalid_data(zmq_context, 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).
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch `logging.warn`.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_warn = mocker.patch("logging.warn")
@@ -77,12 +62,6 @@ def test_speech_no_qi(zmq_context, mocker):
"""
Tests the actuation receiver's behavior when processing a speech request
but the global state does not have an active QI session.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch the global state.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
@@ -99,12 +78,6 @@ def test_speech(zmq_context, mocker):
"""
Tests the core speech actuation functionality by mocking the QI TextToSpeech
service and verifying that it is called correctly.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch state and modules.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")

View File

@@ -24,12 +24,6 @@ def zmq_context():
def test_no_microphone(zmq_context, mocker):
"""
Tests the scenario where no valid microphone can be chosen for recording.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch internal dependencies.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_info_logger = mocker.patch("robot_interface.endpoints.audio_sender.logger.info")
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
@@ -49,12 +43,6 @@ def test_unicode_mic_name(zmq_context, mocker):
"""
Tests the robustness of the `AudioSender` when handling microphone names
that contain Unicode characters.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
:param mocker: The pytest-mock fixture used to patch internal dependencies.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mocker.patch("robot_interface.endpoints.audio_sender.threading")
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
@@ -74,12 +62,6 @@ def test_unicode_mic_name(zmq_context, mocker):
def _fake_read(num_frames):
"""
Helper function to simulate reading raw audio data from a microphone stream.
:param num_frames: The number of audio frames requested.
:type num_frames: int
:return: A byte string containing random data, simulating audio.
:rtype: str
"""
return os.urandom(num_frames * 4)
@@ -87,9 +69,6 @@ def _fake_read(num_frames):
def test_sending_audio(mocker):
"""
Tests the successful sending of audio data over a ZeroMQ socket.
:param mocker: The pytest-mock fixture used to patch internal dependencies.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L}
@@ -118,9 +97,6 @@ def test_sending_audio(mocker):
def _fake_read_error(num_frames):
"""
Helper function to simulate an I/O error during microphone stream reading.
:param num_frames: The number of audio frames requested.
:type num_frames: int
"""
raise IOError()
@@ -128,9 +104,6 @@ def _fake_read_error(num_frames):
def test_break_microphone(mocker):
"""
Tests the error handling when the microphone stream breaks (raises an IOError).
:param mocker: The pytest-mock fixture used to patch internal dependencies.
:type mocker: pytest_mock.plugin.MockerFixture
"""
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L}

View File

@@ -20,9 +20,6 @@ def zmq_context():
def test_handle_ping(zmq_context):
"""
Tests the receiver's ability to handle the "ping" endpoint with data.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = MainReceiver(zmq_context)
response = receiver.handle_message({"endpoint": "ping", "data": "pong"})
@@ -37,9 +34,6 @@ def test_handle_ping_none(zmq_context):
"""
Tests the receiver's ability to handle the ping endpoint when the
data field is explicitly set to None.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = MainReceiver(zmq_context)
response = receiver.handle_message({"endpoint": "ping", "data": None})
@@ -54,12 +48,6 @@ def test_handle_ping_none(zmq_context):
def test_handle_negotiate_ports(mock_state, zmq_context):
"""
Tests the handling of the "negotiate/ports" endpoint.
:param mock_state: Mocked global application state.
:type mock_state: mock.Mock
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = MainReceiver(zmq_context)
mock_state.sockets = [receiver]
@@ -85,9 +73,6 @@ def test_handle_unimplemented_endpoint(zmq_context):
"""
Tests that the receiver correctly handles a request to a completely
unknown or non-existent endpoint.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = MainReceiver(zmq_context)
response = receiver.handle_message({
@@ -108,9 +93,6 @@ def test_handle_unimplemented_negotiation_endpoint(zmq_context):
The expected behavior is to return a specific "negotiate/error" response
with a descriptive error string.
:param zmq_context: The ZeroMQ context fixture.
:type zmq_context: zmq.Context
"""
receiver = MainReceiver(zmq_context)
response = receiver.handle_message({

View File

@@ -15,7 +15,7 @@ class MockPyAudio:
the core PyAudio methods required for device enumeration.
:ivar devices: A list of dictionaries representing mock audio devices.
:type devices: List[Dict[str, Any]]
:vartype devices: List[Dict[str, Any]]
"""
def __init__(self):
# You can predefine fake device info here