MadmanMischief commited on
Commit
de491fe
·
verified ·
1 Parent(s): 61b59af

Add real-arm deployment code and final RL policy

Browse files
Files changed (44) hide show
  1. .gitattributes +24 -0
  2. rl_deploy/README.md +47 -0
  3. rl_deploy/calib_debug/p1_1792287841_after.png +3 -0
  4. rl_deploy/calib_debug/p1_1792287841_bluemask.png +0 -0
  5. rl_deploy/calib_debug/p1_1792287841_ref.png +3 -0
  6. rl_deploy/calib_debug/p2_1792287871_after.png +3 -0
  7. rl_deploy/calib_debug/p2_1792287871_bluemask.png +0 -0
  8. rl_deploy/calib_debug/p2_1792287871_ref.png +3 -0
  9. rl_deploy/calib_debug/p3_1792287891_after.png +3 -0
  10. rl_deploy/calib_debug/p3_1792287891_bluemask.png +0 -0
  11. rl_deploy/calib_debug/p3_1792287891_ref.png +3 -0
  12. rl_deploy/calib_debug/p4_1792287916_after.png +3 -0
  13. rl_deploy/calib_debug/p4_1792287916_bluemask.png +0 -0
  14. rl_deploy/calib_debug/p4_1792287916_ref.png +3 -0
  15. rl_deploy/calib_debug/p5_1792287936_after.png +3 -0
  16. rl_deploy/calib_debug/p5_1792287936_bluemask.png +0 -0
  17. rl_deploy/calib_debug/p5_1792287936_ref.png +3 -0
  18. rl_deploy/calib_debug/p6_1792287959_after.png +3 -0
  19. rl_deploy/calib_debug/p6_1792287959_bluemask.png +0 -0
  20. rl_deploy/calib_debug/p6_1792287959_ref.png +3 -0
  21. rl_deploy/calib_debug/p7_1792287977_after.png +3 -0
  22. rl_deploy/calib_debug/p7_1792287977_bluemask.png +0 -0
  23. rl_deploy/calib_debug/p7_1792287977_ref.png +3 -0
  24. rl_deploy/calib_debug/p8_1792287997_after.png +3 -0
  25. rl_deploy/calib_debug/p8_1792287997_bluemask.png +0 -0
  26. rl_deploy/calib_debug/p8_1792287997_ref.png +3 -0
  27. rl_deploy/calib_point_1.png +3 -0
  28. rl_deploy/calib_point_2.png +3 -0
  29. rl_deploy/calib_point_3.png +3 -0
  30. rl_deploy/calib_point_4.png +3 -0
  31. rl_deploy/calib_point_5.png +3 -0
  32. rl_deploy/calib_point_6.png +3 -0
  33. rl_deploy/calib_point_7.png +3 -0
  34. rl_deploy/calib_point_8.png +3 -0
  35. rl_deploy/calib_points.json +162 -0
  36. rl_deploy/calibrate_table.py +143 -0
  37. rl_deploy/common.py +164 -0
  38. rl_deploy/deploy_rl.py +156 -0
  39. rl_deploy/export_policy.py +42 -0
  40. rl_deploy/policy.pt +3 -0
  41. rl_deploy/policy_final.pt +3 -0
  42. rl_deploy/so101_fk.py +83 -0
  43. rl_deploy/so101_new_calib.urdf +453 -0
  44. rl_deploy/table_calib.json +205 -0
.gitattributes CHANGED
@@ -33,3 +33,27 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ rl_deploy/calib_debug/p1_1792287841_after.png filter=lfs diff=lfs merge=lfs -text
37
+ rl_deploy/calib_debug/p1_1792287841_ref.png filter=lfs diff=lfs merge=lfs -text
38
+ rl_deploy/calib_debug/p2_1792287871_after.png filter=lfs diff=lfs merge=lfs -text
39
+ rl_deploy/calib_debug/p2_1792287871_ref.png filter=lfs diff=lfs merge=lfs -text
40
+ rl_deploy/calib_debug/p3_1792287891_after.png filter=lfs diff=lfs merge=lfs -text
41
+ rl_deploy/calib_debug/p3_1792287891_ref.png filter=lfs diff=lfs merge=lfs -text
42
+ rl_deploy/calib_debug/p4_1792287916_after.png filter=lfs diff=lfs merge=lfs -text
43
+ rl_deploy/calib_debug/p4_1792287916_ref.png filter=lfs diff=lfs merge=lfs -text
44
+ rl_deploy/calib_debug/p5_1792287936_after.png filter=lfs diff=lfs merge=lfs -text
45
+ rl_deploy/calib_debug/p5_1792287936_ref.png filter=lfs diff=lfs merge=lfs -text
46
+ rl_deploy/calib_debug/p6_1792287959_after.png filter=lfs diff=lfs merge=lfs -text
47
+ rl_deploy/calib_debug/p6_1792287959_ref.png filter=lfs diff=lfs merge=lfs -text
48
+ rl_deploy/calib_debug/p7_1792287977_after.png filter=lfs diff=lfs merge=lfs -text
49
+ rl_deploy/calib_debug/p7_1792287977_ref.png filter=lfs diff=lfs merge=lfs -text
50
+ rl_deploy/calib_debug/p8_1792287997_after.png filter=lfs diff=lfs merge=lfs -text
51
+ rl_deploy/calib_debug/p8_1792287997_ref.png filter=lfs diff=lfs merge=lfs -text
52
+ rl_deploy/calib_point_1.png filter=lfs diff=lfs merge=lfs -text
53
+ rl_deploy/calib_point_2.png filter=lfs diff=lfs merge=lfs -text
54
+ rl_deploy/calib_point_3.png filter=lfs diff=lfs merge=lfs -text
55
+ rl_deploy/calib_point_4.png filter=lfs diff=lfs merge=lfs -text
56
+ rl_deploy/calib_point_5.png filter=lfs diff=lfs merge=lfs -text
57
+ rl_deploy/calib_point_6.png filter=lfs diff=lfs merge=lfs -text
58
+ rl_deploy/calib_point_7.png filter=lfs diff=lfs merge=lfs -text
59
+ rl_deploy/calib_point_8.png filter=lfs diff=lfs merge=lfs -text
rl_deploy/README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RL candy-pick policy on the real SO101
2
+
3
+ Trained in Isaac Lab (`Isaac-SO101-CandyPick-Direct`, state-based PPO). The policy sees joint
4
+ angles plus the blue candy's table position, which is detected once from the phone camera. So the
5
+ phone camera has to be calibrated against the table first.
6
+
7
+ None of these scripts move the arm unless you pass `--go` to `deploy_rl.py` and type `go`.
8
+
9
+ ## 1. Calibrate the phone against the table (once per camera/robot placement)
10
+
11
+ ```bash
12
+ cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8
13
+ ```
14
+ The follower's torque is switched off. For each point, put one blue candy on the table and press
15
+ Enter. Then move the arm by hand until the gripper tip touches the middle of the candy, and press
16
+ Enter. Spread the points over the area where candies go. The script saves `table_calib.json` and
17
+ prints the fit error, which should be under 2 cm.
18
+
19
+ ## 2. Get the policy (export runs on the L40S)
20
+
21
+ ```bash
22
+ ssh drishti-karma-devarshi 'cd ~/work/twin && ~/miniconda3/envs/lerobot/bin/python export_policy.py $(ls -d ~/work/IsaacLab/logs/rsl_rl/so101_candy_pick/*v2* | tail -1)/model_1400.pt ~/work/twin/rl_policy.pt'
23
+ ```
24
+ ```bash
25
+ scp drishti-karma-devarshi:work/twin/rl_policy.pt ~/B1Hackathon/tasks/candy_sort/rl_deploy/policy.pt
26
+ ```
27
+ Replace `model_1400.pt` with whichever checkpoint the thread recommends.
28
+
29
+ ## 3. Run
30
+
31
+ Dry run first. It detects the candy, prints its table position and the targets it would send, and
32
+ does not move the arm:
33
+ ```bash
34
+ cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt
35
+ ```
36
+ Real run (the arm moves for 10 s; keep a hand on the power switch):
37
+ ```bash
38
+ cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt --go
39
+ ```
40
+
41
+ ## Notes
42
+ - Candies must lie in the trained area. That's where you picked them in the grid recordings, in
43
+ front of the arm: about x -0.03..0.15 m and y -0.29..-0.15 m in the policy frame. The towel stays
44
+ where it was during those recordings.
45
+ - `so101_fk.py` is the arm's forward kinematics, from the official URDF. Its tip positions agree with
46
+ the simulator's to 1 cm (median) on 146 real poses.
47
+ - Only one blue candy is targeted per run. Candies the calibration places on the towel are ignored.
rl_deploy/calib_debug/p1_1792287841_after.png ADDED

Git LFS Details

  • SHA256: 387fc18317be9c6297b7ff3d17d44fd6b2df94986f2e012e2d1cf8a0d1cdf6c8
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB
rl_deploy/calib_debug/p1_1792287841_bluemask.png ADDED
rl_deploy/calib_debug/p1_1792287841_ref.png ADDED

Git LFS Details

  • SHA256: afd93e7f93d60c2c3b4771ab7d91459a287db358f0f3ea2cc4fec3e2f8b587f4
  • Pointer size: 131 Bytes
  • Size of remote file: 495 kB
rl_deploy/calib_debug/p2_1792287871_after.png ADDED

