Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/litegrip/gripper.py
Original file line number Diff line number Diff line change
Expand Up @@ -1899,10 +1899,15 @@ def teleop_start(
if transport is None:
if link == "zenoh":
if mode == "master":
# ⚠ 常驻 publisher **不登记**成 per-session transport:登记了
# `teleop_stop` 就会 close 它,而 `_teleop_pub` 仍指向这个
# 已死的端点 ⇒ 此后每一轮都在往死会话里 put。(真机实测:
# 第一次配对正常,之后每次从端 0 帧。)常驻端点的生命周期
# 只归 `disconnect()` → `_close_teleop_pub()`。
transport = self._open_teleop_pub(port, key)
else:
transport = _zenoh_transport("slave", key, port, host)
created_transport = transport
created_transport = transport
elif link == "udp":
if host is None:
raise ValueError("host is required for the udp link")
Expand Down
50 changes: 50 additions & 0 deletions tests/test_teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,56 @@ def test_start_refuses_zenoh_link_without_the_extra(self):
with self.assertRaises(ImportError):
g.teleop_start("master")

def test_master_keeps_its_resident_publisher_across_sessions(self):
"""A stop must not close the leader's resident zenoh publisher.

Registering it as the per-session transport made ``teleop_stop`` close
it, while ``_teleop_pub`` kept pointing at the dead endpoint — so every
session after the first published into nothing. On hardware the first
pairing delivered frames and every later one delivered none, while the
leader's own frame counter kept climbing.
"""
g, _ = make_gripper()
from litegrip import gripper as gripper_mod
built = []

class _FakeLink:
def __init__(self):
self.closed = False
self.puts = 0

def pub(self, topic, payload):
self.puts += 1

def close(self):
self.closed = True

def _factory(role, key, port, host):
built.append(_FakeLink())
return built[-1]

with unittest.mock.patch.object(gripper_mod, "_zenoh_transport",
_factory):
g.teleop_start("master", rate_hz=200.0)
g.teleop_stop()
resident = g._teleop_pub
self.assertIsInstance(resident, _FakeLink)
self.assertFalse(
resident.closed,
"teleop_stop must not close the resident publisher")
self.assertTrue(_wait_until(lambda: resident.puts > 0))

g.teleop_start("master", rate_hz=200.0)
self.assertIs(g._teleop_pub, resident)
self.assertEqual(len(built), 1,
"the listener is built once, not per session")
g.teleop_stop()
self.assertFalse(resident.closed)

g.disconnect()
self.assertTrue(resident.closed,
"disconnect() owns the resident publisher")


class ReadinessTest(unittest.TestCase):
"""The three preconditions that must hold before anything is enabled."""
Expand Down
Loading