Skip to content

Commit fc84bb1

Browse files
dennys246claude
andcommitted
Add mDNS discovery and migrate localhost_only to connection_mode
Replace deprecated localhost_only parameter with connection_mode="network" across all ReachyMini SDK call sites to skip unnecessary localhost probe. Fix flaky LSH similarity test by using higher-recall index configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dbc878d commit fc84bb1

5 files changed

Lines changed: 213 additions & 9 deletions

File tree

src/maxim/conscience/connection.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class ConnectionConfig:
2828
robot_name: str = "reachy_mini"
2929
timeout: float = 30.0
3030
media_backend: str = "default"
31-
localhost_only: bool = False
31+
localhost_only: bool = False # Deprecated: use connection_mode instead
32+
connection_mode: str = "network" # "auto", "localhost_only", or "network"
3233
spawn_daemon: bool = False
3334
use_sim: bool = False
3435

@@ -185,7 +186,7 @@ def connect(self, start_recording: bool = True) -> Any:
185186

186187
self._mini = ReachyMini(
187188
robot_name=self.config.robot_name,
188-
localhost_only=self.config.localhost_only,
189+
connection_mode=self.config.connection_mode,
189190
spawn_daemon=self.config.spawn_daemon,
190191
use_sim=self.config.use_sim,
191192
timeout=self.config.timeout,
@@ -340,7 +341,7 @@ def reconnect(
340341

341342
self._mini = ReachyMini(
342343
robot_name=self.config.robot_name,
343-
localhost_only=self.config.localhost_only,
344+
connection_mode=self.config.connection_mode,
344345
spawn_daemon=self.config.spawn_daemon,
345346
use_sim=self.config.use_sim,
346347
timeout=self.config.timeout,

src/maxim/conscience/selfy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def __init__(
122122
self.log.info("Connecting to Reachy Mini '%s'...", self.name)
123123
self._connect_kwargs = {
124124
"robot_name": self.name,
125-
"localhost_only": False,
125+
"connection_mode": "network",
126126
"spawn_daemon": False,
127127
"use_sim": False,
128128
"timeout": float(timeout),

src/tests/basic_move.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ except Exception as e:
3434
robot_name = os.environ.get("MAXIM_ROBOT_NAME", "reachy_mini")
3535
3636
try:
37-
mini = ReachyMini(robot_name=robot_name, localhost_only=False, spawn_daemon=False, use_sim=False, timeout=5.0)
37+
mini = ReachyMini(robot_name=robot_name, connection_mode="network", spawn_daemon=False, use_sim=False, timeout=5.0)
3838
except Exception as e:
3939
msg = f"[basic_move] {'FAIL' if require_robot else 'SKIP'}: could not connect to Reachy Mini '{robot_name}': {e}"
4040
print(msg)

tests/unit/test_context_index.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ def test_identical_text_returns_high_similarity(self, index):
6868
assert results[0][0] == "mem_1"
6969
assert results[0][1] >= 0.9
7070

71-
def test_similar_text_found(self, index):
72-
# Use nearly identical text to ensure LSH band collision
73-
index.register("mem_1", "the robot saw a person near the table in the kitchen")
74-
results = index.query_similar(
71+
def test_similar_text_found(self):
72+
# LSH is probabilistic; use more bands (higher recall) to make collisions reliable
73+
from maxim.memory.context_index import SimilarityIndex
74+
75+
hi_recall = SimilarityIndex(num_hashes=64, num_bands=16)
76+
hi_recall.register("mem_1", "the robot saw a person near the table in the kitchen")
77+
results = hi_recall.query_similar(
7578
"the robot saw a person near the table in the room", min_similarity=0.2
7679
)
7780
result_ids = {r[0] for r in results}

tests/unit/test_mdns_discovery.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""Unit tests for mDNS discovery and ReachyMiniController connection logic."""
2+
3+
from __future__ import annotations
4+
5+
import socket
6+
from unittest.mock import MagicMock, patch
7+
8+
import pytest
9+
10+
11+
class TestMdnsResolution:
12+
"""Test _resolve_mdns method on ReachyMiniController."""
13+
14+
def _make_controller(self, robot_name: str = "reachy_mini") -> object:
15+
"""Create a ReachyMiniController without connecting."""
16+
from maxim.hardware.reachy.controller import ReachyMiniController
17+
18+
return ReachyMiniController(robot_name=robot_name)
19+
20+
def test_resolve_success(self):
21+
"""Successful mDNS resolution returns IP."""
22+
ctrl = self._make_controller("reachy_mini")
23+
24+
with patch("socket.gethostbyname", return_value="192.168.50.150"):
25+
ip = ctrl._resolve_mdns(timeout=2.0)
26+
27+
assert ip == "192.168.50.150"
28+
29+
def test_resolve_converts_underscores_to_hyphens(self):
30+
"""Robot name underscores are converted to hyphens for mDNS."""
31+
ctrl = self._make_controller("reachy_mini")
32+
33+
with patch("socket.gethostbyname") as mock_resolve:
34+
mock_resolve.return_value = "10.0.0.1"
35+
ctrl._resolve_mdns()
36+
37+
mock_resolve.assert_called_once_with("reachy-mini.local")
38+
39+
def test_resolve_appends_dot_local(self):
40+
"""mDNS name gets .local suffix."""
41+
ctrl = self._make_controller("my-robot")
42+
43+
with patch("socket.gethostbyname") as mock_resolve:
44+
mock_resolve.return_value = "10.0.0.2"
45+
ctrl._resolve_mdns()
46+
47+
mock_resolve.assert_called_once_with("my-robot.local")
48+
49+
def test_resolve_failure_returns_none(self):
50+
"""Failed resolution returns None."""
51+
ctrl = self._make_controller("reachy_mini")
52+
53+
with patch("socket.gethostbyname", side_effect=socket.gaierror("not found")):
54+
ip = ctrl._resolve_mdns(timeout=1.0)
55+
56+
assert ip is None
57+
58+
def test_resolve_restores_default_timeout(self):
59+
"""Socket default timeout is restored after resolution."""
60+
ctrl = self._make_controller("reachy_mini")
61+
original_timeout = socket.getdefaulttimeout()
62+
63+
with patch("socket.gethostbyname", return_value="10.0.0.1"):
64+
ctrl._resolve_mdns(timeout=3.0)
65+
66+
assert socket.getdefaulttimeout() == original_timeout
67+
68+
def test_resolve_restores_timeout_on_failure(self):
69+
"""Socket default timeout is restored even on failure."""
70+
ctrl = self._make_controller("reachy_mini")
71+
original_timeout = socket.getdefaulttimeout()
72+
73+
with patch("socket.gethostbyname", side_effect=socket.gaierror("fail")):
74+
ctrl._resolve_mdns(timeout=3.0)
75+
76+
assert socket.getdefaulttimeout() == original_timeout
77+
78+
def test_resolve_no_underscores_in_name(self):
79+
"""Robot name without underscores works correctly."""
80+
ctrl = self._make_controller("reachymini")
81+
82+
with patch("socket.gethostbyname") as mock_resolve:
83+
mock_resolve.return_value = "10.0.0.3"
84+
ctrl._resolve_mdns()
85+
86+
mock_resolve.assert_called_once_with("reachymini.local")
87+
88+
89+
class TestControllerConnect:
90+
"""Test ReachyMiniController.connect() integration with mDNS."""
91+
92+
def _make_controller(self, robot_name: str = "reachy_mini") -> object:
93+
from maxim.hardware.reachy.controller import ReachyMiniController
94+
95+
return ReachyMiniController(robot_name=robot_name)
96+
97+
@patch("maxim.hardware.reachy.controller.ReachyMiniController._resolve_mdns")
98+
def test_connect_calls_mdns_resolve(self, mock_mdns):
99+
"""connect() pre-resolves mDNS before SDK init."""
100+
mock_mdns.return_value = "192.168.50.150"
101+
ctrl = self._make_controller()
102+
103+
with patch.dict("sys.modules", {"reachy_mini": MagicMock()}):
104+
import sys
105+
106+
mock_sdk_module = sys.modules["reachy_mini"]
107+
mock_mini = MagicMock()
108+
mock_sdk_module.ReachyMini.return_value = mock_mini
109+
110+
ctrl.connect(timeout=5.0)
111+
112+
mock_mdns.assert_called_once_with(timeout=5.0)
113+
114+
@patch("maxim.hardware.reachy.controller.ReachyMiniController._resolve_mdns")
115+
def test_connect_caps_mdns_timeout_at_5s(self, mock_mdns):
116+
"""mDNS timeout is capped at 5 seconds even with larger connect timeout."""
117+
mock_mdns.return_value = "192.168.50.150"
118+
ctrl = self._make_controller()
119+
120+
with patch.dict("sys.modules", {"reachy_mini": MagicMock()}):
121+
import sys
122+
123+
mock_sdk_module = sys.modules["reachy_mini"]
124+
mock_mini = MagicMock()
125+
mock_sdk_module.ReachyMini.return_value = mock_mini
126+
127+
ctrl.connect(timeout=30.0)
128+
129+
mock_mdns.assert_called_once_with(timeout=5.0)
130+
131+
@patch("maxim.hardware.reachy.controller.ReachyMiniController._resolve_mdns")
132+
def test_connect_uses_network_mode(self, mock_mdns):
133+
"""connect() passes connection_mode='network' to SDK."""
134+
mock_mdns.return_value = "192.168.50.150"
135+
ctrl = self._make_controller()
136+
137+
with patch.dict("sys.modules", {"reachy_mini": MagicMock()}):
138+
import sys
139+
140+
mock_sdk_module = sys.modules["reachy_mini"]
141+
mock_mini = MagicMock()
142+
mock_sdk_module.ReachyMini.return_value = mock_mini
143+
144+
ctrl.connect(timeout=5.0)
145+
146+
mock_sdk_module.ReachyMini.assert_called_once()
147+
call_kwargs = mock_sdk_module.ReachyMini.call_args[1]
148+
assert call_kwargs["connection_mode"] == "network"
149+
150+
@patch("maxim.hardware.reachy.controller.ReachyMiniController._resolve_mdns")
151+
def test_connect_proceeds_when_mdns_fails(self, mock_mdns):
152+
"""connect() still attempts SDK connection when mDNS fails."""
153+
mock_mdns.return_value = None
154+
ctrl = self._make_controller()
155+
156+
with patch.dict("sys.modules", {"reachy_mini": MagicMock()}):
157+
import sys
158+
159+
mock_sdk_module = sys.modules["reachy_mini"]
160+
mock_mini = MagicMock()
161+
mock_sdk_module.ReachyMini.return_value = mock_mini
162+
163+
result = ctrl.connect(timeout=5.0)
164+
165+
assert result is True
166+
167+
def test_connect_returns_false_on_import_error(self):
168+
"""connect() returns False when reachy-mini SDK is not installed."""
169+
ctrl = self._make_controller()
170+
171+
# Ensure reachy_mini is not importable
172+
with patch.dict("sys.modules", {"reachy_mini": None}):
173+
result = ctrl.connect(timeout=1.0)
174+
175+
assert result is False
176+
177+
178+
class TestConnectionConfigMigration:
179+
"""Test that ConnectionConfig uses connection_mode correctly."""
180+
181+
def test_default_connection_mode_is_network(self):
182+
"""Default connection_mode is 'network'."""
183+
from maxim.conscience.connection import ConnectionConfig
184+
185+
config = ConnectionConfig()
186+
assert config.connection_mode == "network"
187+
188+
def test_custom_connection_mode(self):
189+
"""Can set custom connection_mode."""
190+
from maxim.conscience.connection import ConnectionConfig
191+
192+
config = ConnectionConfig(connection_mode="auto")
193+
assert config.connection_mode == "auto"
194+
195+
def test_localhost_only_still_exists_for_compat(self):
196+
"""localhost_only field still exists for backward compatibility."""
197+
from maxim.conscience.connection import ConnectionConfig
198+
199+
config = ConnectionConfig()
200+
assert hasattr(config, "localhost_only")

0 commit comments

Comments
 (0)