Git LFS Details

  • SHA256: b429669644ebb1d3c0e8a689e9ffd66c4fd31cb033d475906855004da5556776
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB
rl_deploy/calib_debug/p2_1792287871_bluemask.png ADDED
rl_deploy/calib_debug/p2_1792287871_ref.png ADDED

Git LFS Details

  • SHA256: 52033a10c29568266ef2abdf088b89c8d0f0d471647da7089a552b2d0bf8d45e
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB
rl_deploy/calib_debug/p3_1792287891_after.png ADDED

Git LFS Details

  • SHA256: a3a14616a8ce3898c1c7784b44d13f3ed1d2fd1fae934ab51164dafc81560e06
  • Pointer size: 131 Bytes
  • Size of remote file: 499 kB
rl_deploy/calib_debug/p3_1792287891_bluemask.png ADDED
rl_deploy/calib_debug/p3_1792287891_ref.png ADDED

Git LFS Details

  • SHA256: 1183deca710eb697be82f71568d2ab414e87ba9619dc19611fd313dc0043b9bc
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_debug/p4_1792287916_after.png ADDED

Git LFS Details

  • SHA256: 4b17c4cbe65fcfb1f61959632a315c87cda89dc72eec0e2a31740cd8edf67063
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_debug/p4_1792287916_bluemask.png ADDED
rl_deploy/calib_debug/p4_1792287916_ref.png ADDED

Git LFS Details

  • SHA256: 225d5f8b8b69eb924d13cc922aae857da9804adee8b10cbef4e7fd14d16f2639
  • Pointer size: 131 Bytes
  • Size of remote file: 506 kB
rl_deploy/calib_debug/p5_1792287936_after.png ADDED

Git LFS Details

  • SHA256: 4c83dffaeaeda1feb3f697e589fc16070e3c6ce04b43489951d81ef9617e4c29
  • Pointer size: 131 Bytes
  • Size of remote file: 505 kB
rl_deploy/calib_debug/p5_1792287936_bluemask.png ADDED
rl_deploy/calib_debug/p5_1792287936_ref.png ADDED

Git LFS Details

  • SHA256: 01a95b00275118220e3d8c7e34dd13972f953ec0df98528794abff0271b4118b
  • Pointer size: 131 Bytes
  • Size of remote file: 504 kB
rl_deploy/calib_debug/p6_1792287959_after.png ADDED

Git LFS Details

  • SHA256: 83ec90c0fb33611241b433c88844bcf0a434af0cadfb6f1e4a98b5692faa94f0
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_debug/p6_1792287959_bluemask.png ADDED
rl_deploy/calib_debug/p6_1792287959_ref.png ADDED

Git LFS Details

  • SHA256: a0aea5beabc1e07b57e53018d55dfee4f8395a1cce7ee66343f5f2fb77e21982
  • Pointer size: 131 Bytes
  • Size of remote file: 496 kB
rl_deploy/calib_debug/p7_1792287977_after.png ADDED

Git LFS Details

  • SHA256: 4c2e424af0ac49d253d1afef0fafada8b93054b2abb11edb3ddb01ce550d0fca
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_debug/p7_1792287977_bluemask.png ADDED
rl_deploy/calib_debug/p7_1792287977_ref.png ADDED

Git LFS Details

  • SHA256: 7ddbcc6acd53594fae8711a0940eb0a0d583d43af3cb378d7958291f85352311
  • Pointer size: 131 Bytes
  • Size of remote file: 503 kB
rl_deploy/calib_debug/p8_1792287997_after.png ADDED

Git LFS Details

  • SHA256: ca4171228de2b5330912bc955cdc2230199d0fb429a9bbe7d2c9467055b3b434
  • Pointer size: 131 Bytes
  • Size of remote file: 504 kB
rl_deploy/calib_debug/p8_1792287997_bluemask.png ADDED
rl_deploy/calib_debug/p8_1792287997_ref.png ADDED

Git LFS Details

  • SHA256: 3eb338b0efdd2ffde2fece773190d81ec9f70d0b9d183cb529d0ce4e57f9afb5
  • Pointer size: 131 Bytes
  • Size of remote file: 504 kB
rl_deploy/calib_point_1.png ADDED

Git LFS Details

  • SHA256: 16e1724208fd28ab9267bf54586d0ee921a767f4590f574cf6fde48bf5918c68
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB
rl_deploy/calib_point_2.png ADDED

Git LFS Details

  • SHA256: 598d467f2ae5fbbb53957c24fad90790054c4b1ced26d9c6f1d4d5e81232ac83
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB
rl_deploy/calib_point_3.png ADDED

Git LFS Details

  • SHA256: cbb7ad61c75a30b5cc81e9d218f4ebc61d1a1de185eb7f0b3ac095925de097a8
  • Pointer size: 131 Bytes
  • Size of remote file: 499 kB
rl_deploy/calib_point_4.png ADDED

Git LFS Details

  • SHA256: 754970adca1d214730534b2f1e119dbcca93561f89881f4b6db0f78b4513db4f
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_point_5.png ADDED

Git LFS Details

  • SHA256: 2781d652816c4c2a353c953b05325213999f342d86c29f7f76a71e2557b3131c
  • Pointer size: 131 Bytes
  • Size of remote file: 505 kB
rl_deploy/calib_point_6.png ADDED

Git LFS Details

  • SHA256: a087fd266da74440c7844ed456cdd70e675de6608d066049eee658fbff464aca
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_point_7.png ADDED

Git LFS Details

  • SHA256: 0a295d122d489f0dbc0776535c09eafe82fadf2270ca2ddbca39f42f9ff9be0e
  • Pointer size: 131 Bytes
  • Size of remote file: 507 kB
rl_deploy/calib_point_8.png ADDED

Git LFS Details

  • SHA256: 11af50eb28890f1e8c4c7771c1af4c519216f8c02c27ad4fb30243bc74d0e0a4
  • Pointer size: 131 Bytes
  • Size of remote file: 504 kB
