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.
- pylekiwi-0.1.0/PKG-INFO +33 -0
- pylekiwi-0.1.0/README.md +15 -0
- pylekiwi-0.1.0/pyproject.toml +28 -0
- pylekiwi-0.1.0/src/pylekiwi/__init__.py +12 -0
- pylekiwi-0.1.0/src/pylekiwi/arm_controller.py +72 -0
- pylekiwi-0.1.0/src/pylekiwi/base_controller.py +102 -0
- pylekiwi-0.1.0/src/pylekiwi/commands/__init__.py +0 -0
- pylekiwi-0.1.0/src/pylekiwi/commands/launch_host_controller.py +16 -0
- pylekiwi-0.1.0/src/pylekiwi/commands/launch_web_ui.py +13 -0
- pylekiwi-0.1.0/src/pylekiwi/commands/web_ui.py +462 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/README.md +35 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/base_motor_holder_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/base_so101_v2.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/motor_holder_so101_base_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/motor_holder_so101_wrist_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/moving_jaw_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/rotation_pitch_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/sts3215_03a_no_horn_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/sts3215_03a_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/under_arm_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/upper_arm_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/waveshare_mounting_plate_so101_v2.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/wrist_roll_follower_so101_v1.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/assets/wrist_roll_pitch_so101_v2.ply +0 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/joints_properties.xml +12 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/scene.xml +24 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_new_calib.urdf +453 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_new_calib.xml +162 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_old_calib.urdf +435 -0
- pylekiwi-0.1.0/src/pylekiwi/data/SO101/so101_old_calib.xml +160 -0
- pylekiwi-0.1.0/src/pylekiwi/filter.py +11 -0
- pylekiwi-0.1.0/src/pylekiwi/models.py +37 -0
- pylekiwi-0.1.0/src/pylekiwi/nodes.py +60 -0
- pylekiwi-0.1.0/src/pylekiwi/py.typed +0 -0
- pylekiwi-0.1.0/src/pylekiwi/settings.py +7 -0
pylekiwi-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
pylekiwi-0.1.0/README.md
ADDED
|
@@ -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
|