chessnut-move 1.0.2

Typed, transport-independent SDK for Chessnut Move boards.
Documentation

A typed, transport-independent Rust SDK for Chessnut Move boards.

GitHub Repo Crates.io Package Docs


Demo

use std::error::Error;
use std::io;
use std::time::Duration;

use btleplug::platform::Peripheral;
use chessnut_move::protocol::BoardEvent;
use chessnut_move::transport::btleplug::BtleplugTransport;
use chessnut_move::transport::tokio::{ActorConfig, BoardHandle, EventStreamError, spawn};
use tokio::time::{sleep, timeout};

type AnyError = Box<dyn Error + Send + Sync>;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), AnyError> {
  let peripheral = connect_board().await?;
  let operation = run_session(peripheral.clone()).await;
  let disconnect = peripheral.disconnect().await;

  operation?;
  disconnect?;
  Ok(())
}

async fn run_session(peripheral: Peripheral) -> Result<(), AnyError> {
  let transport = BtleplugTransport::new(peripheral).await?;
  let (board, task) = spawn(transport, ActorConfig::default())?;

  let operation = use_board(&board).await;
  let shutdown = board.shutdown().await;
  let actor_result = task.await?.into_result();

  actor_result?;
  operation?;
  shutdown?;
  Ok(())
}

async fn use_board(board: &BoardHandle) -> Result<(), AnyError> {
  let mut events = board.subscribe_events().await?;

  let battery = board.battery_status().await?;
  println!("Board battery: {}%", battery.percentage);

  let piece_status = board.piece_status().await?;
  for piece in piece_status.pieces.iter() {
    match piece.battery_percentage {
      Some(battery_percentage) => {
        println!("{:?} ({:?}%)", piece.piece, battery_percentage);
      }
      None => {
        println!("{:?} (unavailable)", piece.piece);
      }
    }
  }

  println!("Move a piece to produce a position update...");
  let position = timeout(Duration::from_secs(30), async {
    loop {
      if let BoardEvent::PositionChanged(position) = events.recv().await? {
        break Ok::<_, EventStreamError>(position);
      }
    }
  })
    .await??;

  println!("Position update: {position:?}");
  Ok(())
}

Installation

The SDK is published on crates.io under chessnut-move.

cargo add chessnut-move

Overview

chessnut-move is an SDK for interfacing with Chessnut Move boards, allowing you to interact with your boards via a type-safe Rust interface.

I was looking for a way to interact with my board via Rust, but none of the existing libraries offered support for the Move boards (only the Evo + other chessnut offerings).

This SDK also offers no_std support, and is runtime agnostic; so you can use whatever bluetooth library you'd like.

[!Note] This is an independent community project and is not affiliated with or endorsed by Chessnut.

Usage

Architecture

The crate separates the Chessnut Move protocol from Bluetooth and runtime choices. This results in the crate being seperated into two layers:

Layer Purpose
protocol Commands, decoded events, positions, squares, pieces, LED patterns, and protocol errors
transport Notification decoding, runtime-neutral sessions, and traits for Bluetooth adapters

The protocol layer is just a raw interface of the wire protcol for the Chessnut Move board. It makes no assumptions about what you'll be using to talk with the board; it just provides the necessary types to do so.

The transport layer offers the actual transport mechanisim for interacting with the Chessnut Move board. It uses the protocol layer under the hood, and facilitates the Bluetooth connections.

This separation allows protocol tests and embedded integrations to avoid specific Bluetooth dependencies, and keeps things extensible. It also keeps scan duration, device selection, pairing and reconnection in your application; where those policies can be chosen deliberately.

Transports

Transports are delibrated defined in a way that you can provide your own (eg; for your own embedded hardware or a custom Bluetooth library).

But the crate does provide a few common transports out of the box.

Tokio

[!TIP] You can find a more comprehensive example of this in the basic.rs example.

If you're using the popular async library tokio, you can take advantage of the tokio feature flag and the transports it provides.

use std::error::Error;
use std::io;
use std::time::Duration;

use chessnut_move::protocol::BoardEvent;
use chessnut_move::transport::tokio::{BoardHandle, EventStreamError};
use tokio::time::timeout;

type AnyError = Box<dyn Error + Send + Sync>;

