Skip to content

Commit 6f55c1a

Browse files
Merge pull request #78 from pollen-robotics/python-bindings
Python bindings
2 parents 783e311 + 74227e5 commit 6f55c1a

29 files changed

+1055
-642
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/target
22
/Cargo.lock
3+
4+
__pycache__

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ readme = "README.md"
1111
keywords = ["robotics", "dynamixel", "feetech"]
1212
categories = ["science::robotics"]
1313

14+
[lib]
15+
crate-type = ["lib", "cdylib"]
16+
17+
[features]
18+
default = []
19+
python = ["dep:pyo3", "dep:pyo3-log"]
1420

1521
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1622

@@ -22,6 +28,8 @@ clap = { version = "4.0.32", features = ["derive"] }
2228
proc-macro2 = { version = "=1.0.89", features=["default", "proc-macro"] }
2329
signal-hook = "0.3.4"
2430
num_enum = "0.7.3"
31+
pyo3 = { version = "0.24.1", optional = true, features = ["multiple-pymethods"] }
32+
pyo3-log = { version = "0.12.3", optional = true }
2533

2634
[dev-dependencies]
2735
env_logger = "0.10.0"

Changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## version 1.0.0
2+
3+
- Cleanup APIs to offer two interfaces:
4+
- high-level interface (Controller) with a simple API for the most common use cases.
5+
- low-level interface (DynamixelProtocolHandler) for direct access to the protocol and fine-grained control of the bus ownership.
6+
- Add Python bindings for the library (controller API).
7+
- Add support for the feetech servo.
8+
- Define register conversion at the macro level to simplify the code.
9+
110
## Version 0.6.0
211

312
- Add dxl XL330 support

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010

1111
## Getting started
1212

