"""Typed packet models for HTS telemetry."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any
from hand_tracking_sdk._compat import StrEnum
from hand_tracking_sdk.constants import STREAMED_JOINT_NAMES
[docs]
class HandSide(StrEnum):
"""Logical side for a tracked hand."""
LEFT = "Left"
RIGHT = "Right"
HEAD = "Head"
[docs]
class PacketType(StrEnum):
"""Packet data category emitted by HTS."""
WRIST = "wrist"
LANDMARKS = "landmarks"
POSE = "pose"
[docs]
class JointName(StrEnum):
"""Canonical joint names matching HTS landmark order."""
WRIST = "Wrist"
THUMB_METACARPAL = "ThumbMetacarpal"
THUMB_PROXIMAL = "ThumbProximal"
THUMB_DISTAL = "ThumbDistal"
THUMB_TIP = "ThumbTip"
INDEX_PROXIMAL = "IndexProximal"
INDEX_INTERMEDIATE = "IndexIntermediate"
INDEX_DISTAL = "IndexDistal"
INDEX_TIP = "IndexTip"
MIDDLE_PROXIMAL = "MiddleProximal"
MIDDLE_INTERMEDIATE = "MiddleIntermediate"
MIDDLE_DISTAL = "MiddleDistal"
MIDDLE_TIP = "MiddleTip"
RING_PROXIMAL = "RingProximal"
RING_INTERMEDIATE = "RingIntermediate"
RING_DISTAL = "RingDistal"
RING_TIP = "RingTip"
LITTLE_PROXIMAL = "LittleProximal"
LITTLE_INTERMEDIATE = "LittleIntermediate"
LITTLE_DISTAL = "LittleDistal"
LITTLE_TIP = "LittleTip"
[docs]
class FingerName(StrEnum):
"""Supported finger groups for convenience accessors."""
WRIST = "wrist"
THUMB = "thumb"
INDEX = "index"
MIDDLE = "middle"
RING = "ring"
LITTLE = "little"
_JOINT_INDEX_BY_NAME: dict[str, int] = {
name: index for index, name in enumerate(STREAMED_JOINT_NAMES)
}
[docs]
@dataclass(frozen=True, slots=True)
class PacketDebugInfo:
"""Optional source-side metadata attached to one streamed packet."""
source_frame_seq: int | None = None
source_ts_ns: int | None = None
[docs]
@dataclass(frozen=True, slots=True)
class WristPose:
"""Cartesian wrist position and orientation quaternion."""
x: float
y: float
z: float
qx: float
qy: float
qz: float
qw: float
[docs]
def to_dict(self) -> dict[str, float]:
"""Serialize wrist pose into a mapping-friendly dictionary.
:returns:
Deterministic dictionary with position and quaternion fields.
"""
return {
"x": self.x,
"y": self.y,
"z": self.z,
"qx": self.qx,
"qy": self.qy,
"qz": self.qz,
"qw": self.qw,
}
[docs]
@classmethod
def from_dict(cls, values: Mapping[str, Any]) -> WristPose:
"""Build :class:`WristPose` from serialized mapping data.
:param values:
Mapping containing ``x, y, z, qx, qy, qz, qw``.
:returns:
Parsed wrist pose instance.
"""
return cls(
x=float(values["x"]),
y=float(values["y"]),
z=float(values["z"]),
qx=float(values["qx"]),
qy=float(values["qy"]),
qz=float(values["qz"]),
qw=float(values["qw"]),
)
[docs]
@dataclass(frozen=True, slots=True)
class HeadPose:
"""Cartesian head position and orientation quaternion."""
x: float
y: float
z: float
qx: float
qy: float
qz: float
qw: float
[docs]
def to_dict(self) -> dict[str, float]:
"""Serialize head pose into a mapping-friendly dictionary."""
return {
"x": self.x,
"y": self.y,
"z": self.z,
"qx": self.qx,
"qy": self.qy,
"qz": self.qz,
"qw": self.qw,
}
[docs]
@classmethod
def from_dict(cls, values: Mapping[str, Any]) -> HeadPose:
"""Build :class:`HeadPose` from serialized mapping data."""
return cls(
x=float(values["x"]),
y=float(values["y"]),
z=float(values["z"]),
qx=float(values["qx"]),
qy=float(values["qy"]),
qz=float(values["qz"]),
qw=float(values["qw"]),
)
[docs]
@dataclass(frozen=True, slots=True)
class HandLandmarks:
"""Ordered set of 21 hand landmarks as ``(x, y, z)`` points."""
points: tuple[tuple[float, float, float], ...]
[docs]
def get_joint(self, joint: JointName | str) -> tuple[float, float, float]:
"""Return one joint point by name.
:param joint:
Joint to query, either as :class:`JointName` or canonical joint string
(for example ``"IndexTip"``).
:returns:
Joint ``(x, y, z)`` tuple.
:raises ValueError:
If the joint name is unknown.
"""
joint_name = joint.value if isinstance(joint, JointName) else joint
index = _JOINT_INDEX_BY_NAME.get(joint_name)
if index is None:
raise ValueError(f"Unknown joint name: {joint_name!r}")
return self.points[index]
[docs]
def get_finger(
self,
finger: FingerName | str,
) -> dict[JointName, tuple[float, float, float]]:
"""Return all joint points for one finger group.
:param finger:
Finger group to query. Accepts :class:`FingerName` or one of
``wrist``, ``thumb``, ``index``, ``middle``, ``ring``, ``little``.
:returns:
Dictionary mapping :class:`JointName` to ``(x, y, z)`` points for
the selected finger group.
:raises ValueError:
If the finger group is unknown.
"""
finger_name = finger.value if isinstance(finger, FingerName) else finger.lower()
if finger_name == FingerName.WRIST.value:
return {JointName.WRIST: self.get_joint(JointName.WRIST)}
prefixes = {
FingerName.THUMB.value: "Thumb",
FingerName.INDEX.value: "Index",
FingerName.MIDDLE.value: "Middle",
FingerName.RING.value: "Ring",
FingerName.LITTLE.value: "Little",
}
prefix = prefixes.get(finger_name)
if prefix is None:
raise ValueError(f"Unknown finger name: {finger_name!r}")
result: dict[JointName, tuple[float, float, float]] = {}
for joint in JointName:
if joint is JointName.WRIST:
continue
if joint.value.startswith(prefix):
result[joint] = self.get_joint(joint)
return result
[docs]
def to_dict(self) -> dict[str, list[list[float]]]:
"""Serialize landmarks into a mapping-friendly dictionary.
:returns:
Dictionary with ordered ``points`` list.
"""
return {"points": [[x, y, z] for x, y, z in self.points]}
[docs]
@classmethod
def from_dict(cls, values: Mapping[str, Any]) -> HandLandmarks:
"""Build :class:`HandLandmarks` from serialized mapping data.
:param values:
Mapping containing ``points`` as nested coordinate lists.
:returns:
Parsed landmarks instance preserving point order.
"""
raw_points = values["points"]
parsed_points = tuple(
(float(point[0]), float(point[1]), float(point[2])) for point in raw_points
)
return cls(points=parsed_points)
[docs]
@dataclass(frozen=True, slots=True)
class WristPacket:
"""Parsed wrist packet for one hand side."""
side: HandSide
kind: PacketType
data: WristPose
debug: PacketDebugInfo | None = None
[docs]
@dataclass(frozen=True, slots=True)
class LandmarksPacket:
"""Parsed landmark packet for one hand side."""
side: HandSide
kind: PacketType
data: HandLandmarks
debug: PacketDebugInfo | None = None
[docs]
@dataclass(frozen=True, slots=True)
class HeadPosePacket:
"""Parsed head pose packet."""
side: HandSide
kind: PacketType
data: HeadPose
debug: PacketDebugInfo | None = None
ParsedPacket = WristPacket | LandmarksPacket | HeadPosePacket