feat: package TG3 TS1P OmniSocket teleoperation
This commit is contained in:
132
tg3_omnisocket_transport/test_session_gate.py
Normal file
132
tg3_omnisocket_transport/test_session_gate.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline tests for the EAI teleoperation session gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
fake_omnisocket = ModuleType("omnisocket")
|
||||
fake_omnisocket.CONTROL_DEFAULTS = {}
|
||||
fake_omnisocket.MSG_TYPE_ERROR = 5
|
||||
fake_omnisocket.Session = object
|
||||
sys.modules.setdefault("omnisocket", fake_omnisocket)
|
||||
|
||||
module_path = Path(__file__).with_name("omnisocket_xtele_sender.py")
|
||||
spec = importlib.util.spec_from_file_location("omnisocket_xtele_sender", module_path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
sender_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(sender_module)
|
||||
XteleSender = sender_module.XteleSender
|
||||
|
||||
|
||||
def buttons(pressed: bool) -> dict[str, object]:
|
||||
return {
|
||||
"button": {
|
||||
"left": [False, False, pressed],
|
||||
"right": [False, False, pressed],
|
||||
},
|
||||
"hand": {"position": {"left": 0.0, "right": 0.0}},
|
||||
}
|
||||
|
||||
|
||||
class SessionGateTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
args = SimpleNamespace(
|
||||
start_stop_hold_s=3.0,
|
||||
start_marker_frames=50,
|
||||
source_timeout_s=0.25,
|
||||
max_feedback_age_ms=500.0,
|
||||
max_pending_frames=100,
|
||||
)
|
||||
self.sender = XteleSender(args)
|
||||
|
||||
def test_start_and_stop_each_require_a_new_continuous_hold(self) -> None:
|
||||
# Boot requires a release, so a button held across service restart
|
||||
# cannot start a session.
|
||||
self.assertIsNone(self.sender._update_teleop_gate(0.0, buttons(True)))
|
||||
self.assertIsNone(self.sender._update_teleop_gate(0.1, buttons(False)))
|
||||
|
||||
self.assertIsNone(self.sender._update_teleop_gate(1.0, buttons(True)))
|
||||
self.assertIsNone(self.sender._update_teleop_gate(3.99, buttons(True)))
|
||||
self.assertEqual(
|
||||
self.sender._update_teleop_gate(4.01, buttons(True)), "start"
|
||||
)
|
||||
first_id = self.sender.teleop_session_id
|
||||
self.assertTrue(self.sender.teleop_active)
|
||||
self.assertIsNotNone(first_id)
|
||||
|
||||
# Keeping the same hold cannot immediately toggle the new session off.
|
||||
self.assertIsNone(self.sender._update_teleop_gate(8.0, buttons(True)))
|
||||
self.assertTrue(self.sender.teleop_active)
|
||||
self.assertIsNone(self.sender._update_teleop_gate(8.1, buttons(False)))
|
||||
|
||||
self.assertIsNone(self.sender._update_teleop_gate(9.0, buttons(True)))
|
||||
self.assertIsNone(self.sender._update_teleop_gate(11.99, buttons(True)))
|
||||
self.assertEqual(
|
||||
self.sender._update_teleop_gate(12.01, buttons(True)), "stop"
|
||||
)
|
||||
self.assertFalse(self.sender.teleop_active)
|
||||
self.assertEqual(self.sender.teleop_session_id, first_id)
|
||||
|
||||
def test_releasing_during_hold_resets_the_timer(self) -> None:
|
||||
self.sender._update_teleop_gate(0.0, buttons(False))
|
||||
self.sender._update_teleop_gate(1.0, buttons(True))
|
||||
self.sender._update_teleop_gate(2.0, buttons(False))
|
||||
self.sender._update_teleop_gate(3.0, buttons(True))
|
||||
self.assertIsNone(self.sender._update_teleop_gate(5.9, buttons(True)))
|
||||
self.assertEqual(
|
||||
self.sender._update_teleop_gate(6.01, buttons(True)), "start"
|
||||
)
|
||||
|
||||
def test_transport_metadata_is_overwritten(self) -> None:
|
||||
data = buttons(False)
|
||||
data["tg3_transport"] = {
|
||||
"protocol_version": 999,
|
||||
"session_id": "forged",
|
||||
"session_state": "stop",
|
||||
}
|
||||
payload, merged = self.sender._build_payload(
|
||||
data,
|
||||
None,
|
||||
"a" * 32,
|
||||
7,
|
||||
"start",
|
||||
)
|
||||
self.assertFalse(merged)
|
||||
metadata = json.loads(payload)["tg3_transport"]
|
||||
self.assertEqual(metadata["protocol_version"], 2)
|
||||
self.assertEqual(metadata["session_id"], "a" * 32)
|
||||
self.assertEqual(metadata["session_seq"], 7)
|
||||
self.assertEqual(metadata["session_state"], "start")
|
||||
|
||||
def test_stale_or_backlogged_transport_is_rejected(self) -> None:
|
||||
class FakeSession:
|
||||
def __init__(self, feedback_age: int, pending: int) -> None:
|
||||
self.feedback_age = feedback_age
|
||||
self.pending = pending
|
||||
|
||||
def stats(self) -> dict[str, int]:
|
||||
return {"connected": 1, "registered": 1}
|
||||
|
||||
def kcp_stats(self) -> dict[str, int]:
|
||||
return {
|
||||
"snd_queue": self.pending,
|
||||
"snd_buffer": 0,
|
||||
"last_feedback_age_ms": self.feedback_age,
|
||||
}
|
||||
|
||||
self.sender.session_connected_at = 0.0
|
||||
self.sender.session = FakeSession(600, 0)
|
||||
self.assertIn("feedback stale", self.sender._session_unhealthy_reason())
|
||||
self.sender.session = FakeSession(1, 101)
|
||||
self.assertIn("pending queue", self.sender._session_unhealthy_reason())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user