13-
Rustypot is a communication library for Dynamixel/Feetech motors. It is used in the [Reachy project](https://www.pollen-robotics.com/reachy/). More types of servo can be added in the future.
13+
Rustypot is a communication library for Dynamixel/Feetech motors. It is notably used in the [Reachy project](https://www.pollen-robotics.com/reachy/). More types of servo can be added in the future.
1414

1515
## Feature Overview
1616

1717
* Relies on [serialport](https://docs.rs/serialport/latest/serialport/) for serial communication
1818
* Support for dynamixel protocol v1 and v2 (can also use both on the same bus)
1919
* Support for sync read and sync write operations
2020
* Easy support for new type of motors (register definition through macros). Currently support for dynamixel XL320, XL330, XL430, XM430, MX*, Orbita 2D & 3D.
21-
* Pure Rust
21+
* Pure Rust plus python bindings (using [pyo3](https://pyo3.rs/)).
2222

2323
To add new servo, please refer to the [Servo documentation](./servo/README.md).
2424

@@ -76,11 +76,36 @@ fn main() {
7676

7777
See https://docs.rs/rustypot for more information on APIs and examples.
7878

79+
See [python/README.md](./python/README.md) for information on how to use the python bindings.
80+
81+
## Python bindings
82+
83+
The python bindings are generated using [pyo3](https://pyo3.rs/). They are available on `pypi`(https://pypi.org/project/rustypot/). You can install them using pip, pix, uv, etc.
84+
85+
To build them locally, you can use [maturin](https://www.maturin.rs).
86+
87+
```bash
88+
maturin build --release --features python
89+
```
90+
91+
or, if you want to install them in your local python environment:
92+
93+
```bash
94+
maturin develop --release --features python
95+
```
96+
97+
See [maturin official documentation](https://maturin.rs) for more information on how to use it.
98+
99+
## Contributing
100+
101+
If you want to contribute to Rustypot, please fork the repository and create a pull request. We welcome any contributions, including bug fixes, new features, and documentation improvements.
102+
We especially appreciate support for new servos. If you want to add support for a new servo, please follow the instructions in the [Servo documentation](./servo/README.md).
103+
79104
## License
80105

81106
This library is licensed under the [Apache License 2.0](./LICENSE).
82107

83108
## Support
84109

85-
Rustypot is developed and maintained by [Pollen-Robotics](https://pollen-robotics.com). They developped open-source tools for robotics manipulation.
110+
Rustypot is developed and maintained by [Pollen-Robotics](https://pollen-robotics.com). They developed open-source hardware and tools for robotics.
86111
Visit https://pollen-robotics.com to learn more or join the [Discord community](https://discord.com/invite/Kg3mZHTKgs) if you have any questions or want to share your projects.

examples/dxl_mx_example.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ fn main() -> Result<(), Box<dyn Error>> {
1010

1111
let io = DynamixelProtocolHandler::v1();
1212

13-
let _x: i16 = mx::read_present_position(&io, serial_port.as_mut(), 11)?;
14-
let _x: Vec<i16> = mx::sync_read_present_position(&io, serial_port.as_mut(), &[11, 12])?;
13+
let _x: f64 = mx::read_present_position(&io, serial_port.as_mut(), 11)?;
14+
let _x: Vec<f64> = mx::sync_read_present_position(&io, serial_port.as_mut(), &[11, 12])?;
1515

16-
mx::sync_write_goal_position(&io, serial_port.as_mut(), &[11, 12], &[2048, 2048])?;
16+
mx::sync_write_raw_goal_position(&io, serial_port.as_mut(), &[11, 12], &[2048, 2048])?;
1717

1818
loop {
19-
mx::sync_write_goal_position(&io, serial_port.as_mut(), &[11], &[2048])?;
19+
mx::sync_write_raw_goal_position(&io, serial_port.as_mut(), &[11], &[2048])?;
2020

2121
let temp = mx::read_present_temperature(&io, serial_port.as_mut(), 11)?;
2222
println!("{:?}", temp);
2323

2424
thread::sleep(Duration::from_millis(500));
2525

26-
mx::sync_write_goal_position(&io, serial_port.as_mut(), &[11], &[1000])?;
27-
let pos = mx::read_present_position(&io, serial_port.as_mut(), 11)?;
26+
mx::sync_write_raw_goal_position(&io, serial_port.as_mut(), &[11], &[1000])?;
27+
let pos = mx::read_raw_present_position(&io, serial_port.as_mut(), 11)?;
2828
println!("{:?}", pos);
2929

3030
thread::sleep(Duration::from_millis(500));

examples/dxl_sinus.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::f32::consts::PI;
1+
use std::f64::consts::PI;
22
use std::time::SystemTime;
33
use std::{error::Error, thread, time::Duration};
44

5-
use rustypot::servo::dynamixel::mx::{self, conv};
5+
use rustypot::servo::dynamixel::mx;
66
use rustypot::DynamixelProtocolHandler;
77

88
use clap::Parser;
@@ -28,20 +28,20 @@ struct Args {
2828

2929
///sinus amplitude (f64)
3030
#[arg(short, long, default_value_t = 10.0)]
31-
amplitude: f32,
31+
amplitude: f64,
3232

3333
///sinus frequency (f64)
3434
#[arg(short, long, default_value_t = 1.0)]
35-
frequency: f32,
35+
frequency: f64,
3636
}
3737

3838
fn main() -> Result<(), Box<dyn Error>> {
3939
let args = Args::parse();
4040
let serialportname: String = args.serialport;
4141
let baudrate: u32 = args.baudrate;
4242
let id: u8 = args.id;
43-
let amplitude: f32 = args.amplitude;
44-
let frequency: f32 = args.frequency;
43+
let amplitude: f64 = args.amplitude;
44+
let frequency: f64 = args.frequency;
4545

4646
//print all the argument values
4747
println!("serialport: {}", serialportname);
@@ -60,22 +60,17 @@ fn main() -> Result<(), Box<dyn Error>> {
6060

6161
let io = DynamixelProtocolHandler::v1();
6262

63-
let x: i16 = mx::read_present_position(&io, serial_port.as_mut(), id)?;
63+
let x = mx::read_present_position(&io, serial_port.as_mut(), id)?;
6464
println!("present pos: {}", x);
6565

6666
mx::write_torque_enable(&io, serial_port.as_mut(), id, 1)?;
6767

6868
let now = SystemTime::now();
6969
while !term.load(Ordering::Relaxed) {
70-
let t = now.elapsed().unwrap().as_secs_f32();
70+
let t = now.elapsed().unwrap().as_secs_f64();
7171
let target = amplitude * (2.0 * PI * frequency * t).sin().to_radians();
7272
println!("target: {}", target);
73-
mx::write_goal_position(
74-
&io,
75-
serial_port.as_mut(),
76-
id,
77-
conv::radians_to_dxl_pos(target.into()),
78-
)?;
73+
mx::write_goal_position(&io, serial_port.as_mut(), id, target)?;
7974

8075
thread::sleep(Duration::from_millis(10));
8176
}

examples/feetech_controller.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{error::Error, thread, time::Duration};
22

3-
use rustypot::servo::feetech::sts3215::STS3215Controller;
3+
use rustypot::servo::feetech::sts3215::Sts3215Controller;
44
fn main() -> Result<(), Box<dyn Error>> {
55
let serialportname: String = "/dev/tty.usbmodem58FA0822621".to_string();
66
let baudrate: u32 = 1_000_000;
@@ -11,7 +11,7 @@ fn main() -> Result<(), Box<dyn Error>> {
1111
.open()?;
1212
println!("serial port opened");
1313

14-
let mut c = STS3215Controller::new()
14+
let mut c = Sts3215Controller::new()
1515
.with_protocol_v1()
1616
.with_serial_port(serial_port);
1717

examples/feetech_lowlevel_api.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,8 @@ fn main() -> Result<(), Box<dyn Error>> {
2020
while start_overall.elapsed() < duration {
2121
let start_time = std::time::Instant::now();
2222

23-
let _x: i16 = sts3215::read_present_position(&io, serial_port.as_mut(), ids[0])?;
23+
let _x: i16 = sts3215::read_raw_present_position(&io, serial_port.as_mut(), ids[0])?;
2424
let x = sts3215::sync_read_present_position(&io, serial_port.as_mut(), &ids)?;
25-
let x: Vec<f64> = x
26-
.iter()
27-
.map(|p| sts3215::conv::dxl_pos_to_radians(*p))
28-
.map(f64::to_degrees)
29-
.collect();
3025
println!("present pos: {:?}", x);
3126

3227
let elapsed_time = start_time.elapsed();

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["maturin>=1.8,<2.0"]
3+
build-backend = "maturin"
4+
5+
[project]
6+
name = "rustypot"
7+
requires-python = ">=3.8"
8+
classifiers = [
9+
"Programming Language :: Rust",
10+
"Programming Language :: Python :: Implementation :: CPython",
11+
"Programming Language :: Python :: Implementation :: PyPy",
12+
]
13+
dynamic = ["version"]
14+
[project.optional-dependencies]
15+
tests = [
16+
"pytest",
17+
]
18+
[tool.maturin]
19+
python-source = "python"
20+
features = ["pyo3/extension-module"]

python/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Rustypot python bindings
2+
3+
## Installation
4+
5+
Rustypot bindings are available on PyPI. You can install them using pip:
6+
7+
```bash
8+
pip install rustypot
9+
```
10+
11+
### Building the bindings
12+
13+
If you want to build the bindings from source, you can clone the repository and run the following command:
14+
15+
```bash
16+
maturin develop --release --features python
17+
```
18+
19+
## Usage
20+
21+
The Python bindings exposes the same API as the Controller API in the rust crate.
22+
23+
You first need to create a Controller object. For instance, to communicate with a serial port to Feetech STS3215 motors, you can do the following:
24+
25+
```python
26+
from rustypot.servo import Sts3215SyncController
27+
28+
c = Sts3215SyncController(serial_port='/dev/ttyUSB0', baudrate=100000, timeout=0.1)
29+
```
30+
31+
Then, you can directly read/write any register of the motor. For instance, to read the present position of the motors with id 1 and 2, you can do:
32+
33+
```python
34+
35+
pos = c.read_present_position([1, 2])
36+
print(pos)
37+
```
38+
39+
You can also write to the motors. For instance, to set the goal position of the motors with id 1 and 2 to 0.0 and 90° respectively, you can do:
40+
41+
```python
42+
import numpy as np
43+
c.write_goal_position([1, 2], [0.0, np.deg2rad(90.0)])
44+
```

0 commit comments

Comments
 (0)