unitree-g1-sim 0.1.0__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.
@@ -0,0 +1,31 @@
1
+ """
2
+ unitree-g1-sim: Lightweight diagnostic simulator for Unitree G1 humanoid robot.
3
+ """
4
+
5
+ from .config import G1_JOINTS, G1_SPECS, JOINT_GROUPS, JOINT_NAME_MAP
6
+ from .simulator import (
7
+ FaultType,
8
+ G1Simulator,
9
+ IMUTelemetry,
10
+ JointTelemetry,
11
+ PowerTelemetry,
12
+ RobotState,
13
+ RobotTelemetry,
14
+ create_simulator,
15
+ )
16
+
17
+ __all__ = [
18
+ "G1Simulator",
19
+ "create_simulator",
20
+ "RobotState",
21
+ "FaultType",
22
+ "JointTelemetry",
23
+ "IMUTelemetry",
24
+ "PowerTelemetry",
25
+ "RobotTelemetry",
26
+ "G1_JOINTS",
27
+ "G1_SPECS",
28
+ "JOINT_NAME_MAP",
29
+ "JOINT_GROUPS",
30
+ ]
31
+ __version__ = "0.1.0"
@@ -0,0 +1,504 @@
1
+ """
2
+ Built-in diagnostic test cases for Unitree G1 robot.
3
+
4
+ Covers: joint range of motion, temperature, IMU calibration,
5
+ standing stability, and power consumption.
6
+
7
+ Each test returns a structured ``TestResult`` with measurements,
8
+ thresholds, and pass/fail/warning verdicts.
9
+ """
10
+
11
+ import asyncio
12
+ import math
13
+ import time
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ import numpy as np
19
+
20
+ from .config import G1_JOINTS, G1_SPECS
21
+ from .simulator import G1Simulator, RobotState
22
+
23
+
24
+ class TestStatus(str, Enum):
25
+ PASS = "PASS"
26
+ FAIL = "FAIL"
27
+ WARNING = "WARNING"
28
+ SKIP = "SKIP"
29
+ RUNNING = "RUNNING"
30
+
31
+
32
+ @dataclass
33
+ class TestStep:
34
+ name: str
35
+ status: TestStatus
36
+ measured: Any
37
+ threshold: Any
38
+ unit: str
39
+ detail: str = ""
40
+
41
+
42
+ @dataclass
43
+ class TestResult:
44
+ test_id: str
45
+ test_name: str
46
+ status: TestStatus
47
+ duration_s: float
48
+ steps: list[TestStep]
49
+ raw_data: dict[str, Any]
50
+ summary: str
51
+
52
+
53
+ # ═══════════════════════════════════════════════════════════════════
54
+ # TC-001: Joint Range of Motion
55
+ # ═══════════════════════════════════════════════════════════════════
56
+
57
+
58
+ async def test_joint_range_of_motion(sim: G1Simulator) -> TestResult:
59
+ """Verify each joint stays within its hardware limits during TESTING state."""
60
+ start = time.time()
61
+ steps: list[TestStep] = []
62
+ raw: dict[str, Any] = {"joints": {}}
63
+
64
+ sim.set_state(RobotState.TESTING)
65
+ await asyncio.sleep(0.3)
66
+
67
+ samples = []
68
+ for _ in range(5):
69
+ tele = sim.get_telemetry()
70
+ samples.append(tele.joints)
71
+ await asyncio.sleep(0.1)
72
+
73
+ fail_count = 0
74
+ warn_count = 0
75
+ for j_config in G1_JOINTS:
76
+ lo, hi = j_config["limits"]
77
+ name = j_config["name"]
78
+ positions = [s[j_config["id"]].position for s in samples]
79
+ max_pos = max(abs(p) for p in positions)
80
+ limit_max = max(abs(lo), abs(hi))
81
+ ratio = max_pos / limit_max if limit_max > 0 else 0
82
+ raw["joints"][name] = {"positions": positions, "limit_ratio": ratio}
83
+
84
+ over_limit = any(p < lo - 0.05 or p > hi + 0.05 for p in positions)
85
+ if over_limit:
86
+ status = TestStatus.FAIL
87
+ fail_count += 1
88
+ detail = "超出关节限位!"
89
+ elif ratio > 0.95:
90
+ status = TestStatus.WARNING
91
+ warn_count += 1
92
+ detail = f"接近限位边界 ({ratio:.1%})"
93
+ else:
94
+ status = TestStatus.PASS
95
+ detail = "正常"
96
+
97
+ steps.append(
98
+ TestStep(
99
+ name=f"[{name}] 位置限位检查",
100
+ status=status,
101
+ measured=round(max_pos, 3),
102
+ threshold=round(limit_max, 3),
103
+ unit="rad",
104
+ detail=detail,
105
+ )
106
+ )
107
+
108
+ duration = time.time() - start
109
+ if fail_count > 0:
110
+ overall = TestStatus.FAIL
111
+ summary = f"❌ {fail_count}个关节超出限位,{warn_count}个接近边界"
112
+ elif warn_count > 0:
113
+ overall = TestStatus.WARNING
114
+ summary = f"⚠️ {warn_count}个关节接近限位边界,建议检查"
115
+ else:
116
+ overall = TestStatus.PASS
117
+ summary = f"✅ 全部{len(G1_JOINTS)}个关节位置正常"
118
+
119
+ return TestResult(
120
+ test_id="TC-001",
121
+ test_name="关节运动范围测试",
122
+ status=overall,
123
+ duration_s=round(duration, 2),
124
+ steps=steps,
125
+ raw_data=raw,
126
+ summary=summary,
127
+ )
128
+
129
+
130
+ # ═══════════════════════════════════════════════════════════════════
131
+ # TC-002: Joint Temperature
132
+ # ═══════════════════════════════════════════════════════════════════
133
+
134
+
135
+ async def test_joint_temperature(sim: G1Simulator) -> TestResult:
136
+ """Verify all joint temperatures are within safe limits."""
137
+ start = time.time()
138
+ steps: list[TestStep] = []
139
+ raw: dict[str, Any] = {"temperatures": {}}
140
+
141
+ sim.set_state(RobotState.STANDING)
142
+ await asyncio.sleep(0.5)
143
+
144
+ tele = sim.get_telemetry()
145
+ fail_count = 0
146
+ warn_count = 0
147
+
148
+ for j in tele.joints:
149
+ raw["temperatures"][j.name] = j.temperature
150
+ warn_t = G1_SPECS["warning_joint_temp_celsius"]
151
+ max_t = G1_SPECS["max_joint_temp_celsius"]
152
+
153
+ if j.temperature >= max_t:
154
+ status = TestStatus.FAIL
155
+ fail_count += 1
156
+ detail = f"超过最大温度限制 {max_t}°C!"
157
+ elif j.temperature >= warn_t:
158
+ status = TestStatus.WARNING
159
+ warn_count += 1
160
+ detail = f"接近温度警戒值 {warn_t}°C"
161
+ else:
162
+ status = TestStatus.PASS
163
+ detail = "温度正常"
164
+
165
+ steps.append(
166
+ TestStep(
167
+ name=f"[{j.name}] 温度",
168
+ status=status,
169
+ measured=j.temperature,
170
+ threshold=max_t,
171
+ unit="°C",
172
+ detail=detail,
173
+ )
174
+ )
175
+
176
+ duration = time.time() - start
177
+ if fail_count > 0:
178
+ overall = TestStatus.FAIL
179
+ summary = f"❌ {fail_count}个关节过温,立即停机检查"
180
+ elif warn_count > 0:
181
+ overall = TestStatus.WARNING
182
+ summary = f"⚠️ {warn_count}个关节温度偏高"
183
+ else:
184
+ overall = TestStatus.PASS
185
+ summary = (
186
+ f"✅ 全部关节温度正常"
187
+ f"(最高 {max(j.temperature for j in tele.joints):.1f}°C)"
188
+ )
189
+
190
+ return TestResult(
191
+ test_id="TC-002",
192
+ test_name="关节温度检测",
193
+ status=overall,
194
+ duration_s=round(duration, 2),
195
+ steps=steps,
196
+ raw_data=raw,
197
+ summary=summary,
198
+ )
199
+
200
+
201
+ # ═══════════════════════════════════════════════════════════════════
202
+ # TC-003: IMU Calibration
203
+ # ═══════════════════════════════════════════════════════════════════
204
+
205
+
206
+ async def test_imu_calibration(sim: G1Simulator) -> TestResult:
207
+ """Verify IMU bias and noise while standing still."""
208
+ start = time.time()
209
+ steps: list[TestStep] = []
210
+ raw: dict[str, Any] = {"imu_samples": []}
211
+
212
+ sim.set_state(RobotState.STANDING)
213
+ await asyncio.sleep(0.2)
214
+
215
+ samples = []
216
+ for _ in range(20):
217
+ tele = sim.get_telemetry()
218
+ imu = tele.imu
219
+ samples.append(
220
+ {
221
+ "roll": imu.roll,
222
+ "pitch": imu.pitch,
223
+ "yaw": imu.yaw,
224
+ "gyro_x": imu.gyro_x,
225
+ "gyro_y": imu.gyro_y,
226
+ "gyro_z": imu.gyro_z,
227
+ "accel_z": imu.accel_z,
228
+ }
229
+ )
230
+ await asyncio.sleep(0.05)
231
+
232
+ raw["imu_samples"] = samples
233
+
234
+ rolls = [s["roll"] for s in samples]
235
+ pitches = [s["pitch"] for s in samples]
236
+ gyros = [
237
+ math.sqrt(s["gyro_x"] ** 2 + s["gyro_y"] ** 2 + s["gyro_z"] ** 2)
238
+ for s in samples
239
+ ]
240
+ accel_z = [s["accel_z"] for s in samples]
241
+
242
+ roll_std = float(np.std(rolls))
243
+ pitch_std = float(np.std(pitches))
244
+ gyro_bias = float(np.mean(gyros))
245
+ accel_error = abs(float(np.mean(accel_z)) - 9.81)
246
+
247
+ checks = [
248
+ ("姿态Roll抖动 (σ)", roll_std, 0.05, "rad", roll_std < 0.05),
249
+ ("姿态Pitch抖动 (σ)", pitch_std, 0.05, "rad", pitch_std < 0.05),
250
+ ("陀螺仪偏置", gyro_bias, 0.02, "rad/s", gyro_bias < 0.02),
251
+ ("加速度计Z轴误差", accel_error, 0.1, "m/s²", accel_error < 0.1),
252
+ ]
253
+
254
+ fail_count = 0
255
+ for name, measured, threshold, unit, ok in checks:
256
+ status = TestStatus.PASS if ok else TestStatus.FAIL
257
+ if not ok:
258
+ fail_count += 1
259
+ steps.append(
260
+ TestStep(
261
+ name=f"[IMU] {name}",
262
+ status=status,
263
+ measured=round(measured, 5),
264
+ threshold=threshold,
265
+ unit=unit,
266
+ detail="正常" if ok else "超出校准阈值,需重新标定",
267
+ )
268
+ )
269
+
270
+ overall = TestStatus.FAIL if fail_count > 0 else TestStatus.PASS
271
+ summary = (
272
+ f"❌ IMU校准异常,{fail_count}项超标"
273
+ if fail_count > 0
274
+ else "✅ IMU校准正常,姿态估计精度良好"
275
+ )
276
+
277
+ return TestResult(
278
+ test_id="TC-003",
279
+ test_name="IMU校准验证",
280
+ status=overall,
281
+ duration_s=round(time.time() - start, 2),
282
+ steps=steps,
283
+ raw_data=raw,
284
+ summary=summary,
285
+ )
286
+
287
+
288
+ # ═══════════════════════════════════════════════════════════════════
289
+ # TC-004: Standing Stability
290
+ # ═══════════════════════════════════════════════════════════════════
291
+
292
+
293
+ async def test_standing_stability(sim: G1Simulator) -> TestResult:
294
+ """Check ZMP stability margins while standing."""
295
+ start = time.time()
296
+ steps: list[TestStep] = []
297
+ raw: dict[str, Any] = {"stability_data": []}
298
+
299
+ sim.set_state(RobotState.STANDING)
300
+ await asyncio.sleep(0.3)
301
+
302
+ roll_maxes = []
303
+ pitch_maxes = []
304
+ for _ in range(30):
305
+ tele = sim.get_telemetry()
306
+ roll_maxes.append(abs(tele.imu.roll))
307
+ pitch_maxes.append(abs(tele.imu.pitch))
308
+ raw["stability_data"].append(
309
+ {
310
+ "roll": tele.imu.roll,
311
+ "pitch": tele.imu.pitch,
312
+ "t": tele.timestamp,
313
+ }
314
+ )
315
+ await asyncio.sleep(0.05)
316
+
317
+ max_roll = max(roll_maxes)
318
+ max_pitch = max(pitch_maxes)
319
+ avg_roll = float(np.mean(roll_maxes))
320
+ avg_pitch = float(np.mean(pitch_maxes))
321
+
322
+ max_roll_deg = math.degrees(max_roll)
323
+ max_pitch_deg = math.degrees(max_pitch)
324
+
325
+ ROLL_THRESHOLD_DEG = 5.0
326
+ PITCH_THRESHOLD_DEG = 5.0
327
+
328
+ results = [
329
+ (
330
+ "最大横滚角",
331
+ max_roll_deg,
332
+ ROLL_THRESHOLD_DEG,
333
+ "°",
334
+ max_roll_deg < ROLL_THRESHOLD_DEG,
335
+ ),
336
+ (
337
+ "最大俯仰角",
338
+ max_pitch_deg,
339
+ PITCH_THRESHOLD_DEG,
340
+ "°",
341
+ max_pitch_deg < PITCH_THRESHOLD_DEG,
342
+ ),
343
+ ("平均横滚偏差", math.degrees(avg_roll), 2.0, "°", math.degrees(avg_roll) < 2.0),
344
+ (
345
+ "平均俯仰偏差",
346
+ math.degrees(avg_pitch),
347
+ 2.0,
348
+ "°",
349
+ math.degrees(avg_pitch) < 2.0,
350
+ ),
351
+ ]
352
+
353
+ fail_count = 0
354
+ for name, measured, threshold, unit, ok in results:
355
+ if not ok:
356
+ fail_count += 1
357
+ steps.append(
358
+ TestStep(
359
+ name=f"[稳定性] {name}",
360
+ status=TestStatus.PASS if ok else TestStatus.FAIL,
361
+ measured=round(measured, 2),
362
+ threshold=threshold,
363
+ unit=unit,
364
+ detail="稳定" if ok else "超出稳定裕度",
365
+ )
366
+ )
367
+
368
+ overall = TestStatus.FAIL if fail_count > 0 else TestStatus.PASS
369
+ summary = (
370
+ f"❌ 站立稳定性不足,{fail_count}项超标"
371
+ if fail_count > 0
372
+ else "✅ 站立稳定性良好"
373
+ )
374
+
375
+ return TestResult(
376
+ test_id="TC-004",
377
+ test_name="站立稳定性测试",
378
+ status=overall,
379
+ duration_s=round(time.time() - start, 2),
380
+ steps=steps,
381
+ raw_data=raw,
382
+ summary=summary,
383
+ )
384
+
385
+
386
+ # ═══════════════════════════════════════════════════════════════════
387
+ # TC-005: Power Consumption
388
+ # ═══════════════════════════════════════════════════════════════════
389
+
390
+
391
+ async def test_power_consumption(sim: G1Simulator) -> TestResult:
392
+ """Measure current draw and battery health across operating states."""
393
+ start = time.time()
394
+ steps: list[TestStep] = []
395
+ raw: dict[str, Any] = {"power_readings": {}}
396
+
397
+ scenarios = [
398
+ (RobotState.STANDBY, "待机", 5.0, 0.5),
399
+ (RobotState.STANDING, "站立", 15.0, 2.0),
400
+ (RobotState.WALKING, "行走", 35.0, 5.0),
401
+ ]
402
+
403
+ for state, label, max_power_a, duration in scenarios:
404
+ sim.set_state(state)
405
+ await asyncio.sleep(duration * 0.2)
406
+
407
+ readings = []
408
+ for _ in range(5):
409
+ tele = sim.get_telemetry()
410
+ readings.append(
411
+ {
412
+ "current": tele.power.current,
413
+ "voltage": tele.power.voltage,
414
+ "power_w": tele.power.power_w,
415
+ }
416
+ )
417
+ await asyncio.sleep(0.05)
418
+
419
+ avg_current = float(np.mean([r["current"] for r in readings]))
420
+ avg_power = float(np.mean([r["power_w"] for r in readings]))
421
+ raw["power_readings"][label] = readings
422
+
423
+ ok = avg_current < max_power_a
424
+ steps.append(
425
+ TestStep(
426
+ name=f"[功耗] {label}电流",
427
+ status=TestStatus.PASS if ok else TestStatus.FAIL,
428
+ measured=round(avg_current, 2),
429
+ threshold=max_power_a,
430
+ unit="A",
431
+ detail=f"平均功率 {avg_power:.1f}W"
432
+ + ("" if ok else " — 超出额定值"),
433
+ )
434
+ )
435
+
436
+ # battery voltage check
437
+ tele = sim.get_telemetry()
438
+ voltage_ok = tele.power.voltage >= G1_SPECS["battery_voltage_min"]
439
+ steps.append(
440
+ TestStep(
441
+ name="[功耗] 电池电压",
442
+ status=TestStatus.PASS if voltage_ok else TestStatus.FAIL,
443
+ measured=tele.power.voltage,
444
+ threshold=G1_SPECS["battery_voltage_min"],
445
+ unit="V",
446
+ detail=f"SOC {tele.power.soc:.1f}%",
447
+ )
448
+ )
449
+
450
+ fail_count = sum(1 for s in steps if s.status == TestStatus.FAIL)
451
+ overall = TestStatus.FAIL if fail_count > 0 else TestStatus.PASS
452
+ summary = (
453
+ f"❌ {fail_count}项功耗测试不通过"
454
+ if fail_count > 0
455
+ else f"✅ 功耗测试通过,当前SOC {tele.power.soc:.1f}%"
456
+ )
457
+
458
+ return TestResult(
459
+ test_id="TC-005",
460
+ test_name="功耗测试",
461
+ status=overall,
462
+ duration_s=round(time.time() - start, 2),
463
+ steps=steps,
464
+ raw_data=raw,
465
+ summary=summary,
466
+ )
467
+
468
+
469
+ # ═══════════════════════════════════════════════════════════════════
470
+ # Test catalog
471
+ # ═══════════════════════════════════════════════════════════════════
472
+
473
+ TEST_CATALOG: dict[str, dict] = {
474
+ "TC-001": {
475
+ "fn": test_joint_range_of_motion,
476
+ "name": "关节运动范围测试",
477
+ "category": "机械",
478
+ "duration_estimate": 2,
479
+ },
480
+ "TC-002": {
481
+ "fn": test_joint_temperature,
482
+ "name": "关节温度检测",
483
+ "category": "热管理",
484
+ "duration_estimate": 3,
485
+ },
486
+ "TC-003": {
487
+ "fn": test_imu_calibration,
488
+ "name": "IMU校准验证",
489
+ "category": "传感器",
490
+ "duration_estimate": 2,
491
+ },
492
+ "TC-004": {
493
+ "fn": test_standing_stability,
494
+ "name": "站立稳定性测试",
495
+ "category": "控制",
496
+ "duration_estimate": 4,
497
+ },
498
+ "TC-005": {
499
+ "fn": test_power_consumption,
500
+ "name": "功耗测试",
501
+ "category": "电源",
502
+ "duration_estimate": 5,
503
+ },
504
+ }
@@ -0,0 +1,72 @@
1
+ """
2
+ Unitree G1 Robot — Hardware Specification Configuration
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Reference: Unitree G1 official technical documentation.
6
+ Joint limits, torque ratings, and battery specs used by the simulator.
7
+
8
+ :copyright: (c) 2025 ChenYue
9
+ :license: Apache 2.0
10
+ """
11
+
12
+ # ── Joint definitions (29 DOF) ────────────────────────────────────
13
+ # fmt: off
14
+ G1_JOINTS = [
15
+ # Left leg (6 DOF)
16
+ {"name": "left_hip_pitch", "id": 0, "limits": (-2.87, 2.87), "max_velocity": 20.0, "max_torque": 88.0, "group": "left_leg"},
17
+ {"name": "left_hip_roll", "id": 1, "limits": (-0.52, 2.35), "max_velocity": 20.0, "max_torque": 88.0, "group": "left_leg"},
18
+ {"name": "left_hip_yaw", "id": 2, "limits": (-2.75, 2.75), "max_velocity": 20.0, "max_torque": 88.0, "group": "left_leg"},
19
+ {"name": "left_knee", "id": 3, "limits": (-0.26, 2.87), "max_velocity": 20.0, "max_torque": 139.0, "group": "left_leg"},
20
+ {"name": "left_ankle_pitch", "id": 4, "limits": (-0.90, 0.70), "max_velocity": 20.0, "max_torque": 50.0, "group": "left_leg"},
21
+ {"name": "left_ankle_roll", "id": 5, "limits": (-0.35, 0.35), "max_velocity": 20.0, "max_torque": 50.0, "group": "left_leg"},
22
+ # Right leg (6 DOF)
23
+ {"name": "right_hip_pitch", "id": 6, "limits": (-2.87, 2.87), "max_velocity": 20.0, "max_torque": 88.0, "group": "right_leg"},
24
+ {"name": "right_hip_roll", "id": 7, "limits": (-2.35, 0.52), "max_velocity": 20.0, "max_torque": 88.0, "group": "right_leg"},
25
+ {"name": "right_hip_yaw", "id": 8, "limits": (-2.75, 2.75), "max_velocity": 20.0, "max_torque": 88.0, "group": "right_leg"},
26
+ {"name": "right_knee", "id": 9, "limits": (-0.26, 2.87), "max_velocity": 20.0, "max_torque": 139.0, "group": "right_leg"},
27
+ {"name": "right_ankle_pitch","id": 10, "limits": (-0.90, 0.70), "max_velocity": 20.0, "max_torque": 50.0, "group": "right_leg"},
28
+ {"name": "right_ankle_roll", "id": 11, "limits": (-0.35, 0.35), "max_velocity": 20.0, "max_torque": 50.0, "group": "right_leg"},
29
+ # Waist (3 DOF)
30
+ {"name": "waist_yaw", "id": 12, "limits": (-2.62, 2.62), "max_velocity": 20.0, "max_torque": 88.0, "group": "waist"},
31
+ {"name": "waist_roll", "id": 13, "limits": (-0.52, 0.52), "max_velocity": 20.0, "max_torque": 88.0, "group": "waist"},
32
+ {"name": "waist_pitch", "id": 14, "limits": (-0.52, 0.52), "max_velocity": 20.0, "max_torque": 88.0, "group": "waist"},
33
+ # Left arm (7 DOF)
34
+ {"name": "left_shoulder_pitch","id": 15,"limits": (-3.14, 3.14), "max_velocity": 20.0, "max_torque": 25.0, "group": "left_arm"},
35
+ {"name": "left_shoulder_roll", "id": 16,"limits": (-1.57, 3.14), "max_velocity": 20.0, "max_torque": 25.0, "group": "left_arm"},
36
+ {"name": "left_shoulder_yaw", "id": 17,"limits": (-3.14, 3.14), "max_velocity": 20.0, "max_torque": 25.0, "group": "left_arm"},
37
+ {"name": "left_elbow", "id": 18,"limits": (-1.57, 4.71), "max_velocity": 20.0, "max_torque": 25.0, "group": "left_arm"},
38
+ {"name": "left_wrist_roll", "id": 19,"limits": (-3.14, 3.14), "max_velocity": 20.0, "max_torque": 5.0, "group": "left_arm"},
39
+ {"name": "left_wrist_pitch", "id": 20,"limits": (-1.57, 1.57), "max_velocity": 20.0, "max_torque": 5.0, "group": "left_arm"},
40
+ {"name": "left_wrist_yaw", "id": 21,"limits": (-1.57, 1.57), "max_velocity": 20.0, "max_torque": 5.0, "group": "left_arm"},
41
+ # Right arm (7 DOF)
42
+ {"name": "right_shoulder_pitch","id": 22,"limits": (-3.14, 3.14),"max_velocity": 20.0, "max_torque": 25.0, "group": "right_arm"},
43
+ {"name": "right_shoulder_roll", "id": 23,"limits": (-3.14, 1.57),"max_velocity": 20.0, "max_torque": 25.0, "group": "right_arm"},
44
+ {"name": "right_shoulder_yaw", "id": 24,"limits": (-3.14, 3.14),"max_velocity": 20.0, "max_torque": 25.0, "group": "right_arm"},
45
+ {"name": "right_elbow", "id": 25,"limits": (-1.57, 4.71),"max_velocity": 20.0, "max_torque": 25.0, "group": "right_arm"},
46
+ {"name": "right_wrist_roll", "id": 26,"limits": (-3.14, 3.14),"max_velocity": 20.0, "max_torque": 5.0, "group": "right_arm"},
47
+ {"name": "right_wrist_pitch", "id": 27,"limits": (-1.57, 1.57),"max_velocity": 20.0, "max_torque": 5.0, "group": "right_arm"},
48
+ {"name": "right_wrist_yaw", "id": 28,"limits": (-1.57, 1.57),"max_velocity": 20.0, "max_torque": 5.0, "group": "right_arm"},
49
+ ]
50
+ # fmt: on
51
+
52
+ # ── General specifications ────────────────────────────────────────
53
+
54
+ G1_SPECS = {
55
+ "model": "Unitree G1",
56
+ "dof": 29,
57
+ "height_m": 1.27,
58
+ "weight_kg": 35.0,
59
+ "battery_voltage_nominal": 48.0,
60
+ "battery_voltage_min": 42.0,
61
+ "battery_voltage_max": 54.6,
62
+ "battery_capacity_wh": 864.0,
63
+ "max_joint_temp_celsius": 80.0,
64
+ "warning_joint_temp_celsius": 65.0,
65
+ "imu_gyro_range_dps": 2000.0,
66
+ "imu_accel_range_g": 16.0,
67
+ }
68
+
69
+ # ── Lookup tables ──────────────────────────────────────────────────
70
+
71
+ JOINT_NAME_MAP: dict[str, dict] = {j["name"]: j for j in G1_JOINTS}
72
+ JOINT_GROUPS: list[str] = sorted({j["group"] for j in G1_JOINTS})