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.
- unitree_g1_sim/__init__.py +31 -0
- unitree_g1_sim/builtin_tests.py +504 -0
- unitree_g1_sim/config.py +72 -0
- unitree_g1_sim/simulator.py +544 -0
- unitree_g1_sim-0.1.0.dist-info/METADATA +155 -0
- unitree_g1_sim-0.1.0.dist-info/RECORD +8 -0
- unitree_g1_sim-0.1.0.dist-info/WHEEL +4 -0
- unitree_g1_sim-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|
+
}
|
unitree_g1_sim/config.py
ADDED
|
@@ -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})
|