litegrip-python 1.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
litegrip/models.py ADDED
@@ -0,0 +1,210 @@
1
+ """LiteGrip SDK — data models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import IntEnum, IntFlag
7
+ from typing import Optional
8
+
9
+ from .constants import GripperGeometry, UnitConversion
10
+
11
+
12
+ class GripperMode(IntEnum):
13
+ """Gripper control mode."""
14
+ MIT = 0
15
+ POSITION = 1
16
+ VELOCITY = 2
17
+ FORCE = 3
18
+
19
+
20
+ class GripperStatus(IntFlag):
21
+ """Gripper status flags (bitmask)."""
22
+ NONE = 0x00
23
+ ENABLED = 0x01
24
+ MOVING = 0x02
25
+ AT_TARGET = 0x04
26
+ GRASPED = 0x08
27
+ ERROR = 0x10
28
+
29
+
30
+ #: Age past which a :class:`GripperState` counts as stale, in seconds. The same
31
+ #: threshold the C++ SDK uses, so a guard reads the same on either side.
32
+ STALE_AFTER_S = 0.5
33
+
34
+
35
+ @dataclass
36
+ class GripperState:
37
+ """Gripper state snapshot, and how fresh it is.
38
+
39
+ The readings are a cache, updated only by a status frame. Whether such a
40
+ frame arrived is what :attr:`fresh`, :attr:`data_age_s` and
41
+ :meth:`is_stale` report — a snapshot from a bus that said nothing is not a
42
+ measurement, and it must not look like one. Check them before trusting the
43
+ numbers; :meth:`LiteGrip.refresh_status` elicits a frame when the bus is
44
+ idle.
45
+ """
46
+
47
+ position_rad: float = 0.0
48
+ velocity_rad_s: float = 0.0
49
+ torque_nm: float = 0.0
50
+ temperature_mos: int = 0
51
+ temperature_coil: int = 0
52
+ error_code: int = 0
53
+ #: Wall-clock time the frame behind this snapshot arrived — not the time of
54
+ #: the call that returned it. ``0.0`` means no frame has ever arrived, so
55
+ #: every reading above is still a constructor default.
56
+ timestamp: float = 0.0
57
+
58
+ # Convenience — computed from raw values with unit conversion
59
+ position_mm: float = 0.0
60
+ force_n: float = 0.0
61
+
62
+ #: Whether a status frame answered *this* read. False means the readings
63
+ #: are the ones from the previous frame, however old that is.
64
+ fresh: bool = False
65
+ #: Seconds since the frame behind this snapshot arrived, or ``None`` when
66
+ #: there is no such frame at all.
67
+ data_age_s: Optional[float] = None
68
+
69
+ def is_stale(self, stale_after_s: float = STALE_AFTER_S) -> bool:
70
+ """True when the newest frame is older than *stale_after_s*, or absent.
71
+
72
+ This is about the snapshot's age, not about the read that produced it:
73
+ a read no frame answered has ``fresh=False`` while the snapshot may
74
+ still be younger than the threshold. Ask both questions when the
75
+ difference matters — "how old is this" and "did anyone answer just now".
76
+
77
+ ``stale_after_s`` defaults to :data:`STALE_AFTER_S`, the same threshold
78
+ the C++ SDK uses.
79
+ """
80
+ return self.data_age_s is None or self.data_age_s > stale_after_s
81
+
82
+ @property
83
+ def is_enabled(self) -> bool:
84
+ """True if the motor is enabled (error_code == 1)."""
85
+ return self.error_code == 1
86
+
87
+ @property
88
+ def is_error(self) -> bool:
89
+ """True if a fault is active (error_code ∉ {0, 1})."""
90
+ return self.error_code not in (0, 1)
91
+
92
+ @property
93
+ def is_moving(self) -> bool:
94
+ """True if velocity exceeds a small threshold."""
95
+ return abs(self.velocity_rad_s) > 0.01
96
+
97
+ @property
98
+ def aperture_mm(self) -> float:
99
+ """Opening distance in mm (single-side displacement).
100
+
101
+ For total jaw separation multiply by 2.
102
+ """
103
+ return self.position_mm
104
+
105
+
106
+ @dataclass
107
+ class GripperConfig:
108
+ """Gripper configuration — tune these for your hardware."""
109
+
110
+ # CAN
111
+ can_channel: str = "can0"
112
+ can_id: int = 0x08
113
+ mst_id: Optional[int] = None # None = auto-detect
114
+ canfd_mode: bool = False
115
+
116
+ # Control gains (MIT mode)
117
+ kp: float = 100.0 # position stiffness [0, 500]
118
+ kd: float = 2.0 # velocity damping [0, 5]
119
+
120
+ # Position limits (rad) — updated by calibrate() / load_calibration().
121
+ # Which of the two is numerically larger is NOT assumed: a gripper whose
122
+ # motor is mounted the other way round (a "reverse mount") simply has
123
+ # pos_closed_rad < pos_open_rad. See :attr:`close_sign`.
124
+ pos_closed_rad: float = 1.14 # closed → 0 mm
125
+ pos_open_rad: float = 0.0 # open → full stroke
126
+
127
+ # True once the limits above come from a real calibration (or from a
128
+ # calibration template) rather than from the placeholder defaults. The
129
+ # motion actions refuse to run while this is False, because with the
130
+ # defaults still in place every direction is a guess.
131
+ calibrated: bool = False
132
+
133
+ # Jaw travel (mm) — the caliper measurement of your gripper's travel.
134
+ # :meth:`LiteGrip.calibrate` derives ``rad_to_mm`` from it plus the probe
135
+ # inset, because the recorded extremes span that inset more than the jaws
136
+ # actually open; see :class:`~litegrip.constants.GripperGeometry`.
137
+ max_stroke_mm: float = GripperGeometry.JAW_TRAVEL_MM
138
+
139
+ # Working stroke (mm) — the opening the motions are allowed to reach, from
140
+ # the closed zero. ``0`` (or anything ≥ the jaw travel) means "no limit":
141
+ # :meth:`actions.open` then presses onto the open mechanical stop as before.
142
+ # Set it below the jaw travel to keep a margin at the open end (e.g. 80 mm on
143
+ # an 85 mm travel leaves 5 mm), so a plain open no longer drives into the
144
+ # stop.
145
+ work_stroke_mm: float = 0.0
146
+
147
+ # Unit conversion — update after calibration
148
+ rad_to_mm: float = UnitConversion.RAD_TO_MM # rad → mm
149
+ nm_to_n: float = 10.0 # Nm → N (approximate)
150
+
151
+ # Grasp detection
152
+ grasp_torque_threshold: float = 0.5 # Nm
153
+
154
+ @property
155
+ def close_sign(self) -> float:
156
+ """Which way the motor counts when the jaws close.
157
+
158
+ ``+1.0`` when closing means increasing radians (the usual mounting),
159
+ ``-1.0`` for a reverse mount. Derived from the ordering of the two
160
+ calibrated limits, so direction is data, not a separate switch — and
161
+ it is only meaningful once :attr:`calibrated` is True.
162
+ """
163
+ return 1.0 if self.pos_closed_rad >= self.pos_open_rad else -1.0
164
+
165
+ @property
166
+ def mount(self) -> Optional[str]:
167
+ """``"normal"`` / ``"reverse"`` — the name the limits spell out.
168
+
169
+ ``None`` while :attr:`calibrated` is False: the placeholder defaults
170
+ also happen to order close above open, so reporting ``"normal"``
171
+ before a calibration has been loaded would be a claim, not a reading.
172
+ Handy for a caller that has to show the operator which way this unit
173
+ is mounted. Compare with the ``mount=`` argument of
174
+ :class:`~litegrip.LiteGrip`, which *declares* a mount by loading a
175
+ template rather than reading one back.
176
+ """
177
+ if not self.calibrated:
178
+ return None
179
+ return "normal" if self.close_sign > 0 else "reverse"
180
+
181
+
182
+ @dataclass
183
+ class GripperInfo:
184
+ """Static device information."""
185
+
186
+ model: str = "LiteGrip"
187
+ motor_type: str = "DM4310"
188
+ can_id: int = 0x08
189
+ mst_id: int = 0x18
190
+ firmware_version: str = ""
191
+ serial_number: str = ""
192
+
193
+
194
+ @dataclass
195
+ class CalibrationData:
196
+ """Result of a gripper calibration run."""
197
+
198
+ zero_position: float = 1.14 # closed limit (rad)
199
+ max_position: float = 0.0 # open limit (rad)
200
+ travel_range: float = 1.14 # |max - zero| (rad)
201
+ rad_to_mm: float = UnitConversion.RAD_TO_MM # calibrated conversion
202
+ motor_type: str = "DM4310"
203
+ can_id: int = 0x08
204
+ mst_id: int = 0x18
205
+ calibration_time: str = ""
206
+
207
+ @property
208
+ def travel_mm(self) -> float:
209
+ """Full stroke in mm."""
210
+ return self.travel_range * self.rad_to_mm
@@ -0,0 +1,10 @@
1
+ """LiteGrip SDK — communication protocols.
2
+
3
+ The CAN bus layer (LiteGripCAN) is self-contained: it implements the
4
+ Damiao motor protocol directly over Linux SocketCAN with zero
5
+ dependencies on damiao_socketcan or any arm-specific library.
6
+ """
7
+
8
+ from .can_bus import LiteGripCAN
9
+
10
+ __all__ = ["LiteGripCAN"]