Skip to content

Commit 88d19b0

Browse files
committed
Add kinimatics class, update dm_driver, cleanup README
1 parent 632f567 commit 88d19b0

38 files changed

+533
-267
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ sh scripts/reset_all_can.sh
4343

4444
## Test YAM Zero Gravity mode
4545

46+
This enables you to launch the robot in zero gravity mode.
4647
```bash
4748
python i2rt/robots/motor_chain_robot.py --model yam --channel can0 --operation_mode gravity_comp
4849
```
4950

50-
5151
## YAM Robot Arm Usage
5252

5353
### Getting started

doc/set_persist_id_socket_can.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
# Socket can is easy to use, but the default devie name is can{idx} and will vary depend on the order the device got connected to the computer. Below is the precedure of setting up persist ID for those socket CAN devices.
2-
3-
## For canable devices, goto https://canable.io/updater/ to flush its firmware to candlelight to use socketcan, YAM comes with pre-flushed candlelight firmware.
1+
# Setting Persistent IDs for SocketCAN Devices
42

3+
SocketCAN is easy to use, but the default device name is `can{idx}`, which can vary depending on the order in which the device is connected to the computer. Below is the procedure for setting up persistent IDs for these SocketCAN devices.
54

6-
## Step1: find sysfd paths for can devices
5+
## For Canable Devices
6+
Visit [Canable Updater](https://canable.io/updater/) to flash the firmware to candlelight to use with SocketCAN. YAM comes with pre-flashed candlelight firmware.
7+
8+
## Step 1: Find sysfd Paths for CAN Devices
79

810
```shell
911
$ ls -l /sys/class/net/can*
1012
```
1113

12-
this should give you something liks
14+
This should give you an output similar to:
1315
```shell
1416
lrwxrwxrwx 1 root root 0 Jul 15 14:35 /sys/class/net/can0 -> ../../devices/platform/soc/your_can_device/can0
1517
lrwxrwxrwx 1 root root 0 Jul 15 14:35 /sys/class/net/can1 -> ../../devices/platform/soc/your_can_device/can1
1618
lrwxrwxrwx 1 root root 0 Jul 15 14:35 /sys/class/net/can2 -> ../../devices/platform/soc/your_can_device/can2
1719
```
1820

19-
## Step 2: Use udevadm to Gather Attributes
21+
## Step 2: Use `udevadm` to Gather Attributes
2022
```shell
2123
udevadm info -a -p /sys/class/net/can0 | grep -i serial
2224
```
@@ -32,21 +34,21 @@ SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="004E00275548501220373234", NAME
3234
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="0031005F5548501220373234", NAME="can_follow_r"
3335
```
3436

35-
IMPORTANT!!!: Name should start with can (for USB-CAN adapter) or en/eth (for EtherCAT-CAN adapter). And the maximum length limit for a CAN interface name is 13 characters.
37+
**Important:** The name should start with `can` (for USB-CAN adapters) or `en`/`eth` (for EtherCAT-CAN adapters). The maximum length for a CAN interface name is 13 characters.
3638

3739
## Step 4: Reload udev Rules
3840
```shell
3941
sudo udevadm control --reload-rules && sudo systemctl restart systemd-udevd && sudo udevadm trigger
4042
```
4143

42-
If needed, plug unplug the candevice to make sure the change is effective.
44+
Unplug and replug the CAN device to ensure the changes take effect.
4345

44-
run the following command to set up the can device, and you need to run this command after every reboot.
46+
Run the following command to set up the CAN device, and you need to run this command after every reboot.
4547
```
4648
sudo ip link set up can_right type can bitrate 1000000
4749
```
4850

49-
## Step 5: Verify the can device
51+
## Step 5: Verify the CAN device
5052
```shell
5153
$ ip link show
5254
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
@@ -61,5 +63,4 @@ $ ip link show
6163
link/can
6264
```
6365

64-
You can see that this can device got it's name can_right/can_left
65-
66+
You should see that the CAN device is named `can_right`/`can_left`.

i2rt/motor_drivers/dm_driver.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import can
1010
import numpy as np
1111

12+
# set control frequence
13+
CONTROL_FREQ = 250
14+
CONTROL_PERIOD = 1.0 / CONTROL_FREQ # 4 ms
15+
1216

1317
@dataclass
1418
class MotorConstants:
@@ -33,6 +37,7 @@ class MotorType:
3337
DM4310V = "DM4310V"
3438
DM4340 = "DM4340"
3539
DMH6215 = "DMH6215"
40+
DM3507 = "DM3507"
3641

3742
@classmethod
3843
def get_motor_constants(cls, motor_type: str) -> MotorConstants:
@@ -85,6 +90,15 @@ def get_motor_constants(cls, motor_type: str) -> MotorConstants:
8590
TORQUE_MAX=10,
8691
TORQUE_MIN=-10,
8792
)
93+
elif motor_type == cls.DM3507:
94+
return MotorConstants(
95+
POSITION_MAX=12.5,
96+
POSITION_MIN=-12.5,
97+
VELOCITY_MAX=50,
98+
VELOCITY_MIN=-50,
99+
TORQUE_MAX=5,
100+
TORQUE_MIN=-5,
101+
)
88102
else:
89103
raise ValueError(f"Motor type '{motor_type}' not recognized.")
90104

@@ -484,6 +498,7 @@ def parse_recv_message(self, message: can.Message, motor_type: str) -> FeedbackF
484498
"""
485499
data = message.data
486500
error_int = (data[0] & 0xF0) >> 4 # TODO: error code seems incorrect, double check
501+
487502
# convert error into hex
488503
error_hex = hex(error_int)
489504
error_message = MotorErrorCode.get_error_message(error_int)
@@ -652,46 +667,32 @@ def start_thread(self) -> None:
652667
print("waiting for the first state")
653668

654669
def _set_torques_and_update_state(self) -> None:
655-
last_time = time.time()
656-
iteration_count = 0
657-
self.motor_interface.try_receive_message()
670+
last_step_time = time.time()
658671
while self.running:
659672
try:
660-
current_time = time.time()
661-
elapsed_time = current_time - last_time
673+
# Maintain desired control frequency.
674+
while time.time() - last_step_time < CONTROL_PERIOD - 0.001:
675+
time.sleep(0.001)
676+
curr_time = time.time()
677+
step_time = curr_time - last_step_time
678+
last_step_time = curr_time
679+
if step_time > 0.005: # 5 ms
680+
print(f"Warning: Step time {1000 * step_time:.3f} ms in {self.__class__.__name__} control_loop")
681+
682+
# Update state.
662683
with self.command_lock:
663684
motor_feedback = self._set_commands(self.commands)
664685
with self.state_lock:
665686
self.state = motor_feedback
666-
667-
self._update_absolute_positions(motor_feedback)
668-
time.sleep(0.001)
669-
iteration_count += 1
670-
# Optionally reset counter and timer every second or another fixed interval
671-
# to keep the displayed frequency relevant to a recent time window
672-
if elapsed_time >= 10.0:
673-
control_frequency = iteration_count / elapsed_time
674-
675-
# # Overwrite the current line with the new frequency information
676-
# sys.stdout.write(f"\rControl Frequency: {control_frequency:.2f} Hz")
677-
# sys.stdout.flush() # Ensure it's displayed
678-
679-
# print(f"Control Frequency: {control_frequency:.2f} Hz")
680-
681-
# Reset the counter and timer
682-
last_time = current_time
683-
iteration_count = 0
684-
687+
self._update_absolute_positions(motor_feedback)
685688
except Exception as e:
686689
print(f"DM Error in control loop: {e}")
687690
raise e
688691

689692
def _set_commands(self, commands: List[MotorCmd]) -> List[MotorInfo]:
690-
# for joint 0 and joint1
691693
motor_feedback = []
692694
for idx, motor_info in enumerate(self.motor_list):
693695
motor_id, motor_type = motor_info
694-
# torque = torques[idx] * self.motor_direction[idx]
695696
torque = commands[idx].torque * self.motor_direction[idx]
696697
pos = self._joint_position_sim_to_real_idx(commands[idx].pos, idx)
697698

@@ -713,8 +714,6 @@ def _set_commands(self, commands: List[MotorCmd]) -> List[MotorInfo]:
713714
raise e
714715

715716
motor_feedback.append(fd_back)
716-
# motor_pos.append(fd_back.position)
717-
# motor_vel.append(fd_back.velocity * self.motor_direction[idx])
718717
return motor_feedback
719718

720719
def read_states(self, torques: Optional[np.ndarray] = None) -> List[MotorInfo]:

i2rt/robots/kinematics.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import time
2+
from typing import List, Optional, Tuple
3+
4+
import mink
5+
import mujoco
6+
import numpy as np
7+
8+
9+
class Kinematics:
10+
def __init__(self, xml_path: str, site_name: Optional[str]):
11+
"""Initialize the Kinematics object.
12+
13+
Args:
14+
xml_path (str): Path to the MuJoCo XML model file.
15+
site_name (Optional[str]): Name of the site for which to compute the forward kinematics.
16+
"""
17+
model = mujoco.MjModel.from_xml_path(xml_path)
18+
self._configuration = mink.Configuration(model)
19+
self._site_name = site_name
20+
21+
def fk(self, q: np.ndarray, site_name: Optional[str] = None) -> np.ndarray:
22+
"""Compute the forward kinematics for the given joint configuration.
23+
24+
Args:
25+
q (np.ndarray): The joint configuration.
26+
site_name (Optional[str]): Name of the site for which to compute the forward kinematics.
27+
If not provided, the default site name is used.
28+
29+
Returns:
30+
(np.ndarray): Site frame in world frame. Shape: (4, 4)
31+
"""
32+
self._configuration.update(q)
33+
site_name = site_name or self._site_name
34+
assert site_name is not None, "site_name must be provided"
35+
return self._configuration.get_transform_frame_to_world(site_name, "site").as_matrix()
36+
37+
def ik(
38+
self,
39+
target_pose: np.ndarray,
40+
site_name: str,
41+
init_q: Optional[np.ndarray] = None,
42+
limits: Optional[List[mink.Limit]] = None,
43+
dt: float = 0.01,
44+
solver: str = "quadprog",
45+
pos_threshold: float = 1e-4,
46+
ori_threshold: float = 1e-4,
47+
damping: float = 1e-4,
48+
max_iters: int = 200,
49+
verbose: bool = False,
50+
) -> Tuple[bool, np.ndarray]:
51+
"""Differential ik solver, leverging mink.
52+
53+
Args:
54+
target_pose (np.ndarray): The target pose to reach.
55+
site_name (str): Name of the desired site.
56+
limits (List[mink.Limit]): List of limits to enforce.
57+
init_q (Optional[np.ndarray]): Initial joint configuration.
58+
dt (float): Integration timestep in [s].
59+
solver (str): Quadratic program solver.
60+
pos_threshold (float): Position threshold for convergence.
61+
ori_threshold (float): Orientation threshold for convergence.
62+
damping (float): Levenberg-Marquardt damping.
63+
max_iters (int): Maximum number of iterations.
64+
verbose (bool): Whether to print debug information.
65+
66+
Returns:
67+
Tuple[bool, np.ndarray]: Success flag and the converged joint configuration.
68+
"""
69+
if init_q is not None:
70+
self._configuration.update(init_q)
71+
72+
end_effector_task = mink.FrameTask(
73+
frame_name=site_name,
74+
frame_type="site",
75+
position_cost=1.0,
76+
orientation_cost=1.0,
77+
lm_damping=1.0,
78+
)
79+
80+
end_effector_task.set_target(mink.SE3.from_matrix(target_pose))
81+
tasks = [end_effector_task]
82+
83+
start_time = time.time() # Start timing
84+
85+
for j in range(max_iters):
86+
vel = mink.solve_ik(self._configuration, tasks, dt, solver, damping=damping, limits=limits)
87+
self._configuration.integrate_inplace(vel, dt)
88+
err = end_effector_task.compute_error(self._configuration)
89+
90+
pos_achieved = np.linalg.norm(err[:3]) <= pos_threshold
91+
ori_achieved = np.linalg.norm(err[3:]) <= ori_threshold
92+
if pos_achieved and ori_achieved:
93+
end_time = time.time() # End timing
94+
elapsed_time = end_time - start_time
95+
if verbose:
96+
print(
97+
f"Exiting after {j} iterations, configuration: {self._configuration.q}, time taken: {elapsed_time:.4f} seconds"
98+
)
99+
return True, self._configuration.q
100+
101+
end_time = time.time() # End timing
102+
elapsed_time = end_time - start_time
103+
if verbose:
104+
print(
105+
f"Failed to converge after {max_iters} iterations, time taken: {elapsed_time:.4f} seconds, pos_err: {err[:3]}, rot_err: {err[3:]}"
106+
)
107+
return False, self._configuration.q
108+
109+
110+
def main() -> None:
111+
from i2rt.robots.motor_chain_robot import YAM_XML_PATH
112+
113+
mj_model = Kinematics(YAM_XML_PATH, "grasp_site")
114+
q = np.zeros(6)
115+
pose = mj_model.fk(q)
116+
print(pose)
117+
118+
pose[0, 3] -= 0.1
119+
pose[2, 3] += 0.1
120+
print(pose)
121+
q_ik = mj_model.ik(pose, "grasp_site")
122+
print(q_ik)
123+
124+
125+
if __name__ == "__main__":
126+
main()

i2rt/robots/motor_chain_robot.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from i2rt.utils.mujoco_utils import MuJoCoKDL
1919

2020
I2RT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21-
YAM_XML_PATH = os.path.join(I2RT_ROOT, "robot_models/yam/yam.urdf")
22-
ARX_XML_PATH = os.path.join(I2RT_ROOT, "robot_models/arx_r5/arx.urdf")
21+
YAM_XML_PATH = os.path.join(I2RT_ROOT, "robot_models/yam/yam.xml")
22+
ARX_XML_PATH = os.path.join(I2RT_ROOT, "robot_models/arx_r5/arx.xml")
2323

2424

2525
@dataclass
@@ -358,6 +358,8 @@ def get_observations(self) -> Dict[str, np.ndarray]:
358358
if self._gripper_index is None:
359359
return {
360360
"joint_pos": self._joint_state.pos,
361+
"joint_vel": self._joint_state.vel,
362+
"joint_eff": self._joint_state.eff,
361363
}
362364
else:
363365
return {
@@ -416,7 +418,7 @@ def get_yam_robot(channel: str = "can0", model_path: str = YAM_XML_PATH) -> Moto
416418
motor_chain,
417419
xml_path=model_path,
418420
use_gravity_comp=True,
419-
gravity_comp_factor=1.2,
421+
gravity_comp_factor=1.3,
420422
gripper_index=6,
421423
gripper_limits=np.array([0.0, -2.7]),
422424
kp=np.array([80, 80, 80, 40, 10, 10, 20]),
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import numpy as np
2+
import pytest
3+
4+
from i2rt.robots.kinematics import Kinematics
5+
from i2rt.robots.motor_chain_robot import YAM_XML_PATH
6+
7+
8+
@pytest.fixture
9+
def kinematics_yam() -> Kinematics:
10+
return Kinematics(YAM_XML_PATH, "grasp_site")
11+
12+
13+
def test_fk(kinematics_yam: Kinematics) -> None:
14+
q = np.zeros(6)
15+
pose = kinematics_yam.fk(q)
16+
assert pose.shape == (4, 4), "FK should return a 4x4 matrix"
17+
18+
# Add more assertions based on expected pose values
19+
rotation = pose[:3, :3]
20+
translation = pose[:3, 3]
21+
22+
start_rot = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
23+
start_trans = np.array([0.245, 0.0, 0.164])
24+
np.testing.assert_allclose(rotation, start_rot, atol=1e-5)
25+
np.testing.assert_allclose(translation, start_trans, atol=1e-5)
26+
27+
28+
def test_ik_smoke(kinematics_yam: Kinematics) -> None:
29+
q = np.zeros(6)
30+
pose = kinematics_yam.fk(q)
31+
pose[0, 3] -= 0.1
32+
pose[2, 3] += 0.1
33+
success, q_ik = kinematics_yam.ik(pose, "grasp_site")
34+
assert success, "IK should succeed"
35+
assert q_ik.shape == (6,), "IK should return a joint configuration of size 6"
36+
37+
38+
def test_cycle(kinematics_yam: Kinematics) -> None:
39+
q = np.zeros(6)
40+
pose = kinematics_yam.fk(q)
41+
success, q_ik = kinematics_yam.ik(pose, "grasp_site")
42+
assert success, "IK should succeed"
43+
pose_reconstructed = kinematics_yam.fk(q_ik)
44+
np.testing.assert_allclose(pose, pose_reconstructed, atol=1e-5)

0 commit comments

Comments
 (0)