diff --git a/src/control_backend/agents/communication/ri_communication_agent.py b/src/control_backend/agents/communication/ri_communication_agent.py index 9f4bc4d..3bca6e4 100644 --- a/src/control_backend/agents/communication/ri_communication_agent.py +++ b/src/control_backend/agents/communication/ri_communication_agent.py @@ -167,7 +167,7 @@ class RICommunicationAgent(BaseAgent): bind = port_data["bind"] if not bind: - addr = f"tcp://localhost:{port}" + addr = f"tcp://{settings.ri_host}:{port}" else: addr = f"tcp://*:{port}" diff --git a/src/control_backend/agents/perception/vad_agent.py b/src/control_backend/agents/perception/vad_agent.py index 8ccff0a..5d0c497 100644 --- a/src/control_backend/agents/perception/vad_agent.py +++ b/src/control_backend/agents/perception/vad_agent.py @@ -103,12 +103,11 @@ class VADAgent(BaseAgent): self._connect_audio_in_socket() - audio_out_port = self._connect_audio_out_socket() - if audio_out_port is None: + audio_out_address = self._connect_audio_out_socket() + if audio_out_address is None: self.logger.error("Could not bind output socket, stopping.") await self.stop() return - audio_out_address = f"tcp://localhost:{audio_out_port}" # Connect to internal communication socket self.program_sub_socket = azmq.Context.instance().socket(zmq.SUB) @@ -161,13 +160,15 @@ class VADAgent(BaseAgent): self.audio_in_socket.connect(self.audio_in_address) self.audio_in_poller = SocketPoller[bytes](self.audio_in_socket) - def _connect_audio_out_socket(self) -> int | None: + def _connect_audio_out_socket(self) -> str | None: """ - Returns the port bound, or None if binding failed. + Returns the address that was bound to, or None if binding failed. """ try: self.audio_out_socket = azmq.Context.instance().socket(zmq.PUB) - return self.audio_out_socket.bind_to_random_port("tcp://localhost", max_tries=100) + address = "inproc://vad_stream" + self.audio_out_socket.bind(address) + return address except zmq.ZMQBindError: self.logger.error("Failed to bind an audio output socket after 100 tries.") self.audio_out_socket = None diff --git a/src/control_backend/core/config.py b/src/control_backend/core/config.py index fa105a5..0154c28 100644 --- a/src/control_backend/core/config.py +++ b/src/control_backend/core/config.py @@ -125,6 +125,7 @@ class Settings(BaseSettings): :ivar app_title: Title of the application. :ivar ui_url: URL of the frontend UI. + :ivar ui_url: The hostname of the Robot Interface. :ivar zmq_settings: ZMQ configuration. :ivar agent_settings: Agent name configuration. :ivar behaviour_settings: Behavior configuration. @@ -137,6 +138,8 @@ class Settings(BaseSettings): ui_url: str = "http://localhost:5173" + ri_host: str = "localhost" + zmq_settings: ZMQSettings = ZMQSettings() agent_settings: AgentSettings = AgentSettings() diff --git a/test/integration/agents/perception/vad_agent/test_vad_agent.py b/test/integration/agents/perception/vad_agent/test_vad_agent.py index f5f2615..668d1ce 100644 --- a/test/integration/agents/perception/vad_agent/test_vad_agent.py +++ b/test/integration/agents/perception/vad_agent/test_vad_agent.py @@ -91,7 +91,7 @@ def test_out_socket_creation(zmq_context): assert per_vad_agent.audio_out_socket is not None zmq_context.return_value.socket.assert_called_once_with(zmq.PUB) - zmq_context.return_value.socket.return_value.bind_to_random_port.assert_called_once() + zmq_context.return_value.socket.return_value.bind.assert_called_once_with("inproc://vad_stream") @pytest.mark.asyncio diff --git a/test/unit/agents/perception/vad_agent/test_vad_streaming.py b/test/unit/agents/perception/vad_agent/test_vad_streaming.py index 4440cae..166919f 100644 --- a/test/unit/agents/perception/vad_agent/test_vad_streaming.py +++ b/test/unit/agents/perception/vad_agent/test_vad_streaming.py @@ -7,6 +7,15 @@ import zmq from control_backend.agents.perception.vad_agent import VADAgent +# We don't want to use real ZMQ in unit tests, for example because it can give errors when sockets +# aren't closed properly. +@pytest.fixture(autouse=True) +def mock_zmq(): + with patch("zmq.asyncio.Context") as mock: + mock.instance.return_value = MagicMock() + yield mock + + @pytest.fixture def audio_out_socket(): return AsyncMock() @@ -140,12 +149,10 @@ async def test_vad_model_load_failure_stops_agent(vad_agent): # Patch stop to an AsyncMock so we can check it was awaited vad_agent.stop = AsyncMock() - result = await vad_agent.setup() + await vad_agent.setup() # Assert stop was called vad_agent.stop.assert_awaited_once() - # Assert setup returned None - assert result is None @pytest.mark.asyncio @@ -155,7 +162,7 @@ async def test_audio_out_bind_failure_sets_none_and_logs(vad_agent, caplog): audio_out_socket is set to None, None is returned, and an error is logged. """ mock_socket = MagicMock() - mock_socket.bind_to_random_port.side_effect = zmq.ZMQBindError() + mock_socket.bind.side_effect = zmq.ZMQBindError() with patch("control_backend.agents.perception.vad_agent.azmq.Context.instance") as mock_ctx: mock_ctx.return_value.socket.return_value = mock_socket