pylekiwi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. pylekiwi-0.1.0/PKG-INFO +33 -0
  2. pylekiwi-0.1.0/README.md +15 -0
  3. pylekiwi-0.1.0/pyproject.toml +28 -0
  4. pylekiwi-0.1.0/src/pylekiwi/__init__.py +12 -0
  5. pylekiwi-0.1.0/src/pylekiwi/arm_controller.py +72 -0
  6. pylekiwi-0.1.0/src/pylekiwi/base_controller.py +102 -0
  7. pylekiwi-0.1.0/src/pylekiwi/commands/__init__.py +0 -0
  8. pylekiwi-0.1.0/src/pylekiwi/commands/launch_host_controller.py +16 -0
  9. pylekiwi-0.1.0/src/pylekiwi/commands/launch_web_ui.py +13 -0
  10. pylekiwi-0.1.0/src/pylekiwi/commands/web_ui.py +462 -0
  11. pylekiwi-0.1.0/src/pylekiwi/data/SO101/README.md +35 -0
  12. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/base_motor_holder_so101_v1.ply +0 -0
  13. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/base_so101_v2.ply +0 -0
  14. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/motor_holder_so101_base_v1.ply +0 -0
  15. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/motor_holder_so101_wrist_v1.ply +0 -0
  16. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/moving_jaw_so101_v1.ply +0 -0
  17. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/rotation_pitch_so101_v1.ply +0 -0
  18. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/sts3215_03a_no_horn_v1.ply +0 -0
  19. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/sts3215_03a_v1.ply +0 -0
  20. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/under_arm_so101_v1.ply +0 -0
  21. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/upper_arm_so101_v1.ply +0 -0
  22. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/waveshare_mounting_plate_so101_v2.ply +0 -0
  23. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/wrist_roll_follower_so101_v1.ply +0 -0
  24. pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/wrist_roll_pitch_so101_v2.ply +0 -0
  25. pylekiwi-0.1.0/src/pylekiwi/data/SO101/joints_properties.xml +12 -0
  26. pylekiwi-0.1.0/src/pylekiwi/data/SO101/scene.xml +24 -0
  27. pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_new_calib.urdf +453 -0
  28. pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_new_calib.xml +162 -0
  29. pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_old_calib.urdf +435 -0
  30. pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_old_calib.xml +160 -0
  31. pylekiwi-0.1.0/src/pylekiwi/filter.py +11 -0
  32. pylekiwi-0.1.0/src/pylekiwi/models.py +37 -0
  33. pylekiwi-0.1.0/src/pylekiwi/nodes.py +60 -0
  34. pylekiwi-0.1.0/src/pylekiwi/py.typed +0 -0
  35. pylekiwi-0.1.0/src/pylekiwi/settings.py +7 -0
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.3
2
+ Name: pylekiwi
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: neka-nat
6
+ Author-email: neka-nat <nekanat.stock@gmail.com>
7
+ Requires-Dist: eclipse-zenoh>=1.5.1
8
+ Requires-Dist: gunicorn>=23.0.0
9
+ Requires-Dist: kinpy>=0.5.4
10
+ Requires-Dist: loguru>=0.7.3
11
+ Requires-Dist: mesop>=1.1.0
12
+ Requires-Dist: numpy>=2.3.3
13
+ Requires-Dist: opencv-python-headless>=4.11.0.86
14
+ Requires-Dist: pydantic>=2.11.10
15
+ Requires-Dist: rustypot>=1.3.0
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pylekiwi
20
+
21
+ Python package for controlling the LeKiwi robot.
22
+
23
+ ## Quick Start
24
+
25
+ Log into the robot and run the following command:
26
+
27
+ ```bash
28
+ ssh <your robot ip>
29
+ sudo chmod 666 /dev/ttyACM0
30
+ uvx launch-lekiwi-webui --serial-port /dev/ttyACM0
31
+ ```
32
+
33
+ Then, open a web browser and navigate to `http://<your robot ip>:8080` to see the web UI.
@@ -0,0 +1,15 @@
1
+ # pylekiwi
2
+
3
+ Python package for controlling the LeKiwi robot.
4
+
5
+ ## Quick Start
6
+
7
+ Log into the robot and run the following command:
8
+
9
+ ```bash
10
+ ssh <your robot ip>
11
+ sudo chmod 666 /dev/ttyACM0
12
+ uvx launch-lekiwi-webui --serial-port /dev/ttyACM0
13
+ ```
14
+
15
+ Then, open a web browser and navigate to `http://<your robot ip>:8080` to see the web UI.
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "pylekiwi"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "neka-nat", email = "nekanat.stock@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "eclipse-zenoh>=1.5.1",
12
+ "gunicorn>=23.0.0",
13
+ "kinpy>=0.5.4",
14
+ "loguru>=0.7.3",
15
+ "mesop>=1.1.0",
16
+ "numpy>=2.3.3",
17
+ "opencv-python-headless>=4.11.0.86",
18
+ "pydantic>=2.11.10",
19
+ "rustypot>=1.3.0",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.8.15,<0.9.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [project.scripts]
27
+ launch-lekiwi-host = "pylekiwi.commands.launch_host_controller:main"
28
+ launch-lekiwi-webui = "pylekiwi.commands.launch_web_ui:main"
@@ -0,0 +1,12 @@
1
+ from pylekiwi.arm_controller import ArmController
2
+ from pylekiwi.base_controller import BaseController
3
+ from pylekiwi.models import ArmJointCommand, BaseCommand
4
+ from pylekiwi.settings import Settings
5
+
6
+ __all__ = [
7
+ "ArmController",
8
+ "ArmJointCommand",
9
+ "BaseController",
10
+ "BaseCommand",
11
+ "Settings",
12
+ ]
@@ -0,0 +1,72 @@
1
+ import os
2
+
3
+ import numpy as np
4
+ from loguru import logger
5
+ from rustypot import Sts3215PyController
6
+
7
+ from kinpy import build_serial_chain_from_mjcf
8
+
9
+ from pylekiwi.models import ArmJointCommand, ArmState
10
+ from pylekiwi.settings import Settings
11
+
12
+
13
+ _MODEL_FILE = os.path.join(os.path.dirname(__file__), "data/SO101/so101_new_calib.xml")
14
+
15
+
16
+ class ArmController:
17
+ JOINT_IDS = (1, 2, 3, 4, 5)
18
+ GRIPPER_ID = 6
19
+
20
+ def __init__(self, motor_controller: Sts3215PyController | Settings | None = None):
21
+ if motor_controller is None:
22
+ settings = Settings() if not isinstance(motor_controller, Settings) else motor_controller
23
+ motor_controller = Sts3215PyController(
24
+ serial_port=settings.serial_port,
25
+ baudrate=settings.baudrate,
26
+ timeout=settings.timeout,
27
+ )
28
+ elif isinstance(motor_controller, Settings):
29
+ motor_controller = Sts3215PyController(
30
+ serial_port=motor_controller.serial_port,
31
+ baudrate=motor_controller.baudrate,
32
+ timeout=motor_controller.timeout,
33
+ )
34
+ self.motor_controller = motor_controller
35
+
36
+ # self.chain = build_serial_chain_from_mjcf(
37
+ # open(_MODEL_FILE, "rb").read(),
38
+ # "gripper",
39
+ # model_dir=os.path.dirname(_MODEL_FILE),
40
+ # )
41
+
42
+ def set_torque(self):
43
+ for i in self.JOINT_IDS:
44
+ self.motor_controller.write_torque_enable(i, True)
45
+ self.motor_controller.write_torque_enable(self.GRIPPER_ID, True)
46
+ all_ids = list(self.JOINT_IDS) + [self.GRIPPER_ID]
47
+ self.motor_controller.sync_write_maximum_acceleration(
48
+ all_ids, [600] * len(all_ids)
49
+ )
50
+
51
+ def disable_torque(self):
52
+ for i in self.JOINT_IDS:
53
+ self.motor_controller.write_torque_enable(i, False)
54
+ self.motor_controller.write_torque_enable(self.GRIPPER_ID, False)
55
+
56
+ def get_current_state(self) -> ArmState:
57
+ all_ids = list(self.JOINT_IDS) + [self.GRIPPER_ID]
58
+ joint_angles = self.motor_controller.sync_read_present_position(all_ids)
59
+ gripper_position = joint_angles[-1]
60
+ joint_angles = joint_angles[:-1]
61
+ logger.debug(f"Joint angles: {joint_angles}, Gripper position: {gripper_position}")
62
+ return ArmState(joint_angles=tuple(joint_angles), gripper_position=gripper_position)
63
+
64
+ def send_joint_action(self, action: ArmJointCommand):
65
+ target_ids = list(self.JOINT_IDS)
66
+ if action.gripper_position is not None:
67
+ target_ids += [self.GRIPPER_ID]
68
+ command = list(action.joint_angles)
69
+ if action.gripper_position is not None:
70
+ command += [action.gripper_position]
71
+ logger.debug(f"Arm command: {command}")
72
+ self.motor_controller.sync_write_goal_position(target_ids, command)
@@ -0,0 +1,102 @@
1
+ import numpy as np
2
+ from loguru import logger
3
+ from rustypot import Sts3215PyController
4
+
5
+ from pylekiwi.models import BaseCommand
6
+ from pylekiwi.settings import Settings
7
+
8
+
9
+ class BaseController:
10
+ LEFT_WHEEL_ID = 7
11
+ BACK_WHEEL_ID = 8
12
+ RIGHT_WHEEL_ID = 9
13
+
14
+ # 角度φの定義(+x 前方を 0°、反時計回り)
15
+ _PHI_DEG = np.array([60.0, 180.0, 300.0]) # [LEFT, BACK, RIGHT]
16
+
17
+ def __init__(self, motor_controller: Sts3215PyController | Settings | None = None):
18
+ if motor_controller is None:
19
+ settings = Settings() if not isinstance(motor_controller, Settings) else motor_controller
20
+ motor_controller = Sts3215PyController(
21
+ serial_port=settings.serial_port,
22
+ baudrate=settings.baudrate,
23
+ timeout=settings.timeout,
24
+ )
25
+ elif isinstance(motor_controller, Settings):
26
+ motor_controller = Sts3215PyController(
27
+ serial_port=motor_controller.serial_port,
28
+ baudrate=motor_controller.baudrate,
29
+ timeout=motor_controller.timeout,
30
+ )
31
+ self.motor_controller = motor_controller
32
+
33
+ def set_torque(self):
34
+ for i in (self.LEFT_WHEEL_ID, self.BACK_WHEEL_ID, self.RIGHT_WHEEL_ID):
35
+ self.motor_controller.write_torque_enable(i, True)
36
+
37
+ def disable_torque(self):
38
+ for i in (self.LEFT_WHEEL_ID, self.BACK_WHEEL_ID, self.RIGHT_WHEEL_ID):
39
+ self.motor_controller.write_torque_enable(i, False)
40
+
41
+ @staticmethod
42
+ def _saturate_radps(w_radps: np.ndarray, max_radps: float = 10.0) -> np.ndarray:
43
+ m = np.max(np.abs(w_radps))
44
+ return w_radps if m <= max_radps else (w_radps * (max_radps / m))
45
+
46
+ @classmethod
47
+ def _body_to_wheel_radps(
48
+ cls,
49
+ vx: float, # m/s
50
+ vy: float, # m/s
51
+ vtheta_deg: float, # deg/s
52
+ wheel_radius: float = 0.05,
53
+ base_radius: float = 0.125,
54
+ max_radps: float = 10.0, # 速度上限(rad/s)
55
+ ) -> list[float]:
56
+ # 角速度を rad/s に
57
+ omega = np.deg2rad(vtheta_deg)
58
+ phi = np.deg2rad(cls._PHI_DEG) # [LEFT, BACK, RIGHT] の順
59
+
60
+ # 標準形のキネマ行列(接線方向ベクトル)
61
+ M = np.column_stack((-np.sin(phi), np.cos(phi), np.full(3, base_radius)))
62
+ body = np.array([vx, vy, omega])
63
+
64
+ # 車輪接線速度 -> 角速度(rad/s)
65
+ wheel_radps = (M @ body) / wheel_radius
66
+
67
+ # 安全のため物理上限で正規化
68
+ wheel_radps = cls._saturate_radps(wheel_radps, max_radps=max_radps)
69
+ return wheel_radps.tolist()
70
+
71
+ @classmethod
72
+ def _wheel_radps_to_body(
73
+ cls,
74
+ left_wheel_radps: float,
75
+ back_wheel_radps: float,
76
+ right_wheel_radps: float,
77
+ wheel_radius: float = 0.05,
78
+ base_radius: float = 0.125,
79
+ ) -> BaseCommand:
80
+ phi = np.deg2rad(cls._PHI_DEG) # [LEFT, BACK, RIGHT]
81
+ M = np.column_stack((-np.sin(phi), np.cos(phi), np.full(3, base_radius)))
82
+ # 角速度 -> 接線速度(m/s)
83
+ wheel_linear = wheel_radius * np.array([left_wheel_radps, back_wheel_radps, right_wheel_radps])
84
+ # 逆運動学
85
+ v = np.linalg.inv(M) @ wheel_linear
86
+ vx, vy, omega = v
87
+ return BaseCommand(x_vel=vx, y_vel=vy, theta_deg_vel=np.rad2deg(omega))
88
+
89
+ def send_action(self, action: BaseCommand):
90
+ wheel_radps = self._body_to_wheel_radps(
91
+ action.x_vel, action.y_vel, action.theta_deg_vel
92
+ )
93
+ logger.debug(f"Wheel radps: {wheel_radps}")
94
+ self.motor_controller.sync_write_goal_speed(
95
+ [self.LEFT_WHEEL_ID, self.BACK_WHEEL_ID, self.RIGHT_WHEEL_ID],
96
+ wheel_radps # rad/s
97
+ )
98
+
99
+ def stop(self):
100
+ self.motor_controller.sync_write_goal_speed(
101
+ [self.LEFT_WHEEL_ID, self.BACK_WHEEL_ID, self.RIGHT_WHEEL_ID], [0.0, 0.0, 0.0]
102
+ )
File without changes
@@ -0,0 +1,16 @@
1
+ import argparse
2
+
3
+ from pylekiwi.nodes import HostControllerNode
4
+ from pylekiwi.settings import Settings
5
+
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser()
9
+ parser.add_argument("--serial-port", type=str, default="/dev/ttyACM0")
10
+ args = parser.parse_args()
11
+ host_controller_node = HostControllerNode(Settings(serial_port=args.serial_port))
12
+ host_controller_node.run()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
@@ -0,0 +1,13 @@
1
+ import argparse
2
+ import subprocess
3
+
4
+ def main() -> int:
5
+ parser = argparse.ArgumentParser()
6
+ parser.add_argument("--port", type=int, default=8080)
7
+ parser.add_argument("--serial-port", type=str, default="/dev/ttyACM0")
8
+ args = parser.parse_args()
9
+ port = args.port
10
+ proc = subprocess.run(
11
+ ["gunicorn", "--bind", f"0.0.0.0:{port}", "--env", f"LEKIWI_SERIAL_PORT={args.serial_port}", "pylekiwi.commands.web_ui:me"]
12
+ )
13
+ return proc.returncode