Skip to content

DoraCN/dynamixel-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dynamixel-rs

A pure-Rust, no_std-compatible driver library for ROBOTIS DYNAMIXEL Protocol 2.0

crates.io docs.rs MSRV CI License: MIT OR Apache-2.0 no_std

English · 简体中文


Overview

dynamixel-rs is a zero-dependency Rust implementation of the ROBOTIS DYNAMIXEL Protocol 2.0. It provides a type-safe, allocation-free API for building and parsing instruction and status packets, plus a thin Serial trait abstraction that bridges the protocol layer to any transport — std::io, embedded-hal, RS-485 transceivers, or custom UART drivers on bare metal.

The crate ships with std enabled by default for host-side convenience (serial-port adapters, std::error::Error integration), and can be switched to #![no_std] by disabling default features — see no_std Usage.

Protocol 2.0 is the successor to DYNAMIXEL's original Protocol 1.0 and is used by every current X / P / Y / PRO / PRO+ series servo. The wire format features a 4-byte magic header, little-endian length/CRC fields, CRC-16/BUYPASS integrity checking, and byte-stuffing to prevent header collisions in the payload.


Features

  • Pure Rust, no_std-compatiblestd on by default for convenience; disable default features for Cortex-M / RISC-V / ESP32 / any bare-metal target
  • Zero-copy parsingPacket<'a> borrows the byte slice; no heap, no copies
  • All 16 Protocol 2.0 instructions exposed as ergonomic Builder types
  • Automatic byte-stuffing and CRC-16/BUYPASS on both TX and RX paths
  • Serial trait abstraction — wire the protocol to any read_exact / write_all transport
  • StdSerial<T> adapter wraps any std::io::Read + Write (e.g. serialport, TcpStream)
  • High-level Driver APIping(), read_bytes(), write_bytes() with built-in framing
  • Strongly-typed errorsError::DeviceError(StatusError) carries the bit-packed status-packet error byte
  • 72+ unit tests covering official protocol byte sequences, CRC edge cases, and byte-stuffing round-trips

Supported Instructions

All 16 instructions from the Protocol 2.0 specification are available as builder types under dynamixel_rs::commands.

Byte Instruction Builder Description
0x01 Ping unicast::Ping Check device presence, get model + firmware
0x02 Read unicast::Read Read N bytes from the Control Table
0x03 Write unicast::Write Write data to the Control Table
0x04 Reg Write unicast::RegWrite Register a Write for later synchronised execution
0x05 Action unicast::Action Trigger pending Reg Writes (unicast or broadcast)
0x06 Factory Reset unicast::FactoryReset Restore factory defaults
0x08 Reboot unicast::Reboot Reboot the target device
0x10 Clear unicast::Clear Reset multi-turn position or clear hardware error
0x20 Control Table Backup unicast::ControlTableBackup Store or restore the Control Table
0x55 Status (Return) packet::build_status_packet Status packet sent by devices (builder for testing)
0x82 Sync Read multi::SyncRead Read the same address from multiple devices
0x83 Sync Write multi::SyncWrite Write the same address to multiple devices
0x8A Fast Sync Read multi::FastSyncRead Sync Read with combined single-response format
0x92 Bulk Read multi::BulkRead Read different addresses from multiple devices
0x93 Bulk Write multi::BulkWrite Write different addresses to multiple devices
0x9A Fast Bulk Read multi::FastBulkRead Bulk Read with combined single-response format

Tested Hardware

Series Model Model Number Firmware Status
X-Series XL330-M288-T 1200 52 ✅ Verified — 8-servo chain at 2 Mbps

Any Protocol 2.0-compatible device should work. Contributions adding to this table via the examples in examples/ are welcome.


Installation

Add to your Cargo.toml:

[dependencies]
dynamixel-rs = "0.1"

Feature Flags

Feature Default Description
std Enables StdSerial<T> adapter and std::error::Error impl
embedded-hal (Planned) Integration with the embedded-hal 1.x serial traits

For a no_std build:

[dependencies]
dynamixel-rs = { version = "0.1", default-features = false }

Minimum Supported Rust Version

The crate compiles on Rust 1.70.0 and above. MSRV bumps will be documented in the changelog and are considered non-breaking.


Quick Start

Open a serial port, ping servo ID 1, and read its Present Position. Runs on any macOS / Linux / Windows host with a USB ↔ RS-485 adapter.