async fn use_board(board: &BoardHandle) -> Result<(), AnyError> {
  let mut events = board.subscribe_events().await?;

  let battery = board.battery_status().await?;
  println!("Board battery: {}%", battery.percentage);

  let piece_status = board.piece_status().await?;
  for piece in piece_status.pieces.iter() {
    match piece.battery_percentage {
      Some(battery_percentage) => {
        println!("{:?} ({:?}%)", piece.piece, battery_percentage);
      }
      None => {
        println!("{:?} (unavailable)", piece.piece);
      }
    }
  }

  println!("Move a piece to produce a position update...");
  let position = timeout(Duration::from_secs(30), async {
    loop {
      if let BoardEvent::PositionChanged(position) = events.recv().await? {
        break Ok::<_, EventStreamError>(position);
      }
    }
  })
    .await??;

  println!("Position update: {position:?}");
  Ok(())
}

Async

[!TIP] You can find a more comprehensive example of this in the async_without_tokio.rs example.

If you're using native async, but don't want to use tokio, you can use the async feature flag to access a runtime-netural async transport.

use chessnut_move::protocol::{BoardEvent, Command, LedPattern};
use chessnut_move::transport::{AsyncBoard, AsyncTransport, BoardError};

async fn run<T: AsyncTransport>(
  transport: T,
) -> Result<(), BoardError<T::Error>> {
  let mut board = AsyncBoard::new(transport);
  board.initialize().await?;

  board.send(&Command::set_leds(&LedPattern::default())).await?;
  match board.next_event().await? {
    BoardEvent::PositionChanged(position) => {
      println!("position: {position:?}");
    }
    BoardEvent::BatteryStatus(_) | BoardEvent::PieceStatus(_) => {}
  }

  board.shutdown().await
}

btleplug

[!TIP] You can find a more comprehensive example of this in the basic.rs example.

If you're using the popular Bluetooth library blteplug, we offer additional adapters for using it as a transport via the blteplug feature flag.

use std::error::Error;
use std::io;
use btleplug::api::{
  Central, Manager as _, Peripheral as _, ScanFilter,
};
use btleplug::platform::{Manager, Peripheral};
use chessnut_move::transport::btleplug::BtleplugTransport;
use chessnut_move::transport::gatt::DEVICE_NAME;

async fn connect() -> Result<BtleplugTransport<Peripheral>, Box<dyn Error>> {
  let manager = Manager::new().await?;
  let adapter = manager
    .adapters()
    .await?
    .into_iter()
    .next()
    .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no BLE adapter"))?;  

  adapter.start_scan(ScanFilter::default()).await?;
  let mut board = None;
  for peripheral in adapter.peripherals().await? {
    let is_move = peripheral
      .properties()
      .await?
      .and_then(|properties| properties.local_name)
      .is_some_and(|name| name == DEVICE_NAME);
    if is_move {
      board = Some(peripheral);
      break;
    }
  }

  let board = board
    .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "board not found"))?;

  board.connect().await?;
  Ok(BtleplugTransport::new(board).await?)
}

Blocking

[!TIP] You can find a more comprehensive example of this in the blocking_no_std.rs example.

For no_std environvments, you can use the blocking feature flag to access the Allocation-free BlockingTransport and BlockingBoard.

use chessnut_move::protocol::{BoardEvent, Command, LedPattern};
use chessnut_move::transport::{
  BlockingBoard, BlockingTransport, BoardError,
};

fn run<T: BlockingTransport>(
  transport: T,
) -> Result<(), BoardError<T::Error>> {
  let mut board = BlockingBoard::new(transport);
  board.initialize()?;

  board.send(&Command::set_leds(&LedPattern::default()))?;
  match board.next_event()? {
    BoardEvent::PositionChanged(position) => {
      println!("position: {position:?}");
    }
    BoardEvent::BatteryStatus(_) | BoardEvent::PieceStatus(_) => {}
  }

  board.shutdown()
}

Custom Bluetooth Transports

If you want to add support for a custom Bluetooth library, you'll need to intregrate through one of three public traits:

  • Implement AsyncTransport for runtime-neutral async programs.
  • Implement BlockingTransport for synchronous or embedded programs.
  • Implement TokioTransport when the transport futures are Send and will run inside the Tokio actor.

Each trait exposes the same essential operations:

  1. Subscribe to a NotificationSource.
  2. Write a typed Command.
  3. Copy the next notification into the supplied buffer and return a borrowed Notification.

Additionally, it's worth noting that unsubscribe and close hooks have no-op defaults for transports that do not need explicit cleanup. The board sessions also own fixed-size notification buffers, so a transport does not need to allocate or expose its Bluetooth library's channel and notification-session types.

The UUIDs and characteristic mapping are public in transport::gatt. Implementations use Command::bytes() and Command::write_kind() and do not need access to the actual (private) wire-format types.

To learn more, you can look at how the AsyncTransport is implemented.

