chessnut_move/protocol/event.rs
1// Copyright 2026 Daymon Littrell-Reyes
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Decoded board and tracked-piece status values.
16
17use crate::protocol::{Color, Piece, PieceKind, Position};
18#[cfg(doc)]
19use crate::{protocol, transport};
20
21/// A decoded board-state or status notification.
22///
23/// # Examples
24///
25/// ```
26/// use chessnut_move::protocol::{BoardEvent, File, Rank, Square};
27///
28/// fn report(event: BoardEvent) {
29/// match event {
30/// BoardEvent::PositionChanged(position) => {
31/// let e4 = Square::new(File::E, Rank::Four);
32/// println!("e4 contains {:?}", position.piece_at(e4));
33/// }
34/// BoardEvent::BatteryStatus(status) => {
35/// println!("board battery: {}%", status.percentage);
36/// }
37/// BoardEvent::PieceStatus(status) => {
38/// let unavailable = status
39/// .pieces
40/// .iter()
41/// .filter(|piece| piece.battery_percentage.is_none())
42/// .count();
43/// println!("{unavailable} piece batteries are unavailable");
44/// }
45/// }
46/// }
47/// # let _ = report;
48/// ```
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub enum BoardEvent {
51 /// The occupancy or piece arrangement of the 64 squares changed.
52 ///
53 /// This event is only fired when [realtime updates] is enabled, and there's not an active
54 /// [auto-move] occurring.
55 ///
56 /// [realtime updates]: protocol::Command::enable_realtime_updates
57 /// [auto-move]: protocol::Command::auto_move
58 PositionChanged(Position),
59
60 /// The board responded to a [battery-status query].
61 ///
62 /// [battery-status query]: protocol::Command::read_battery_level
63 BatteryStatus(BatteryStatus),
64
65 /// The board responded to a [piece-status query].
66 ///
67 /// [piece-status query]: protocol::Command::read_piece_status
68 PieceStatus(PieceStatus),
69}
70
71/// Charging state and battery percentage reported by the board.
72///
73/// This value is carried by [`BoardEvent::BatteryStatus`] after a
74/// [`Command::read_battery_level`][protocol::Command::read_battery_level]
75/// response is decoded.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub struct BatteryStatus {
78 /// Whether the board is actively plugged in and charging.
79 pub charging: bool,
80
81 /// The battery percentage of the board.
82 ///
83 /// The decoder guarantees a value from `0` through `100`.
84 pub percentage: u8,
85}
86
87/// Status reported for one tracked physical chess piece.
88///
89/// Coordinates use the board firmware's normalized `0..=255` coordinate
90/// system and are independent of [`Square`] indexes.
91///
92/// [`Default`] produces an unavailable white-pawn placeholder used to
93/// initialize fixed response arrays. A default value is not an observation
94/// received from a board.
95///
96/// [`Square`]: protocol::Square
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub struct TrackedPieceStatus {
99 /// The chess piece that this status report represents.
100 pub piece: Piece,
101
102 /// The normalized X coordinate of the piece on the board.
103 ///
104 /// Values range from `0` through `255`.
105 pub x: u8,
106
107 /// The normalized Y coordinate of the piece on the board.
108 ///
109 /// Values range from `0` through `255`.
110 pub y: u8,
111
112 /// The piece's reported battery percentage.
113 ///
114 /// This is `None` when the board reports `0xff`, which indicates that the
115 /// piece's battery level is currently unavailable.
116 pub battery_percentage: Option<u8>,
117}
118
119impl Default for TrackedPieceStatus {
120 fn default() -> Self {
121 Self {
122 piece: Piece {
123 color: Color::White,
124 kind: PieceKind::Pawn,
125 },
126 x: 0,
127 y: 0,
128 battery_percentage: None,
129 }
130 }
131}
132
133/// Number of physical piece records in a tracked-piece response.
134///
135/// The Move protocol reports eight pawns, two rooks, two knights, two bishops,
136/// two queens, and one king for each color.
137pub const TRACKED_PIECE_COUNT: usize = 34;
138
139/// Status records for every physical piece tracked by the board.
140///
141/// Records are ordered as white pawns, rooks, knights, bishops, queens, king,
142/// followed by the corresponding black pieces.
143///
144/// This value is carried by [`BoardEvent::PieceStatus`] after
145/// [`Command::read_piece_status`][protocol::Command::read_piece_status] is sent.
146/// Low-level transport implementations can obtain it through
147/// [`decode_response_notification`][protocol::decode_response_notification].
148#[cfg_attr(
149 feature = "tokio",
150 doc = r#"
151# Examples
152
153Query the running board actor with
154[`BoardHandle::piece_status`](transport::tokio::BoardHandle::piece_status),
155then inspect the records returned by the board:
156
157```no_run
158use chessnut_move::transport::tokio::{BoardHandle, HandleError};
159
160async fn report_piece_batteries(board: &BoardHandle) -> Result<(), HandleError> {
161 let status = board.piece_status().await?;
162
163 for tracked in &status.pieces {
164 match tracked.battery_percentage {
165 Some(percentage) => {
166 println!("{:?}: {percentage}%", tracked.piece);
167 }
168 None => println!("{:?}: unavailable", tracked.piece),
169 }
170 }
171
172 Ok(())
173}
174```
175"#
176)]
177#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
178pub struct PieceStatus {
179 /// Piece records in the order defined by the Move protocol.
180 pub pieces: [TrackedPieceStatus; TRACKED_PIECE_COUNT],
181}