use dynamixel_rs::driver::{Driver, StdSerial};
use serialport::{DataBits, FlowControl, Parity, StopBits};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Open the serial port (DYNAMIXEL default: 2 Mbps, 8N1, no flow control).
    let port = serialport::new("/dev/ttyUSB0", 2_000_000)
        .data_bits(DataBits::Eight)
        .parity(Parity::None)
        .stop_bits(StopBits::One)
        .flow_control(FlowControl::None)
        .timeout(Duration::from_millis(100))
        .open()?;

    // 2. Wrap it in the high-level Driver.
    let mut driver = Driver::new(StdSerial::new(port));

    // 3. Ping — returns model number + firmware version.
    let mut tx = [0u8; 32];
    let mut rx = [0u8; 256];
    let info = driver.ping(1, &mut tx, &mut rx)?;
    println!("ID=1 model={} firmware={}", info.model_number, info.firmware_version);

    // 4. Read 4 bytes at address 132 (Present Position on X-series).
    let data = driver.read_bytes(1, 132, 4, &mut tx, &mut rx)?;
    let position = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
    println!("Present Position = {} ticks", position);

    Ok(())
}

Usage Guide

Building Raw Instruction Packets

Every builder follows the same pattern: construct with new(...), then call build(&mut [u8]) -> Result<usize, Error> to serialise into a caller-supplied buffer. No heap allocation is ever performed.

use dynamixel_rs::commands::unicast::{Ping, Read, Write};

let mut buf = [0u8; 64];

// Ping to device 1 → FF FF FD 00 01 03 00 01 19 4E
let n = Ping::new(1).build(&mut buf)?;

// Read 4 bytes at address 0x0084 on device 1
let n = Read::new(1, 0x0084, 4).build(&mut buf)?;

// Write [0x01, 0x02, 0x03, 0x04] at address 0x0074 on device 1
let n = Write::new(1, 0x0074, &[0x01, 0x02, 0x03, 0x04]).build(&mut buf)?;

Parsing Incoming Status Packets

use dynamixel_rs::packet::Packet;

let pkt = Packet::parse(&rx_buf[..n])?;
println!("from ID {}", pkt.id());
if let Some(err) = pkt.status_error() {
    eprintln!("device reported error: {err:?}");
}
let params: heapless::Vec<u8, 64> = pkt.params_iter().collect();

The params_iter() method yields de-stuffed payload bytes lazily, so the caller never needs to pre-allocate a buffer for unstuffing.

High-level Driver Methods

The Driver<S: Serial> wrapper handles protocol framing, send/receive, and CRC validation so application code stays minimal:

driver.ping(id, &mut tx, &mut rx)?;              // → PingResponse
driver.read_bytes(id, addr, len, &mut tx, &mut rx)?;  // → &[u8] into rx
driver.write_bytes(id, addr, data, &mut tx, &mut rx)?;

Multi-device Instructions

SyncRead, SyncWrite, BulkRead, BulkWrite, and their Fast variants accept an iterator of per-device entries and stream the bytes directly into the caller's buffer — still zero heap allocations.

use dynamixel_rs::commands::multi::{SyncWrite, BulkRead};

// SyncWrite: set Goal Position (addr 116, 4 bytes) on servos 1, 2, 3.
let entries = [
    (1u8, 1000_u32.to_le_bytes()),
    (2u8, 2000_u32.to_le_bytes()),
    (3u8, 3000_u32.to_le_bytes()),
];
let n = SyncWrite::new(116, 4, entries.iter().map(|(id, d)| (*id, d.as_ref())))
    .build(&mut buf)?;

// BulkRead: pull different data from different servos in one packet.
let plan = [
    (1u8, 132_u16, 4_u16),  // Present Position (4 B) from ID 1
    (2u8, 146_u16, 1_u16),  // Present Temperature (1 B) from ID 2
];
let n = BulkRead::new(plan).build(&mut buf)?;

Examples

Runnable examples live in examples/:

Example Command Purpose
ping_motor cargo run --example ping_motor In-memory loopback demo — no hardware required
ping_motor (real) cargo run --example ping_motor -- /dev/ttyUSB0 Ping ID 1 on a real port
read_all_servos cargo run --example read_all_servos Scan IDs 1–8 and print model/position/voltage/temperature
monitor_servo cargo run --example monitor_servo -- --port /dev/ttyUSB0 --ids 1,2,3 Real-time monitoring loop with clap argument parsing
test_port cargo run --example test_port Low-level byte-level protocol verification