Commands

Commands are transport-independent values; they can be created before a board is connected and sent through async, blocking, or Tokio-backed sessions.

These represent the actual "commands" you'll be sending to the Chessnut Move board.

Square lights

use chessnut_move::protocol::{
  Command, LedColor, LedPattern, Square,
};

let mut leds = LedPattern::default ();
leds.set_color("e2".parse::<Square>() ?, LedColor::Green);
leds.set_color("e4".parse::<Square>() ?, LedColor::Green);

let command = Command::set_leds( & leds);

board.send(command).await?;

Board information

let battery = board.battery_status().await?;
let pieces = board.piece_status().await?;

println!("Battery: {}%", battery.percentage);
for tracked in pieces.pieces {
println!("{:?}", tracked);
}

Auto Move

[!NOTE] The Chessnut Move baord requires the full board FEN for auto-moves.

This means you have to provide the full Position when executing auto-moves.

In a real application, derive the target from the board's latest reported position.

use chessnut_move::protocol::{
  AutoMoveMode, Command, Position, Square,
};

fn move_e2_to_e4(mut target: Position) -> Result<Command, chessnut_move::protocol::ParseSquareError> {
  let e2 = "e2".parse::<Square>()?;
  let e4 = "e4".parse::<Square>()?;

  let pawn = target.piece_at(e2);
  target.set_piece(e2, None);
  target.set_piece(e4, pawn);

  Ok(Command::auto_move(target, AutoMoveMode::Normal))
}

let stop = Command::stop_auto_move();

Tracing

For tracing support, we provide structured diagnostics for protocol decoding, transport I/O, lifecycle transitions, actor queries, timeouts, and recoverable failures via the tracing feature.

The SDK never installs a global subscriber; applications decide how events are formatted, filtered, and collected.

tracing_subscriber::fmt()
  .with_env_filter("chessnut_move=debug")
  .init();

Use chessnut_move=trace to include routine command and notification traffic.

[!NOTE] Raw command and notification payloads are not recorded.

no-std usage

Disable the default features to use protocol types and notification decoding without std, allocation, async, or a Bluetooth dependency:

[dependencies]
chessnut-move = {
  version = "*",
  default-features = false,
}

If you don't want to implement your own transport, you can use the allocation-free blocking session:

[dependencies]
chessnut-move = {
  version = "*",
  default-features = false,
  features = ["blocking"],
}

Then implement BlockingTransport and use BlockingBoard.

Note that enabling tracing in a no_std application also enables alloc.

Feature flags

Feature Default Enables
std Yes Standard-library error integration
async Yes AsyncTransport, AsyncBoard, and the Board alias
blocking No Allocation-free BlockingTransport and BlockingBoard
btleplug No BtleplugTransport; also enables std and async
tokio No Actor task, cloneable handles, request helpers, lifecycle state, and event streams; also enables std, alloc, and async
tracing No Structured spans and events; also enables alloc
alloc No Allocation-dependent integrations without otherwise requiring std

Examples

You can find example implementations under the examples directory:

Example Configuration Purpose
basic.rs btleplug,tokio,tracing Complete desktop BLE application with scanning, status queries, events, tracing, and graceful shutdown
blocking_no_std.rs --no-default-features --features blocking Allocation-free firmware integration using a platform-provided blocking BLE transport
async_without_tokio.rs --no-default-features --features async Runtime-neutral async integration for a non-Tokio executor and BLE transport

The blocking and runtime-neutral async examples are compiled as libraries because the crate cannot select an embedded platform entry point, BLE stack, or executor for the application. Each exposes a run function that accepts a platform connector and demonstrates connection, initialization, battery and tracked-piece queries, position updates, shutdown, and disconnection.

Additional Notes

  • The Chessnut Move boards only allow a single bluetooth connection; so ensure you're not connecting with the native app when using the SDK.
  • Bluetooth discovery and connection behavior varies by operating system and adapter. The btleplug adapter inherits the platform behavior of btleplug. Add support for your own bluetooth adapter if blteplug doesn't fit your requirements.
  • The protocol implementation is based on Chessnut's published Move API, and some manual testing.
  • This library is specifically made for the Chessnut Move boards. I'm willing to adapt the library for other Chessnut boards, but I would need someone else with the boards available for testing purposes.

Support

Use the GitHub's issue tracker for reproducible bugs and feature request.

Issue templates are provided for both.

Contributing

If you're interested in contributing to the SDK, give the CONTRIBUTING doc a read.

Contributors using AI-assisted tools must also follow our AI policy.

License

Apache 2.0