1use crate::protocol::wire::{PackedSquares, encode_piece};
18use crate::protocol::{BOARD_STATE_LENGTH, LedPattern, Position};
19#[cfg(doc)]
20use crate::{protocol, transport};
21
22const AUTO_MOVE_PREFIX: [u8; 2] = [0x42, 0x21];
23const SET_LED_PREFIX: [u8; 2] = [0x43, 0x20];
24const AUTO_MOVE_COMMAND_LENGTH: usize = 35;
25const SET_LED_COMMAND_LENGTH: usize = SET_LED_PREFIX.len() + BOARD_STATE_LENGTH;
26
27pub const MAX_COMMAND_LEN: usize = const_max(AUTO_MOVE_COMMAND_LENGTH, SET_LED_COMMAND_LENGTH);
32
33const fn const_max(left: usize, right: usize) -> usize {
35 if left > right { left } else { right }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Command {
61 bytes: [u8; MAX_COMMAND_LEN],
62 len: u8,
63 write_kind: WriteKind,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68pub enum WriteKind {
69 WithResponse,
71
72 WithoutResponse,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum AutoMoveMode {
81 Force,
83
84 Normal,
86}
87
88impl AutoMoveMode {
89 pub const fn flag(self) -> u8 {
102 match self {
103 Self::Force => 0x00,
104 Self::Normal => 0x01,
105 }
106 }
107}
108
109impl Command {
110 pub(crate) const fn from_bytes<const N: usize>(input: &[u8; N], write_kind: WriteKind) -> Self {
114 const {
115 assert!(N <= MAX_COMMAND_LEN);
116 assert!(MAX_COMMAND_LEN <= u8::MAX as usize);
117 }
118
119 let mut bytes = [0; MAX_COMMAND_LEN];
120 let mut index = 0;
121 while index < N {
122 bytes[index] = input[index];
123 index += 1;
124 }
125
126 Self {
127 bytes,
128 len: N as u8,
129 write_kind,
130 }
131 }
132
133 pub fn auto_move(position: Position, mode: AutoMoveMode) -> Self {
164 trace_event!(
165 mode = ?mode,
166 occupied_squares = position
167 .squares()
168 .iter()
169 .filter(|piece| piece.is_some())
170 .count(),
171 "encoding auto-move command"
172 );
173 let mut command = [0; AUTO_MOVE_COMMAND_LENGTH];
174 let bytes = PackedSquares::encode(position.squares(), encode_piece).into_bytes();
175
176 command[..AUTO_MOVE_PREFIX.len()].copy_from_slice(&AUTO_MOVE_PREFIX);
177 command[AUTO_MOVE_PREFIX.len()..AUTO_MOVE_PREFIX.len() + BOARD_STATE_LENGTH]
178 .copy_from_slice(&bytes);
179 command[AUTO_MOVE_COMMAND_LENGTH - 1] = mode.flag();
180
181 Self::from_bytes(&command, WriteKind::WithoutResponse)
182 }
183
184 #[cfg_attr(
190 feature = "async",
191 doc = r#"
192# Examples
193
194Send the stop command through an initialized async session:
195
196```no_run
197use chessnut_move::protocol::Command;
198use chessnut_move::transport::{AsyncBoard, AsyncTransport, BoardError};
199
200async fn stop<T: AsyncTransport>(
201 board: &mut AsyncBoard<T>,
202) -> Result<(), BoardError<T::Error>> {
203 board.send(&Command::stop_auto_move()).await
204}
205```
206"#
207 )]
208 pub const fn stop_auto_move() -> Self {
209 let mut command = [0; AUTO_MOVE_COMMAND_LENGTH];
210 command[0] = AUTO_MOVE_PREFIX[0];
211 command[1] = AUTO_MOVE_PREFIX[1];
212
213 Self::from_bytes(&command, WriteKind::WithoutResponse)
214 }
215
216 #[cfg_attr(
235 feature = "async",
236 doc = "See [`AsyncBoard::initialize`](transport::AsyncBoard::initialize) for the runtime-neutral async initialization procedure."
237 )]
238 #[cfg_attr(
239 feature = "blocking",
240 doc = "See [`BlockingBoard::initialize`](transport::BlockingBoard::initialize) for the blocking initialization procedure."
241 )]
242 #[cfg_attr(
243 feature = "tokio",
244 doc = "The [Tokio actor](transport::tokio::spawn) performs initialization in its spawned task."
245 )]
246 pub const fn enable_realtime_updates() -> Self {
247 Self::from_bytes(&[0x21, 0x01, 0x00], WriteKind::WithResponse)
248 }
249
250 #[cfg_attr(
257 feature = "async",
258 doc = r#"
259# Examples
260
261Send the query through an initialized async session and wait for its response:
262
263```no_run
264use chessnut_move::protocol::{BatteryStatus, BoardEvent, Command};
265use chessnut_move::transport::{AsyncBoard, AsyncTransport, BoardError};
266
267async fn read_battery<T: AsyncTransport>(
268 board: &mut AsyncBoard<T>,
269) -> Result<BatteryStatus, BoardError<T::Error>> {
270 board.send(&Command::read_battery_level()).await?;
271
272 loop {
273 if let BoardEvent::BatteryStatus(status) = board.next_event().await? {
274 return Ok(status);
275 }
276 }
277}
278```
279"#
280 )]
281 #[cfg_attr(
282 feature = "tokio",
283 doc = "Tokio actor consumers can use [`BoardHandle::battery_status`](transport::tokio::BoardHandle::battery_status) to send and correlate this query."
284 )]
285 pub const fn read_battery_level() -> Self {
286 query_command(0x0c)
287 }
288
289 #[cfg_attr(
296 feature = "async",
297 doc = r#"
298# Examples
299
300Send the query through an initialized async session and wait for its response:
301
302```no_run
303use chessnut_move::protocol::{BoardEvent, Command, PieceStatus};
304use chessnut_move::transport::{AsyncBoard, AsyncTransport, BoardError};
305
306async fn read_pieces<T: AsyncTransport>(
307 board: &mut AsyncBoard<T>,
308) -> Result<PieceStatus, BoardError<T::Error>> {
309 board.send(&Command::read_piece_status()).await?;
310
311 loop {
312 if let BoardEvent::PieceStatus(status) = board.next_event().await? {
313 return Ok(status);
314 }
315 }
316}
317```
318"#
319 )]
320 #[cfg_attr(
321 feature = "tokio",
322 doc = "Tokio actor consumers can use [`BoardHandle::piece_status`](transport::tokio::BoardHandle::piece_status) to send and correlate this query."
323 )]
324 pub const fn read_piece_status() -> Self {
325 query_command(0x0b)
326 }
327
328 pub fn set_leds(pattern: &LedPattern) -> Self {
347 trace_event!("encoding LED command");
348 let packed = pattern.encode();
349
350 let mut command = [0; SET_LED_COMMAND_LENGTH];
351
352 command[..SET_LED_PREFIX.len()].copy_from_slice(&SET_LED_PREFIX);
353 command[SET_LED_PREFIX.len()..].copy_from_slice(&packed);
354
355 Self::from_bytes(&command, WriteKind::WithoutResponse)
356 }
357
358 pub fn bytes(&self) -> &[u8] {
368 &self.bytes[..self.len as usize]
369 }
370
371 pub const fn write_kind(&self) -> WriteKind {
384 self.write_kind
385 }
386}
387
388const fn query_command(register: u8) -> Command {
390 Command::from_bytes(&[0x41, 0x01, register], WriteKind::WithResponse)
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::protocol::{Color, File, LedColor, Piece, PieceKind, Rank, SQUARE_COUNT, Square};
397
398 #[test]
399 fn fixed_commands_can_be_constructed_at_compile_time() {
400 const STOP: Command = Command::stop_auto_move();
401 const ENABLE_UPDATES: Command = Command::enable_realtime_updates();
402 const READ_BATTERY: Command = Command::read_battery_level();
403 const READ_PIECES: Command = Command::read_piece_status();
404
405 assert_eq!(&STOP.bytes()[..2], AUTO_MOVE_PREFIX);
406 assert_eq!(ENABLE_UPDATES.bytes(), [0x21, 0x01, 0x00]);
407 assert_eq!(READ_BATTERY.bytes(), [0x41, 0x01, 0x0c]);
408 assert_eq!(READ_PIECES.bytes(), [0x41, 0x01, 0x0b]);
409 }
410
411 #[test]
412 fn auto_move_command_encodes_target_position_and_mode() {
413 let mut position = Position::new([None; SQUARE_COUNT]);
414 position.set_piece(
415 Square::new(File::H, Rank::Eight),
416 Some(Piece {
417 color: Color::Black,
418 kind: PieceKind::Queen,
419 }),
420 );
421 position.set_piece(
422 Square::new(File::G, Rank::Eight),
423 Some(Piece {
424 color: Color::Black,
425 kind: PieceKind::King,
426 }),
427 );
428
429 let command = Command::auto_move(position, AutoMoveMode::Normal);
430
431 assert_eq!(&command.bytes()[..2], AUTO_MOVE_PREFIX);
432 assert_eq!(command.bytes()[2], 0x21);
433 assert!(command.bytes()[3..34].iter().all(|byte| *byte == 0));
434 assert_eq!(command.bytes()[34], AutoMoveMode::Normal.flag());
435 assert_eq!(command.write_kind(), WriteKind::WithoutResponse);
436 }
437
438 #[test]
439 fn stop_auto_move_command_zeroes_the_target_and_force_flag() {
440 let command = Command::stop_auto_move();
441
442 assert_eq!(&command.bytes()[..2], AUTO_MOVE_PREFIX);
443 assert!(command.bytes()[2..].iter().all(|byte| *byte == 0));
444 assert_eq!(command.write_kind(), WriteKind::WithoutResponse);
445 }
446
447 #[test]
448 fn battery_level_command_matches_move_api() {
449 let command = Command::read_battery_level();
450
451 assert_eq!(command.bytes(), [0x41, 0x01, 0x0c]);
452 assert_eq!(command.write_kind(), WriteKind::WithResponse);
453 }
454
455 #[test]
456 fn piece_status_command_matches_move_api() {
457 let command = Command::read_piece_status();
458
459 assert_eq!(command.bytes(), [0x41, 0x01, 0x0b]);
460 assert_eq!(command.write_kind(), WriteKind::WithResponse);
461 }
462
463 #[test]
464 fn led_command_uses_chessnut_square_and_nibble_order() {
465 let mut pattern = LedPattern::default();
466 pattern.set_color(Square::new(File::H, Rank::Eight), LedColor::Red);
467 pattern.set_color(Square::new(File::G, Rank::Eight), LedColor::Green);
468 pattern.set_color(Square::new(File::A, Rank::One), LedColor::Blue);
469
470 let command = Command::set_leds(&pattern);
471
472 assert_eq!(command.bytes().len(), SET_LED_COMMAND_LENGTH);
473 assert_eq!(&command.bytes()[..2], SET_LED_PREFIX);
474 assert_eq!(command.bytes()[2], 0x21);
475 assert!(command.bytes()[3..33].iter().all(|byte| *byte == 0));
476 assert_eq!(command.bytes()[33], 0x30);
477 assert_eq!(command.write_kind(), WriteKind::WithoutResponse);
478 }
479}