use crate::protocol::wire::{PackedSquares, decode_piece};
#[cfg(doc)]
use crate::transport;
mod command;
mod errors;
mod event;
mod led;
mod position;
mod square;
mod wire;
pub use command::*;
pub use errors::*;
pub use event::*;
pub use led::*;
pub use position::*;
pub use square::*;
const BOARD_STATE_OFFSET: usize = 2;
const RESPONSE_HEADER: u8 = 0x41;
const RESPONSE_HEADER_LENGTH: usize = 3;
const BATTERY_RESPONSE_LENGTH: usize = 5;
const BATTERY_RESPONSE_PAYLOAD_LENGTH: u8 = 0x03;
const BATTERY_RESPONSE_TYPE: u8 = 0x0c;
const PIECE_STATUS_RESPONSE_PAYLOAD_LENGTH: u8 = 0x89;
const PIECE_STATUS_RESPONSE_TYPE: u8 = 0x0b;
const TRACKED_PIECE_BYTES: usize = 4;
const PIECE_STATUS_RESPONSE_LENGTH: usize =
RESPONSE_HEADER_LENGTH + TRACKED_PIECE_COUNT * TRACKED_PIECE_BYTES;
pub fn decode_position_notification(
bytes: &[u8],
) -> Result<Position, DecodePositionNotificationError> {
let _span = trace_span!(
"decode_position_notification",
notification_len = bytes.len()
);
let expected_minimum = BOARD_STATE_OFFSET + BOARD_STATE_LENGTH;
let payload = bytes
.get(BOARD_STATE_OFFSET..)
.and_then(|payload| payload.first_chunk::<BOARD_STATE_LENGTH>())
.ok_or(NotificationTooShortError {
expected: expected_minimum,
actual: bytes.len(),
})?;
let packed = PackedSquares::from_bytes(*payload);
let squares = packed.decode(decode_piece)?;
trace_event!("decoded position notification");
Ok(Position::new(squares))
}
pub fn decode_response_notification(
bytes: &[u8],
) -> Result<BoardEvent, DecodeResponseNotificationError> {
let _span = trace_span!(
"decode_response_notification",
notification_len = bytes.len()
);
require_length(bytes, RESPONSE_HEADER_LENGTH)?;
require_header_byte(bytes, 0, RESPONSE_HEADER)?;
let event = match bytes[2] {
BATTERY_RESPONSE_TYPE => decode_battery_status(bytes).map(BoardEvent::BatteryStatus),
PIECE_STATUS_RESPONSE_TYPE => decode_piece_status(bytes).map(BoardEvent::PieceStatus),
response_type => Err(DecodeResponseNotificationError::UnexpectedNotification(
response_type,
)),
}?;
match event {
BoardEvent::BatteryStatus(_status) => trace_event!(
response = "battery_status",
charging = _status.charging,
percentage = _status.percentage,
"decoded command response"
),
BoardEvent::PieceStatus(_) => trace_event!(
response = "piece_status",
piece_count = TRACKED_PIECE_COUNT,
"decoded command response"
),
BoardEvent::PositionChanged(_) => {}
}
Ok(event)
}
fn decode_battery_status(bytes: &[u8]) -> Result<BatteryStatus, DecodeResponseNotificationError> {
require_length(bytes, BATTERY_RESPONSE_LENGTH)?;
require_header_byte(bytes, 1, BATTERY_RESPONSE_PAYLOAD_LENGTH)?;
let charging = match bytes[3] {
0 => false,
1 => true,
value => return Err(BatteryStatusError::InvalidChargingFlag(value).into()),
};
let percentage = validate_battery_percentage(bytes[4])?;
Ok(BatteryStatus {
charging,
percentage,
})
}
fn decode_piece_status(bytes: &[u8]) -> Result<PieceStatus, DecodeResponseNotificationError> {
require_length(bytes, PIECE_STATUS_RESPONSE_LENGTH)?;
require_header_byte(bytes, 1, PIECE_STATUS_RESPONSE_PAYLOAD_LENGTH)?;
let mut pieces = [TrackedPieceStatus::default(); TRACKED_PIECE_COUNT];
for (index, status) in pieces.iter_mut().enumerate() {
let offset = RESPONSE_HEADER_LENGTH + index * TRACKED_PIECE_BYTES;
let identity = bytes[offset];
let battery_percentage = bytes[offset + 3];
*status = TrackedPieceStatus {
piece: decode_tracked_piece(index, identity)?,
x: bytes[offset + 1],
y: bytes[offset + 2],
battery_percentage: validate_tracked_piece_battery(index, battery_percentage)?,
};
}
Ok(PieceStatus { pieces })
}
fn decode_tracked_piece(index: usize, value: u8) -> Result<Piece, PieceStatusError> {
use Color::{Black, White};
use PieceKind::{Bishop, King, Knight, Pawn, Queen, Rook};
let piece = match value {
0x01 => Piece {
color: White,
kind: Pawn,
},
0x02 => Piece {
color: White,
kind: Rook,
},
0x03 => Piece {
color: White,
kind: Knight,
},
0x04 => Piece {
color: White,
kind: Bishop,
},
0x05 => Piece {
color: White,
kind: Queen,
},
0x06 => Piece {
color: White,
kind: King,
},
0x07 => Piece {
color: Black,
kind: Pawn,
},
0x08 => Piece {
color: Black,
kind: Rook,
},
0x09 => Piece {
color: Black,
kind: Knight,
},
0x0a => Piece {
color: Black,
kind: Bishop,
},
0x0b => Piece {
color: Black,
kind: Queen,
},
0x0c => Piece {
color: Black,
kind: King,
},
value => {
return Err(PieceStatusError::InvalidTrackedPiece { index, value });
}
};
Ok(piece)
}
fn validate_battery_percentage(value: u8) -> Result<u8, BatteryStatusError> {
if value <= 100 {
Ok(value)
} else {
Err(BatteryStatusError::InvalidBatteryPercentage(value))
}
}
fn validate_tracked_piece_battery(index: usize, value: u8) -> Result<Option<u8>, PieceStatusError> {
match value {
0..=100 => Ok(Some(value)),
0xff => Ok(None),
_ => Err(PieceStatusError::InvalidTrackedPieceBattery { index, value }),
}
}
fn require_length(bytes: &[u8], expected: usize) -> Result<(), NotificationTooShortError> {
if bytes.len() < expected {
Err(NotificationTooShortError {
expected,
actual: bytes.len(),
})
} else {
Ok(())
}
}
fn require_header_byte(
bytes: &[u8],
offset: usize,
expected: u8,
) -> Result<(), DecodeResponseNotificationError> {
let actual = bytes[offset];
if actual == expected {
Ok(())
} else {
Err(DecodeResponseNotificationError::InvalidNotificationHeader {
offset,
expected,
actual,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRACKED_PIECE_IDENTITIES: [u8; TRACKED_PIECE_COUNT] = [
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 9, 9, 10, 10,
11, 11, 12,
];
#[test]
fn decodes_battery_status_response() {
let event = decode_response_notification(&[0x41, 0x03, 0x0c, 0x01, 87]);
assert_eq!(
event,
Ok(BoardEvent::BatteryStatus(BatteryStatus {
charging: true,
percentage: 87,
}))
);
}
#[test]
fn rejects_invalid_battery_status_fields() {
assert_eq!(
decode_response_notification(&[0x41, 0x03, 0x0c, 0x02, 87]),
Err(BatteryStatusError::InvalidChargingFlag(0x02).into())
);
assert_eq!(
decode_response_notification(&[0x41, 0x03, 0x0c, 0x00, 101]),
Err(BatteryStatusError::InvalidBatteryPercentage(101).into())
);
}
#[test]
fn decodes_all_tracked_piece_statuses() {
let response = piece_status_response();
let BoardEvent::PieceStatus(status) = decode_response_notification(&response).unwrap() else {
panic!("expected a piece-status event");
};
assert_eq!(
status.pieces[0],
TrackedPieceStatus {
piece: Piece {
color: Color::White,
kind: PieceKind::Pawn,
},
x: 0,
y: 255,
battery_percentage: Some(50),
}
);
assert_eq!(
status.pieces[16].piece,
Piece {
color: Color::White,
kind: PieceKind::King,
}
);
assert_eq!(
status.pieces[17].piece,
Piece {
color: Color::Black,
kind: PieceKind::Pawn,
}
);
assert_eq!(
status.pieces[33].piece,
Piece {
color: Color::Black,
kind: PieceKind::King,
}
);
assert_eq!(status.pieces[33].x, 33);
assert_eq!(status.pieces[33].y, 222);
assert_eq!(status.pieces[33].battery_percentage, Some(83));
}
#[test]
fn decodes_unavailable_tracked_piece_battery() {
let mut response = piece_status_response();
response[RESPONSE_HEADER_LENGTH + 3] = 0xff;
let BoardEvent::PieceStatus(status) = decode_response_notification(&response).unwrap() else {
panic!("expected a piece-status event");
};
assert_eq!(status.pieces[0].battery_percentage, None);
}
#[test]
fn rejects_invalid_tracked_piece_fields() {
let mut invalid_identity = piece_status_response();
invalid_identity[RESPONSE_HEADER_LENGTH + 5 * TRACKED_PIECE_BYTES] = 0xff;
assert_eq!(
decode_response_notification(&invalid_identity),
Err(
PieceStatusError::InvalidTrackedPiece {
index: 5,
value: 0xff,
}
.into()
)
);
let mut invalid_battery = piece_status_response();
invalid_battery[RESPONSE_HEADER_LENGTH + 7 * TRACKED_PIECE_BYTES + 3] = 101;
assert_eq!(
decode_response_notification(&invalid_battery),
Err(
PieceStatusError::InvalidTrackedPieceBattery {
index: 7,
value: 101,
}
.into()
)
);
}
#[test]
fn validates_response_length_and_header() {
assert_eq!(
decode_response_notification(&[0x41, 0x03]),
Err(
NotificationTooShortError {
expected: RESPONSE_HEADER_LENGTH,
actual: 2,
}
.into()
)
);
assert_eq!(
decode_response_notification(&[0x40, 0x03, 0x0c, 0, 50]),
Err(DecodeResponseNotificationError::InvalidNotificationHeader {
offset: 0,
expected: 0x41,
actual: 0x40,
})
);
assert_eq!(
decode_response_notification(&[0x41, 0x04, 0x0c, 0, 50]),
Err(DecodeResponseNotificationError::InvalidNotificationHeader {
offset: 1,
expected: 0x03,
actual: 0x04,
})
);
assert_eq!(
decode_response_notification(&[0x41, 0x01, 0xff]),
Err(DecodeResponseNotificationError::UnexpectedNotification(
0xff
))
);
}
#[test]
fn rejects_short_piece_status_response() {
let response = piece_status_response();
assert_eq!(
decode_response_notification(&response[..response.len() - 1]),
Err(
NotificationTooShortError {
expected: PIECE_STATUS_RESPONSE_LENGTH,
actual: PIECE_STATUS_RESPONSE_LENGTH - 1,
}
.into()
)
);
}
fn piece_status_response() -> [u8; PIECE_STATUS_RESPONSE_LENGTH] {
let mut response = [0; PIECE_STATUS_RESPONSE_LENGTH];
response[..RESPONSE_HEADER_LENGTH].copy_from_slice(&[
RESPONSE_HEADER,
PIECE_STATUS_RESPONSE_PAYLOAD_LENGTH,
PIECE_STATUS_RESPONSE_TYPE,
]);
for (index, identity) in TRACKED_PIECE_IDENTITIES.iter().copied().enumerate() {
let offset = RESPONSE_HEADER_LENGTH + index * TRACKED_PIECE_BYTES;
response[offset] = identity;
response[offset + 1] = index as u8;
response[offset + 2] = u8::MAX - index as u8;
response[offset + 3] = 50 + index as u8;
}
response
}
}