Compare commits

...

341 Commits

Author SHA1 Message Date
Pepijn ac1c2454c5 Small fixes 2025-07-28 09:13:01 +02:00
Pepijn 2e3c116fad add urdfs 2025-07-28 08:52:37 +02:00
Pepijn 65c174e9f8 fix record and replay and startup issue 2025-07-22 14:07:20 +02:00
Pepijn 005d4bb011 Modify replay 2025-07-18 16:38:09 +02:00
Pepijn 779d38fff0 hack to get images "at" 100fps 2025-07-18 16:33:38 +02:00
Pepijn c0ffb92735 Update record 2025-07-17 09:56:31 +02:00
Pepijn baa9b95b97 add acc, vel to dataset 2025-07-17 09:56:23 +02:00
Pepijn 75ce54e212 remove settings add record 2025-07-16 16:06:53 +02:00
Pepijn 05a2316a63 modify gains 2025-07-16 14:26:29 +02:00
Pepijn 2437decd3f Cleanup unneeded code 2025-07-16 10:40:58 +02:00
Pepijn 2d2f5d3d60 remove set_motors 2025-07-15 14:08:41 +02:00
Pepijn 2d608f086a Merge branch 'main' into feat/add-biteleop-so101 2025-07-15 14:03:15 +02:00
Pepijn e925ef3f18 tune 2025-07-11 13:39:54 +02:00
Pepijn fbf5f04545 Add vel filter and better static friction parameters 2025-07-11 13:34:28 +02:00
Pepijn 9fdec23cee uncomment handshake (issue on this model) 2025-07-11 10:41:22 +02:00
Pepijn d9af2f1b89 set direction bit 2025-07-11 10:17:55 +02:00
Pepijn 57f7c8b03e Use multi turn, single turn is problem! 2025-07-10 19:31:14 +02:00
Pepijn e9c795e479 remove set phase 2025-07-10 12:27:43 +02:00
Pepijn c9cff132c3 Add better hls table 2025-07-10 10:56:47 +02:00
Pepijn 0136912fa4 Print more 2025-07-09 19:21:44 +02:00
Pepijn 67d6bfee78 increase protection current 2025-07-08 15:51:15 +02:00
Pepijn a3feadbbfb Increase max torque limit 2025-07-08 15:26:13 +02:00
Pepijn 25e22ea3ba Add friction to distribiutor estimation 2025-07-08 15:08:35 +02:00
Pepijn 5e27248bba Tune everything a bit 2025-07-08 14:55:34 +02:00
Pepijn 7f7b45cfbb Add rerun vis 2025-07-04 17:33:25 +02:00
Pepijn 28857dccb1 Add friction component (helps!) but not right one yet 2025-07-04 14:48:31 +02:00
Pepijn a4d46d4adb modify gains 2025-07-04 14:36:37 +02:00
Pepijn 043b720505 Add inertia 2025-07-04 14:25:14 +02:00
Pepijn d985f4b1db fix: current impl 2025-07-04 13:20:27 +02:00
Pepijn ab53de989a fix: current 2025-07-04 09:21:53 +02:00
Pepijn a56cf87f42 fix gravity compensation 2025-07-02 15:16:58 +02:00
Pepijn 12d1629aae Subtract middle 2025-07-01 18:09:36 +02:00
Pepijn 63e2a2e129 fix: change to actual degrees 2025-07-01 16:32:43 +02:00
Pepijn 2a46f3a53f Merge branch 'main' into feat/add-biteleop-so101 2025-07-01 14:59:26 +02:00
Pepijn 171c355858 Add grav compensation 2025-07-01 14:56:37 +02:00
Pepijn 9ad19d4e81 Add pseudo code for bi teleoperation (4channel) 2025-06-26 18:28:25 +02:00
Pepijn e171fa788a First bi teleop so101 2025-06-12 09:53:30 +02:00
Simon Alibert b1386fd79e Disconnect after scan_port 2025-06-04 17:12:30 +02:00
Simon Alibert b47620cd59 Remove comment 2025-06-04 16:59:44 +02:00
Simon Alibert a32d988536 Refactor feetech _broadcast_ping 2025-06-04 16:41:33 +02:00
Simon Alibert 9571a713df Refactor record_ranges_of_motion 2025-06-04 14:54:29 +02:00
Pepijn b418409b24 Fix small issues in docs and refactor (#1194)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-04 14:27:57 +02:00
Simon Alibert 0a6b3992ee Fix docstring 2025-06-04 13:16:41 +02:00
pre-commit-ci[bot] e6d19116c4 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-06-04 11:14:42 +00:00
Simon Alibert 92ea7fc0fb Apply suggestions from code review
Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
2025-06-04 13:13:50 +02:00
Simon Alibert 46cd157c55 Dirty fix nightlies 2025-06-04 12:54:09 +02:00
Simon Alibert 52028f5201 Address Michel's comments 2025-06-04 12:47:24 +02:00
Simon Alibert f5b1ef0045 Remove unused variable 2025-06-04 12:18:54 +02:00
Simon Alibert 81a4deadc3 Address potential None in _assert_same_firmware 2025-06-04 12:17:18 +02:00
Simon Alibert fef83ce349 Simplify feetech read_calibration 2025-06-04 12:09:48 +02:00
Simon Alibert eb3986e131 Fix docstring 2025-06-04 11:49:02 +02:00
Simon Alibert d45226ad06 Remove unused max id 2025-06-04 11:46:10 +02:00
Simon Alibert fe43f93553 Remove more code 2025-06-04 11:39:19 +02:00
Simon Alibert 40e0a311b5 Remove deprecated code 2025-06-04 11:33:33 +02:00
Simon Alibert 13677cb720 Remove os.name in favor of platform.system() 2025-06-04 11:21:33 +02:00
Simon Alibert 247d493d06 Add TODO 2025-06-03 19:53:25 +02:00
Simon Alibert 2f00475fc6 Fix snippet error 2025-06-03 19:34:06 +02:00
Simon Alibert 4687296d93 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-06-03 19:10:17 +02:00
Simon Alibert 5c2f8ccd14 Remove dead code & cleanup 2025-06-03 18:30:51 +02:00
Simon Alibert d25e3bd989 Apply suggestions from code review
Co-authored-by: Adil Zouitine <adilzouitinegm@gmail.com>
2025-06-03 18:18:44 +02:00
Simon Alibert adcb07bf62 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-06-02 19:41:50 +02:00
Pepijn 67e3383ffc Refactor LeKiwi (#1136)
Co-authored-by: Simon Alibert <simon.alibert@huggingface.co>
Co-authored-by: Steven Palma <steven.palma@huggingface.co>
Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-02 19:40:48 +02:00
Pepijn ac5a9b90c7 Update the docs for the robots refactor (#1115)
Co-authored-by: Simon Alibert <simon.alibert@huggingface.co>
Co-authored-by: Steven Palma <steven.palma@huggingface.co>
Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-02 18:14:21 +02:00
Simon Alibert f35d24a9c3 Cleanup control_utils 2025-06-02 17:09:08 +02:00
Steven Palma fbdefb2e3e fix: several fixes identified in the docs PR (#1181)
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
2025-06-02 16:05:05 +02:00
Simon Alibert 0e39d0f6e6 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-06-02 13:19:26 +02:00
Simon Alibert b8eecba63d Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-28 15:39:30 +02:00
Steven Palma 7308aa57a2 fix(scripts): reconstructs action dict from policy output (#1162)
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
2025-05-28 15:36:21 +02:00
Steven Palma 1fd3b2e2db fix(utils): Convert observation values in predict_action to torch.Tensor (#1157)
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
2025-05-28 15:29:08 +02:00
Simon Alibert 69e48bbe19 Merge branch 'user/aliberts/2025_02_25_refactor_robots' of github.com:huggingface/lerobot into user/aliberts/2025_02_25_refactor_robots 2025-05-28 15:08:48 +02:00
Steven Palma 0db1a67eaf fix(dataset): key is an action if it starts with such prefix in dataset_to_policy_features (#1156) 2025-05-28 15:08:10 +02:00
Simon Alibert ccb8468e9b Complete TODO for cameras on robots 'is_connected' 2025-05-28 10:15:19 +02:00
Simon Alibert f6198d20c6 Add suggestion from Caroline 2025-05-26 17:57:51 +02:00
Simon Alibert 78e29f4f20 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-26 10:54:07 +02:00
Steven Palma fb4bfaf029 fix(scripts): parser instead of draccus in record + add __get_path_fields__() to RecordConfig (#1155) 2025-05-26 10:51:05 +02:00
Steven Palma 809a9c6de0 fix(cameras): update docstring + handle sn when starts with 0 + update timeouts to more reasonable value (#1154) 2025-05-26 10:48:42 +02:00
Simon Alibert f4c11593d4 Fix predict_action from record 2025-05-24 10:48:06 +02:00
Steven Palma 71e6520cd1 refactor(cameras): cameras implementations + tests improvements (#1108)
Co-authored-by: Simon Alibert <simon.alibert@huggingface.co>
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-23 14:47:37 +02:00
Simon Alibert a5f15db057 Add changes from #1117 2025-05-23 13:16:14 +02:00
Simon Alibert edec51988d Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-23 13:13:37 +02:00
Simon Alibert ddca6765b8 Fix feetech test_is_calibrated 2025-05-23 11:46:26 +02:00
Simon Alibert cedaa83bce Update feetech read_calibration 2025-05-22 17:59:54 +02:00
Simon Alibert 4bb965c283 Better MotorsBus error messages 2025-05-22 17:59:27 +02:00
Simon Alibert 4feaef3436 Adapt feetech calibration 2025-05-22 16:02:55 +02:00
Simon Alibert e9aac40ba8 nit 2025-05-22 11:34:16 +02:00
Simon Alibert 386ad61007 Fix normalization drive_mode 2025-05-22 11:32:52 +02:00
Simon Alibert cac4289619 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-21 20:19:33 +02:00
Simon Alibert 0bda18eab5 Move make_robot_config 2025-05-21 20:18:47 +02:00
Simon Alibert 8ab2227148 Replace deprecated abc.abstractproperty 2025-05-20 13:16:34 +02:00
Simon Alibert 9dab08dfbc Remove old .cache folder 2025-05-20 09:53:01 +02:00
Simon Alibert 05dfa26c54 Fix test 2025-05-19 11:24:10 +02:00
Simon Alibert edbba48e81 Add so100_follower tests 2025-05-19 10:58:35 +02:00
Simon Alibert 10119c1a59 Move MockMotorsBus 2025-05-18 11:51:47 +02:00
Simon Alibert c7ef189da0 Add check for same min and max during calibration 2025-05-16 10:48:45 +02:00
Simon Alibert 51efe6dfee Add setup_motors for lekiwi 2025-05-15 11:46:41 +02:00
Simon Alibert b0592d9bc8 Fix dxl _find_single_motor 2025-05-14 13:43:36 +02:00
Simon Alibert 363fe64ff9 Add copyrights 2025-05-13 17:38:39 +02:00
Simon Alibert bbcb12e919 Fix test_calibrate 2025-05-13 17:19:40 +02:00
Simon Alibert 3e87b09d34 Fix setup_motors & calibrate configs 2025-05-13 17:06:24 +02:00
Simon Alibert 81de27dc9a Remove Moss arm 2025-05-13 16:30:50 +02:00
Simon Alibert eb94a5f03f Rename arm -> bus 2025-05-13 13:26:04 +02:00
Simon Alibert 742708942c Add MotorsBus docstrings 2025-05-13 13:24:46 +02:00
Simon Alibert 5a2f9b6589 Remove unecessary id 2025-05-12 19:01:30 +02:00
Simon Alibert 06f0c579b7 Rename example 7 2025-05-12 18:56:22 +02:00
Simon Alibert 7890767d34 Remove pynput from optional deps 2025-05-12 18:54:08 +02:00
Simon Alibert d322cb0220 Add SO101 2025-05-11 13:15:28 +02:00
Simon Alibert f011173ff6 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-11 12:53:04 +02:00
Simon Alibert 20129cd4c2 Fix tests (no-extras install) 2025-05-11 12:52:17 +02:00
Simon Alibert 307823bc8d Add docstrings 2025-05-11 12:45:22 +02:00
Simon Alibert 64303781c2 Add calibrate 2025-05-08 18:27:19 +02:00
Simon Alibert dd3e305164 Remove deprecated scripts & tests 2025-05-08 18:08:38 +02:00
Simon Alibert cb9cac6a1b Add test_record_and_resume 2025-05-08 17:54:58 +02:00
Simon Alibert 95f9b45418 Add new test_control_robot 2025-05-08 17:38:16 +02:00
Simon Alibert f9db727647 Add mock robot & teleop 2025-05-08 17:37:49 +02:00
Simon Alibert 230c7fdfab Fix test_datasets 2025-05-08 14:57:12 +02:00
Simon Alibert 320f7e8450 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-05-08 13:24:12 +02:00
Simon Alibert 08fbbb318f Add replay 2025-05-08 13:21:42 +02:00
Simon Alibert 8b98399206 Add record 2025-05-08 13:21:42 +02:00
Simon Alibert 237b14a6ec Add teleoperate 2025-05-08 13:21:42 +02:00
Simon Alibert 2e705ff554 Add setup_motors 2025-05-08 13:21:42 +02:00
Simon Alibert d72a3f9c32 Remove app script 2025-05-08 13:21:42 +02:00
Simon Alibert 73ac4f38b2 Fix config parsing 2025-05-08 13:21:18 +02:00
Simon Alibert a0e69dd708 Rename find_port 2025-05-08 13:21:18 +02:00
Simon Alibert b207babd9e Add make_teleoperator_from_config 2025-05-08 13:21:18 +02:00
Simon Alibert 293870d0f6 Update teleop features & naming 2025-05-08 13:21:17 +02:00
Simon Alibert 87a8cb6d89 Update robot features & naming 2025-05-08 13:20:32 +02:00
Simon Alibert 69dc3f5c9c Remove deprecated manipulator 2025-05-08 13:17:16 +02:00
Steven Palma e4d4754600 fix(teleoperators): use property is_connected (#1075) 2025-05-07 10:52:44 +02:00
Steven Palma 2e528a8b12 refactor/lekiwi robot (#863)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Simon Alibert <simon.alibert@huggingface.co>
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
2025-04-29 17:48:41 +02:00
Simon Alibert b7a9b0689a Remove deprecated import 2025-04-18 17:13:08 +02:00
Simon Alibert b6b9635be6 Remove names 2025-04-18 09:48:16 +02:00
Simon Alibert 21b1026872 Remove deprecated dynamixel_calibration 2025-04-18 09:34:46 +02:00
Simon Alibert 8c3eab32b0 Remove deprecated configure_motor 2025-04-18 09:19:43 +02:00
Simon Alibert 29633865c7 Fix _find_single_motor 2025-04-18 09:18:56 +02:00
Simon Alibert 702749b7d3 Fix setup_motor & add it to robots 2025-04-17 16:56:38 +02:00
Simon Alibert bf1c737858 Fix calibration msg display 2025-04-17 13:18:32 +02:00
Simon Alibert d07c7347f8 Add setup_motor 2025-04-17 13:14:06 +02:00
Simon Alibert 57e5e4cc07 Move read/write_calibration implementations 2025-04-16 11:23:33 +02:00
Simon Alibert 2743c29a96 Update feetech tables 2025-04-16 11:01:12 +02:00
Simon Alibert 2bb73ac431 Add torque_disabled context 2025-04-15 11:43:22 +02:00
Simon Alibert 9afc4b771c Motors config & disconnect fixes 2025-04-15 11:20:42 +02:00
Simon Alibert f71e224023 Fix tests 2025-04-15 11:18:44 +02:00
Simon Alibert 889de7c415 Add handshake, fix feetech _read_firmware_version 2025-04-14 17:14:06 +02:00
Simon Alibert 3539251b18 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-04-14 15:30:35 +02:00
Simon Alibert 1f210bc8a3 Refactor tests 2025-04-14 15:26:29 +02:00
Simon Alibert d70bc4bde9 Add more segmented tests (dynamixel) 2025-04-14 15:16:38 +02:00
Simon Alibert bdbca09cb2 Add more segmented tests (base motor bus & feetech), add feetech protocol 1 support 2025-04-14 11:56:53 +02:00
Simon Alibert e0b292ab51 Remove test_motors_bus fixtures 2025-04-11 12:24:30 +02:00
Simon Alibert f960f4d8d4 Fix unormalize 2025-04-11 11:58:31 +02:00
Simon Alibert 9e57ec7837 Add support for feetech protocol 1 to _split_into_byte_chunks 2025-04-11 11:58:09 +02:00
Simon Alibert 0a7f51f0da Cleanup 2025-04-11 11:03:09 +02:00
Simon Alibert 4ca92a28e9 Make feetech broadcast ping faster in protocol 1 2025-04-11 11:02:54 +02:00
Simon Alibert 0464dc91b3 Add feetech sm8512bl 2025-04-11 11:02:01 +02:00
Simon Alibert d32daebf75 Refactor & add _serialize_data 2025-04-11 11:01:12 +02:00
Simon Alibert 27cb0c40bd Add protocol 1 broadcast ping 2025-04-10 17:14:40 +02:00
Simon Alibert 12abc9ca86 Fix broadcast ping type hint 2025-04-10 00:53:17 +02:00
Simon Alibert 4005065223 (nit) move write 2025-04-10 00:51:23 +02:00
Simon Alibert 443fed216c Use constants from sdks 2025-04-10 00:49:03 +02:00
Simon Alibert 42a87e7211 Implement read 2025-04-10 00:35:14 +02:00
Simon Alibert 034171a89a Add Feetech protocol version 2025-04-09 10:26:30 +02:00
pre-commit-ci[bot] 782dff1163 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-04-08 08:48:18 +00:00
Simon Alibert 8924ccbbab Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-04-08 10:47:40 +02:00
Simon Alibert 792c3d961d Update dynamixel with motors bus & tables changes 2025-04-08 10:47:11 +02:00
Simon Alibert e998dddcfa Add support for feetech scs series + various fixes 2025-04-08 10:46:29 +02:00
Steven Palma 99c0938b42 feat(teleop): thread-safe keyboard teleop implementation (#869)
Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
2025-04-04 09:45:18 +02:00
Simon Alibert 716029b1e3 Remove old calibration 2025-04-03 18:42:39 +02:00
Simon Alibert 3848a8f9aa Rename viperx & widowx 2025-04-03 18:37:21 +02:00
Simon Alibert f7672e14c7 Update viperx & widowx 2025-04-03 18:34:08 +02:00
Simon Alibert e393af2d88 Add is_calibrated test 2025-04-03 17:35:10 +02:00
Simon Alibert 0dcb2caba8 Simplify motors mocks 2025-04-03 16:43:23 +02:00
Simon Alibert 4679725957 Revert feetech hack and monkeypatch instead 2025-04-03 15:53:54 +02:00
Simon Alibert 57319062aa Remove old calibration tests 2025-04-03 12:17:43 +02:00
Simon Alibert 078f59bfd1 Add calibration tests 2025-04-03 12:14:15 +02:00
Simon Alibert 36fcea2002 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-04-03 08:40:39 +02:00
Simon Alibert 2971bdfed5 Rename Koch classes 2025-04-03 08:23:31 +02:00
Simon Alibert 28cd3a6f3a Rename SO-100 classes 2025-04-03 08:14:35 +02:00
Simon Alibert c0570b3003 Improve format 2025-04-02 22:40:00 +02:00
Simon Alibert eeb8490016 Update Koch & SO-100 2025-04-02 22:33:48 +02:00
Simon Alibert 854b78975a Update tests 2025-04-02 22:31:53 +02:00
Simon Alibert e55d2ffe50 Hack feetech firmware bug 2025-04-02 22:31:45 +02:00
Simon Alibert 1ebd81552c Fix calibration 2025-04-02 22:27:49 +02:00
Simon Alibert 65569ba90e Add test_scan_port (TODO) 2025-03-31 18:40:23 +02:00
Simon Alibert 79293800f1 Implement Koch calibration 2025-03-31 18:18:27 +02:00
Simon Alibert bc765f9e95 Implement SO-100 follower calibration 2025-03-31 18:17:20 +02:00
Simon Alibert 201311503f Implement SO-100 leader calibration 2025-03-31 18:16:42 +02:00
Simon Alibert 8cc0232e73 Format baudrate tables 2025-03-31 18:14:57 +02:00
Simon Alibert 6bfcc18e73 Add more calibration utilities 2025-03-31 18:14:11 +02:00
Simon Alibert e096754d14 Rename test 2025-03-31 00:41:25 +02:00
Simon Alibert 02803f545d Add test_encoding_utils 2025-03-31 00:37:28 +02:00
Simon Alibert 8503e8e166 Move encoding functions to encoding_utils 2025-03-31 00:35:31 +02:00
Simon Alibert d6007c6e7d Add calibration utilities 2025-03-30 15:41:39 +02:00
Simon Alibert 50963fcf13 Add scan_port utility 2025-03-30 15:32:25 +02:00
Simon Alibert 051a52a4ce Remove todo 2025-03-25 21:32:30 +01:00
Simon Alibert 2292b514aa Fix calibration functions 2025-03-25 17:58:54 +01:00
Simon Alibert 1f1a01a798 Rename CalibrationMode -> MotorNormMode 2025-03-25 17:42:18 +01:00
Simon Alibert faa476f0d2 Remove deprecated scripts 2025-03-25 17:33:05 +01:00
Simon Alibert 5130b69ece Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-25 16:25:47 +01:00
Simon Alibert aed85241b7 Merge branch 'user/aliberts/2025_02_25_refactor_robots' of github.com:huggingface/lerobot into user/aliberts/2025_02_25_refactor_robots 2025-03-25 16:24:40 +01:00
Pepijn 21c3ac42ee Add new calibration method for robot refactor (#896)
Co-authored-by: Simon Alibert <simon.alibert@huggingface.co>
2025-03-25 16:24:04 +01:00
Simon Alibert 2d3a5fb2be (WIP) _async_read 2025-03-25 15:37:18 +01:00
Simon Alibert a631e4c11c Rename idx -> id_ 2025-03-25 15:36:36 +01:00
Simon Alibert 222d6f104e Rename idx -> id_ 2025-03-25 14:20:12 +01:00
Simon Alibert 7a3b424cd3 Add calibration 2025-03-25 14:13:55 +01:00
Simon Alibert af295fadb5 Fix imports 2025-03-25 12:48:58 +01:00
Simon Alibert 9644e2b086 Fix visualize_motors_bus 2025-03-25 12:47:44 +01:00
Simon Alibert 6ccf083127 Update so100 imports 2025-03-25 12:32:38 +01:00
Simon Alibert bb774e7acd Update Koch imports 2025-03-25 12:31:51 +01:00
Simon Alibert dcbbeab80b Move DriveMode & TorqueMode 2025-03-25 12:30:07 +01:00
Simon Alibert b71ac34214 Add test_motors_bus 2025-03-25 12:11:56 +01:00
Simon Alibert c237d1379e Update tests 2025-03-25 11:12:52 +01:00
Simon Alibert cf963eb1b0 Ensure motors exist at connection time 2025-03-25 11:12:26 +01:00
Simon Alibert 4293b6a4fb Fix feetech ping tests 2025-03-25 07:26:34 +01:00
Simon Alibert 7a75bb9f61 Improve errors 2025-03-24 21:13:26 +01:00
Simon Alibert 0c1d4cb323 Rename idx -> id_ 2025-03-24 20:58:56 +01:00
Simon Alibert c6212d585d Add raw_values option 2025-03-24 20:56:58 +01:00
Simon Alibert 7c8ab8e2d6 Implement feetech broadcast ping 2025-03-24 20:46:36 +01:00
Simon Alibert 1de75c46c0 Return models (str) with pings 2025-03-24 20:42:43 +01:00
Simon Alibert 4ad109cff8 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-24 13:25:29 +01:00
Simon Alibert 8994252019 Add _configure_motors & move ping methods 2025-03-24 12:19:03 +01:00
Simon Alibert 9832daf08d Fix dict 2025-03-24 12:16:54 +01:00
Simon Alibert 39d8f45810 Privatize methods & renames 2025-03-24 11:57:12 +01:00
Simon Alibert 30fcd3d417 Update so100 2025-03-23 20:15:47 +01:00
Simon Alibert 039b437ef0 Update ensure_safe_goal_position 2025-03-23 19:43:58 +01:00
Simon Alibert 7582a0a2b0 Caps dxl OperatingMode 2025-03-23 19:42:21 +01:00
Simon Alibert 25388d0947 Add feetech operating modes 2025-03-23 19:41:46 +01:00
Simon Alibert 7152bc8aa7 Update Koch 2025-03-23 19:32:26 +01:00
Simon Alibert 5b46dc0b6a Add is_connected in robots and teleops 2025-03-23 19:26:10 +01:00
Simon Alibert 4273f1f384 Add dxl operating modes 2025-03-23 19:25:21 +01:00
Simon Alibert 97194bf7f3 Nit 2025-03-23 17:05:08 +01:00
Simon Alibert 0ac026b521 Remove test skips & fix docstrings 2025-03-23 17:04:30 +01:00
Simon Alibert ff7cfdaf40 Move mock_serial patch to dedicated file 2025-03-23 17:03:04 +01:00
Simon Alibert 57c97762e1 Simplify _is_comm_success & _is_error 2025-03-23 16:52:29 +01:00
Simon Alibert a38bb15e79 Add feetech write test 2025-03-23 16:48:32 +01:00
Simon Alibert 3ceaee999d Refactor feetech tests by functionality 2025-03-23 16:25:12 +01:00
Simon Alibert d485dc1313 Refactor _is_comm_success 2025-03-23 16:15:53 +01:00
Simon Alibert 329d103453 Add dxl write test 2025-03-23 16:12:24 +01:00
Simon Alibert 9f46a3d8f9 Refactor dxl tests by functionality 2025-03-23 16:11:24 +01:00
Simon Alibert c9ca9e4316 Rename tests 2025-03-23 13:32:08 +01:00
Simon Alibert 5a57e6f4a7 Rename read/write -> sync_read/write, refactor, add write 2025-03-23 13:25:45 +01:00
Simon Alibert a2f5c34625 Simplify split_int_bytes 2025-03-23 11:55:39 +01:00
Simon Alibert 1f1e1bcfe8 Add Motor in dxl robots 2025-03-23 11:08:20 +01:00
Simon Alibert e047074825 Add CalibrationMode 2025-03-23 10:20:08 +01:00
Simon Alibert c2e761437d Assert ping stub called 2025-03-22 18:53:57 +01:00
Simon Alibert fedac994c3 Add autoclosing fixture 2025-03-22 18:16:13 +01:00
Simon Alibert 7d558d058e Nit 2025-03-22 17:03:18 +01:00
Simon Alibert 1d3e1cbdbd Add feetech write tests 2025-03-22 17:02:01 +01:00
Simon Alibert 0ccc957d5c Fix imports 2025-03-22 16:58:41 +01:00
Simon Alibert a4d487bc1d Remove comment 2025-03-22 16:52:42 +01:00
Simon Alibert 8ca03a7255 Add dxl write tests 2025-03-22 14:50:05 +01:00
Simon Alibert f2ed2bfb2f Improve logging & typing 2025-03-22 11:11:39 +01:00
Simon Alibert 40675ec76c Add logger, rm logs 2025-03-22 10:33:42 +01:00
Simon Alibert 9e34c1d731 Move feetech table & cleanup 2025-03-22 01:24:48 +01:00
Simon Alibert 857f335be9 Improve feetech mocking 2025-03-22 01:19:51 +01:00
Simon Alibert fc4a95f187 Add CRC docstring 2025-03-22 00:50:01 +01:00
Simon Alibert 4fe1880887 Add ping testing 2025-03-22 00:40:22 +01:00
Simon Alibert 6fa859fa19 Improve dynamixel mocking 2025-03-22 00:39:41 +01:00
Simon Alibert 2abfa5838d Improve read ergonomics & typing, rm find_motor_indices 2025-03-22 00:34:07 +01:00
Simon Alibert 3d119c0ccb Add single value write 2025-03-21 12:31:41 +01:00
Simon Alibert a32081757d Add Motor class 2025-03-21 12:13:44 +01:00
Simon Alibert 56c04ffc53 Move dxl table & cleanup 2025-03-21 11:28:11 +01:00
Simon Alibert 715d4557af Ruff ignore F401 & F403 for init files 2025-03-21 11:22:02 +01:00
Simon Alibert 6541982dff Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-20 14:48:19 +01:00
Simon Alibert 43bc9404bb Remove motors from koch teleop config 2025-03-20 14:47:53 +01:00
Simon Alibert 375499c323 Remove set_operating_mode 2025-03-20 14:47:17 +01:00
Simon Alibert 17a4447cef Add debugging init 2025-03-20 14:45:18 +01:00
Simon Alibert 287dc13d96 Remove CLI for calibration visualization + move to debugging 2025-03-20 14:44:23 +01:00
Simon Alibert 02a1cf6a4e Fix calibration visualization 2025-03-20 14:33:36 +01:00
Simon Alibert 34cd1e47bf Remove obsolete test 2025-03-20 14:07:55 +01:00
Simon Alibert 74d56834af Fix dxl calib import 2025-03-20 14:03:11 +01:00
Simon Alibert dd80dbb4cd Simplify Dxl motors bus import 2025-03-20 14:01:34 +01:00
Simon Alibert bc020ee0a4 Remove mock_feetech sdk & add feetech new tests 2025-03-20 14:00:10 +01:00
Simon Alibert a15767aff1 Fix feetech reader/writer 2025-03-20 13:59:00 +01:00
Simon Alibert 9af0a9bf37 Add mock_feetech 2025-03-20 13:58:02 +01:00
Simon Alibert e2c8bc6948 Fix packet length, remove bytearray for easier debug, improve doctrings 2025-03-20 13:57:15 +01:00
Simon Alibert 2c68c6ca40 Implement FeetechMotorsBus & move functions to calibration 2025-03-20 10:22:47 +01:00
Simon Alibert dd1f33e5ed Add pytest param ids 2025-03-20 09:44:47 +01:00
Simon Alibert 2c1bb766ff Refactor MockMotors, add return values 2025-03-20 09:40:58 +01:00
Simon Alibert c1c71fb994 Ignore patching when not on MacOS 2025-03-20 09:38:36 +01:00
Simon Alibert 2d56f35071 Improve formats & docstrings 2025-03-20 09:36:17 +01:00
Simon Alibert 64ce2669ca Add bytes stuffing 2025-03-20 09:33:33 +01:00
Simon Alibert f527adf7a9 Add mock-serial 2025-03-19 19:03:34 +01:00
Simon Alibert 6a77189f50 Fix import 2025-03-19 19:02:58 +01:00
Simon Alibert e4a6d035f9 Remove Dxl mock sdk & update tests 2025-03-19 19:02:25 +01:00
Simon Alibert 794f6e00fc Introduce Dxl packet mocking logic 2025-03-19 18:57:29 +01:00
Simon Alibert 97494c6a39 (WIP) Implement Dynamixel 2025-03-19 18:46:04 +01:00
Simon Alibert 9358d334c7 Rewrite MotorsBus 2025-03-19 18:44:05 +01:00
Simon Alibert c85a9253e7 Move imports 2025-03-15 23:43:26 +01:00
Simon Alibert 8d659a6aa9 Update mock SDKs 2025-03-15 22:26:47 +01:00
Simon Alibert f6a2396484 Move test_configure_motors_all_ids_1 2025-03-15 22:19:50 +01:00
Simon Alibert 7a7af82e35 Nit docstring 2025-03-15 21:53:42 +01:00
Simon Alibert 7f23972f3f Add Feetech & Dxl basic tests 2025-03-15 21:45:05 +01:00
Simon Alibert 3362b665e6 Move test files 2025-03-15 21:44:01 +01:00
Simon Alibert eeeccdba53 Update docstrings 2025-03-15 21:42:54 +01:00
Simon Alibert bd5b181dfd Improve type hints 2025-03-15 21:33:45 +01:00
Simon Alibert 858678786a Remove unused functions 2025-03-15 19:22:40 +01:00
Simon Alibert 0f972661e1 Move imports & remove mock entirely 2025-03-15 19:22:12 +01:00
Simon Alibert 2e9b144c56 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-15 13:15:28 +01:00
Simon Alibert fa8ba9e4e2 Rename set_operating_mode arg 2025-03-15 13:14:29 +01:00
Simon Alibert 2037cc0219 Rename ID -> id 2025-03-15 13:14:05 +01:00
Simon Alibert 5006da72ff Update configure_motor script 2025-03-15 13:13:26 +01:00
Simon Alibert ad0bacbfe4 Ass model_baudrate_table 2025-03-15 13:11:56 +01:00
Simon Alibert e33ca2c980 Fix TorqueMode imports 2025-03-15 13:10:57 +01:00
Simon Alibert f0505e81cc Move common Feetech/Dxl code into MotorsBus base class 2025-03-14 22:00:09 +01:00
Simon Alibert 1f7ddc1d76 New Feetech calibration (#859)
Co-authored-by: Pepijn <pepijn@huggingface.co>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-14 11:31:23 +01:00
Simon Alibert ce63cfdb25 Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-13 14:24:50 +01:00
Simon Alibert d6f1359e69 Remove motors from Koch config 2025-03-12 17:16:09 +01:00
Simon Alibert 2357d4aceb Update base classes typing 2025-03-12 17:15:39 +01:00
Simon Alibert d6ccdc222c Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-03-10 18:39:48 +01:00
Simon Alibert 9bd0788131 Update paths 2025-03-10 18:34:01 +01:00
Simon Alibert 1ae62c28f7 Move lekiwi files 2025-03-10 18:33:28 +01:00
Simon Alibert baf6e66c3d Add init files 2025-03-10 18:29:58 +01:00
Simon Alibert a065bd61ae Add keyboard teleop 2025-03-10 18:28:50 +01:00
Simon Alibert 5dc3c74e64 Add WidowX 2025-03-06 21:31:35 +01:00
Simon Alibert 4214b01703 Add ViperX 2025-03-06 12:53:55 +01:00
Simon Alibert b974e5541f Update stretch teleop 2025-03-06 11:46:06 +01:00
Simon Alibert fd64dc84ae Move stretch3 teleop 2025-03-06 10:24:27 +01:00
Simon Alibert 06988b2135 WIP stretch 3 robot & teleop 2025-03-04 13:32:58 +01:00
Simon Alibert 7ed7570b17 WIP Add stretch 2025-03-04 11:42:07 +01:00
Simon Alibert e2d13ba7e4 Update paths 2025-03-04 11:38:31 +01:00
Simon Alibert f6c1049474 Update errors 2025-03-04 11:24:05 +01:00
Simon Alibert 2b24feb604 Update constants 2025-03-04 11:07:15 +01:00
Simon Alibert a13e49073c Add Moss Robot 2025-03-03 20:42:48 +01:00
Simon Alibert 2c7e0f17b6 Add SO-100 teleop 2025-03-03 20:31:04 +01:00
Simon Alibert 418866007e Fixes for Koch robot 2025-03-03 20:19:23 +01:00
Simon Alibert 5ab418dbeb Add feetech calibration 2025-03-03 20:17:54 +01:00
Simon Alibert 95f61ee9d4 Add SO-100 robot 2025-03-03 20:17:18 +01:00
Simon Alibert ac89c8d226 Add Koch teleop 2025-03-03 18:58:54 +01:00
Simon Alibert d75d904e43 Add teleoperator base class 2025-03-03 18:55:59 +01:00
Simon Alibert ea4d8d990c Add Koch robot 2025-03-03 18:53:45 +01:00
Simon Alibert c93cbb8311 Fix base robot class 2025-03-03 18:49:40 +01:00
Simon Alibert c0137e89b9 Add calibration dir 2025-03-03 18:44:39 +01:00
Simon Alibert 3111ba78ad Add errors 2025-03-03 18:44:15 +01:00
Simon Alibert 3d3a176940 Move & organize motors, add base class 2025-03-03 18:18:24 +01:00
Simon Alibert 212c6095a2 Move & organize cameras, add base class 2025-03-03 18:16:30 +01:00
Simon Alibert 48469ec674 Move motor files 2025-03-02 21:33:22 +01:00
Simon Alibert c7dfd32b43 Update DynamixelMotorsBus signature 2025-03-02 21:29:35 +01:00
Simon Alibert 731fb6ebaf Fix import 2025-02-26 19:02:15 +01:00
Simon Alibert 13e124302f Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-02-26 18:49:18 +01:00
Simon Alibert 59bdd29106 Move more files & objects around 2025-02-26 18:48:58 +01:00
Simon Alibert 124829104b Merge remote-tracking branch 'origin/main' into user/aliberts/2025_02_25_refactor_robots 2025-02-26 16:26:03 +01:00
Simon Alibert 21cd2940a9 Reorganize files 2025-02-26 16:22:07 +01:00
44 changed files with 2408 additions and 100 deletions
+7 -1
View File
@@ -116,7 +116,7 @@ packages = [
[tool.ruff]
line-length = 110
target-version = "py310"
exclude = ["tests/artifacts/**/*.safetensors", "*_pb2.py", "*_pb2_grpc.py"]
exclude = ["tests/artifacts/**/*.safetensors", "*_pb2.py", "*_pb2_grpc.py", "*.part", "*.stl"]
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "N", "B", "C4", "SIM"]
@@ -148,6 +148,12 @@ default.extend-ignore-identifiers-re = [
"ein",
]
[tool.typos.files]
extend-exclude = [
"*.stl",
"*.part",
]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
+175
View File
@@ -0,0 +1,175 @@
import math
import sys
import time
from lerobot.robots.so101_follower_torque.config_so101_follower_t import SO101FollowerTConfig
from lerobot.robots.so101_follower_torque.so101_follower_t import SO101FollowerT
from lerobot.utils.robot_utils import busy_wait
from lerobot.utils.visualization_utils import _init_rerun, log_rerun_data
FRQ = 100
PRINT_HZ = 10
RERUN_HZ = 100
ESC_CLR_EOL = "\x1b[K"
CURSOR_UP = "\x1b[F"
follower_cfg = SO101FollowerTConfig(
port="/dev/tty.usbmodem58760432961",
id="follower_arm_torque",
)
leader_cfg = SO101FollowerTConfig(
port="/dev/tty.usbmodem58760432571",
id="leader_arm_torque",
)
follower = SO101FollowerT(follower_cfg)
leader = SO101FollowerT(leader_cfg)
follower.connect()
leader.connect()
_init_rerun("bilateral_teleoperation")
print("Starting 4-channel bilateral teleoperation")
first_print = True
loop_count = 0
tic_prev = time.perf_counter()
while True:
tic = time.perf_counter()
obs_l, obs_f = leader.get_observation(), follower.get_observation()
dt = tic - tic_prev
tic_prev = tic
if dt <= 0.0:
dt = 0.01 # avoid div-by-zero
tau_cmd_f, tau_cmd_l = [], []
debug_info_f, debug_info_l = {}, {}
pos_f = {j: obs_f[f"{j}.pos"] for j in follower.bus.motors}
vel_f = {j: obs_f[f"{j}.vel"] for j in follower.bus.motors}
tau_reaction_f = {j: obs_f[f"{j}.effort"] for j in follower.bus.motors}
pos_l = {j: obs_l[f"{j}.pos"] for j in leader.bus.motors}
vel_l = {j: obs_l[f"{j}.vel"] for j in leader.bus.motors}
tau_reaction_l = {j: obs_l[f"{j}.effort"] for j in leader.bus.motors}
# Joint-specific control gains
kp_gains = follower.kp_gains
kd_gains = follower.kd_gains
kf_gains = follower.kf_gains
# Compute torque commands
tau_cmd_f = [
kp_gains[j] * (pos_l[j] - pos_f[j]) # Position tracking
+ kd_gains[j] * (vel_l[j] - vel_f[j]) # Velocity damping
+ kf_gains[j] * (-tau_reaction_l[j] - tau_reaction_f[j]) # Force reflection
for j in follower.bus.motors
]
tau_cmd_l = [
kp_gains[j] * (pos_f[j] - pos_l[j]) # Position tracking
+ kd_gains[j] * (vel_f[j] - vel_l[j]) # Velocity damping
+ kf_gains[j] * (-tau_reaction_f[j] - tau_reaction_l[j]) # Force reflection
for j in leader.bus.motors
]
# Store debug info
for i, j in enumerate(follower.bus.motors):
debug_info_f[j] = {
"τ_reaction": tau_reaction_f[j],
"τ_ref": tau_cmd_f[i],
"θ_err": pos_l[j] - pos_f[j],
"ω_err": vel_l[j] - vel_f[j],
"τ_err": -tau_reaction_l[j] - tau_reaction_f[j],
}
debug_info_l[j] = {
"τ_reaction": tau_reaction_l[j],
"τ_ref": tau_cmd_l[i],
"θ_err": pos_f[j] - pos_l[j],
"ω_err": vel_f[j] - vel_l[j],
"τ_err": -tau_reaction_f[j] - tau_reaction_l[j],
}
# Send torques to both arms
follower.send_action({f"{m}.effort": tau_cmd_f[i] for i, m in enumerate(follower.bus.motors)})
leader.send_action({f"{m}.effort": tau_cmd_l[i] for i, m in enumerate(leader.bus.motors)})
observation = {
"follower_joint_angles": pos_f, # θ_f: current angles
"follower_angular_velocities": vel_f, # ω_f: current velocities
"follower_external_torques": tau_reaction_f, # τ_ext: measured minus deterministic components
}
action = {
"leader_target_angles": pos_l, # θ_leader[τ]: absolute target angles
"leader_target_velocities": vel_l, # ω_leader[τ]: absolute target velocities
"leader_interaction_torques": tau_reaction_l, # τ_leader[τ]: cmd minus deterministic components
}
if loop_count % (FRQ // RERUN_HZ) == 0:
log_rerun_data(observation, action)
loop_count += 1
if loop_count % (FRQ // PRINT_HZ) == 0:
hz = 1.0 / dt
lines = [f"Loop {hz:6.1f} Hz Δt {dt * 1e3:5.2f} ms"]
lines.append("=" * 106)
lines.append("LEADER ARM TORQUE ANALYSIS:")
lines.append(f"{'Joint':<13}{'Pos':>8}{'React':>6}{'Cmd':>6}")
lines.append(f"{'':13}{'(deg)':>8}{'(Nm)':>6}{'(Nm)':>6}")
lines.append("-" * 86)
for i, j in enumerate(leader.bus.motors):
debug_l = debug_info_l[j]
lines.append(
f"{j:<13s}{math.degrees(pos_l[j]):+8.1f}{debug_l['τ_reaction']:+6.2f}{tau_cmd_l[i]:+6.2f}"
)
lines.append("")
lines.append("FOLLOWER ARM TORQUE ANALYSIS:")
lines.append(f"{'Joint':<13}{'Pos':>8}{'React':>6}{'Cmd':>6}")
lines.append(f"{'':13}{'(deg)':>8}{'(Nm)':>6}{'(Nm)':>6}")
lines.append("-" * 86)
for i, j in enumerate(follower.bus.motors):
debug_f = debug_info_f[j]
lines.append(
f"{j:<13s}{math.degrees(pos_f[j]):+8.1f}{debug_f['τ_reaction']:+6.2f}{tau_cmd_f[i]:+6.2f}"
)
lines.append("")
lines.append("=" * 86)
lines.append("TORQUE COMPONENT EXPLANATIONS:")
lines.append("• Pos (joint pos) = Joint position in degrees")
lines.append("• React (reaction) = External forces (human interaction, contact)")
lines.append("• Meas (measured) = Raw torque from motor current sensor")
lines.append("• Cmd (command) = Final torque sent to motor")
lines.append("-" * 86)
lines.append(
"Cmd = Track + Vel + Force + (Added as feedforward in send_action: Grav + Inert + Frict)"
)
lines.append("React = Meas - Grav - Inert - Frict (external forces)")
lines.append("Force = Kf × (reflect_other_robot - React) (telepresence)")
lines.append("Frict = b_visc×ω + f_coulomb×sign(ω) (transparency)")
lines.append(
f"Joint Gains: shoulder_pan Kp={kp_gains['shoulder_pan']:.1f} | shoulder_pan Kd={kd_gains['shoulder_pan']:.1f} | shoulder_pan Kf={kf_gains['shoulder_pan']:.1f}"
)
lines.append(
f"Friction Comp, Viscous: {follower.friction_viscous['shoulder_pan']:.3f} | Coulomb: {follower.friction_coulomb['shoulder_pan']:.3f} (robot-class)"
)
block = "\n".join(lines)
if first_print:
sys.stdout.write(block + "\n")
first_print = False
else:
sys.stdout.write(CURSOR_UP * len(lines) + ESC_CLR_EOL + block + "\n")
sys.stdout.flush()
busy_wait(max(0.0, 1.0 / FRQ - (time.perf_counter() - tic)))
+29 -18
View File
@@ -112,7 +112,8 @@ class OpenCVCamera(Camera):
self.config = config
self.index_or_path = config.index_or_path
self.fps = config.fps
self.wanted_fps = config.fps
self.camera_fps = None
self.color_mode = config.color_mode
self.warmup_s = config.warmup_s
@@ -200,10 +201,9 @@ class OpenCVCamera(Camera):
if not self.is_connected:
raise DeviceNotConnectedError(f"Cannot configure settings for {self} as it is not connected.")
if self.fps is None:
self.fps = self.videocapture.get(cv2.CAP_PROP_FPS)
else:
self._validate_fps()
# We don't set the FPS. We GET the actual (max) FPS from the camera.
self.camera_fps = self.videocapture.get(cv2.CAP_PROP_FPS)
logger.info(f"{self} is running at its default/max FPS: {self.camera_fps:.2f}")
default_width = int(round(self.videocapture.get(cv2.CAP_PROP_FRAME_WIDTH)))
default_height = int(round(self.videocapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
@@ -316,19 +316,23 @@ class OpenCVCamera(Camera):
if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.")
start_time = time.perf_counter()
# Start the background capture thread if it's not running
if self.thread is None or not self.thread.is_alive():
# Perform an initial blocking read to populate the first frame
ret, frame = self.videocapture.read()
if not ret or frame is None:
raise RuntimeError(f"{self} failed to read initial frame.")
ret, frame = self.videocapture.read()
self.latest_frame = self._postprocess_image(frame)
self._start_read_thread()
if not ret or frame is None:
raise RuntimeError(f"{self} read failed (status={ret}).")
with self.frame_lock:
frame = self.latest_frame
processed_frame = self._postprocess_image(frame, color_mode)
if frame is None:
raise RuntimeError(f"Internal error: Read thread started but no frame is available for {self}.")
read_duration_ms = (time.perf_counter() - start_time) * 1e3
logger.debug(f"{self} read took: {read_duration_ms:.1f}ms")
return processed_frame
return frame.copy()
def _postprocess_image(self, image: np.ndarray, color_mode: ColorMode | None = None) -> np.ndarray:
"""
@@ -386,16 +390,23 @@ class OpenCVCamera(Camera):
"""
while not self.stop_event.is_set():
try:
color_image = self.read()
ret, frame = self.videocapture.read()
if not ret or frame is None:
logger.warning(f"Failed to read frame in background for {self}.")
time.sleep(0.01)
continue
processed_frame = self._postprocess_image(frame)
with self.frame_lock:
self.latest_frame = color_image
self.latest_frame = processed_frame
self.new_frame_event.set()
except DeviceNotConnectedError:
break
except Exception as e:
logger.warning(f"Error reading frame in background thread for {self}: {e}")
if not self.is_connected:
break
def _start_read_thread(self) -> None:
"""Starts or restarts the background read thread if it's not running."""
+3
View File
@@ -28,6 +28,9 @@ ROBOTS = "robots"
ROBOT_TYPE = "robot_type"
TELEOPERATORS = "teleoperators"
ROBOTS = "robots"
TELEOPERATORS = "teleoperators"
# files & directories
CHECKPOINTS_DIR = "checkpoints"
LAST_CHECKPOINT_LINK = "last"
+28 -52
View File
@@ -164,8 +164,9 @@ class FeetechMotorsBus(MotorsBus):
)
def _handshake(self) -> None:
self._assert_motors_exist()
self._assert_same_firmware()
# self._assert_motors_exist()
# self._assert_same_firmware()
return
def _find_single_motor(self, motor: str, initial_baudrate: int | None = None) -> tuple[int, int]:
if self.protocol_version == 0:
@@ -223,7 +224,7 @@ class FeetechMotorsBus(MotorsBus):
for motor in self.motors:
# By default, Feetech motors have a 500µs delay response time (corresponding to a value of 250 on
# the 'Return_Delay_Time' address). We ensure this is reduced to the minimum of 2µs (value of 0).
self.write("Return_Delay_Time", motor, return_delay_time)
# self.write("Return_Delay_Time", motor, 0) # THIS DOES NOT WORK FOR HLS3625
# Set 'Maximum_Acceleration' to 254 to speedup acceleration and deceleration of the motors.
if self.protocol_version == 0:
self.write("Maximum_Acceleration", motor, maximum_acceleration)
@@ -231,83 +232,58 @@ class FeetechMotorsBus(MotorsBus):
@property
def is_calibrated(self) -> bool:
motors_calibration = self.read_calibration()
if set(motors_calibration) != set(self.calibration):
return False
same_ranges = all(
self.calibration[motor].range_min == cal.range_min
and self.calibration[motor].range_max == cal.range_max
for motor, cal in motors_calibration.items()
)
if self.protocol_version == 1:
return same_ranges
same_offsets = all(
self.calibration[motor].homing_offset == cal.homing_offset
for motor, cal in motors_calibration.items()
)
return same_ranges and same_offsets
# Check if calibration data has been loaded from file
return bool(self.calibration)
def read_calibration(self) -> dict[str, MotorCalibration]:
offsets, mins, maxes = {}, {}, {}
for motor in self.motors:
mins[motor] = self.read("Min_Position_Limit", motor, normalize=False)
maxes[motor] = self.read("Max_Position_Limit", motor, normalize=False)
offsets[motor] = (
self.read("Homing_Offset", motor, normalize=False) if self.protocol_version == 0 else 0
)
# Return empty calibration - we don't read from motors anymore
calibration = {}
for motor, m in self.motors.items():
calibration[motor] = MotorCalibration(
id=m.id,
drive_mode=0,
homing_offset=offsets[motor],
range_min=mins[motor],
range_max=maxes[motor],
homing_offset=0,
range_min=0,
range_max=4095, # Default max resolution
)
return calibration
def write_calibration(self, calibration_dict: dict[str, MotorCalibration], cache: bool = True) -> None:
for motor, calibration in calibration_dict.items():
if self.protocol_version == 0:
self.write("Homing_Offset", motor, calibration.homing_offset)
self.write("Min_Position_Limit", motor, calibration.range_min)
self.write("Max_Position_Limit", motor, calibration.range_max)
if cache:
self.calibration = calibration_dict
def write_calibration(self, calibration_dict: dict[str, MotorCalibration]) -> None:
# Only update the in-memory calibration, don't write to motors
self.calibration = calibration_dict
def _get_half_turn_homings(self, positions: dict[NameOrID, Value]) -> dict[NameOrID, Value]:
"""
On Feetech Motors:
Present_Position = Actual_Position - Homing_Offset
Calculate homing offsets such that the current position becomes 0 degrees.
For Feetech motors:
- The homing offset is subtracted from the raw position during normalization
- So to make current position = 0 degrees, homing_offset = current_raw_position
"""
half_turn_homings = {}
for motor, pos in positions.items():
model = self._get_motor_model(motor)
max_res = self.model_resolution_table[model] - 1
half_turn_homings[motor] = pos - int(max_res / 2)
# The homing offset should be the current position
# This way, when we normalize: (pos - homing_offset) = 0
half_turn_homings[motor] = pos
return half_turn_homings
def disable_torque(self, motors: str | list[str] | None = None, num_retry: int = 0) -> None:
def disable_torque(self, motors: str | list[str] | None = None, num_retry: int = 5) -> None:
for motor in self._get_motors_list(motors):
self.write("Torque_Enable", motor, TorqueMode.DISABLED.value, num_retry=num_retry)
self.write("Lock", motor, 0, num_retry=num_retry)
# self.write("Lock", motor, 0, num_retry=num_retry)
def _disable_torque(self, motor_id: int, model: str, num_retry: int = 0) -> None:
def _disable_torque(self, motor_id: int, model: str, num_retry: int = 5) -> None:
addr, length = get_address(self.model_ctrl_table, model, "Torque_Enable")
self._write(addr, length, motor_id, TorqueMode.DISABLED.value, num_retry=num_retry)
addr, length = get_address(self.model_ctrl_table, model, "Lock")
self._write(addr, length, motor_id, 0, num_retry=num_retry)
# addr, length = get_address(self.model_ctrl_table, model, "Lock")
# self._write(addr, length, motor_id, 0, num_retry=num_retry)
def enable_torque(self, motors: str | list[str] | None = None, num_retry: int = 0) -> None:
def enable_torque(self, motors: str | list[str] | None = None, num_retry: int = 5) -> None:
for motor in self._get_motors_list(motors):
self.write("Torque_Enable", motor, TorqueMode.ENABLED.value, num_retry=num_retry)
self.write("Lock", motor, 1, num_retry=num_retry)
# self.write("Lock", motor, 1, num_retry=num_retry)
def _encode_sign(self, data_name: str, ids_values: dict[int, int]) -> dict[int, int]:
for id_ in ids_values:
+107
View File
@@ -151,6 +151,95 @@ SCS_SERIES_CONTROL_TABLE = {
"Acceleration_2": (83, 1), # don't know what that is
}
# http://doc.feetech.cn/#/prodinfodownload?srcType=FT-SMS-STS-emanual-229f4476422d4059abfb1cb0
HLS_SERIES_CONTROL_TABLE = {
# Version Information (0-4) - read-only
"Firmware_Major_Version": FIRMWARE_MAJOR_VERSION, # (0, 1) read-only
"Firmware_Minor_Version": FIRMWARE_MINOR_VERSION, # (1, 1) read-only
"End_Type": (2, 1), # read-only - 0 represents little-endian storage
"Model_Number": MODEL_NUMBER, # (3, 2) read-only
# EPROM configuration (5-39)
"ID": (5, 1), # Main ID - unique identifier on bus
"Baud_Rate": (6, 1), # 0-7 for different baud rates
"Secondary_ID": (7, 1), # Secondary ID for write instructions
"Response_Status_Level": (8, 1), # 0: limited response, 1: full response
"Min_Position_Limit": (9, 2), # 0-4094 (0.087 degrees per unit)
"Max_Position_Limit": (11, 2), # 1-4095 (0.087 degrees per unit)
"Max_Temperature_Limit": (13, 1), # 0-100 (°C)
"Max_Voltage_Limit": (14, 1), # 0-254 (0.1V per unit)
"Min_Voltage_Limit": (15, 1), # 0-254 (0.1V per unit)
"Max_Torque_Limit": (16, 2), # 0-1000 (0.1% per unit)
"Phase": (18, 1), # Special function byte for motor phase configuration
"Unloading_Condition": (19, 1), # Bit flags for protection conditions
"LED_Alarm_Condition": (20, 1), # Bit flags for LED alarm conditions
"P_Coefficient": (21, 1), # Position ring P proportional coefficient
"D_Coefficient": (22, 1), # Position ring D differential coefficient
"I_Coefficient": (23, 1), # Position ring I integral coefficient
"Minimum_Startup_Force": (24, 1), # 0-254 (0.1% per unit)
"Point_Limit_Value": (25, 1), # 0-254 - maximum point value = point_limit * 4
"CW_Dead_Zone": (26, 1), # 0-16 (0.087 degrees per unit)
"CCW_Dead_Zone": (27, 1), # 0-16 (0.087 degrees per unit)
"Protection_Current": (28, 2), # 0-2047 (6.5 mA per unit)
"Angle_Resolution": (30, 1), # 1-128 - amplification coefficient
"Homing_Offset": (31, 2), # -4095 to 4095 (0.087 degrees per unit)
"Operating_Mode": (33, 1), # 0: position, 1: speed, 2: current, 3: PWM
"P_Coefficient_Curr": (34, 1), # Current ring P proportional coefficient
"I_Coefficient_Curr": (35, 1), # Current ring I integral coefficient
# Address 36 undefined
"Speed_P_Coefficient": (37, 1), # Speed closed-loop P proportional coefficient
"Overcurrent_Protection_Time": (38, 1), # 0-254 (10ms per unit)
"Speed_I_Coefficient": (39, 1), # Speed closed-loop I integral coefficient
# SRAM control (40-55)
"Torque_Enable": (40, 1), # 0: off, 1: on, 2: damping
"Acceleration": (41, 1), # 0-254 (8.7 degrees/second² per unit)
"Goal_Position": (42, 2), # -32767 to 32767 (0.087 degrees per unit)
"Target_Torque": (44, 2), # -2047 to 2047 (6.5 mA per unit)
"Goal_Velocity": (46, 2), # -32767 to 32767 (0.732 RPM per unit)
"Torque_Limit": (48, 2), # 0-1000 (0.1% per unit)
"P_Coefficient_Ring": (50, 1), # Motor position ring proportional coefficient
"D_Coefficient_Ring": (51, 1), # Motor position ring differential coefficient
"I_Coefficient_Ring": (52, 1), # Motor position ring integral coefficient
"km": (53, 1), # 0: position+current dual loop, 1: position single loop
# Address 54 undefined
"Lock": (55, 1), # 0: close write lock, 1: open write lock
# SRAM feedback (56-73) - read-only
"Present_Position": (56, 2), # read-only - current absolute position
"Present_Velocity": (58, 2), # read-only - current motor rotation speed
"Present_Load": (60, 2), # read-only - current load (0.1% per unit)
"Present_Voltage": (62, 1), # read-only - current voltage (0.1V per unit)
"Present_Temperature": (63, 1), # read-only - current temperature (°C)
"Async_Write_Flag": (64, 1), # read-only - async write instruction flag
"Status": (65, 1), # read-only - servo status bit flags
"Moving": (66, 1), # read-only - movement status flags
"Target_Position": (67, 2), # read-only - current target position
"Present_Current": (69, 2), # read-only - current motor phase current (6.5 mA per unit)
# Address 71 undefined
"Present_Bias": (73, 2), # read-only - current 0-point offset value
# Factory parameters (77-86) - read-only
"VFk_x10": (77, 1), # read-only - factory parameter
"vKgI": (78, 1), # read-only - factory parameter
"PFk_x10": (79, 1), # read-only - factory parameter
"Moving_Velocity_Threshold": (80, 1), # read-only - factory parameter
"DTs_ms": (81, 1), # read-only - factory parameter
"eFk_x10": (82, 1), # read-only - factory parameter
"Vk_ms": (83, 1), # read-only - factory parameter
"Maximum_Velocity_Limit": (84, 1), # read-only - factory parameter
"Maximum_Acceleration": (85, 1), # read-only - factory parameter
"Acceleration_Multiplier": (86, 1), # read-only - factory parameter
}
# HLS series baud rate table (same as STS/SMS series)
HLS_SERIES_BAUDRATE_TABLE = {
1_000_000: 0,
500_000: 1,
250_000: 2,
128_000: 3,
115_200: 4,
76_800: 5, # Note: HLS documentation mentions 76800 instead of 57600
57_600: 6,
38_400: 7,
}
STS_SMS_SERIES_BAUDRATE_TABLE = {
1_000_000: 0,
500_000: 1,
@@ -181,6 +270,7 @@ MODEL_CONTROL_TABLE = {
"sts3250": STS_SMS_SERIES_CONTROL_TABLE,
"scs0009": SCS_SERIES_CONTROL_TABLE,
"sm8512bl": STS_SMS_SERIES_CONTROL_TABLE,
"hls3625": HLS_SERIES_CONTROL_TABLE,
}
MODEL_RESOLUTION = {
@@ -191,6 +281,7 @@ MODEL_RESOLUTION = {
"sts3250": 4096,
"sm8512bl": 4096,
"scs0009": 1024,
"hls3625": 4096,
}
MODEL_BAUDRATE_TABLE = {
@@ -201,6 +292,7 @@ MODEL_BAUDRATE_TABLE = {
"sts3215": STS_SMS_SERIES_BAUDRATE_TABLE,
"sts3250": STS_SMS_SERIES_BAUDRATE_TABLE,
"scs0009": SCS_SERIES_BAUDRATE_TABLE,
"hls3625": HLS_SERIES_BAUDRATE_TABLE,
}
# Sign-Magnitude encoding bits
@@ -210,6 +302,18 @@ STS_SMS_SERIES_ENCODINGS_TABLE = {
"Present_Velocity": 15,
}
# HLS series sign-magnitude encoding bits
HLS_SERIES_ENCODINGS_TABLE = {
"Homing_Offset": 15, # BIT15 represents positive/negative direction
"Goal_Position": 15, # BIT15 represents positive/negative direction
"Target_Torque": 15, # BIT15 represents positive/negative direction in constant current mode
"Goal_Velocity": 15, # BIT15 represents positive/negative direction in constant speed mode
"Present_Position": 15, # BIT15 represents positive/negative direction
"Present_Velocity": 15, # BIT15 represents positive/negative direction
"Present_Current": 15, # BIT15 represents positive/negative direction
"Present_Load": 10, # BIT10 represents positive/negative direction
}
MODEL_ENCODING_TABLE = {
"sts_series": STS_SMS_SERIES_ENCODINGS_TABLE,
"sms_series": STS_SMS_SERIES_ENCODINGS_TABLE,
@@ -218,6 +322,7 @@ MODEL_ENCODING_TABLE = {
"sts3250": STS_SMS_SERIES_ENCODINGS_TABLE,
"sm8512bl": STS_SMS_SERIES_ENCODINGS_TABLE,
"scs0009": {},
"hls3625": HLS_SERIES_ENCODINGS_TABLE,
}
SCAN_BAUDRATES = [
@@ -239,6 +344,7 @@ MODEL_NUMBER_TABLE = {
"sts3250": 2825,
"sm8512bl": 11272,
"scs0009": 1284,
"hls3625": 3338,
}
MODEL_PROTOCOL = {
@@ -249,4 +355,5 @@ MODEL_PROTOCOL = {
"sts3250": 0,
"sm8512bl": 0,
"scs0009": 1,
"hls3625": 0, # Uses FT-SCS protocol
}
+39 -11
View File
@@ -83,6 +83,9 @@ class MotorNormMode(str, Enum):
DEGREES = "degrees"
COUNT_TO_DEG = 0.087 # 1 encoder count = 0.087 °
@dataclass
class MotorCalibration:
id: int
@@ -441,8 +444,8 @@ class MotorsBus(abc.ABC):
try:
if not self.port_handler.openPort():
raise OSError(f"Failed to open port '{self.port}'.")
elif handshake:
self._handshake()
# elif handshake:
# self._handshake()
except (FileNotFoundError, OSError, serial.SerialException) as e:
raise ConnectionError(
f"\nCould not connect on port '{self.port}'. Make sure you are using the correct port."
@@ -711,9 +714,8 @@ class MotorsBus(abc.ABC):
self.reset_calibration(motors)
actual_positions = self.sync_read("Present_Position", motors, normalize=False)
homing_offsets = self._get_half_turn_homings(actual_positions)
for motor, offset in homing_offsets.items():
self.write("Homing_Offset", motor, offset)
# Don't write to motors, just return the calculated offsets
return homing_offsets
@abc.abstractmethod
@@ -782,21 +784,32 @@ class MotorsBus(abc.ABC):
motor = self._id_to_name(id_)
min_ = self.calibration[motor].range_min
max_ = self.calibration[motor].range_max
homing_offset = self.calibration[motor].homing_offset
drive_mode = self.apply_drive_mode and self.calibration[motor].drive_mode
if max_ == min_:
raise ValueError(f"Invalid calibration for motor '{motor}': min and max are equal.")
bounded_val = min(max_, max(min_, val))
if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100:
bounded_val = min(max_, max(min_, val))
norm = (((bounded_val - min_) / (max_ - min_)) * 200) - 100
normalized_values[id_] = -norm if drive_mode else norm
elif self.motors[motor].norm_mode is MotorNormMode.RANGE_0_100:
bounded_val = min(max_, max(min_, val))
norm = ((bounded_val - min_) / (max_ - min_)) * 100
normalized_values[id_] = 100 - norm if drive_mode else norm
elif self.motors[motor].norm_mode is MotorNormMode.DEGREES:
mid = (min_ + max_) / 2
max_res = self.model_resolution_table[self._id_to_model(id_)] - 1
normalized_values[id_] = (val - mid) * 360 / max_res
# For motors without wrap-around handling
# The homing offset becomes 0 degrees
# Calculate difference from homing position
diff = val - homing_offset
# Convert to degrees
deg = diff * COUNT_TO_DEG
# Apply drive mode if needed
normalized_values[id_] = -deg if drive_mode else deg
else:
raise NotImplementedError
@@ -811,7 +824,9 @@ class MotorsBus(abc.ABC):
motor = self._id_to_name(id_)
min_ = self.calibration[motor].range_min
max_ = self.calibration[motor].range_max
homing_offset = self.calibration[motor].homing_offset
drive_mode = self.apply_drive_mode and self.calibration[motor].drive_mode
if max_ == min_:
raise ValueError(f"Invalid calibration for motor '{motor}': min and max are equal.")
@@ -824,9 +839,22 @@ class MotorsBus(abc.ABC):
bounded_val = min(100.0, max(0.0, val))
unnormalized_values[id_] = int((bounded_val / 100) * (max_ - min_) + min_)
elif self.motors[motor].norm_mode is MotorNormMode.DEGREES:
mid = (min_ + max_) / 2
max_res = self.model_resolution_table[self._id_to_model(id_)] - 1
unnormalized_values[id_] = int((val * max_res / 360) + mid)
# For motors without wrap-around, simple conversion back
# Apply drive mode if needed
val = -val if drive_mode else val
# Convert degrees to raw counts
raw_counts = int(round(val / COUNT_TO_DEG))
# Add back the homing offset
raw_counts_with_offset = raw_counts + homing_offset
# Ensure value stays within calibrated motor range
# Use the calibration min/max if available
if min_ is not None and max_ is not None:
raw_counts_with_offset = max(min_, min(max_, raw_counts_with_offset))
unnormalized_values[id_] = raw_counts_with_offset
else:
raise NotImplementedError
+220 -11
View File
@@ -21,7 +21,7 @@ Example:
python -m lerobot.record \
--robot.type=so100_follower \
--robot.port=/dev/tty.usbmodem58760431541 \
--robot.cameras="{laptop: {type: opencv, camera_index: 0, width: 640, height: 480}}" \
--robot.cameras="{laptop: {type: opencv, index_or_path: 0, width: 640, height: 480}}" \
--robot.id=black \
--dataset.repo_id=aliberts/record-test \
--dataset.num_episodes=2 \
@@ -33,6 +33,41 @@ python -m lerobot.record \
# <- Policy optional if you want to record with a policy \
# --policy.path=${HF_USER}/my_policy \
```
Example with bilateral teleoperation:
```shell
python -m lerobot.record \
--robot.type=so101_follower_t \
--robot.port=/dev/tty.usbmodem58760432961 \
--robot.id=follower_arm_torque \
--dataset.repo_id=pepijn/bilateral-teleop-test \
--dataset.num_episodes=5 \
--dataset.single_task="Wipe the table" \
--biteleop=true \
--teleop.type=so101_follower_t \
--teleop.port=/dev/tty.usbmodem58760432571 \
--teleop.id=leader_arm_torque \
--dataset.fps=100 \
--robot.cameras="{side: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 100}}" \
--display_data=true
```
Example Eval with bilateral teleoperation:
```
python -m lerobot.record \
--robot.type=so101_follower_t \
--robot.port=/dev/tty.usbmodem58760432961 \
--robot.id=follower_arm_torque \
--robot.cameras="{side: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 100}}" \
--display_data=true \
--dataset.repo_id=pepijn223/eval_bilateral-wipe-large \
--dataset.single_task="Wipe the table" \
--policy.path=pepijn223/bilateral-wipe-large-single \
--dataset.fps=100 \
--biteleop=true
```
"""
import logging
@@ -62,7 +97,9 @@ from lerobot.robots import ( # noqa: F401
make_robot_from_config,
so100_follower,
so101_follower,
so101_follower_torque,
)
from lerobot.robots.so101_follower_torque import SO101FollowerT
from lerobot.teleoperators import ( # noqa: F401
Teleoperator,
TeleoperatorConfig,
@@ -89,6 +126,24 @@ from lerobot.utils.utils import (
from lerobot.utils.visualization_utils import _init_rerun, log_rerun_data
def split_interleaved_action(vec, motors):
"""
vec : 1D tensor/array, length = 3*len(motors)
motors : ['shoulder_pan', 'shoulder_lift', ]
returns : pos, vel, tau (three dicts keyed by joint name)
"""
pos = {}
vel = {}
tau = {}
for i, j in enumerate(motors):
base = 3 * i
pos[j] = float(vec[base + 0])
vel[j] = float(vec[base + 1])
tau[j] = float(vec[base + 2])
return pos, vel, tau
@dataclass
class DatasetRecordConfig:
# Dataset identifier. By convention it should match '{hf_username}/{dataset_name}' (e.g. `lerobot/test`).
@@ -133,7 +188,7 @@ class RecordConfig:
robot: RobotConfig
dataset: DatasetRecordConfig
# Whether to control the robot with a teleoperator
teleop: TeleoperatorConfig | None = None
teleop: TeleoperatorConfig | RobotConfig | None = None
# Whether to control the robot with a policy
policy: PreTrainedConfig | None = None
# Display all cameras on screen
@@ -142,6 +197,8 @@ class RecordConfig:
play_sounds: bool = True
# Resume recording on an existing dataset.
resume: bool = False
# Enable bilateral teleoperation with force feedback
biteleop: bool = False
def __post_init__(self):
# HACK: We parse again the cli args here to get the pretrained path if there was one.
@@ -166,15 +223,23 @@ def record_loop(
events: dict,
fps: int,
dataset: LeRobotDataset | None = None,
teleop: Teleoperator | List[Teleoperator] | None = None,
teleop: Teleoperator | List[Teleoperator] | Robot | None = None,
policy: PreTrainedPolicy | None = None,
control_time_s: int | None = None,
single_task: str | None = None,
display_data: bool = False,
biteleop: bool = False,
):
if dataset is not None and dataset.fps != fps:
raise ValueError(f"The dataset fps should be equal to requested fps ({dataset.fps} != {fps}).")
if biteleop and policy is None:
if not isinstance(robot, SO101FollowerT):
raise ValueError(
"Bilateral teleoperation requires both robot and teleop to be of type SO101FollowerT"
)
logging.info("Bilateral teleoperation mode enabled")
teleop_arm = teleop_keyboard = None
if isinstance(teleop, list):
teleop_keyboard = next((t for t in teleop if isinstance(t, KeyboardTeleop)), None)
@@ -198,7 +263,11 @@ def record_loop(
timestamp = 0
start_episode_t = time.perf_counter()
while timestamp < control_time_s:
loop_count = 0
rerun_log_freq = max(1, int(fps / 10))
while control_time_s is not None and timestamp < control_time_s:
start_loop_t = time.perf_counter()
if events["exit_early"]:
@@ -210,7 +279,67 @@ def record_loop(
if policy is not None or dataset is not None:
observation_frame = build_dataset_frame(dataset.features, observation, prefix="observation")
if policy is not None:
if (
biteleop
and isinstance(robot, SO101FollowerT)
and isinstance(teleop, SO101FollowerT)
and policy is None
):
obs_f = observation # robot is the follower
obs_l = teleop.get_observation()
pos_f = {j: obs_f[f"{j}.pos"] for j in robot.bus.motors}
vel_f = {j: obs_f[f"{j}.vel"] for j in robot.bus.motors}
tau_reaction_f = {j: obs_f[f"{j}.effort"] for j in robot.bus.motors}
pos_l = {j: obs_l[f"{j}.pos"] for j in teleop.bus.motors}
vel_l = {j: obs_l[f"{j}.vel"] for j in teleop.bus.motors}
acc_l = {j: obs_l[f"{j}.acc"] for j in teleop.bus.motors}
tau_reaction_l = {j: obs_l[f"{j}.effort"] for j in teleop.bus.motors}
# Get control gains from robot
kp_gains = robot.kp_gains
kd_gains = robot.kd_gains
kf_gains = robot.kf_gains
# Compute torque commands
tau_cmd_f = [
(
kp_gains[j] * (pos_l[j] - pos_f[j]) # Position tracking
+ kd_gains[j] * (vel_l[j] - vel_f[j]) # Velocity damping
+ kf_gains[j] * (-tau_reaction_l[j] - tau_reaction_f[j])
) # Force reflection
for j in robot.bus.motors
]
tau_cmd_l = [
(
kp_gains[j] * (pos_f[j] - pos_l[j]) # Position tracking
+ kd_gains[j] * (vel_f[j] - vel_l[j]) # Velocity damping
+ kf_gains[j] * (-tau_reaction_f[j] - tau_reaction_l[j])
) # Force reflection
for j in teleop.bus.motors
]
action = {f"{m}.effort": tau_cmd_f[i] for i, m in enumerate(robot.bus.motors)}
teleop_action = {f"{m}.effort": tau_cmd_l[i] for i, m in enumerate(teleop.bus.motors)}
teleop.send_action(teleop_action)
robot.send_action(action)
# For bilateral teleoperation, create custom observation and action for dataset
bilateral_action = {}
for j in teleop.bus.motors:
bilateral_action[f"{j}.pos"] = pos_l[j]
bilateral_action[f"{j}.vel"] = vel_l[j]
bilateral_action[f"{j}.acc"] = acc_l[j]
bilateral_action[f"{j}.effort"] = -tau_reaction_l[j]
# Override the observation_frame and action for dataset recording
if dataset is not None:
observation_frame = build_dataset_frame(dataset.features, observation, prefix="observation")
action = bilateral_action
elif policy is not None and biteleop and isinstance(robot, SO101FollowerT):
action_values = predict_action(
observation_frame,
policy,
@@ -219,10 +348,57 @@ def record_loop(
task=single_task,
robot_type=robot.robot_type,
)
pos_f = {j: observation[f"{j}.pos"] for j in robot.bus.motors}
vel_f = {j: observation[f"{j}.vel"] for j in robot.bus.motors}
tau_reaction_f = {j: observation[f"{j}.effort"] for j in robot.bus.motors}
# The model returns [pos1, pos2, …, vel1, vel2, …, tau1, tau2, …]
motors = robot.bus.motors # 6 joints
pos_l, vel_l, neg_tau_reaction_l = split_interleaved_action(
action_values, motors
) # The model is trained and returns the effort already as negative: -tau_reaction_l
kp, kd, kf = robot.kp_gains, robot.kd_gains, robot.kf_gains
# Compute torque command for the follower robot
tau_cmd_f = [
(
kp[j] * (pos_l[j] - pos_f[j]) # Position tracking
+ kd[j] * (vel_l[j] - vel_f[j]) # Velocity damping
+ kf[j] * (neg_tau_reaction_l[j] - tau_reaction_f[j]) # Force reflection
)
for j in robot.bus.motors
]
# Format action with calculated torques and send to robot
action = {f"{m}.effort": tau_cmd_f[i] for i, m in enumerate(robot.bus.motors)}
robot.send_action(action)
bilateral_action = {}
for j in robot.bus.motors:
bilateral_action[f"{j}.pos"] = pos_l[j]
bilateral_action[f"{j}.vel"] = vel_l[j]
bilateral_action[f"{j}.effort"] = neg_tau_reaction_l[j]
# Override the observation_frame and action for dataset recording
if dataset is not None:
observation_frame = build_dataset_frame(dataset.features, observation, prefix="observation")
action = bilateral_action
elif policy is not None and not biteleop:
action_values = predict_action(
observation_frame,
policy,
get_safe_torch_device(policy.config.device),
policy.config.use_amp,
task=single_task,
robot_type=robot.robot_type,
)
action = {key: action_values[i].item() for i, key in enumerate(robot.action_features)}
elif policy is None and isinstance(teleop, Teleoperator):
elif policy is None and isinstance(teleop, Teleoperator) and not biteleop:
action = teleop.get_action()
elif policy is None and isinstance(teleop, list):
elif policy is None and isinstance(teleop, list) and not biteleop:
# TODO(pepijn, steven): clean the record loop for use of multiple robots (possibly with pipeline)
arm_action = teleop_arm.get_action()
arm_action = {f"arm_{k}": v for k, v in arm_action.items()}
@@ -241,31 +417,62 @@ def record_loop(
# Action can eventually be clipped using `max_relative_target`,
# so action actually sent is saved in the dataset.
sent_action = robot.send_action(action)
if not biteleop:
sent_action = robot.send_action(action)
if dataset is not None:
action_frame = build_dataset_frame(dataset.features, sent_action, prefix="action")
# For bilateral teleoperation, use the bilateral_action (leader pos & torque)
# For other modes, use sent_action as usual
if biteleop and isinstance(robot, SO101FollowerT):
action_frame = build_dataset_frame(dataset.features, action, prefix="action")
else:
action_frame = build_dataset_frame(dataset.features, sent_action, prefix="action")
frame = {**observation_frame, **action_frame}
dataset.add_frame(frame, task=single_task)
if display_data:
if display_data and loop_count % rerun_log_freq == 0:
log_rerun_data(observation, action)
dt_s = time.perf_counter() - start_loop_t
busy_wait(1 / fps - dt_s)
timestamp = time.perf_counter() - start_episode_t
loop_count += 1
@parser.wrap()
def record(cfg: RecordConfig) -> LeRobotDataset:
init_logging()
logging.info(pformat(asdict(cfg)))
if cfg.display_data:
_init_rerun(session_name="recording")
robot = make_robot_from_config(cfg.robot)
teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None
if cfg.biteleop and cfg.teleop is not None:
print("Bilateral teleoperation enabled")
# For bilateral teleoperation, both arms must be SO101FollowerT robots
from lerobot.robots.so101_follower_torque.config_so101_follower_t import SO101FollowerTConfig
# Check if teleop config has the right type
if cfg.teleop.type != "so101_follower_t":
raise ValueError("Bilateral teleoperation requires teleop.type to be 'so101_follower_t'")
port = getattr(cfg.teleop, "port", None)
if port is None:
raise ValueError("Bilateral teleoperation requires teleop.port to be specified")
teleop_robot_config = SO101FollowerTConfig(
port=port,
id=getattr(cfg.teleop, "id", "leader_arm_torque"),
cameras=getattr(cfg.teleop, "cameras", {}),
disable_torque_on_disconnect=getattr(cfg.teleop, "disable_torque_on_disconnect", True),
)
teleop = SO101FollowerT(teleop_robot_config)
else:
teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None
action_features = hw_to_dataset_features(robot.action_features, "action", cfg.dataset.video)
obs_features = hw_to_dataset_features(robot.observation_features, "observation", cfg.dataset.video)
@@ -319,6 +526,7 @@ def record(cfg: RecordConfig) -> LeRobotDataset:
control_time_s=cfg.dataset.episode_time_s,
single_task=cfg.dataset.single_task,
display_data=cfg.display_data,
biteleop=cfg.biteleop,
)
# Execute a few seconds without recording to give time to manually reset the environment
@@ -335,6 +543,7 @@ def record(cfg: RecordConfig) -> LeRobotDataset:
control_time_s=cfg.dataset.reset_time_s,
single_task=cfg.dataset.single_task,
display_data=cfg.display_data,
biteleop=cfg.biteleop,
)
if events["rerecord_episode"]:
+69 -6
View File
@@ -25,6 +25,17 @@ python -m lerobot.replay \
--dataset.repo_id=aliberts/record-test \
--dataset.episode=2
```
Biteleop example:
```shell
python -m lerobot.replay \
--robot.type=so101_follower_t \
--robot.port=/dev/tty.usbmodem58760432961 \
--robot.id=follower_arm_torque \
--dataset.repo_id=pepijn223/bilateral-wipe-large \
--dataset.episode=10 \
--biteleop=true
```
"""
import logging
@@ -44,7 +55,9 @@ from lerobot.robots import ( # noqa: F401
make_robot_from_config,
so100_follower,
so101_follower,
so101_follower_torque,
)
from lerobot.robots.so101_follower_torque import SO101FollowerT
from lerobot.utils.robot_utils import busy_wait
from lerobot.utils.utils import (
init_logging,
@@ -70,6 +83,8 @@ class ReplayConfig:
dataset: DatasetReplayConfig
# Use vocal synthesis to read events.
play_sounds: bool = True
# Use biteleop to replay the dataset
biteleop: bool = False
@draccus.wrap()
@@ -80,22 +95,70 @@ def replay(cfg: ReplayConfig):
robot = make_robot_from_config(cfg.robot)
dataset = LeRobotDataset(cfg.dataset.repo_id, root=cfg.dataset.root, episodes=[cfg.dataset.episode])
actions = dataset.hf_dataset.select_columns("action")
if cfg.biteleop:
if not isinstance(robot, SO101FollowerT):
raise ValueError(
"Bilateral teleoperation replay requires the robot to be of type SO101FollowerT."
)
log_say("Bilateral teleoperation replay enabled.", cfg.play_sounds)
robot.connect()
log_say("Replaying episode", cfg.play_sounds, blocking=True)
start_time_all = time.perf_counter()
for idx in range(dataset.num_frames):
start_episode_t = time.perf_counter()
start_loop_t = time.perf_counter()
action_array = actions[idx]["action"]
action = {}
action_from_ds_array = actions[idx]["action"]
action_from_ds = {}
for i, name in enumerate(dataset.features["action"]["names"]):
action[name] = action_array[i]
action_from_ds[name] = action_from_ds_array[i]
robot.send_action(action)
# Bilateral teleoperation
if cfg.biteleop:
obs_f = robot.get_observation()
pos_f = {j: obs_f[f"{j}.pos"] for j in robot.bus.motors}
vel_f = {j: obs_f[f"{j}.vel"] for j in robot.bus.motors}
tau_reaction_f = {j: obs_f[f"{j}.effort"] for j in robot.bus.motors}
dt_s = time.perf_counter() - start_episode_t
pos_l = {j: action_from_ds[f"{j}.pos"] for j in robot.bus.motors}
vel_l = {j: action_from_ds[f"{j}.vel"] for j in robot.bus.motors}
# The saved effort in dataset is -tau_reaction_l
neg_tau_reaction_l = {j: action_from_ds[f"{j}.effort"] for j in robot.bus.motors}
# Get control gains from the robot instance
kp_gains = robot.kp_gains
kd_gains = robot.kd_gains
kf_gains = robot.kf_gains
# Compute torque command for the follower robot
tau_cmd_f = [
(
kp_gains[j] * (pos_l[j] - pos_f[j]) # Position tracking
+ kd_gains[j] * (vel_l[j] - vel_f[j]) # Velocity damping
+ kf_gains[j] * (neg_tau_reaction_l[j] - tau_reaction_f[j]) # Force reflection
)
for j in robot.bus.motors
]
# Format action with calculated torques and send to robot
action_to_send = {f"{m}.effort": tau_cmd_f[i] for i, m in enumerate(robot.bus.motors)}
robot.send_action(action_to_send)
else:
# Original logic for standard position-based replay
robot.send_action(action_from_ds)
dt_s = time.perf_counter() - start_loop_t
busy_wait(1 / dataset.fps - dt_s)
total_time = time.perf_counter() - start_time_all
actual_fps = idx / total_time if total_time > 0 else float("inf")
logging.info(f"Average FPS achieved over episode: {actual_fps:.2f}")
log_say(f"Average FPS achieved: {actual_fps:.2f}", cfg.play_sounds)
robot.disconnect()
@@ -0,0 +1,2 @@
from .config_so101_follower_t import SO101FollowerTConfig
from .so101_follower_t import SO101FollowerT
@@ -0,0 +1,38 @@
#!/usr/bin/env python
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, field
from lerobot.cameras import CameraConfig
from ..config import RobotConfig
@RobotConfig.register_subclass("so101_follower_t")
@dataclass
class SO101FollowerTConfig(RobotConfig):
# Port to connect to the arm
port: str
disable_torque_on_disconnect: bool = True
# `max_relative_target` limits the magnitude of the relative positional target vector for safety purposes.
# Set this to a positive scalar to have the same value for all motors, or a list that is the same length as
# the number of motors in your follower arms.
max_relative_target: int | None = None
# cameras
cameras: dict[str, CameraConfig] = field(default_factory=dict)
@@ -0,0 +1,553 @@
#!/usr/bin/env python
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import logging
import time
from functools import cached_property
from typing import Any
import numpy as np
import pinocchio as pin
from scipy.signal import butter, lfilter
from lerobot.cameras.utils import make_cameras_from_configs
from lerobot.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
from lerobot.motors.feetech import (
FeetechMotorsBus,
)
from ..robot import Robot
from .config_so101_follower_t import SO101FollowerTConfig
logger = logging.getLogger(__name__)
class SO101FollowerT(Robot):
"""
SO-101 Arm with HLS3625 motors with current control.
"""
config_class = SO101FollowerTConfig
name = "so101_follower_t"
_CURRENT_STEP_A: float = 6.5e-3 # 6.5 mA per register LSB #http://doc.feetech.cn/#/prodinfodownload?srcType=FT-SMS-STS-emanual-229f4476422d4059abfb1cb0
_KT_NM_PER_AMP: float = 0.814 # Torque constant Kt [N·m/A] #https://www.feetechrc.com/811177.html
_MAX_CURRENT_A: float = 4.0 # Safe driver limit
# Position gains
_KP_GAINS = {
"shoulder_pan": 5.0,
"shoulder_lift": 7.0,
"elbow_flex": 7.0,
"wrist_flex": 5.0,
"wrist_roll": 5.0,
"gripper": 5.0,
}
# Velocity gains
_KD_GAINS = {
"shoulder_pan": 0.4,
"shoulder_lift": 0.6,
"elbow_flex": 0.6,
"wrist_flex": 0.4,
"wrist_roll": 0.4,
"gripper": 0.4,
}
# Force gains
_KF_GAINS = {
"shoulder_pan": 0.05,
"shoulder_lift": 0.05,
"elbow_flex": 0.05,
"wrist_flex": 0.05,
"wrist_roll": 0.05,
"gripper": 0.05,
}
# Viscous friction coefficient
_FRICTION_VISCOUS = {
"shoulder_pan": 0.05,
"shoulder_lift": 0.08,
"elbow_flex": 0.05,
"wrist_flex": 0.05,
"wrist_roll": 0.05,
"gripper": 0.05,
}
# Coulomb/static friction
_FRICTION_COULOMB = {
"shoulder_pan": 0.15,
"shoulder_lift": 0.25,
"elbow_flex": 0.25,
"wrist_flex": 0.20,
"wrist_roll": 0.20,
"gripper": 0.20,
}
def __init__(self, config: SO101FollowerTConfig):
super().__init__(config)
self.config = config
if self.calibration_fpath.is_file() and not self.calibration:
self._load_calibration()
self.bus = FeetechMotorsBus(
port=self.config.port,
motors={
"shoulder_pan": Motor(1, "hls3625", MotorNormMode.DEGREES),
"shoulder_lift": Motor(2, "hls3625", MotorNormMode.DEGREES),
"elbow_flex": Motor(3, "hls3625", MotorNormMode.DEGREES),
"wrist_flex": Motor(4, "hls3625", MotorNormMode.DEGREES),
"wrist_roll": Motor(5, "hls3625", MotorNormMode.DEGREES),
"gripper": Motor(6, "hls3625", MotorNormMode.DEGREES),
},
calibration=self.calibration,
)
self.cameras = make_cameras_from_configs(config.cameras)
self.pin_robot = pin.RobotWrapper.BuildFromURDF("urdf/so101_new_calib.urdf", "urdf")
flip = {
"shoulder_pan": True,
"shoulder_lift": True,
"elbow_flex": True,
"wrist_flex": True,
"wrist_roll": True,
"gripper": True,
}
self.torque_sign = {n: (-1.0 if flip[n] else 1.0) for n in self.bus.motors}
self._prev_pos_rad: dict[str, float] | None = None
self._prev_vel_rad: dict[str, float] | None = None
self._prev_t: float | None = None
# Butterworth low-pass filter parameters
self._cutoff_freq = 10.0 # Hz, cutoff frequency for the filter
self._filter_order = 2 # Filter order
self._sampling_freq = 100.0 # Hz, (control loop frequency)
nyquist_freq = self._sampling_freq / 2
normalized_cutoff = self._cutoff_freq / nyquist_freq
self._b, self._a = butter(self._filter_order, normalized_cutoff, btype="low")
# History buffers
self._pos_history = {m: collections.deque(maxlen=20) for m in self.bus.motors}
self._vel_raw_history = {m: collections.deque(maxlen=20) for m in self.bus.motors}
self._time_history = collections.deque(maxlen=20)
self._last_observation = None
@property
def _motors_ft(self) -> dict[str, type]:
d: dict[str, type] = {}
for motor in self.bus.motors:
d[f"{motor}.pos"] = float
d[f"{motor}.vel"] = float
d[f"{motor}.effort"] = float
return d
@property
def _cameras_ft(self) -> dict[str, tuple]:
return {
cam: (self.config.cameras[cam].height, self.config.cameras[cam].width, 3) for cam in self.cameras
}
@cached_property
def observation_features(self) -> dict[str, type | tuple]:
return {**self._motors_ft, **self._cameras_ft}
@cached_property
def action_features(self) -> dict[str, type]:
d: dict[str, type] = {}
for motor in self.bus.motors:
d[f"{motor}.pos"] = float
d[f"{motor}.vel"] = float
d[f"{motor}.effort"] = float
return d
@property
def is_connected(self) -> bool:
return self.bus.is_connected and all(cam.is_connected for cam in self.cameras.values())
@property
def kp_gains(self) -> dict[str, float]:
"""Position control gains [Nm/rad] for bilateral teleoperation"""
return self._KP_GAINS.copy()
@property
def kd_gains(self) -> dict[str, float]:
"""Velocity control gains [Nm⋅s/rad] for bilateral teleoperation"""
return self._KD_GAINS.copy()
@property
def kf_gains(self) -> dict[str, float]:
"""Force control gains for bilateral teleoperation"""
return self._KF_GAINS.copy()
@property
def friction_viscous(self) -> dict[str, float]:
"""Viscous friction coefficients [Nm⋅s/rad] for friction compensation"""
return self._FRICTION_VISCOUS.copy()
@property
def friction_coulomb(self) -> dict[str, float]:
"""Coulomb friction coefficients [Nm] for friction compensation"""
return self._FRICTION_COULOMB.copy()
def set_butterworth_params(self, cutoff_freq: float = 10.0, order: int = 2) -> None:
"""Configure Butterworth low-pass filter parameters for velocity/acceleration estimation.
Args:
cutoff_freq: Cutoff frequency in Hz (default: 10 Hz)
order: Filter order (default: 2)
"""
if cutoff_freq <= 0:
raise ValueError("Cutoff frequency must be positive")
if cutoff_freq >= self._sampling_freq / 2:
raise ValueError(
f"Cutoff frequency must be less than Nyquist frequency ({self._sampling_freq / 2} Hz)"
)
if order < 1:
raise ValueError("Filter order must be at least 1")
self._cutoff_freq = cutoff_freq
self._filter_order = order
nyquist_freq = self._sampling_freq / 2
normalized_cutoff = self._cutoff_freq / nyquist_freq
self._b, self._a = butter(self._filter_order, normalized_cutoff, btype="low")
# Clear buffers
for m in self.bus.motors:
self._pos_history[m].clear()
self._vel_raw_history[m].clear()
self._time_history.clear()
logger.info(f"Butterworth filter updated: cutoff_freq={cutoff_freq} Hz, order={order}")
def _current_to_torque_nm(self, raw: dict[str, Any]) -> dict[str, float]:
"""Convert "Present_Current" register counts (±2047) → torque [Nm].
Values are clamped to ±3A before conversion for protection.
"""
max_cnt = int(round(self._MAX_CURRENT_A / self._CURRENT_STEP_A)) # ≈ 462
coef = self._CURRENT_STEP_A * self._KT_NM_PER_AMP
return {k: self.torque_sign[k] * max(-max_cnt, min(max_cnt, v)) * coef for k, v in raw.items()}
def _torque_nm_to_current(self, torque: dict[str, float]) -> dict[str, int]:
"""Convert torque [Nm] to register counts, clamped to ±3A (2.44 Nm)."""
inv_coef = 1.0 / (self._CURRENT_STEP_A * self._KT_NM_PER_AMP)
max_cnt = int(round(self._MAX_CURRENT_A / self._CURRENT_STEP_A))
counts = {}
for k, τ in torque.items():
cnt = τ * self.torque_sign[k] * inv_coef
cnt = max(-max_cnt, min(max_cnt, cnt))
counts[k] = int(round(cnt))
return counts
def _deg_to_rad(self, deg: dict[str, float | int]) -> dict[str, float]:
"""Degrees to radians."""
return {m: np.deg2rad(float(v)) for m, v in deg.items()}
def _gravity_from_q(self, q_rad: dict[str, float]) -> dict[str, float]:
"""
Compute g(q) [N m] for all joints in the robot.
The order of joints in the URDF matches self.bus.motors.
"""
q = np.zeros(self.pin_robot.model.nq)
for i, motor_name in enumerate(self.bus.motors):
q[i] = q_rad[motor_name]
g = pin.computeGeneralizedGravity(self.pin_robot.model, self.pin_robot.data, q)
return {motor_name: float(g[i]) for i, motor_name in enumerate(self.bus.motors)}
def _inertia_from_q_dq(
self, q_rad: dict[str, float], dq_rad: dict[str, float], ddq_rad: dict[str, float]
) -> dict[str, float]:
"""
Compute inertia torques τ_inertia = M(q) * ddq directly from URDF model.
"""
q = np.zeros(self.pin_robot.model.nq)
dq = np.zeros(self.pin_robot.model.nv)
ddq = np.zeros(self.pin_robot.model.nv)
for i, motor_name in enumerate(self.bus.motors):
q[i] = q_rad[motor_name]
dq[i] = dq_rad[motor_name]
ddq[i] = ddq_rad[motor_name]
# Compute mass matrix M(q)
mass_matrix = pin.crba(self.pin_robot.model, self.pin_robot.data, q)
# Compute inertia torques: τ_inertia = M(q) * ddq
tau_inertia = mass_matrix @ ddq
return {motor_name: float(tau_inertia[i]) for i, motor_name in enumerate(self.bus.motors)}
def _compute_model_based_disturbance(
self,
q_rad: dict[str, float],
dq_rad: dict[str, float],
ddq_rad: dict[str, float],
tau_measured: dict[str, float],
) -> dict[str, float]:
"""
Compute disturbance torques using direct model-based approach:
τ_disturbance = τ_measured - τ_gravity - τ_inertia - τ_friction
Args:
include_friction: If True, also removes friction from the disturbance calculation
"""
tau_gravity = self._gravity_from_q(q_rad)
tau_inertia = self._inertia_from_q_dq(q_rad, dq_rad, ddq_rad)
# Compute disturbance
tau_disturbance = {}
tau_friction = {}
for motor_name in self.bus.motors:
tau_dist = tau_measured[motor_name] - tau_gravity[motor_name] - tau_inertia[motor_name]
# Calculate friction torque
omega = dq_rad[motor_name]
tau_friction_motor = self._FRICTION_VISCOUS[motor_name] * omega + self._FRICTION_COULOMB[
motor_name
] * (1.0 if omega > 0.01 else -1.0 if omega < -0.01 else 0.0)
# Apply torque sign correction
tau_friction_motor = -tau_friction_motor
tau_friction[motor_name] = tau_friction_motor
tau_dist -= tau_friction_motor
tau_disturbance[motor_name] = tau_dist
return tau_disturbance
def connect(self, calibrate: bool = True) -> None:
"""
We assume that at connection time, arm is in a rest position,
and torque can be safely disabled to run calibration.
"""
if self.is_connected:
raise DeviceAlreadyConnectedError(f"{self} already connected")
# Ensure calibration is loaded from file if it exists
if self.calibration_fpath.is_file() and not self.calibration:
self._load_calibration()
# Update the bus with the loaded calibration
self.bus.calibration = self.calibration
self.bus.connect()
if not self.is_calibrated and calibrate:
self.calibrate()
for cam in self.cameras.values():
cam.connect()
self.configure()
logger.info(f"{self} connected.")
@property
def is_calibrated(self) -> bool:
# Check if calibration file exists and is loaded
return self.calibration_fpath.is_file() and bool(self.calibration)
def calibrate(self) -> None:
logger.info(f"\nRunning calibration of {self}")
self.bus.disable_torque()
for motor in self.bus.motors:
self.bus.write("Operating_Mode", motor, 2, num_retry=2) # Set to current mode
input(f"Move {self} to the middle of its range of motion and press ENTER....")
homing_offsets = self.bus.set_half_turn_homings()
print(
"Move all joints sequentially through their entire ranges "
"of motion.\nRecording positions. Press ENTER to stop..."
)
range_mins, range_maxes = self.bus.record_ranges_of_motion()
self.calibration = {}
for motor, m in self.bus.motors.items():
self.calibration[motor] = MotorCalibration(
id=m.id,
drive_mode=0,
homing_offset=int(homing_offsets[motor]),
range_min=int(range_mins[motor]),
range_max=int(range_maxes[motor]),
)
# Update the bus calibration with the new values
self.bus.calibration = self.calibration
# Save calibration to file only
self._save_calibration()
print("Calibration saved to", self.calibration_fpath)
def configure(self) -> None:
self.bus.disable_torque() # here was issue at startup previously
for motor in self.bus.motors:
self.bus.write("Operating_Mode", motor, 2, num_retry=2) # Set to current mode
self.bus.write("Present_Current", motor, 0, normalize=False, num_retry=5)
def setup_motors(self) -> None:
for motor in reversed(self.bus.motors):
input(f"Connect the controller board to the '{motor}' motor only and press enter.")
self.bus.setup_motor(motor)
print(f"'{motor}' motor id set to {self.bus.motors[motor].id}")
def get_observation(self) -> dict[str, Any]:
if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.")
t_now = time.perf_counter()
# Position
pos_deg = self.bus.sync_read("Present_Position", num_retry=5)
pos_rad = self._deg_to_rad(pos_deg)
# Store position and time history
for m in pos_rad:
self._pos_history[m].append(pos_rad[m])
self._time_history.append(t_now)
# Calculate raw velocity
vel_rad_raw = {}
if self._prev_pos_rad is None or self._prev_t is None:
vel_rad_raw = dict.fromkeys(pos_rad, 0.0)
else:
dt = t_now - self._prev_t
dt = max(dt, 1e-4) # Avoid division by zero
vel_rad_raw = {m: (pos_rad[m] - self._prev_pos_rad[m]) / dt for m in pos_rad}
# Store raw velocity history
for m in vel_rad_raw:
self._vel_raw_history[m].append(vel_rad_raw[m])
# Apply Butterworth low-pass filter to velocity
vel_rad = {}
for m in pos_rad:
if len(self._vel_raw_history[m]) >= 10:
vel_raw_array = np.array(list(self._vel_raw_history[m]))
# Apply Butterworth filter
vel_filtered = lfilter(self._b, self._a, vel_raw_array)
vel_rad[m] = vel_filtered[-1]
else:
vel_rad[m] = vel_rad_raw[m]
# Calculate acceleration from filtered velocity
acc_rad = {}
if self._prev_vel_rad is None or self._prev_t is None:
acc_rad = dict.fromkeys(pos_rad, 0.0)
else:
dt = t_now - self._prev_t
dt = max(dt, 1e-4) # Avoid division by zero
acc_rad = {m: (vel_rad[m] - self._prev_vel_rad[m]) / dt for m in vel_rad}
self._prev_pos_rad = pos_rad.copy()
self._prev_vel_rad = vel_rad.copy()
self._prev_t = t_now
# Measured torque (Nm)
cur_raw = self.bus.sync_read("Present_Current", normalize=False, num_retry=5)
tau_meas = self._current_to_torque_nm(cur_raw)
# Compute reaction torques using model-based approach
tau_reaction = self._compute_model_based_disturbance(pos_rad, vel_rad, acc_rad, tau_meas)
obs_dict = {}
obs_dict |= {f"{m}.pos": pos_rad[m] for m in self.bus.motors}
obs_dict |= {f"{m}.vel": vel_rad[m] for m in self.bus.motors}
obs_dict |= {f"{m}.acc": acc_rad[m] for m in self.bus.motors}
obs_dict |= {f"{m}.effort": tau_reaction[m] for m in self.bus.motors}
# Capture images from cameras
for cam_key, cam in self.cameras.items():
start = time.perf_counter()
obs_dict[cam_key] = cam.read()
dt_ms = (time.perf_counter() - start) * 1e3
logger.debug(f"{self} read {cam_key}: {dt_ms:.1f}ms")
# Store observation for feedforward compensation
self._last_observation = obs_dict.copy()
return obs_dict
def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
"""Command arm to move to a target torque for a joint.
Raises:
RobotDeviceNotConnectedError: if robot is not connected.
Returns:
the action sent to the motors.
"""
if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.")
# Extract torque commands
tau_cmd_nm = {k.removesuffix(".effort"): float(v) for k, v in action.items() if k.endswith(".effort")}
if not tau_cmd_nm:
return action
# Add feedforward compensation if we have a last observation
if self._last_observation is not None:
# Extract position, velocity, acceleration from last observation
pos_rad = {m: self._last_observation[f"{m}.pos"] for m in self.bus.motors}
vel_rad = {m: self._last_observation[f"{m}.vel"] for m in self.bus.motors}
acc_rad = {m: self._last_observation[f"{m}.acc"] for m in self.bus.motors}
# Compute feedforward terms
tau_gravity = self._gravity_from_q(pos_rad)
tau_inertia = self._inertia_from_q_dq(pos_rad, vel_rad, acc_rad)
# Add feedforward compensation to commanded torques
for motor in tau_cmd_nm:
# Add gravity compensation
tau_cmd_nm[motor] += tau_gravity[motor]
# Add inertia compensation
tau_cmd_nm[motor] += tau_inertia[motor]
# Add friction compensation
omega = vel_rad[motor]
tau_friction = self._FRICTION_VISCOUS[motor] * omega + self._FRICTION_COULOMB[motor] * (
1.0 if omega > 0.01 else -1.0 if omega < -0.01 else 0.0
)
tau_friction = -tau_friction # Apply torque sign correction
tau_cmd_nm[motor] += tau_friction
inv_coef = 1.0 / (self._CURRENT_STEP_A * self._KT_NM_PER_AMP)
max_cnt = int(round(self._MAX_CURRENT_A / self._CURRENT_STEP_A))
counts = {}
for joint, τ in tau_cmd_nm.items():
cnt = τ * self.torque_sign[joint] * inv_coef # flip SIGN
cnt = max(-max_cnt, min(max_cnt, cnt))
counts[joint] = int(round(cnt))
self.bus.sync_write("Target_Torque", counts, normalize=False, num_retry=2)
self._last_cmd_nm = tau_cmd_nm
return action
def disconnect(self):
if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.")
self.bus.disconnect(self.config.disable_torque_on_disconnect)
for cam in self.cameras.values():
cam.disconnect()
logger.info(f"{self} disconnected.")
+4
View File
@@ -37,6 +37,10 @@ def make_robot_from_config(config: RobotConfig) -> Robot:
from .so101_follower import SO101Follower
return SO101Follower(config)
elif config.type == "so101_follower_t":
from .so101_follower_torque import SO101FollowerT
return SO101FollowerT(config)
elif config.type == "lekiwi":
from .lekiwi import LeKiwi
+2
View File
@@ -35,6 +35,7 @@ from lerobot.robots import ( # noqa: F401
make_robot_from_config,
so100_follower,
so101_follower,
so101_follower_torque,
)
from lerobot.teleoperators import ( # noqa: F401
TeleoperatorConfig,
@@ -52,6 +53,7 @@ COMPATIBLE_DEVICES = [
"so101_follower",
"so101_leader",
"lekiwi",
"so101_follower_t",
]
+6
View File
@@ -33,6 +33,12 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator:
from .so101_leader import SO101Leader
return SO101Leader(config)
elif config.type == "so101_follower_t":
# For bilateral teleoperation, SO101FollowerT is used as a robot, not a teleoperator
# This should be handled in the record.py file instead
raise ValueError(
"so101_follower_t should be created as a robot instance for bilateral teleoperation, not as a teleoperator"
)
elif config.type == "stretch3":
from .stretch3_gamepad import Stretch3GamePad
+17 -1
View File
@@ -28,19 +28,35 @@ def _init_rerun(session_name: str = "lerobot_control_loop") -> None:
rr.spawn(memory_limit=memory_limit)
def log_rerun_data(observation: dict[str | Any], action: dict[str | Any]):
def log_rerun_data(observation: dict[str, Any], action: dict[str, Any]):
for obs, val in observation.items():
if isinstance(val, float):
rr.log(f"observation.{obs}", rr.Scalar(val))
elif isinstance(val, dict):
# Handle dictionary of joint values
for joint_name, joint_val in val.items():
if isinstance(joint_val, (float, int)):
rr.log(f"observation.{obs}.{joint_name}", rr.Scalar(float(joint_val)))
elif isinstance(val, np.ndarray):
if val.ndim == 1:
for i, v in enumerate(val):
rr.log(f"observation.{obs}_{i}", rr.Scalar(float(v)))
else:
rr.log(f"observation.{obs}", rr.Image(val), static=True)
for act, val in action.items():
if isinstance(val, float):
rr.log(f"action.{act}", rr.Scalar(val))
elif isinstance(val, dict):
# Handle dictionary of joint values
for joint_name, joint_val in val.items():
if isinstance(joint_val, (float, int)):
rr.log(f"action.{act}.{joint_name}", rr.Scalar(float(joint_val)))
elif isinstance(val, np.ndarray):
for i, v in enumerate(val):
rr.log(f"action.{act}_{i}", rr.Scalar(float(v)))
elif isinstance(val, list):
# Handle list of values
for i, v in enumerate(val):
if isinstance(v, (float, int)):
rr.log(f"action.{act}_{i}", rr.Scalar(float(v)))
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "84d8ae1881704ebae1ffb70a",
"documentMicroversion": "0eea3500852bdb2f58b1cb79",
"documentVersion": "a5c3b0dfaa52ddd6829011cd",
"elementId": "22efbe4e0bef24fcd20f96e5",
"fullConfiguration": "default",
"id": "MCOhripg0ry51VlsC",
"isStandardContent": false,
"name": "Base_motor_holder_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424
size 1877084
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "bf61a6bc85b1d1a8bf9ea51b",
"documentMicroversion": "20484d37162a32a8a41a37f2",
"documentVersion": "25801b070e5b360715de8a30",
"elementId": "312f32f0073fa6e8e36fba7a",
"fullConfiguration": "default",
"id": "MY69cJlqvSzIiODdH",
"isStandardContent": false,
"name": "Base_SO101 v2 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d
size 471584
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "652d5731024e57367badfda6",
"documentMicroversion": "56a8b8013480c176fd87df8d",
"documentVersion": "984ac31c92cac3664c8effb3",
"elementId": "6fb7b7f9315511b548d670ff",
"fullConfiguration": "default",
"id": "Mf4ZebMr4BkShucFj",
"isStandardContent": false,
"name": "Motor_holder_SO101_Base v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b
size 1129384
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "4bd66da73cacb4d946d43e44",
"documentMicroversion": "2bf56247e58b70e90806e318",
"documentVersion": "df78bb7089f1de7d5588d238",
"elementId": "d7dfe76e402c21bbd8124e43",
"fullConfiguration": "default",
"id": "MN9BZ1p69dQQtKTjq",
"isStandardContent": false,
"name": "Motor_holder_SO101_Wrist v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011
size 1052184
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "46218c02ef80d36172edbb35",
"documentMicroversion": "68b7d387e2500c451586ae59",
"documentVersion": "79c101d1a0207b77362b561a",
"elementId": "d4b1411d5d7333298f6e2458",
"fullConfiguration": "default",
"id": "MrHPLr9hZkrXwcSA4",
"isStandardContent": false,
"name": "Moving_Jaw_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d
size 1413584
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "14078aa6723c502d07d6902e",
"documentMicroversion": "c0fca717407275159bcc6ed7",
"documentVersion": "3d9a887ff68fa477d98162b8",
"elementId": "43d24b3857ff686b275578bf",
"fullConfiguration": "default",
"id": "MrQ6Kmk9QDZlwbp95",
"isStandardContent": false,
"name": "Rotation_Pitch_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34
size 883684
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "56e5f3702dad85e17841d2e2",
"documentMicroversion": "7958a6acbc8e0d0a0a611746",
"documentVersion": "29a4c51b8bf277a22743a333",
"elementId": "8c14fb13a6557ec89ff5d227",
"fullConfiguration": "default",
"id": "MOcaIFg8XgL+Ybg9z",
"isStandardContent": false,
"name": "STS3215_03a_no_horn v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887
size 865884
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "d2941bdba816affebdc6d6f0",
"documentMicroversion": "5904ef3cea04a0d0bc88b698",
"documentVersion": "dd4f7470101215836a4ae8c9",
"elementId": "e670b72d49b06f88fad5dbd8",
"fullConfiguration": "default",
"id": "M5vQNpe0onRFueych",
"isStandardContent": false,
"name": "STS3215_03a v1 <5>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0
size 954084
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "9f5d6db47eb112442b9f130f",
"documentMicroversion": "e99cf45162e34789bd99512b",
"documentVersion": "817ebf29c5663d412edc0753",
"elementId": "2813aaffe3c8a342616d3527",
"fullConfiguration": "default",
"id": "M9yAEiX02J3c4HqXa",
"isStandardContent": false,
"name": "Under_arm_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71
size 1975884
+14
View File
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "57f3eae43434311c28ac752b",
"documentMicroversion": "33eeab136e831427f0f0ca74",
"documentVersion": "435d47b71ef26075bf82672c",
"elementId": "a8e0c02dc43f7ccb373c52e4",
"fullConfiguration": "default",
"id": "Ml3rwO4kV53jDRgcs",
"isStandardContent": false,
"name": "Upper_arm_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f
size 1303484
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "066f8b5064455ec46759cd8c",
"documentMicroversion": "04c5790374bf3edfbbb7e818",
"documentVersion": "408440a116f7d8700bbb11c2",
"elementId": "dc35e56269e36de39738b34d",
"fullConfiguration": "default",
"id": "MjhXxhyF1+iAgCtUh",
"isStandardContent": false,
"name": "WaveShare_Mounting_Plate_SO101 v2 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9
size 62784
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "e02f1e1d3fdd766a19a55890",
"documentMicroversion": "03f1dfc090db6bbecdb14475",
"documentVersion": "8a15327cfbde0344e0951076",
"elementId": "2317bd70c68862eeebd64492",
"fullConfiguration": "default",
"id": "MpI0voU28BOAZ6D9x",
"isStandardContent": false,
"name": "Wrist_Roll_Follower_SO101 v1 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4
size 1439884
@@ -0,0 +1,14 @@
{
"configuration": "default",
"documentId": "eb144d215e733b8dbbb50b81",
"documentMicroversion": "4fef760722dee3a9b5ff19b1",
"documentVersion": "5880c1e9413206cac10772d0",
"elementId": "3c22c2c23cb0ce545b9df2ba",
"fullConfiguration": "default",
"id": "Ma99J59HxnSe2TArb",
"isStandardContent": false,
"name": "Wrist_Roll_Pitch_SO101 v2 <1>",
"partId": "JFD",
"suppressed": false,
"type": "Part"
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae
size 2699784
+453
View File
@@ -0,0 +1,453 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated using onshape-to-robot -->
<!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
<robot name="so101_new_calib">
<!-- Materials -->
<material name="3d_printed">
<color rgba="1.0 0.82 0.12 1.0"/>
</material>
<material name="sts3215">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
<!-- Link base -->
<link name="base_link">
<inertial>
<origin xyz="0.0137179 -5.19711e-05 0.0334843" rpy="0 0 0"/>
<mass value="0.147"/>
<inertia ixx="0.000114686" ixy="-4.59787e-07" ixz="4.97151e-06" iyy="0.000136117" iyz="9.75275e-08" izz="0.000130364"/>
</inertial>
<!-- Part base_motor_holder_so101_v1 -->
<visual>
<origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
<geometry>
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
<geometry>
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
</geometry>
</collision>
<!-- Part base_so101_v2 -->
<visual>
<origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
<geometry>
<mesh filename="assets/base_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
<geometry>
<mesh filename="assets/base_so101_v2.stl"/>
</geometry>
</collision>
<!-- Part sts3215_03a_v1 -->
<visual>
<origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part waveshare_mounting_plate_so101_v2 -->
<visual>
<origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
<geometry>
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
<geometry>
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
</geometry>
</collision>
</link>
<!-- Link shoulder -->
<link name="shoulder_link">
<inertial>
<origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
<mass value="0.100006"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_v1_2 -->
<visual>
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part motor_holder_so101_base_v1 -->
<visual>
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
</geometry>
</collision>
<!-- Part rotation_pitch_so101_v1 -->
<visual>
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
<geometry>
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
<geometry>
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link upper_arm -->
<link name="upper_arm_link">
<inertial>
<origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0"/>
<mass value="0.103"/>
<inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318" iyz="8.97326e-09" izz="0.000142487"/>
</inertial>
<!-- Part sts3215_03a_v1_3 -->
<visual>
<origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part upper_arm_so101_v1 -->
<visual>
<origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
<geometry>
<mesh filename="assets/upper_arm_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
<geometry>
<mesh filename="assets/upper_arm_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link lower_arm -->
<link name="lower_arm_link">
<inertial>
<origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0"/>
<mass value="0.104"/>
<inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844" iyz="-4.90188e-08" izz="0.00014529"/>
</inertial>
<!-- Part under_arm_so101_v1 -->
<visual>
<origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
<geometry>
<mesh filename="assets/under_arm_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
<geometry>
<mesh filename="assets/under_arm_so101_v1.stl"/>
</geometry>
</collision>
<!-- Part motor_holder_so101_wrist_v1 -->
<visual>
<origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
<geometry>
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
<geometry>
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
</geometry>
</collision>
<!-- Part sts3215_03a_v1_4 -->
<visual>
<origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link wrist -->
<link name="wrist_link">
<inertial>
<origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
<mass value="0.079"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_no_horn_v1 -->
<visual>
<origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
</geometry>
</collision>
<!-- Part wrist_roll_pitch_so101_v2 -->
<visual>
<origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
</geometry>
</collision>
</link>
<!-- Link gripper -->
<link name="gripper_link">
<inertial>
<origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
<mass value="0.087"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_v1_5 -->
<visual>
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part wrist_roll_follower_so101_v1 -->
<visual>
<origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
<geometry>
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
<geometry>
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Gripper frame (dummy link + fixed joint) -->
<link name="gripper_frame_link">
<origin xyz="0 0 0" rpy="0 -0 0"/>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="1e-9"/>
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0"/>
</inertial>
</link>
<joint name="gripper_frame_joint" type="fixed">
<origin xyz="-0.0079 -0.000218121 -0.0981274" rpy="0 3.14159 0"/>
<parent link="gripper_link"/>
<child link="gripper_frame_link"/>
<axis xyz="0 0 0"/>
</joint>
<!-- Link moving_jaw_so101_v1 -->
<link name="moving_jaw_so101_v1_link">
<inertial>
<origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
<mass value="0.012"/>
<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"/>
</inertial>
<!-- Part moving_jaw_so101_v1 -->
<visual>
<origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
<geometry>
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
<geometry>
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Joint from gripper to moving_jaw_so101_v1 -->
<joint name="gripper" type="revolute">
<origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -5.24284e-08 -1.41553e-15"/>
<parent link="gripper_link"/>
<child link="moving_jaw_so101_v1_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
</joint>
<transmission name="gripper_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="gripper">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor6">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from wrist to gripper -->
<joint name="wrist_roll" type="revolute">
<origin xyz="5.55112e-17 -0.0611 0.0181" rpy="1.5708 0.0486795 3.14159"/>
<parent link="wrist_link"/>
<child link="gripper_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-2.74385" upper="2.84121"/>
</joint>
<transmission name="wrist_roll_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="wrist_roll">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor5">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from lower_arm to wrist -->
<joint name="wrist_flex" type="revolute">
<origin xyz="-0.1349 0.0052 3.62355e-17" rpy="4.02456e-15 8.67362e-16 -1.5708"/>
<parent link="lower_arm_link"/>
<child link="wrist_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
</joint>
<transmission name="wrist_flex_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="wrist_flex">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor4">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from upper_arm to lower_arm -->
<!-- Note: 5-degree calibration offset applied to joint limits -->
<joint name="elbow_flex" type="revolute">
<origin xyz="-0.11257 -0.028 1.73763e-16" rpy="-3.63608e-16 8.74301e-16 1.5708"/>
<parent link="upper_arm_link"/>
<child link="lower_arm_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.69" upper="1.69"/>
</joint>
<transmission name="elbow_flex_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="elbow_flex">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor3">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from shoulder to upper_arm -->
<joint name="shoulder_lift" type="revolute">
<origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 -1.5708 0"/>
<parent link="shoulder_link"/>
<child link="upper_arm_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.74533" upper="1.74533"/>
</joint>
<transmission name="shoulder_lift_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="shoulder_lift">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor2">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from base to shoulder -->
<joint name="shoulder_pan" type="revolute">
<origin xyz="0.0388353 -8.97657e-09 0.0624" rpy="3.14159 4.18253e-17 -3.14159"/>
<parent link="base_link"/>
<child link="shoulder_link"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
</joint>
<transmission name="shoulder_pan_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="shoulder_pan">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor1">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
</robot>
+435
View File
@@ -0,0 +1,435 @@
<?xml version='1.0' encoding='utf-8'?>
<!-- Generated using onshape-to-robot -->
<!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
<robot name="so101_old_calib">
<!-- Materials -->
<material name="3d_printed">
<color rgba="1.0 0.82 0.12 1.0"/>
</material>
<material name="sts3215">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
<!-- Link base -->
<link name="base">
<inertial>
<origin xyz="0.020739 0.00204287 0.065966" rpy="0 0 0"/>
<mass value="0.147"/>
<inertia ixx="0.000136117" ixy="4.59787e-07" ixz="9.75275e-08" iyy="0.000114686" iyz="-4.97151e-06" izz="0.000130364"/>
</inertial>
<!-- Part base_motor_holder_so101_v1 -->
<visual>
<origin xyz="0.0206915 0.0221255 0.0300817" rpy="1.5708 -1.23909e-16 2.33147e-15"/>
<geometry>
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.0206915 0.0221255 0.0300817" rpy="1.5708 -1.23909e-16 2.33147e-15"/>
<geometry>
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
</geometry>
</collision>
<!-- Part base_so101_v2 -->
<visual>
<origin xyz="0.0207909 0.0221255 0.0300817" rpy="1.5708 -0 0"/>
<geometry>
<mesh filename="assets/base_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.0207909 0.0221255 0.0300817" rpy="1.5708 -0 0"/>
<geometry>
<mesh filename="assets/base_so101_v2.stl"/>
</geometry>
</collision>
<!-- Part sts3215_03a_v1 -->
<visual>
<origin xyz="0.0207909 -0.0105745 0.0761817" rpy="-2.20282e-15 2.77556e-17 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="0.0207909 -0.0105745 0.0761817" rpy="-2.20282e-15 2.77556e-17 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part waveshare_mounting_plate_so101_v2 -->
<visual>
<origin xyz="0.0205915 0.0467435 0.0798817" rpy="1.5708 -1.21716e-14 2.33147e-15"/>
<geometry>
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.0205915 0.0467435 0.0798817" rpy="1.5708 -1.21716e-14 2.33147e-15"/>
<geometry>
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
</geometry>
</collision>
</link>
<!-- Link shoulder -->
<link name="shoulder">
<inertial>
<origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
<mass value="0.100006"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_v1_2 -->
<visual>
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part motor_holder_so101_base_v1 -->
<visual>
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
</geometry>
</collision>
<!-- Part rotation_pitch_so101_v1 -->
<visual>
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 -0 0"/>
<geometry>
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 -0 0"/>
<geometry>
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link upper_arm -->
<link name="upper_arm">
<inertial>
<origin xyz="-0.00838224 0.0898471 0.0184089" rpy="0 0 0"/>
<mass value="0.103"/>
<inertia ixx="0.000147318" ixy="1.97819e-05" ixz="8.97326e-09" iyy="4.08002e-05" iyz="4.03016e-08" izz="0.000142487"/>
</inertial>
<!-- Part sts3215_03a_v1_3 -->
<visual>
<origin xyz="-0.0155 0.11257 0.0187" rpy="-3.14159 -3.22659e-15 -3.14159"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.0155 0.11257 0.0187" rpy="-3.14159 -3.22659e-15 -3.14159"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part upper_arm_so101_v1 -->
<visual>
<origin xyz="0.012 0.065085 0.0182" rpy="3.14159 -1.22125e-15 -1.5708"/>
<geometry>
<mesh filename="assets/upper_arm_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.012 0.065085 0.0182" rpy="3.14159 -1.22125e-15 -1.5708"/>
<geometry>
<mesh filename="assets/upper_arm_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link lower_arm -->
<link name="lower_arm">
<inertial>
<origin xyz="-0.00324376 -0.0980701 0.0182831" rpy="0 0 0"/>
<mass value="0.104"/>
<inertia ixx="0.000159844" ixy="-7.41152e-06" ixz="4.90188e-08" iyy="2.87438e-05" iyz="1.26409e-06" izz="0.00014529"/>
</inertial>
<!-- Part under_arm_so101_v1 -->
<visual>
<origin xyz="0.032 -0.0648499 0.0182" rpy="3.14159 -1.11022e-16 1.5708"/>
<geometry>
<mesh filename="assets/under_arm_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.032 -0.0648499 0.0182" rpy="3.14159 -1.11022e-16 1.5708"/>
<geometry>
<mesh filename="assets/under_arm_so101_v1.stl"/>
</geometry>
</collision>
<!-- Part motor_holder_so101_wrist_v1 -->
<visual>
<origin xyz="0.032 -0.0648499 0.018" rpy="-3.14159 -1.11022e-16 1.5708"/>
<geometry>
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0.032 -0.0648499 0.018" rpy="-3.14159 -1.11022e-16 1.5708"/>
<geometry>
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
</geometry>
</collision>
<!-- Part sts3215_03a_v1_4 -->
<visual>
<origin xyz="-0.0052 -0.1224 0.0187" rpy="3.14159 -8.24341e-15 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="-0.0052 -0.1224 0.0187" rpy="3.14159 -8.24341e-15 -1.5708"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link wrist -->
<link name="wrist">
<inertial>
<origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
<mass value="0.079"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_no_horn_v1 -->
<visual>
<origin xyz="0 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="0 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
<geometry>
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
</geometry>
</collision>
<!-- Part wrist_roll_pitch_so101_v2 -->
<visual>
<origin xyz="-2.77556e-17 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="-2.77556e-17 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
<geometry>
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
</geometry>
</collision>
</link>
<!-- Link gripper -->
<link name="gripper">
<inertial>
<origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
<mass value="0.087"/>
<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"/>
</inertial>
<!-- Part sts3215_03a_v1_5 -->
<visual>
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.55112e-17 -1.38213e-14"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
<material name="sts3215"/>
</visual>
<collision>
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.55112e-17 -1.38213e-14"/>
<geometry>
<mesh filename="assets/sts3215_03a_v1.stl"/>
</geometry>
</collision>
<!-- Part wrist_roll_follower_so101_v1 -->
<visual>
<origin xyz="1.11022e-16 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 -6.22813e-24"/>
<geometry>
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="1.11022e-16 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 -6.22813e-24"/>
<geometry>
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Link jaw -->
<link name="jaw">
<inertial>
<origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
<mass value="0.012"/>
<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"/>
</inertial>
<!-- Part moving_jaw_so101_v1 -->
<visual>
<origin xyz="0 1.89351e-17 0.0189" rpy="9.53145e-17 -1.20743e-23 0"/>
<geometry>
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
</geometry>
<material name="3d_printed"/>
</visual>
<collision>
<origin xyz="0 1.89351e-17 0.0189" rpy="9.53145e-17 -1.20743e-23 0"/>
<geometry>
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
</geometry>
</collision>
</link>
<!-- Joint from gripper to jaw -->
<joint name="gripper" type="revolute">
<origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -7.91661e-17 -1.0396e-16"/>
<parent link="gripper"/>
<child link="jaw"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
</joint>
<transmission name="gripper_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="gripper">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor6">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from wrist to gripper -->
<joint name="wrist_roll" type="revolute">
<origin xyz="2.77556e-17 -0.0611 0.0181" rpy="1.5708 -9.38083e-08 3.14159"/>
<parent link="wrist"/>
<child link="gripper"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-2.79253" upper="2.79253"/>
</joint>
<transmission name="wrist_roll_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="wrist_roll">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor5">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from lower_arm to wrist -->
<joint name="wrist_flex" type="revolute">
<origin xyz="-0.0052 -0.1349 3.40439e-16" rpy="-1.41553e-15 4.91611e-15 7.56157e-09"/>
<parent link="lower_arm"/>
<child link="wrist"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
</joint>
<transmission name="wrist_flex_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="wrist_flex">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor4">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from upper_arm to lower_arm -->
<joint name="elbow_flex" type="revolute">
<origin xyz="-0.028 0.11257 1.24466e-16" rpy="1.89432e-15 -3.22659e-15 -3.14159"/>
<parent link="upper_arm"/>
<child link="lower_arm"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-0.174533" upper="3.14159"/>
</joint>
<transmission name="elbow_flex_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="elbow_flex">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor3">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from shoulder to upper_arm -->
<joint name="shoulder_lift" type="revolute">
<origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 1.5708 0"/>
<parent link="shoulder"/>
<child link="upper_arm"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-3.31613" upper="0.174533"/>
</joint>
<transmission name="shoulder_lift_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="shoulder_lift">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor2">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<!-- Joint from base to shoulder -->
<joint name="shoulder_pan" type="revolute">
<origin xyz="0.0207909 -0.0230745 0.0948817" rpy="-3.14159 6.03684e-16 1.5708"/>
<parent link="base"/>
<child link="shoulder"/>
<axis xyz="0 0 1"/>
<limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
</joint>
<transmission name="shoulder_pan_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="shoulder_pan">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name="motor1">
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
</robot>