Enable tracing with RUST_LOG=debug to see TX / RX packet dumps:

RUST_LOG=debug cargo run --example read_all_servos

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     User application                        │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  driver.rs      Driver<S: Serial>                           │
│                 ├─ ping()                                   │
│                 ├─ read_bytes()                             │
│                 ├─ write_bytes()                            │
│                 ├─ send_packet() / recv_packet()            │
│                 └─ StdSerial<T: std::io::Read+Write>        │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────┐   ┌─────────────────────────────┐
│  commands/unicast.rs    │   │  commands/multi.rs          │
│  Ping / Read / Write /  │   │  SyncRead / SyncWrite /     │
│  RegWrite / Action /    │   │  BulkRead / BulkWrite /     │
│  FactoryReset / Reboot /│   │  FastSyncRead /             │
│  Clear / CtrlTblBackup  │   │  FastBulkRead               │
└─────────────────────────┘   └─────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  packet.rs      Packet<'a>::parse(&[u8]) → zero-copy view   │
│                 build_packet() / build_status_packet()      │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────┐   ┌─────────────────────────────┐
│  crc.rs                 │   │  stuffing.rs                │
│  CRC-16/BUYPASS         │   │  byte-stuff / UnstuffIter   │
└─────────────────────────┘   └─────────────────────────────┘

Protocol 2.0 Packet Layout

Instruction Packet

 offset │ 0    1    2    3  │ 4  │ 5    6  │ 7    │ 8..N-3       │ N-2   N-1
 field  │       Header      │ ID │ Length  │ Instr│  Parameters  │    CRC
 bytes  │ FF   FF   FD   00 │ XX │ ll   lh │  XX  │  …stuffed…   │ cl    ch

          Length  = bytes from Instruction through CRC (inclusive), little-endian
          CRC     = CRC-16/BUYPASS over bytes 0..N-2 (everything before CRC)

Status Packet

 offset │ 0    1    2    3  │ 4  │ 5    6  │ 7    │ 8     │ 9..N-3       │ N-2   N-1
 field  │       Header      │ ID │ Length  │ 0x55 │ Error │  Parameters  │    CRC
 bytes  │ FF   FF   FD   00 │ XX │ ll   lh │  55  │  EE   │  …stuffed…   │ cl    ch

          Error bit 7      = Alert   (Hardware Error Status non-zero)
          Error bits 6..0  = Error Number  (see error::StatusError)

Byte-stuffing rule: in the payload, whenever the sequence FF FF FD appears it is replaced by FF FF FD FD on the wire.


no_std Usage

std is enabled by default; the crate itself is #![no_std]-compatible once default features are disabled. After switching, implement the Serial trait on top of your platform's UART driver:

#![no_std]
use dynamixel_rs::{driver::{Driver, Serial}, error::Error};

struct MyUart;

impl Serial for MyUart {
    fn write_all(&mut self, bytes: &[u8]) -> Result<(), Error> {
        // Block until every byte has been transmitted.
        todo!()
    }
    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> {
        // Block until the buffer is full.
        todo!()
    }
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        // Read up to buf.len() bytes, return how many.
        todo!()
    }
}

let mut driver = Driver::new(MyUart);

A blanket embedded-hal 1.x serial integration is on the Roadmap.


Testing

# Full test suite (72+ unit tests + doc tests)
cargo test

# no_std build verification
cargo build --no-default-features

# Documentation (opens in browser)
cargo doc --open

Roadmap

  • embedded-hal 1.x serial trait blanket implementation
  • Async I/O support via embedded-io-async
  • Hardware validation across additional series (2XL, P, Y, PRO)
  • Typed Control Table abstractions per model
  • defmt logging feature flag for embedded targets
  • Python bindings via PyO3 (optional crate)

Contributing

Contributions are welcome. Before opening a PR:

  1. Run cargo fmt and cargo clippy --all-targets.
  2. Ensure cargo test and cargo build --no-default-features both pass.
  3. For new instructions or hardware support, add a focused unit test with the official byte sequence from the Protocol 2.0 reference.
  4. Bilingual doc comments (English + 中文) are appreciated but not required for third-party contributions.

Bug reports and feature requests should include the target hardware model, firmware version, and a minimal reproducer.


License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.


References

About

A pure-Rust, no_std compatible driver library for ROBOTIS DYNAMIXEL Protocol 2.0

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages