feat: package TG3 TS1P OmniSocket teleoperation

This commit is contained in:
2026-08-07 16:13:41 +08:00
commit 57e582e1d7
24 changed files with 4055 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# TG3 OmniSocket 同构臂传输代理
链路:
```text
EAI xTELE tcp://127.0.0.1:5003(原始帧)
+ tcp://127.0.0.1:5001(处理后的双手目标)
-> 仅将 5001 hand.position 合并到 5003
-> tg3-omnisocket-sender
-> KCP Hub 175.178.116.187:14049
-> tg3_local_teleop(直接 OmniSocket Session)
```
Peer ID:
- 工控机:`tg3-009027fa8190-iarm`
- 机器人:`tg3-009027fa8190-robot`
EAI 上仅保留 `omnisocket_xtele_sender.py`。它以 5003 最新帧为基础,保留双臂、
Z+C 按键及诊断字段;若 5001 的双侧 `hand.position` 在 250 ms 内有效,则合并厂家
xTELE 已处理的标量或六维 BrainCoRevo2 目标。5002 的界面事件和腰、头、腿、行走、
底盘命令不会进入机器人双臂桥。
公网业务数据由 EAI 本地会话门控:服务启动后仍持续读取本机 5003/5001,但不发送
xTELE 帧;连续长按左 Z + 右 C 3 秒后才生成新的 128-bit `session_id` 并开始发送。
再次长按 3 秒时发送最后一帧 `stop`,等待有界 KCP 刷新后关闭 OmniSocket Session;
之后 `frames_sent/bytes_sent` 不再增长,待机既没有 xTELE 业务帧,也没有该 sender 的
底层注册/心跳。即使 Hub 离线,sender 仍持续读取本机 xTELE 并保持会话关闭;启动连接
失败会使本次会话失效,必须松开组合键后重新长按,不会在 Hub 恢复时续发旧动作。
活动期间若 KCP 反馈超过 `500 ms` 未更新,或 `snd_queue+snd_buffer` 超过 100 帧,sender
立即作废会话并关闭 Session,从源头丢弃待发队列,防止网络恢复后回放旧动作。
每个业务 JSON 的 `tg3_transport` 由 sender 强制覆盖,不能由 5003 输入伪造:
```json
{
"protocol_version": 2,
"session_id": "32位十六进制随机值",
"session_seq": 1,
"session_state": "start | active | stop",
"stop_reason": "operator"
}
```
`start` 连续发送 50 个源帧,避免接收端只取最新帧时错过唯一启动事件;机器人对同一
`session_id` 只做一次启动安全检查。`stop` 是该会话最后一个业务帧。服务重启、源数据
失联或网络错误都会使会话失效,恢复后必须先松开组合键,再重新长按 3 秒。
`tg3_local_teleop` 内部直接拒绝非预期发送端、
乱序、格式错误以及相对本次连接最低包龄额外排队超过 300 ms 的数据;相对包龄会消除
两端系统时钟的固定偏差。遥操桥仍有 250 ms 输入失联保护。
机器人直接在 `tg3_local_teleop` 中接收 OmniSocket 数据,不再安装或运行单独的
`tg3-omnisocket-receiver.service`,也不再经过机器人本机 ZMQ 接收代理。

View File

@@ -0,0 +1,595 @@
#!/usr/bin/env python3
"""Session-gate local xTELE data and send it to the robot through OmniSocket.
The raw/local-data stream remains authoritative for arm positions, physical
buttons and hardware diagnostics. When configured, only the processed hand
target from xTELE's command stream is merged into that raw frame. This keeps
xTELE's built-in combo/gesture state machine and BrainCoRevo2 poses without
letting unrelated UI, walking or base commands reach the dual-arm bridge.
The physical left-Z + right-C hold is evaluated locally: idle frames never
leave EAI, while each active interval carries a unique START/ACTIVE/STOP ID.
"""
from __future__ import annotations
import argparse
import copy
import json
import math
import os
from pathlib import Path
import signal
import struct
import time
from typing import Any
import uuid
import zmq
from omnisocket import CONTROL_DEFAULTS, MSG_TYPE_ERROR, Session
MAGIC = b"TG3A"
HEADER = struct.Struct("!4sQQI")
MAX_PAYLOAD_BYTES = 1_048_576
TELEOP_PROTOCOL_VERSION = 2
class XteleSender:
def __init__(self, args: argparse.Namespace) -> None:
if args.start_stop_hold_s <= 0.0:
raise ValueError("start/stop hold time must be positive")
if args.start_marker_frames <= 0:
raise ValueError("start marker frame count must be positive")
if args.source_timeout_s <= 0.0:
raise ValueError("source timeout must be positive")
if args.max_feedback_age_ms <= 0.0:
raise ValueError("maximum KCP feedback age must be positive")
if args.max_pending_frames <= 0:
raise ValueError("maximum pending frame count must be positive")
self.args = args
self.stop = False
self.session: Session | None = None
self.session_connected_at = 0.0
self.started_at = time.time()
self.last_status_at = 0.0
self.last_frame_at = 0.0
self.last_source_frame_at = 0.0
self.last_command_at = 0.0
self.last_error = ""
self.teleop_active = False
self.teleop_session_id: str | None = None
self.teleop_session_seq = 0
self.packet_sequence = time.time_ns()
self.start_markers_remaining = 0
self.combo_started_at: float | None = None
# A service restart must never turn an already-held combo into a start.
self.require_combo_release = True
self.counters = {
"connected": 0,
"reconnects": 0,
"frames_received": 0,
"frames_sent": 0,
"bytes_received": 0,
"bytes_sent": 0,
"dropped_malformed": 0,
"command_frames_received": 0,
"command_frames_accepted": 0,
"command_frames_malformed": 0,
"command_hand_merges": 0,
"remote_errors": 0,
"frames_suppressed_inactive": 0,
"teleop_starts": 0,
"teleop_stops": 0,
"teleop_aborts": 0,
}
signal.signal(signal.SIGINT, self._request_stop)
signal.signal(signal.SIGTERM, self._request_stop)
def _request_stop(self, _signum: int, _frame: Any) -> None:
self.stop = True
def connect(self) -> bool:
self.close_session()
session = Session()
try:
session.connect(
server_addr=self.args.server,
peer_id=self.args.peer_id,
**CONTROL_DEFAULTS,
)
except OSError as exc:
self.last_error = f"OmniSocket connect failed: {exc}"
session.close()
self.write_status(force=True)
return False
self.session = session
self.session_connected_at = time.monotonic()
self.counters["connected"] = 1
self.counters["reconnects"] += 1
self.last_error = ""
self.write_status(force=True)
return True
def close_session(self) -> None:
if self.session is not None:
try:
self.session.close()
except OSError:
pass
self.session = None
self.session_connected_at = 0.0
self.counters["connected"] = 0
def write_status(self, force: bool = False) -> None:
now = time.monotonic()
if not force and now - self.last_status_at < 0.5:
return
session_stats: dict[str, object] = {}
kcp_stats: dict[str, object] = {}
if self.session is not None:
try:
session_stats = self.session.stats()
kcp_stats = self.session.kcp_stats()
except OSError as exc:
self.last_error = f"OmniSocket stats failed: {exc}"
status = {
"role": "xtele_sender",
"server": self.args.server,
"peer_id": self.args.peer_id,
"target_peer": self.args.target_peer,
"zmq_endpoint": self.args.zmq_endpoint,
"cmd_zmq_endpoint": self.args.cmd_zmq_endpoint or None,
"uptime_s": round(time.time() - self.started_at, 1),
"last_frame_age_s": None
if self.last_frame_at == 0.0
else round(time.monotonic() - self.last_frame_at, 4),
"last_command_age_s": None
if self.last_command_at == 0.0
else round(time.monotonic() - self.last_command_at, 4),
"last_source_frame_age_s": None
if self.last_source_frame_at == 0.0
else round(time.monotonic() - self.last_source_frame_at, 4),
"teleop_active": self.teleop_active,
"teleop_session_id": self.teleop_session_id,
"teleop_session_seq": self.teleop_session_seq,
"teleop_combo_hold_s": 0.0
if self.combo_started_at is None
else round(time.monotonic() - self.combo_started_at, 2),
"teleop_require_combo_release": self.require_combo_release,
"application_data_sending": (
self.teleop_active and self.session is not None
),
"last_error": self.last_error,
"session_stats": session_stats,
"kcp_stats": kcp_stats,
**self.counters,
"updated_unix_s": time.time(),
}
path = Path(self.args.status_file)
try:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(status, indent=2) + "\n")
os.replace(temporary, path)
except OSError:
pass
self.last_status_at = now
def _drain_responses(self) -> None:
if self.session is None:
return
try:
while True:
response = self.session.recv(timeout_ms=0)
if response is None:
break
_from_peer, msg_type, payload = response
if msg_type == MSG_TYPE_ERROR:
self.counters["remote_errors"] += 1
detail = payload.decode("utf-8", errors="replace")
self._abort_teleop(f"OmniSocket remote error: {detail}")
self.close_session()
return
except OSError as exc:
self._abort_teleop(f"OmniSocket receive failed: {exc}")
self.close_session()
@staticmethod
def _valid_hand_side(value: object) -> bool:
if isinstance(value, bool):
return False
if isinstance(value, (int, float)):
numeric = float(value)
return math.isfinite(numeric) and -0.05 <= numeric <= 1.05
if not isinstance(value, list) or len(value) != 6:
return False
try:
numeric_values = [float(item) for item in value]
except (TypeError, ValueError):
return False
return all(
math.isfinite(item) and -0.05 <= item <= 1.05
for item in numeric_values
)
@classmethod
def _processed_hand_position(
cls, command: dict[str, object]
) -> dict[str, object] | None:
try:
position = command["hand"]["position"] # type: ignore[index]
left = position["left"] # type: ignore[index]
right = position["right"] # type: ignore[index]
except (KeyError, TypeError):
return None
if not cls._valid_hand_side(left) or not cls._valid_hand_side(right):
return None
return {"left": copy.deepcopy(left), "right": copy.deepcopy(right)}
@classmethod
def _build_payload(
cls,
data: dict[str, object],
command: dict[str, object] | None,
session_id: str,
session_seq: int,
session_state: str,
stop_reason: str = "",
) -> tuple[bytes, bool]:
merged = False
try:
position = cls._processed_hand_position(command) if command else None
hand = data.get("hand")
if position is not None and isinstance(hand, dict):
hand["position"] = position
merged = True
metadata = data.get("tg3_transport")
if not isinstance(metadata, dict):
metadata = {}
data["tg3_transport"] = metadata
if merged:
metadata["processed_hand_from_xtele_cmd"] = True
assert command is not None
metadata["xtele_cmd_timestamp"] = command.get("timestamp")
else:
metadata.pop("processed_hand_from_xtele_cmd", None)
metadata.pop("xtele_cmd_timestamp", None)
# Always overwrite untrusted source metadata. The robot accepts
# start/active/stop only from this sender and expected Omni peer.
metadata["protocol_version"] = TELEOP_PROTOCOL_VERSION
metadata["session_id"] = session_id
metadata["session_seq"] = session_seq
metadata["session_state"] = session_state
if stop_reason:
metadata["stop_reason"] = stop_reason
else:
metadata.pop("stop_reason", None)
encoded = json.dumps(
data, ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
except (TypeError, ValueError):
raise ValueError("cannot encode xTELE session payload")
return encoded, merged
@staticmethod
def _start_stop_pressed(data: dict[str, object]) -> bool:
try:
buttons = data["button"]
left = buttons["left"] # type: ignore[index]
right = buttons["right"] # type: ignore[index]
return (
len(left) >= 3 # type: ignore[arg-type]
and len(right) >= 3 # type: ignore[arg-type]
and bool(left[2]) # type: ignore[index]
and bool(right[2]) # type: ignore[index]
)
except (KeyError, TypeError):
return False
def _update_teleop_gate(
self, now: float, data: dict[str, object]
) -> str | None:
pressed = self._start_stop_pressed(data)
if self.require_combo_release:
self.combo_started_at = None
if not pressed:
self.require_combo_release = False
return None
if not pressed:
self.combo_started_at = None
return None
if self.combo_started_at is None:
self.combo_started_at = now
if now - self.combo_started_at < self.args.start_stop_hold_s:
return None
self.combo_started_at = None
self.require_combo_release = True
if self.teleop_active:
self.teleop_active = False
self.counters["teleop_stops"] += 1
return "stop"
self.teleop_active = True
self.teleop_session_id = uuid.uuid4().hex
self.teleop_session_seq = 0
self.start_markers_remaining = self.args.start_marker_frames
self.counters["teleop_starts"] += 1
return "start"
def _abort_teleop(self, reason: str) -> None:
if self.teleop_active:
self.counters["teleop_aborts"] += 1
self.teleop_active = False
self.teleop_session_id = None
self.teleop_session_seq = 0
self.start_markers_remaining = 0
self.combo_started_at = None
self.require_combo_release = True
self.last_error = reason
def _send_payload(self, payload: bytes) -> bool:
if self.session is None:
self._abort_teleop("OmniSocket session is unavailable")
return False
unhealthy = self._session_unhealthy_reason()
if unhealthy:
self._abort_teleop(unhealthy)
self.close_session()
return False
self.packet_sequence += 1
packet = HEADER.pack(
MAGIC, self.packet_sequence, time.time_ns(), len(payload)
) + payload
try:
self.session.send(to=self.args.target_peer, data=packet)
except OSError as exc:
self._abort_teleop(f"OmniSocket send failed: {exc}")
self.close_session()
return False
self.counters["frames_sent"] += 1
self.counters["bytes_sent"] += len(payload)
self.last_frame_at = time.monotonic()
self.last_error = ""
return True
def _session_unhealthy_reason(self) -> str | None:
if self.session is None:
return "OmniSocket session is unavailable"
try:
session_stats = self.session.stats()
kcp_stats = self.session.kcp_stats()
if int(session_stats.get("connected", 0)) != 1 or int(
session_stats.get("registered", 0)
) != 1:
return "OmniSocket session lost registration"
pending = int(kcp_stats.get("snd_queue", 0)) + int(
kcp_stats.get("snd_buffer", 0)
)
if pending > self.args.max_pending_frames:
return (
f"OmniSocket pending queue reached {pending} frames; "
"session invalidated to prevent stale replay"
)
feedback_age_ms = float(kcp_stats.get("last_feedback_age_ms", 0.0))
connection_age_s = time.monotonic() - self.session_connected_at
if (
connection_age_s >= 0.5
and feedback_age_ms > self.args.max_feedback_age_ms
):
return (
f"OmniSocket feedback stale for {feedback_age_ms:.0f} ms; "
"session invalidated to prevent stale replay"
)
except (OSError, TypeError, ValueError) as exc:
return f"cannot verify OmniSocket session health: {exc}"
return None
def _flush_session(self, timeout_s: float = 0.75) -> None:
"""Bound the final STOP flush, without sending another business frame."""
deadline = time.monotonic() + timeout_s
while self.session is not None and time.monotonic() < deadline:
try:
stats = self.session.kcp_stats()
pending = int(stats.get("snd_queue", 0)) + int(
stats.get("snd_buffer", 0)
)
except (OSError, TypeError, ValueError):
return
if pending == 0:
return
self._drain_responses()
time.sleep(0.01)
def run(self) -> int:
context = zmq.Context()
source = context.socket(zmq.SUB)
source.setsockopt(zmq.SUBSCRIBE, b"")
source.setsockopt(zmq.CONFLATE, 1)
source.setsockopt(zmq.RCVHWM, 1)
source.setsockopt(zmq.LINGER, 0)
source.connect(self.args.zmq_endpoint)
poller = zmq.Poller()
poller.register(source, zmq.POLLIN)
command_source = None
if self.args.cmd_zmq_endpoint:
command_source = context.socket(zmq.SUB)
command_source.setsockopt(zmq.SUBSCRIBE, b"")
command_source.setsockopt(zmq.CONFLATE, 1)
command_source.setsockopt(zmq.RCVHWM, 1)
command_source.setsockopt(zmq.LINGER, 0)
command_source.connect(self.args.cmd_zmq_endpoint)
poller.register(command_source, zmq.POLLIN)
latest_command: dict[str, object] | None = None
try:
while not self.stop:
events = dict(poller.poll(100))
if command_source is not None and command_source in events:
command_raw = command_source.recv()
self.counters["command_frames_received"] += 1
try:
parsed_command = json.loads(command_raw)
if not isinstance(parsed_command, dict):
raise ValueError("xTELE command must be a JSON object")
if self._processed_hand_position(parsed_command) is None:
raise ValueError(
"xTELE command has no valid bilateral hand target"
)
latest_command = parsed_command
self.last_command_at = time.monotonic()
self.counters["command_frames_accepted"] += 1
except (TypeError, ValueError, json.JSONDecodeError):
self.counters["command_frames_malformed"] += 1
if source not in events:
now = time.monotonic()
if (
self.last_source_frame_at != 0.0
and now - self.last_source_frame_at
> self.args.source_timeout_s
):
self.combo_started_at = None
if self.teleop_active:
self._abort_teleop(
"local xTELE source became stale; a new Z+C hold "
"is required"
)
self.close_session()
self._drain_responses()
self.write_status()
continue
raw = source.recv()
self.counters["frames_received"] += 1
self.counters["bytes_received"] += len(raw)
if not raw or len(raw) > MAX_PAYLOAD_BYTES:
self.counters["dropped_malformed"] += 1
continue
try:
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("xTELE raw frame must be a JSON object")
except (TypeError, ValueError, json.JSONDecodeError):
# A malformed/frozen frame may never contribute time to a
# physical three-second start/stop hold.
self.combo_started_at = None
self.counters["dropped_malformed"] += 1
continue
now = time.monotonic()
self.last_source_frame_at = now
transition = self._update_teleop_gate(now, data)
command = None
if (
latest_command is not None
and now - self.last_command_at <= self.args.cmd_max_age_s
):
command = latest_command
if not self.teleop_active and transition != "stop":
self.counters["frames_suppressed_inactive"] += 1
self._drain_responses()
self.write_status()
continue
session_id = self.teleop_session_id
if not session_id:
self._abort_teleop("teleoperation session ID is unavailable")
self.counters["frames_suppressed_inactive"] += 1
continue
if self.session is None and not self.connect():
self._abort_teleop(
"cannot start teleoperation because OmniSocket Hub is "
"unavailable; release Z+C before retrying"
)
self.counters["frames_suppressed_inactive"] += 1
continue
self.teleop_session_seq += 1
if transition == "stop":
session_state = "stop"
stop_reason = "operator"
elif self.start_markers_remaining > 0:
session_state = "start"
stop_reason = ""
else:
session_state = "active"
stop_reason = ""
try:
payload, merged = self._build_payload(
data,
command,
session_id,
self.teleop_session_seq,
session_state,
stop_reason,
)
except ValueError:
self.counters["dropped_malformed"] += 1
continue
if merged:
self.counters["command_hand_merges"] += 1
if len(payload) > MAX_PAYLOAD_BYTES:
self.counters["dropped_malformed"] += 1
continue
sent = self._send_payload(payload)
if sent and session_state == "start":
self.start_markers_remaining -= 1
if transition == "stop":
# STOP is the final xTELE business frame. The underlying
# registered OmniSocket session remains warm for low-latency
# next start and KCP delivery, but no arm data follows.
self.teleop_session_id = None
self.teleop_session_seq = 0
self.start_markers_remaining = 0
self._flush_session()
self.close_session()
self._drain_responses()
self.write_status()
finally:
source.close()
if command_source is not None:
command_source.close()
context.term()
self.close_session()
self.write_status(force=True)
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--server", required=True)
parser.add_argument("--peer-id", required=True)
parser.add_argument("--target-peer", required=True)
parser.add_argument("--zmq-endpoint", required=True)
parser.add_argument(
"--cmd-zmq-endpoint",
default="",
help=(
"optional xTELE processed-command PUB endpoint; only its bilateral "
"hand.position target is merged into the raw frame"
),
)
parser.add_argument("--cmd-max-age-s", type=float, default=0.25)
parser.add_argument("--source-timeout-s", type=float, default=0.25)
parser.add_argument("--max-feedback-age-ms", type=float, default=500.0)
parser.add_argument("--max-pending-frames", type=int, default=100)
parser.add_argument("--start-stop-hold-s", type=float, default=3.0)
parser.add_argument(
"--start-marker-frames",
type=int,
default=50,
help="repeat START for this many source frames before ACTIVE",
)
parser.add_argument("--status-file", required=True)
return parser.parse_args()
if __name__ == "__main__":
raise SystemExit(XteleSender(parse_args()).run())

View 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()

View File

@@ -0,0 +1,17 @@
[Unit]
Description=TG3 session-gated TS1P OmniSocket KCP sender
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/eai/tg3_omnisocket_transport
Environment=PYTHONPATH=/home/eai/OmniSocketGo/python
ExecStart=/usr/bin/python3 /home/eai/tg3_omnisocket_transport/omnisocket_xtele_sender.py --server 175.178.116.187:14049 --peer-id tg3-009027fa8190-iarm --target-peer tg3-009027fa8190-robot --zmq-endpoint tcp://127.0.0.1:5003 --cmd-zmq-endpoint tcp://127.0.0.1:5001 --cmd-max-age-s 0.25 --source-timeout-s 0.25 --start-stop-hold-s 3.0 --start-marker-frames 50 --max-feedback-age-ms 500 --max-pending-frames 100 --status-file /home/eai/tg3_omnisocket_transport/status.json
Restart=always
RestartSec=1
KillSignal=SIGINT
TimeoutStopSec=5
[Install]
WantedBy=default.target