rl_deploy/calib_points.json ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "uv": [
4
+ 283.3632596685083,
5
+ 232.50552486187846
6
+ ],
7
+ "area": 724,
8
+ "joints_deg": [
9
+ -22.681318681318682,
10
+ -1.2307692307692308,
11
+ 41.18681318681319,
12
+ 39.07692307692308,
13
+ -2.5054945054945055,
14
+ 1.1026878015161956
15
+ ],
16
+ "tip": [
17
+ 0.0885746699621676,
18
+ -0.2211262158968657,
19
+ -0.0018728227502649436
20
+ ]
21
+ },
22
+ {
23
+ "uv": [
24
+ 223.27258566978193,
25
+ 174.84112149532712
26
+ ],
27
+ "area": 642,
28
+ "joints_deg": [
29
+ -6.065934065934066,
30
+ 13.098901098901099,
31
+ 21.846153846153847,
32
+ 39.42857142857143,
33
+ -2.4175824175824174,
34
+ 1.1026878015161956
35
+ ],
36
+ "tip": [
37
+ 0.04521679450712202,
38
+ -0.28363020045294185,
39
+ 0.00034617923123554314
40
+ ]
41
+ },
42
+ {
43
+ "uv": [
44
+ 324.1647940074906,
45
+ 146.14606741573033
46
+ ],
47
+ "area": 534,
48
+ "joints_deg": [
49
+ 10.54945054945055,
50
+ -5.450549450549451,
51
+ 41.714285714285715,
52
+ 58.59340659340659,
53
+ -2.32967032967033,
54
+ 1.1026878015161956
55
+ ],
56
+ "tip": [
57
+ -0.009996185007877991,
58
+ -0.19178363754922642,
59
+ 0.006818864733087288
60
+ ]
61
+ },
62
+ {
63
+ "uv": [
64
+ 323.44,
65
+ 294.63470588235293
66
+ ],
67
+ "area": 1700,
68
+ "joints_deg": [
69
+ -41.67032967032967,
70
+ 7.824175824175824,
71
+ 25.45054945054945,
72
+ 52.879120879120876,
73
+ -2.5934065934065935,
74
+ 1.1026878015161956
75
+ ],
76
+ "tip": [
77
+ 0.14299164473120482,
78
+ -0.19023792065397205,
79
+ 0.003980910785919246
80
+ ]
81
+ },
82
+ {
83
+ "uv": [
84
+ 446.7759784075574,
85
+ 253.0425101214575
86
+ ],
87
+ "area": 1482,
88
+ "joints_deg": [
89
+ -52.30769230769231,
90
+ -23.64835164835165,
91
+ 56.30769230769231,
92
+ 63.51648351648352,
93
+ 84.26373626373626,
94
+ 1.1026878015161956
95
+ ],
96
+ "tip": [
97
+ 0.09268488377845423,
98
+ -0.1230287165562727,
99
+ 0.013203728373691447
100
+ ]
101
+ },
102
+ {
103
+ "uv": [
104
+ 177.62869198312237,
105
+ 300.67148884870403
106
+ ],
107
+ "area": 1659,
108
+ "joints_deg": [
109
+ -32.879120879120876,
110
+ 30.24175824175824,
111
+ -1.2747252747252746,
112
+ 42.15384615384615,
113
+ -17.53846153846154,
114
+ 1.1716057891109581
115
+ ],
116
+ "tip": [
117
+ 0.17211300438132343,
118
+ -0.2762982688066562,
119
+ -0.005484126509456877
120
+ ]
121
+ },
122
+ {
123
+ "uv": [
124
+ 203.57355679702047,
125
+ 143.64245810055866
126
+ ],
127
+ "area": 537,
128
+ "joints_deg": [
129
+ 3.6043956043956045,
130
+ 25.406593406593405,
131
+ 5.934065934065934,
132
+ 46.81318681318681,
133
+ -16.923076923076923,
134
+ 1.1716057891109581
135
+ ],
136
+ "tip": [
137
+ 0.00809345599756148,
138
+ -0.30200666202286097,
139
+ -0.007828562295707622
140
+ ]
141
+ },
142
+ {
143
+ "uv": [
144
+ 326.9448818897638,
145
+ 185.59186351706037
146
+ ],
147
+ "area": 762,
148
+ "joints_deg": [
149
+ -11.516483516483516,
150
+ -11.516483516483516,
151
+ 47.07692307692308,
152
+ 57.45054945054945,
153
+ -17.36263736263736,
154
+ 1.1026878015161956
155
+ ],
156
+ "tip": [
157
+ 0.0432111876213237,
158
+ -0.18203129452029243,
159
+ 0.008625103296823854
160
+ ]
161
+ }
162
+ ]
rl_deploy/calibrate_table.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Calibrate the phone camera against the table, using the arm itself as the measuring stick.
2
+
3
+ The script never moves the arm: the follower's torque is switched OFF and you move it by hand.
4
+ For every point it asks you to:
5
+ A. clear the spot (no candy there yet, hands out of view) -> reference photo
6
+ B. place ONE blue candy at a new spot (hands out of view) -> photo; only blue that is NEW
7
+ compared with the reference counts, so blue things already in the scene are ignored
8
+ C. check the detection (preview saved as calib_point_N.png) and confirm it
9
+ D. touch the gripper tip to the middle of the candy -> joint angles -> tip position
10
+ Points are saved after every step to calib_points.json, so a crash loses nothing: run again with
11
+ --resume to continue. With 6+ points it fits the pixel -> table map and saves table_calib.json.
12
+
13
+ cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8
14
+ ~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8 --resume # continue after a crash
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import time
21
+
22
+ import cv2
23
+ import numpy as np
24
+
25
+ from common import CALIB, HERE, Phone, apply_h, blue_blobs, blue_mask, blue_report, connect_robot, fit_homography, read_joints
26
+ from so101_fk import SO101FK
27
+
28
+ POINTS = os.path.join(HERE, "calib_points.json")
29
+ MIN_PIXEL_GAP = 20 # a new point must be at least this far (px) from earlier ones
30
+
31
+ p = argparse.ArgumentParser()
32
+ p.add_argument("--points", type=int, default=8)
33
+ p.add_argument("--resume", action="store_true", help="keep the points in calib_points.json and add more")
34
+ p.add_argument("--phone-url", default=None, help="phone stream URL (default: PHONE_URL env or http://127.0.0.1:4747/video)")
35
+ a = p.parse_args()
36
+
37
+ pts = json.load(open(POINTS)) if (a.resume and os.path.exists(POINTS)) else []
38
+ if pts:
39
+ print(f"Resuming with {len(pts)} saved point(s).")
40
+ elif os.path.exists(POINTS):
41
+ print(f"Starting fresh (the old {os.path.basename(POINTS)} will be overwritten; use --resume to keep it).")
42
+
43
+
44
+ def save():
45
+ json.dump(pts, open(POINTS, "w"), indent=1)
46
+
47
+
48
+ def fit_and_report():
49
+ uv = np.array([q["uv"] for q in pts])
50
+ xy = np.array([q["tip"][:2] for q in pts])
51
+ H = fit_homography(uv, xy)
52
+ err = np.linalg.norm(apply_h(H, uv) - xy, axis=1)
53
+ loo = [] # leave-one-out: how well each point is predicted by the others
54
+ for i in range(len(pts)):
55
+ m = np.arange(len(pts)) != i
56
+ loo.append(np.linalg.norm(apply_h(fit_homography(uv[m], xy[m]), uv[i:i + 1])[0] - xy[i]))
57
+ print("\nPoint pixel (u, v) tip (x, y, z) [m] fit err leave-one-out err")
58
+ for i, q in enumerate(pts):
59
+ print(f"{i + 1:5d} ({q['uv'][0]:6.1f}, {q['uv'][1]:5.1f}) ({q['tip'][0]:+.3f}, {q['tip'][1]:+.3f}, {q['tip'][2]:+.3f})"
60
+ f" {1000 * err[i]:5.1f} mm {1000 * loo[i]:6.1f} mm")
61
+ print(f"Fit error median {1000 * np.median(err):.1f} mm, max {1000 * err.max():.1f} mm; "
62
+ f"leave-one-out median {1000 * np.median(loo):.1f} mm, max {1000 * max(loo):.1f} mm")
63
+ return H, err, loo
64
+
65
+
66
+ fk = SO101FK()
67
+ robot = connect_robot()
68
+ robot.bus.disable_torque()
69
+ print("\nFollower torque is OFF: you move the arm by hand. Nothing in this script moves it.")
70
+ print("Hold the arm when the script ends: it stays limp.\n")
71
+ phone = Phone(a.phone_url) if a.phone_url else Phone()
72
+ try:
73
+ while len(pts) < a.points:
74
+ k = len(pts) + 1
75
+ print(f"\n===== Point {k}/{a.points} =====")
76
+ input(f"[A] Make sure there is NO candy at the spot you will use next and your hands are out of view.\n"
77
+ f" Press Enter to take the reference photo...")
78
+ ref = phone.frame()
79
+ input("[B] Put ONE blue candy at a new spot in the candy area (not where earlier points were),\n"
80
+ " take your hands out of view, press Enter...")
81
+ img = phone.frame()
82
+ new = sorted(blue_blobs(img, ignore=ref), key=lambda b: -b["area"])
83
+ allb = blue_blobs(img)
84
+ # diagnostics: raw photos and blue masks are saved for every attempt
85
+ dd = os.path.join(HERE, "calib_debug")
86
+ os.makedirs(dd, exist_ok=True)
87
+ tag = f"p{k}_{int(time.time())}"
88
+ cv2.imwrite(os.path.join(dd, f"{tag}_ref.png"), cv2.cvtColor(ref, cv2.COLOR_RGB2BGR))
89
+ cv2.imwrite(os.path.join(dd, f"{tag}_after.png"), cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
90
+ cv2.imwrite(os.path.join(dd, f"{tag}_bluemask.png"), blue_mask(img))
91
+ rep = blue_report(img)
92
+ diff = float(np.abs(img.astype(np.int16) - ref.astype(np.int16)).mean())
93
+ print(f" photo {img.shape[1]}x{img.shape[0]}, brightness {rep['mean_brightness']:.0f}, change vs reference {diff:.1f} "
94
+ f"(near 0 = the two photos are the same frame)")
95
+ print(f" blue pixels {rep['blue_px']}; largest blue patch {rep['largest_area']} px at {rep['largest_at']} "
96
+ f"(blobs under 25 px are ignored); photos saved in calib_debug/{tag}_*.png")
97
+ print(f" blue blobs in the photo: {len(allb)}; NEW since the reference photo: {len(new)}")
98
+ for j, b in enumerate(new):
99
+ print(f" new blob {j + 1}: pixel ({b['uv'][0]:.0f}, {b['uv'][1]:.0f}), {b['area']} px")
100
+ if not new:
101
+ print(" No new blue candy found. Check it is in view and not hidden by the arm, then redo this point.")
102
+ continue
103
+ b = new[0]
104
+ near = [i + 1 for i, q in enumerate(pts) if np.linalg.norm(np.array(q["uv"]) - b["uv"]) < MIN_PIXEL_GAP]
105
+ if near:
106
+ print(f" This is the same spot as point(s) {near}. Move the candy somewhere new and redo this point.")
107
+ continue
108
+ dbg = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
109
+ for j, bb in enumerate(new):
110
+ cv2.circle(dbg, tuple(int(v) for v in bb["uv"]), 8, (0, 0, 255) if j == 0 else (0, 255, 255), 2)
111
+ prev = os.path.join(HERE, f"calib_point_{k}.png")
112
+ cv2.imwrite(prev, dbg)
113
+ print(f"[C] Using new blob 1 (red circle in {os.path.basename(prev)}).")
114
+ if input(" Is that the candy you placed? [Y/n] ").strip().lower().startswith("n"):
115
+ print(" Skipped. Redo this point.")
116
+ continue
117
+ input("[D] Move the gripper by hand until its tip touches the MIDDLE of that candy, press Enter...")
118
+ q = read_joints(robot)
119
+ tip = fk.tcp(q)
120
+ print(f" joints [deg] {np.round(q, 1).tolist()}")
121
+ print(f" tip position {np.round(tip, 3).tolist()} m (z should be about 0.00 to 0.02)")
122
+ if not -0.03 < tip[2] < 0.04:
123
+ print(" The tip is not at table height: lower the gripper onto the candy and redo this point.")
124
+ continue
125
+ pts.append({"uv": b["uv"].tolist(), "area": b["area"], "joints_deg": q.tolist(), "tip": tip.tolist()})
126
+ save()
127
+ print(f" Saved point {k} to {os.path.basename(POINTS)}. You can now remove the candy.")
128
+ if len(pts) >= 4:
129
+ fit_and_report()
130
+ finally:
131
+ phone.close()
132
+ robot.disconnect()
133
+
134
+ if len(pts) < 6:
135
+ raise SystemExit(f"Only {len(pts)} points; need at least 6. Run again with --resume.")
136
+ H, err, loo = fit_and_report()
137
+ json.dump({"H": H.tolist(), "image_size": [640, 360], "points": pts, "err_mm": (1000 * err).tolist(),
138
+ "loo_mm": [1000 * v for v in loo]}, open(CALIB, "w"), indent=1)
139
+ print(f"\nSaved {CALIB}")
140
+ if max(loo) > 0.02:
141
+ worst = int(np.argmax(loo)) + 1
142
+ print(f"Leave-one-out error above 2 cm (worst: point {worst}). Delete it from {os.path.basename(POINTS)} "
143
+ f"and add a point with --resume, or add more points.")
rl_deploy/common.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared pieces for the RL policy on the real SO101: robot/camera setup, blue-candy detection,
2
+ and the phone-pixel -> table homography."""
3
+
4
+ import json
5
+ import os
6
+ import time
7
+
8
+ import cv2
9
+ import numpy as np
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+ CALIB = os.path.join(HERE, "table_calib.json")
13
+ FOLLOWER_PORT = "/dev/serial/by-id/usb-1a86_USB_Single_Serial_5AE6057813-if00"
14
+ FOLLOWER_ID = "my_follower_arm"
15
+ PHONE_URL = os.environ.get("PHONE_URL", "http://127.0.0.1:4747/video") # DroidCam on localhost; PHONE_URL=... to override
16
+ IMG_W, IMG_H = 640, 360 # the phone view is used at this size (same as the half-res datasets)
17
+ JOINTS = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"]
18
+
19
+
20
+ def connect_robot():
21
+ from lerobot.robots.so_follower.config_so_follower import SOFollowerRobotConfig
22
+ from lerobot.robots.so_follower.so_follower import SOFollower
23
+
24
+ robot = SOFollower(SOFollowerRobotConfig(port=FOLLOWER_PORT, id=FOLLOWER_ID))
25
+ robot.connect(calibrate=False)
26
+ return robot
27
+
28
+
29
+ def read_joints(robot):
30
+ obs = robot.get_observation()
31
+ return np.array([obs[f"{n}.pos"] for n in JOINTS], dtype=np.float64)
32
+
33
+
34
+ class Phone:
35
+ """DroidCam stream, resized to 640x360 RGB.
36
+
37
+ A background thread reads the stream continuously and keeps only the newest frame, so a photo
38
+ is always current (OpenCV otherwise hands back frames buffered seconds earlier, which made the
39
+ "before" and "after" photos identical). Reconnects if the HTTP stream drops."""
40
+
41
+ def __init__(self, url=PHONE_URL):
42
+ import threading
43
+
44
+ self.url = url
45
+ self.cap = None
46
+ self.latest, self.stamp, self.count = None, 0.0, 0
47
+ self.lock = threading.Lock()
48
+ self.running = True
49
+ self._open()
50
+ self.thread = threading.Thread(target=self._reader, daemon=True)
51
+ self.thread.start()
52
+ t0 = time.time()
53
+ while self.latest is None and time.time() - t0 < 10:
54
+ time.sleep(0.05)
55
+ if self.latest is None:
56
+ raise RuntimeError(f"no frames from {self.url} within 10 s")
57
+
58
+ def _open(self):
59
+ if self.cap is not None:
60
+ self.cap.release()
61
+ self.cap = cv2.VideoCapture(self.url)
62
+ if not self.cap.isOpened():
63
+ raise RuntimeError(f"cannot open phone stream {self.url} (is DroidCam running? close any browser tab showing it)")
64
+
65
+ def _reader(self):
66
+ fails = 0
67
+ while self.running:
68
+ ok, img = self.cap.read()
69
+ if ok and img is not None:
70
+ fails = 0
71
+ with self.lock:
72
+ self.latest, self.stamp = img, time.time()
73
+ self.count += 1
74
+ continue
75
+ fails += 1
76
+ print(f" (phone stream dropped, reconnecting: attempt {fails})")
77
+ time.sleep(1.0)
78
+ try:
79
+ self._open()
80
+ except RuntimeError as e:
81
+ print(" ", e)
82
+
83
+ def frame(self, max_age=0.5, timeout=10.0):
84
+ """Newest frame (taken after this call started), 640x360 RGB."""
85
+ t_call = time.time()
86
+ while time.time() - t_call < timeout:
87
+ with self.lock:
88
+ img, stamp = self.latest, self.stamp
89
+ if img is not None and stamp > t_call and time.time() - stamp < max_age:
90
+ return cv2.cvtColor(cv2.resize(img, (IMG_W, IMG_H), interpolation=cv2.INTER_AREA), cv2.COLOR_BGR2RGB)
91
+ time.sleep(0.02)
92
+ raise RuntimeError("no fresh phone frame for %.0f s; check the DroidCam app and connection" % timeout)
93
+
94
+ def close(self):
95
+ self.running = False
96
+ self.thread.join(timeout=2)
97
+ self.cap.release()
98
+
99
+
100
+ def blue_mask(rgb):
101
+ hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
102
+ m = cv2.inRange(hsv, (95, 90, 70), (125, 255, 255)) # OpenCV hue 0..180: blue ~ 100-125
103
+ return cv2.morphologyEx(m, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
104
+
105
+
106
+ def blue_report(rgb):
107
+ """Why a photo has (no) blue blobs: blue pixel count and the largest blue component."""
108
+ m = blue_mask(rgb)
109
+ n, _, stats, cent = cv2.connectedComponentsWithStats(m)
110
+ big = max(range(1, n), key=lambda i: stats[i, cv2.CC_STAT_AREA], default=None)
111
+ hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
112
+ return {"blue_px": int((m > 0).sum()), "largest_area": int(stats[big, cv2.CC_STAT_AREA]) if big else 0,
113
+ "largest_at": cent[big].round(0).tolist() if big else None, "mean_brightness": float(hsv[..., 2].mean())}
114
+
115
+
116
+ def blue_blobs(rgb, min_px=25, ignore=None):
117
+ """Blue wrapper blobs: list of dicts with pixel centroid, area and the blob's long-axis angle.
118
+ ``ignore``: a reference image; blue that was already there (dilated a little) is left out."""
119
+ m = blue_mask(rgb)
120
+ if ignore is not None:
121
+ m = cv2.bitwise_and(m, cv2.bitwise_not(cv2.dilate(blue_mask(ignore), np.ones((9, 9), np.uint8))))
122
+ n, lab, stats, cent = cv2.connectedComponentsWithStats(m)
123
+ out = []
124
+ for i in range(1, n):
125
+ if stats[i, cv2.CC_STAT_AREA] < min_px:
126
+ continue
127
+ ys, xs = np.nonzero(lab == i)
128
+ pts = np.c_[xs, ys].astype(np.float64)
129
+ c = pts.mean(0)
130
+ evals, evecs = np.linalg.eigh(np.cov((pts - c).T))
131
+ major = evecs[:, np.argmax(evals)]
132
+ out.append({"uv": c, "area": int(stats[i, cv2.CC_STAT_AREA]), "axis_uv": major})
133
+ return out
134
+
135
+
136
+ def fit_homography(uv, xy):
137
+ """Least-squares DLT homography (u, v) -> (x, y)."""
138
+ A = []
139
+ for (u, v), (x, y) in zip(uv, xy):
140
+ A.append([-u, -v, -1, 0, 0, 0, x * u, x * v, x])
141
+ A.append([0, 0, 0, -u, -v, -1, y * u, y * v, y])
142
+ _, _, Vt = np.linalg.svd(np.array(A))
143
+ return Vt[-1].reshape(3, 3) / Vt[-1, -1]
144
+
145
+
146
+ def apply_h(H, uv):
147
+ uv = np.atleast_2d(uv)
148
+ q = np.c_[uv, np.ones(len(uv))] @ H.T
149
+ return q[:, :2] / q[:, 2:3]
150
+
151
+
152
+ def load_calib():
153
+ if not os.path.exists(CALIB):
154
+ raise SystemExit(f"No {CALIB}. Run calibrate_table.py first.")
155
+ return np.array(json.load(open(CALIB))["H"])
156
+
157
+
158
+ def candy_on_table(H, blob):
159
+ """Table x, y [m] and yaw [rad] of a blob (yaw from the blob's long axis mapped onto the table)."""
160
+ c = blob["uv"]
161
+ p0, p1 = apply_h(H, [c - 8 * blob["axis_uv"], c + 8 * blob["axis_uv"]])
162
+ d = p1 - p0
163
+ xy = apply_h(H, c)[0]
164
+ return xy, float(np.arctan2(d[1], d[0]))
rl_deploy/deploy_rl.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the Isaac Lab RL candy-pick policy on the real SO101 (state-based: joints + detected candy).
2
+
3
+ Same observation and action conventions as Isaac-SO101-CandyPick-Direct:
4
+ obs = arm joints [rad] + gripper opening [0..1], joint velocities x0.1, candy (x, y, sin2yaw,
5
+ cos2yaw) from the phone at the start, towel (x, y), last action, episode time fraction
6
+ act = 5 arm-joint target deltas (x0.06 rad per 30 Hz step, clamped to the real-demo ranges)
7
+ + gripper command (-1 closed .. +1 = LeRobot 45 open)
8
+
9
+ By default it only detects the candy and prints what it would do (DRY RUN, arm not moved).
10
+ Add --go to move the arm; you are asked to confirm first. Keep a hand near the power switch.
11
+
12
+ cd ~/B1Hackathon/tasks/candy_sort/rl_deploy
13
+ ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt # dry run
14
+ ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt --go # moves the arm
15
+ Safety (real arm only): targets change by at most --max-step-deg per step (default 1 deg = 30 deg/s,
16
+ about a third of what the sim policy may command) and actions are low-pass filtered (--smooth).
17
+ Every run saves ../eval_videos/rl_<policy>_<time>.mp4 (phone + wrist) and a .csv of measured vs
18
+ commanded joints, unless --no-video.
19
+ """
20
+
21
+ import argparse
22
+ import time
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+ from common import HERE, JOINTS, Phone, blue_blobs, candy_on_table, connect_robot, load_calib, read_joints
28
+
29
+ WRIST_DEV = "/dev/v4l/by-id/usb-Ausha_Webcam_Ausha_Webcam-video-index0"
30
+
31
+ # Must match so101_candy/candy_env_cfg.py
32
+ REAL_ARM_RANGE_DEG = np.array([(-44.9, 80.9), (-102.9, 32.6), (-12.2, 93.1), (0.2, 104.3), (-61.1, 99.7)])
33
+ ARM_SCALE_RAD = 0.06
34
+ GRIP_OPEN_LR, GRIP_CLOSED_LR = 45.0, 0.0
35
+ GRIP_LO, GRIP_HI = -0.174533, 1.74533 # sim gripper joint limits [rad] (LeRobot 0..100 maps onto them)
36
+ TOWEL_XY = (-0.20, -0.17)
37
+ TOWEL_HALF = (0.08, 0.12)
38
+ CANDY_X, CANDY_Y = (-0.06, 0.18), (-0.32, -0.12) # spawn box of the sim task, with some margin
39
+ EPISODE_STEPS = 300 # 10 s at 30 Hz
40
+ HZ = 30.0
41
+
42
+ p = argparse.ArgumentParser()
43
+ p.add_argument("--policy", required=True, help="TorchScript policy exported from Isaac Lab (policy.pt)")
44
+ p.add_argument("--go", action="store_true", help="actually move the arm")
45
+ p.add_argument("--seconds", type=float, default=10.0)
46
+ p.add_argument("--max-step-deg", type=float, default=1.0,
47
+ help="safety: max change of any joint target per 30 Hz step [deg] (sim policy allows ~3.4)")
48
+ p.add_argument("--smooth", type=float, default=0.5, help="safety: action low-pass, 0 = off, 0.5 = average with last")
49
+ p.add_argument("--no-video", action="store_true", help="don't save the phone/wrist video and the per-step log")
50
+ p.add_argument("--phone-url", default=None, help="phone stream URL (default: PHONE_URL env or http://127.0.0.1:4747/video)")
51
+ a = p.parse_args()
52
+
53
+ policy = torch.jit.load(a.policy, map_location="cpu").eval()
54
+ H = load_calib()
55
+
56
+ # 1) Detect the target candy (blue, not already on the towel, inside the trained workspace).
57
+ phone = Phone(a.phone_url) if a.phone_url else Phone()
58
+ img = phone.frame()
59
+ cands = []
60
+ for b in blue_blobs(img):
61
+ xy, yaw = candy_on_table(H, b)
62
+ on_towel = abs(xy[0] - TOWEL_XY[0]) < TOWEL_HALF[0] and abs(xy[1] - TOWEL_XY[1]) < TOWEL_HALF[1]
63
+ in_ws = CANDY_X[0] <= xy[0] <= CANDY_X[1] and CANDY_Y[0] <= xy[1] <= CANDY_Y[1]
64
+ print(f"blue blob at pixel {b['uv'].round(0)} -> table {xy.round(3)} m, yaw {np.degrees(yaw):.0f} deg"
65
+ f"{' (on towel)' if on_towel else ''}{' (outside trained area)' if not in_ws else ''}")
66
+ if not on_towel and in_ws:
67
+ cands.append((xy, yaw))
68
+ if not cands:
69
+ raise SystemExit("No blue candy inside the trained workspace. Move one closer to the arm and retry.")
70
+ xy, yaw = min(cands, key=lambda c: np.linalg.norm(c[0] - np.array([0.06, -0.22])))
71
+ candy_obs = np.array([xy[0], xy[1], np.sin(2 * yaw), np.cos(2 * yaw)])
72
+ print(f"target candy: {xy.round(3)} m, yaw {np.degrees(yaw):.0f} deg")
73
+
74
+ robot = connect_robot()
75
+ q = read_joints(robot)
76
+ print("current joints [deg]:", q.round(1))
77
+ if a.go:
78
+ if input("The arm WILL move for %.0f s. Type 'go' to start: " % a.seconds).strip() != "go":
79
+ robot.disconnect()
80
+ raise SystemExit("aborted")
81
+ else:
82
+ print("DRY RUN: computing actions without sending them (add --go to move the arm)")
83
+
84
+ # Documentation: phone (+ wrist if available) video and a per-step CSV of commanded vs measured joints.
85
+ import csv
86
+ import os
87
+ from datetime import datetime
88
+
89
+ import cv2
90
+
91
+ rec = None
92
+ if not a.no_video:
93
+ os.makedirs(os.path.join(HERE, "..", "eval_videos"), exist_ok=True)
94
+ stem = os.path.join(HERE, "..", "eval_videos", f"rl_{os.path.splitext(os.path.basename(a.policy))[0]}_{datetime.now():%Y%m%d_%H%M%S}"
95
+ + ("" if a.go else "_dryrun"))
96
+ wrist = cv2.VideoCapture(WRIST_DEV)
97
+ wrist = wrist if wrist.isOpened() else None
98
+ vw = cv2.VideoWriter(stem + ".mp4", cv2.VideoWriter_fourcc(*"mp4v"), HZ, (640 + (320 if wrist else 0), 360))
99
+ logf = open(stem + ".csv", "w", newline="")
100
+ log = csv.writer(logf)
101
+ log.writerow(["t"] + [f"meas_{n}" for n in JOINTS] + [f"target_{n}" for n in JOINTS] + [f"act_{i}" for i in range(6)])
102
+ rec = (vw, wrist, log, logf)
103
+ print(f"recording to {stem}.mp4 / .csv")
104
+
105
+ target = q.copy() # LeRobot units
106
+ last_act = np.zeros(6)
107
+ prev_cmd = np.zeros(6)
108
+ prev_q, prev_t = q.copy(), time.perf_counter()
109
+ steps = int(a.seconds * HZ)
110
+ try:
111
+ for k in range(steps):
112
+ t0 = time.perf_counter()
113
+ q = read_joints(robot)
114
+ dt = max(t0 - prev_t, 1e-3)
115
+ q_rad = np.deg2rad(q[:5])
116
+ grip_open = q[5] / 100.0
117
+ vel = np.r_[np.deg2rad(q[:5] - prev_q[:5]), (q[5] - prev_q[5]) / 100.0 * (GRIP_HI - GRIP_LO)] / dt
118
+ prev_q, prev_t = q.copy(), t0
119
+ obs = np.r_[q_rad, grip_open, 0.1 * vel, candy_obs, TOWEL_XY, last_act, min(k, EPISODE_STEPS) / EPISODE_STEPS]
120
+ with torch.no_grad():
121
+ act = policy(torch.tensor(obs, dtype=torch.float32)[None])[0].numpy()
122
+ act = np.clip(act, -1.0, 1.0)
123
+ last_act = act # the policy sees its own (unsmoothed) action, as in sim
124
+ cmd = a.smooth * prev_cmd + (1.0 - a.smooth) * act if a.smooth > 0 else act
125
+ prev_cmd = cmd
126
+ step = np.clip(np.rad2deg(ARM_SCALE_RAD * cmd[:5]), -a.max_step_deg, a.max_step_deg)
127
+ target[:5] = np.clip(target[:5] + step, REAL_ARM_RANGE_DEG[:, 0], REAL_ARM_RANGE_DEG[:, 1])
128
+ act = cmd
129
+ target[5] = GRIP_CLOSED_LR + (GRIP_OPEN_LR - GRIP_CLOSED_LR) * (act[5] + 1.0) / 2.0
130
+ if a.go:
131
+ robot.send_action({f"{n}.pos": float(v) for n, v in zip(
132
+ ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"], target)})
133
+ elif k % 15 == 0:
134
+ print(f"t={k / HZ:4.1f}s target [deg]: {target.round(1)}")
135
+ if rec:
136
+ vw, wrist, log, _ = rec
137
+ frame = cv2.cvtColor(phone.frame(max_age=1.0), cv2.COLOR_RGB2BGR)
138
+ if wrist is not None:
139
+ ok, w = wrist.read()
140
+ w = cv2.resize(w, (320, 240)) if ok else np.zeros((240, 320, 3), np.uint8)
141
+ frame = np.concatenate([frame, np.vstack([w, np.zeros((120, 320, 3), np.uint8)])], axis=1)
142
+ cv2.putText(frame, f"t={k / HZ:4.1f}s {'GO' if a.go else 'DRY'}", (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
143
+ vw.write(frame)
144
+ log.writerow([round(k / HZ, 3)] + q.round(2).tolist() + target.round(2).tolist() + np.round(act, 3).tolist())
145
+ time.sleep(max(0.0, 1.0 / HZ - (time.perf_counter() - t0)))
146
+ except KeyboardInterrupt:
147
+ print("\nstopped by Ctrl-C")
148
+ finally:
149
+ robot.disconnect()
150
+ phone.close()
151
+ if rec:
152
+ rec[0].release()
153
+ if rec[1] is not None:
154
+ rec[1].release()
155
+ rec[3].close()
156
+ print("saved video and per-step log next to", os.path.basename(stem))
rl_deploy/export_policy.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turn an rsl_rl checkpoint (model_XXXX.pt) of Isaac-SO101-CandyPick-Direct into a TorchScript policy.
2
+
3
+ The exported module takes the raw 25-d observation and returns the deterministic action (mean),
4
+ with the observation normalizer baked in. No Isaac Sim needed, so it can run while training.
5
+
6
+ python export_policy.py <run dir>/model_1200.pt policy.pt
7
+ """
8
+
9
+ import sys
10
+
11
+ import torch
12
+ from torch import nn
13
+
14
+ EPS = 1e-2 # rsl_rl EmpiricalNormalization
15
+
16
+
17
+ class Policy(nn.Module):
18
+ def __init__(self, sd):
19
+ super().__init__()
20
+ self.register_buffer("mean", sd["obs_normalizer._mean"].clone())
21
+ self.register_buffer("std", sd["obs_normalizer._std"].clone())
22
+ layers, i = [], 0
23
+ while f"mlp.{i}.weight" in sd:
24
+ w = sd[f"mlp.{i}.weight"]
25
+ lin = nn.Linear(w.shape[1], w.shape[0])
26
+ lin.weight.data.copy_(w)
27
+ lin.bias.data.copy_(sd[f"mlp.{i}.bias"])
28
+ layers.append(lin)
29
+ if f"mlp.{i + 2}.weight" in sd:
30
+ layers.append(nn.ELU())
31
+ i += 2
32
+ self.mlp = nn.Sequential(*layers)
33
+
34
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
35
+ return self.mlp((obs - self.mean) / (self.std + 1e-2)) # EPS
36
+
37
+
38
+ ckpt = torch.load(sys.argv[1], map_location="cpu", weights_only=False)
39
+ pol = Policy(ckpt["actor_state_dict"]).eval()
40
+ ts = torch.jit.script(pol)
41
+ ts.save(sys.argv[2])
42
+ print("saved", sys.argv[2], "from iteration", ckpt.get("iter"), "| obs dim", pol.mean.shape[-1])
rl_deploy/policy.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd7743047a9c53d7f73cb9fc36fbb311d207c97ee991ff78575ae4db73156015
3
+ size 206011
rl_deploy/policy_final.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1188b99a9d7ea964525e1a63286635d08c3b6c189ec891ca305bd3bf6997758c
3
+ size 206389
rl_deploy/so101_fk.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Forward kinematics of the SO101 from the official so101_new_calib URDF (numpy only).
2
+
3
+ Joint angles are LeRobot degrees (0 = middle of the calibrated range), which map 1:1 onto the URDF
4
+ joints in radians. Positions are returned in the frame the RL policy uses: the robot base on the
5
+ table, z up, with the arm's zero pan pointing along -y (Isaac Lab's so101_new_calib USD, mounted
6
+ 3 cm into the table as in the sim task). See _to_policy_frame.
7
+ """
8
+
9
+ import os
10
+ import xml.etree.ElementTree as ET
11
+
12
+ import numpy as np
13
+
14
+ URDF = os.path.join(os.path.dirname(os.path.abspath(__file__)), "so101_new_calib.urdf")
15
+ ARM = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll"]
16
+
17
+
18
+ def _rpy(r, p, y):
19
+ cr, sr, cp, sp, cy, sy = np.cos(r), np.sin(r), np.cos(p), np.sin(p), np.cos(y), np.sin(y)
20
+ return np.array([[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
21
+ [sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
22
+ [-sp, cp * sr, cp * cr]])
23
+
24
+
25
+ def _axis_angle(axis, a):
26
+ axis = np.asarray(axis, float) / np.linalg.norm(axis)
27
+ K = np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]])
28
+ return np.eye(3) + np.sin(a) * K + (1 - np.cos(a)) * K @ K
29
+
30
+
31
+ def _tf(R, t):
32
+ T = np.eye(4)
33
+ T[:3, :3], T[:3, 3] = R, t
34
+ return T
35
+
36
+
37
+ class SO101FK:
38
+ def __init__(self, urdf=URDF):
39
+ root = ET.parse(urdf).getroot()
40
+ self.joints = {}
41
+ for j in root.findall("joint"):
42
+ o = j.find("origin")
43
+ xyz = [float(v) for v in (o.get("xyz") if o is not None else "0 0 0").split()]
44
+ rpy = [float(v) for v in (o.get("rpy") if o is not None else "0 0 0").split()]
45
+ ax = j.find("axis")
46
+ self.joints[j.get("name")] = {
47
+ "parent": j.find("parent").get("link"), "child": j.find("child").get("link"),
48
+ "T": _tf(_rpy(*rpy), xyz), "axis": [float(v) for v in ax.get("xyz").split()] if ax is not None else None,
49
+ "type": j.get("type"),
50
+ }
51
+ self.by_child = {v["child"]: k for k, v in self.joints.items()}
52
+
53
+ def link_pose(self, link, state_deg):
54
+ """4x4 pose of ``link`` in the policy's base frame for a LeRobot state (deg)."""
55
+ q = {n: np.deg2rad(state_deg[i]) for i, n in enumerate(ARM)}
56
+ chain = []
57
+ while link in self.by_child:
58
+ name = self.by_child[link]
59
+ chain.append(name)
60
+ link = self.joints[name]["parent"]
61
+ T = np.eye(4)
62
+ for name in reversed(chain):
63
+ j = self.joints[name]
64
+ T = T @ j["T"]
65
+ if j["type"] == "revolute" and name in q:
66
+ T = T @ _tf(_axis_angle(j["axis"], q[name]), [0, 0, 0])
67
+ return _to_policy_frame() @ T
68
+
69
+ def tcp(self, state_deg):
70
+ """Gripper tip position (``gripper_frame_link``), metres, policy base frame."""
71
+ return self.link_pose("gripper_frame_link", state_deg)[:3, 3]
72
+
73
+
74
+ def _to_policy_frame():
75
+ # URDF base_link -> sim policy frame. Fitted (least squares, rigid) so URDF tip positions match
76
+ # Isaac Lab's so101_new_calib USD on 146 real grasp/release poses: about -87 deg about z plus a
77
+ # small offset; residual 10 mm median, 27 mm max (the two models differ slightly).
78
+ R = np.array([[5.790e-02, 9.983e-01, 2.300e-03],
79
+ [-9.983e-01, 5.790e-02, -4.000e-04],
80
+ [-5.000e-04, -2.300e-03, 1.000e00]])
81
+ U, _, Vt = np.linalg.svd(R)
82
+ # (fitted on poses already lowered by the 3 cm mount, so that offset is applied first)
83
+ return _tf(U @ Vt, [0.0049, -0.0160, 0.0392]) @ _tf(np.eye(3), [0.0, 0.0, -0.03008])
rl_deploy/so101_new_calib.urdf ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generated using onshape-to-robot -->
3
+ <!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
4
+ <robot name="so101_new_calib">
5
+
6
+ <!-- Materials -->
7
+ <material name="3d_printed">
8
+ <color rgba="1.0 0.82 0.12 1.0"/>
9
+ </material>
10
+ <material name="sts3215">
11
+ <color rgba="0.1 0.1 0.1 1.0"/>
12
+ </material>
13
+
14
+ <!-- Link base -->
15
+ <link name="base_link">
16
+ <inertial>
17
+ <origin xyz="0.0137179 -5.19711e-05 0.0334843" rpy="0 0 0"/>
18
+ <mass value="0.147"/>
19
+ <inertia ixx="0.000114686" ixy="-4.59787e-07" ixz="4.97151e-06" iyy="0.000136117" iyz="9.75275e-08" izz="0.000130364"/>
20
+ </inertial>
21
+ <!-- Part base_motor_holder_so101_v1 -->
22
+ <visual>
23
+ <origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
24
+ <geometry>
25
+ <mesh filename="assets/base_motor_holder_so101_v1.stl"/>
26
+ </geometry>
27
+ <material name="3d_printed"/>
28
+ </visual>
29
+ <collision>
30
+ <origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
31
+ <geometry>
32
+ <mesh filename="assets/base_motor_holder_so101_v1.stl"/>
33
+ </geometry>
34
+ </collision>
35
+ <!-- Part base_so101_v2 -->
36
+ <visual>
37
+ <origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
38
+ <geometry>
39
+ <mesh filename="assets/base_so101_v2.stl"/>
40
+ </geometry>
41
+ <material name="3d_printed"/>
42
+ </visual>
43
+ <collision>
44
+ <origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
45
+ <geometry>
46
+ <mesh filename="assets/base_so101_v2.stl"/>
47
+ </geometry>
48
+ </collision>
49
+ <!-- Part sts3215_03a_v1 -->
50
+ <visual>
51
+ <origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
52
+ <geometry>
53
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
54
+ </geometry>
55
+ <material name="sts3215"/>
56
+ </visual>
57
+ <collision>
58
+ <origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
59
+ <geometry>
60
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
61
+ </geometry>
62
+ </collision>
63
+ <!-- Part waveshare_mounting_plate_so101_v2 -->
64
+ <visual>
65
+ <origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
66
+ <geometry>
67
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
68
+ </geometry>
69
+ <material name="3d_printed"/>
70
+ </visual>
71
+ <collision>
72
+ <origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
73
+ <geometry>
74
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
75
+ </geometry>
76
+ </collision>
77
+ </link>
78
+
79
+ <!-- Link shoulder -->
80
+ <link name="shoulder_link">
81
+ <inertial>
82
+ <origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
83
+ <mass value="0.100006"/>
84
+ <inertia ixx="8.3759e-05" ixy="7.55525e-08" ixz="-1.16342e-06" iyy="8.10403e-05" iyz="1.54663e-07" izz="2.39783e-05"/>
85
+ </inertial>
86
+ <!-- Part sts3215_03a_v1_2 -->
87
+ <visual>
88
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
89
+ <geometry>
90
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
91
+ </geometry>
92
+ <material name="sts3215"/>
93
+ </visual>
94
+ <collision>
95
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
96
+ <geometry>
97
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
98
+ </geometry>
99
+ </collision>
100
+ <!-- Part motor_holder_so101_base_v1 -->
101
+ <visual>
102
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
103
+ <geometry>
104
+ <mesh filename="assets/motor_holder_so101_base_v1.stl"/>
105
+ </geometry>
106
+ <material name="3d_printed"/>
107
+ </visual>
108
+ <collision>
109
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
110
+ <geometry>
111
+ <mesh filename="assets/motor_holder_so101_base_v1.stl"/>
112
+ </geometry>
113
+ </collision>
114
+ <!-- Part rotation_pitch_so101_v1 -->
115
+ <visual>
116
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
117
+ <geometry>
118
+ <mesh filename="assets/rotation_pitch_so101_v1.stl"/>
119
+ </geometry>
120
+ <material name="3d_printed"/>
121
+ </visual>
122
+ <collision>
123
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
124
+ <geometry>
125
+ <mesh filename="assets/rotation_pitch_so101_v1.stl"/>
126
+ </geometry>
127
+ </collision>
128
+ </link>
129
+
130
+ <!-- Link upper_arm -->
131
+ <link name="upper_arm_link">
132
+ <inertial>
133
+ <origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0"/>
134
+ <mass value="0.103"/>
135
+ <inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318" iyz="8.97326e-09" izz="0.000142487"/>
136
+ </inertial>
137
+ <!-- Part sts3215_03a_v1_3 -->
138
+ <visual>
139
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
140
+ <geometry>
141
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
142
+ </geometry>
143
+ <material name="sts3215"/>
144
+ </visual>
145
+ <collision>
146
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
147
+ <geometry>
148
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
149
+ </geometry>
150
+ </collision>
151
+ <!-- Part upper_arm_so101_v1 -->
152
+ <visual>
153
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
154
+ <geometry>
155
+ <mesh filename="assets/upper_arm_so101_v1.stl"/>
156
+ </geometry>
157
+ <material name="3d_printed"/>
158
+ </visual>
159
+ <collision>
160
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
161
+ <geometry>
162
+ <mesh filename="assets/upper_arm_so101_v1.stl"/>
163
+ </geometry>
164
+ </collision>
165
+ </link>
166
+
167
+ <!-- Link lower_arm -->
168
+ <link name="lower_arm_link">
169
+ <inertial>
170
+ <origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0"/>
171
+ <mass value="0.104"/>
172
+ <inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844" iyz="-4.90188e-08" izz="0.00014529"/>
173
+ </inertial>
174
+ <!-- Part under_arm_so101_v1 -->
175
+ <visual>
176
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
177
+ <geometry>
178
+ <mesh filename="assets/under_arm_so101_v1.stl"/>
179
+ </geometry>
180
+ <material name="3d_printed"/>
181
+ </visual>
182
+ <collision>
183
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
184
+ <geometry>
185
+ <mesh filename="assets/under_arm_so101_v1.stl"/>
186
+ </geometry>
187
+ </collision>
188
+ <!-- Part motor_holder_so101_wrist_v1 -->
189
+ <visual>
190
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
191
+ <geometry>
192
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
193
+ </geometry>
194
+ <material name="3d_printed"/>
195
+ </visual>
196
+ <collision>
197
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
198
+ <geometry>
199
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
200
+ </geometry>
201
+ </collision>
202
+ <!-- Part sts3215_03a_v1_4 -->
203
+ <visual>
204
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
205
+ <geometry>
206
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
207
+ </geometry>
208
+ <material name="sts3215"/>
209
+ </visual>
210
+ <collision>
211
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
212
+ <geometry>
213
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
214
+ </geometry>
215
+ </collision>
216
+ </link>
217
+
218
+ <!-- Link wrist -->
219
+ <link name="wrist_link">
220
+ <inertial>
221
+ <origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
222
+ <mass value="0.079"/>
223
+ <inertia ixx="3.68263e-05" ixy="1.7893e-08" ixz="-5.28128e-08" iyy="2.5391e-05" iyz="3.6412e-06" izz="2.1e-05"/>
224
+ </inertial>
225
+ <!-- Part sts3215_03a_no_horn_v1 -->
226
+ <visual>
227
+ <origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
228
+ <geometry>
229
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
230
+ </geometry>
231
+ <material name="sts3215"/>
232
+ </visual>
233
+ <collision>
234
+ <origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
235
+ <geometry>
236
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
237
+ </geometry>
238
+ </collision>
239
+ <!-- Part wrist_roll_pitch_so101_v2 -->
240
+ <visual>
241
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
242
+ <geometry>
243
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
244
+ </geometry>
245
+ <material name="3d_printed"/>
246
+ </visual>
247
+ <collision>
248
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
249
+ <geometry>
250
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
251
+ </geometry>
252
+ </collision>
253
+ </link>
254
+
255
+ <!-- Link gripper -->
256
+ <link name="gripper_link">
257
+ <inertial>
258
+ <origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
259
+ <mass value="0.087"/>
260
+ <inertia ixx="2.75087e-05" ixy="-3.35241e-07" ixz="-5.7352e-06" iyy="4.33657e-05" iyz="-5.17847e-08" izz="3.45059e-05"/>
261
+ </inertial>
262
+ <!-- Part sts3215_03a_v1_5 -->
263
+ <visual>
264
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
265
+ <geometry>
266
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
267
+ </geometry>
268
+ <material name="sts3215"/>
269
+ </visual>
270
+ <collision>
271
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
272
+ <geometry>
273
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
274
+ </geometry>
275
+ </collision>
276
+ <!-- Part wrist_roll_follower_so101_v1 -->
277
+ <visual>
278
+ <origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
279
+ <geometry>
280
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
281
+ </geometry>
282
+ <material name="3d_printed"/>
283
+ </visual>
284
+ <collision>
285
+ <origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
286
+ <geometry>
287
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
288
+ </geometry>
289
+ </collision>
290
+ </link>
291
+
292
+ <!-- Gripper frame (dummy link + fixed joint) -->
293
+ <link name="gripper_frame_link">
294
+ <origin xyz="0 0 0" rpy="0 -0 0"/>
295
+ <inertial>
296
+ <origin xyz="0 0 0" rpy="0 0 0"/>
297
+ <mass value="1e-9"/>
298
+ <inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0"/>
299
+ </inertial>
300
+ </link>
301
+
302
+ <joint name="gripper_frame_joint" type="fixed">
303
+ <origin xyz="-0.0079 -0.000218121 -0.0981274" rpy="0 3.14159 0"/>
304
+ <parent link="gripper_link"/>
305
+ <child link="gripper_frame_link"/>
306
+ <axis xyz="0 0 0"/>
307
+ </joint>
308
+
309
+ <!-- Link moving_jaw_so101_v1 -->
310
+ <link name="moving_jaw_so101_v1_link">
311
+ <inertial>
312
+ <origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
313
+ <mass value="0.012"/>
314
+ <inertia ixx="6.61427e-06" ixy="-3.19807e-07" ixz="-5.90717e-09" iyy="1.89032e-06" iyz="-1.09945e-07" izz="5.28738e-06"/>
315
+ </inertial>
316
+ <!-- Part moving_jaw_so101_v1 -->
317
+ <visual>
318
+ <origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
319
+ <geometry>
320
+ <mesh filename="assets/moving_jaw_so101_v1.stl"/>
321
+ </geometry>
322
+ <material name="3d_printed"/>
323
+ </visual>
324
+ <collision>
325
+ <origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
326
+ <geometry>
327
+ <mesh filename="assets/moving_jaw_so101_v1.stl"/>
328
+ </geometry>
329
+ </collision>
330
+ </link>
331
+
332
+ <!-- Joint from gripper to moving_jaw_so101_v1 -->
333
+ <joint name="gripper" type="revolute">
334
+ <origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -5.24284e-08 -1.41553e-15"/>
335
+ <parent link="gripper_link"/>
336
+ <child link="moving_jaw_so101_v1_link"/>
337
+ <axis xyz="0 0 1"/>
338
+ <limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
339
+ </joint>
340
+
341
+ <transmission name="gripper_trans">
342
+ <type>transmission_interface/SimpleTransmission</type>
343
+ <joint name="gripper">
344
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
345
+ </joint>
346
+ <actuator name="motor6">
347
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
348
+ <mechanicalReduction>1</mechanicalReduction>
349
+ </actuator>
350
+ </transmission>
351
+
352
+ <!-- Joint from wrist to gripper -->
353
+ <joint name="wrist_roll" type="revolute">
354
+ <origin xyz="5.55112e-17 -0.0611 0.0181" rpy="1.5708 0.0486795 3.14159"/>
355
+ <parent link="wrist_link"/>
356
+ <child link="gripper_link"/>
357
+ <axis xyz="0 0 1"/>
358
+ <limit effort="10" velocity="10" lower="-2.74385" upper="2.84121"/>
359
+ </joint>
360
+
361
+ <transmission name="wrist_roll_trans">
362
+ <type>transmission_interface/SimpleTransmission</type>
363
+ <joint name="wrist_roll">
364
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
365
+ </joint>
366
+ <actuator name="motor5">
367
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
368
+ <mechanicalReduction>1</mechanicalReduction>
369
+ </actuator>
370
+ </transmission>
371
+
372
+ <!-- Joint from lower_arm to wrist -->
373
+ <joint name="wrist_flex" type="revolute">
374
+ <origin xyz="-0.1349 0.0052 3.62355e-17" rpy="4.02456e-15 8.67362e-16 -1.5708"/>
375
+ <parent link="lower_arm_link"/>
376
+ <child link="wrist_link"/>
377
+ <axis xyz="0 0 1"/>
378
+ <limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
379
+ </joint>
380
+
381
+ <transmission name="wrist_flex_trans">
382
+ <type>transmission_interface/SimpleTransmission</type>
383
+ <joint name="wrist_flex">
384
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
385
+ </joint>
386
+ <actuator name="motor4">
387
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
388
+ <mechanicalReduction>1</mechanicalReduction>
389
+ </actuator>
390
+ </transmission>
391
+
392
+ <!-- Joint from upper_arm to lower_arm -->
393
+ <!-- Note: 5-degree calibration offset applied to joint limits -->
394
+ <joint name="elbow_flex" type="revolute">
395
+ <origin xyz="-0.11257 -0.028 1.73763e-16" rpy="-3.63608e-16 8.74301e-16 1.5708"/>
396
+ <parent link="upper_arm_link"/>
397
+ <child link="lower_arm_link"/>
398
+ <axis xyz="0 0 1"/>
399
+ <limit effort="10" velocity="10" lower="-1.69" upper="1.69"/>
400
+ </joint>
401
+
402
+ <transmission name="elbow_flex_trans">
403
+ <type>transmission_interface/SimpleTransmission</type>
404
+ <joint name="elbow_flex">
405
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
406
+ </joint>
407
+ <actuator name="motor3">
408
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
409
+ <mechanicalReduction>1</mechanicalReduction>
410
+ </actuator>
411
+ </transmission>
412
+
413
+ <!-- Joint from shoulder to upper_arm -->
414
+ <joint name="shoulder_lift" type="revolute">
415
+ <origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 -1.5708 0"/>
416
+ <parent link="shoulder_link"/>
417
+ <child link="upper_arm_link"/>
418
+ <axis xyz="0 0 1"/>
419
+ <limit effort="10" velocity="10" lower="-1.74533" upper="1.74533"/>
420
+ </joint>
421
+
422
+ <transmission name="shoulder_lift_trans">
423
+ <type>transmission_interface/SimpleTransmission</type>
424
+ <joint name="shoulder_lift">
425
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
426
+ </joint>
427
+ <actuator name="motor2">
428
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
429
+ <mechanicalReduction>1</mechanicalReduction>
430
+ </actuator>
431
+ </transmission>
432
+
433
+ <!-- Joint from base to shoulder -->
434
+ <joint name="shoulder_pan" type="revolute">
435
+ <origin xyz="0.0388353 -8.97657e-09 0.0624" rpy="3.14159 4.18253e-17 -3.14159"/>
436
+ <parent link="base_link"/>
437
+ <child link="shoulder_link"/>
438
+ <axis xyz="0 0 1"/>
439
+ <limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
440
+ </joint>
441
+
442
+ <transmission name="shoulder_pan_trans">
443
+ <type>transmission_interface/SimpleTransmission</type>
444
+ <joint name="shoulder_pan">
445
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
446
+ </joint>
447
+ <actuator name="motor1">
448
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
449
+ <mechanicalReduction>1</mechanicalReduction>
450
+ </actuator>
451
+ </transmission>
452
+
453
+ </robot>
rl_deploy/table_calib.json ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "H": [
3
+ [
4
+ -0.00020641936356017595,
5
+ 0.001605983436679173,
6
+ -0.17743518923975418
7
+ ],
8
+ [
9
+ 0.0009714184562970813,
10
+ -0.0002591502281196994,
11
+ -0.5384275139337167
12
+ ],
13
+ [
14
+ 5.463862597621938e-05,
15
+ 0.0019026390841230777,
16
+ 1.0
17
+ ]
18
+ ],
19
+ "image_size": [
20
+ 640,
21
+ 360
22
+ ],
23
+ "points": [
24
+ {
25
+ "uv": [
26
+ 283.3632596685083,
27
+ 232.50552486187846
28
+ ],
29
+ "area": 724,
30
+ "joints_deg": [
31
+ -22.681318681318682,
32
+ -1.2307692307692308,
33
+ 41.18681318681319,
34
+ 39.07692307692308,
35
+ -2.5054945054945055,
36
+ 1.1026878015161956
37
+ ],
38
+ "tip": [
39
+ 0.0885746699621676,
40
+ -0.2211262158968657,
41
+ -0.0018728227502649436
42
+ ]
43
+ },
44
+ {
45
+ "uv": [
46
+ 223.27258566978193,
47
+ 174.84112149532712
48
+ ],
49
+ "area": 642,
50
+ "joints_deg": [
51
+ -6.065934065934066,
52
+ 13.098901098901099,
53
+ 21.846153846153847,
54
+ 39.42857142857143,
55
+ -2.4175824175824174,
56
+ 1.1026878015161956
57
+ ],
58
+ "tip": [
59
+ 0.04521679450712202,
60
+ -0.28363020045294185,
61
+ 0.00034617923123554314
62
+ ]
63
+ },
64
+ {
65
+ "uv": [
66
+ 324.1647940074906,
67
+ 146.14606741573033
68
+ ],
69
+ "area": 534,
70
+ "joints_deg": [
71
+ 10.54945054945055,
72
+ -5.450549450549451,
73
+ 41.714285714285715,
74
+ 58.59340659340659,
75
+ -2.32967032967033,
76
+ 1.1026878015161956
77
+ ],
78
+ "tip": [
79
+ -0.009996185007877991,
80
+ -0.19178363754922642,
81
+ 0.006818864733087288
82
+ ]
83
+ },
84
+ {
85
+ "uv": [
86
+ 323.44,
87
+ 294.63470588235293
88
+ ],
89
+ "area": 1700,
90
+ "joints_deg": [
91
+ -41.67032967032967,
92
+ 7.824175824175824,
93
+ 25.45054945054945,
94
+ 52.879120879120876,
95
+ -2.5934065934065935,
96
+ 1.1026878015161956
97
+ ],
98
+ "tip": [
99
+ 0.14299164473120482,
100
+ -0.19023792065397205,
101
+ 0.003980910785919246
102
+ ]
103
+ },
104
+ {
105
+ "uv": [
106
+ 446.7759784075574,
107
+ 253.0425101214575
108
+ ],
109
+ "area": 1482,
110
+ "joints_deg": [
111
+ -52.30769230769231,
112
+ -23.64835164835165,
113
+ 56.30769230769231,
114
+ 63.51648351648352,
115
+ 84.26373626373626,
116
+ 1.1026878015161956
117
+ ],
118
+ "tip": [
119
+ 0.09268488377845423,
120
+ -0.1230287165562727,
121
+ 0.013203728373691447
122
+ ]
123
+ },
124
+ {
125
+ "uv": [
126
+ 177.62869198312237,
127
+ 300.67148884870403
128
+ ],
129
+ "area": 1659,
130
+ "joints_deg": [
131
+ -32.879120879120876,
132
+ 30.24175824175824,
133
+ -1.2747252747252746,
134
+ 42.15384615384615,
135
+ -17.53846153846154,
136
+ 1.1716057891109581
137
+ ],
138
+ "tip": [
139
+ 0.17211300438132343,
140
+ -0.2762982688066562,
141
+ -0.005484126509456877
142
+ ]
143
+ },
144
+ {
145
+ "uv": [
146
+ 203.57355679702047,
147
+ 143.64245810055866
148
+ ],
149
+ "area": 537,
150
+ "joints_deg": [
151
+ 3.6043956043956045,
152
+ 25.406593406593405,
153
+ 5.934065934065934,
154
+ 46.81318681318681,
155
+ -16.923076923076923,
156
+ 1.1716057891109581
157
+ ],
158
+ "tip": [
159
+ 0.00809345599756148,
160
+ -0.30200666202286097,
161
+ -0.007828562295707622
162
+ ]
163
+ },
164
+ {
165
+ "uv": [
166
+ 326.9448818897638,
167
+ 185.59186351706037
168
+ ],
169
+ "area": 762,
170
+ "joints_deg": [
171
+ -11.516483516483516,
172
+ -11.516483516483516,
173
+ 47.07692307692308,
174
+ 57.45054945054945,
175
+ -17.36263736263736,
176
+ 1.1026878015161956
177
+ ],
178
+ "tip": [
179
+ 0.0432111876213237,
180
+ -0.18203129452029243,
181
+ 0.008625103296823854
182
+ ]
183
+ }
184
+ ],
185
+ "err_mm": [
186
+ 5.768327483946577,
187
+ 11.168090471611537,
188
+ 10.273185868475645,
189
+ 2.103149561433357,
190
+ 10.31306901446774,
191
+ 4.800515995559936,
192
+ 7.8180204156784345,
193
+ 14.80895048151171
194
+ ],
195
+ "loo_mm": [
196
+ 10.140745687901573,
197
+ 14.396130379327072,
198
+ 23.67257095374759,
199
+ 4.647962466997839,
200
+ 50.30546068867357,
201
+ 21.273626361652976,
202
+ 56.748608120772445,
203
+ 17.030096025168582
204
+ ]
205
+ }