chessnut 0.0.1

Rust driver for Chessnut electronic chess boards
Documentation
use core::time::Duration;

use std::collections::BTreeSet as Set;

#[macro_use(debug, error, info, warn)]
extern crate tracing;

pub use shakmaty::{Color, Piece, Role, Square, board::Board as Position};
use thiserror::Error;

mod hid;
pub mod util;

pub type Result<T, E = Error> = core::result::Result<T, E>;

pub fn init_tracing() {
    use tracing_subscriber::{
        EnvFilter, fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _,
    };
    tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_env("LOG_LEVEL")).init();
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
    #[error("board connect error: {0:?}")]
    Connect(hidapi::HidError),
    #[error("board read error: {0:?}")]
    Read(hidapi::HidError),
    #[error("board write error: {0:?}")]
    Write(hidapi::HidError),
    #[error("no Chessnut board found")]
    NoBoardFound,
    #[error("Chessnut board not turned on")]
    BoardNotOn,
    #[error("invalid report: {0}")]
    InvalidReport(&'static str),
    #[error("unknown report: {0}")]
    UnknownReport(u8),
}

/// Connected Chessnut board
#[derive(Debug)]
pub struct Board<M: Mode> {
    // TODO: turn into ble/hid enum
    connection: hid::Connection,
    lit: Set<Square>,
    #[allow(dead_code)]
    mode: M,
}

#[derive(Debug)]
pub enum Response {
    BatteryLevel(u8),
    BleVersion(String),
    McuVersion(String),
    Position(Position),
}

struct Command;
impl Command {
    const BATTERY_LEVEL: &[u8] = &[0x29, 0x01, 0x00];
    // const GAME_COUNT: &[u8] = &[0x31, 0x01, 0x00];
    const MCU_VERSION: &[u8] = &[0x27, 0x01, 0x01];
    const BLE_VERSION: &[u8] = &[0x27, 0x01, 0x00];

    fn beep(beep: Beep) -> Vec<u8> {
        let Beep { duration, frequency } = beep;
        let duration = duration.as_millis() as u16;
        let mut command: Vec<_> = [0x0b, 0x04].into();
        command.extend(frequency.to_be_bytes());
        command.extend(duration.to_be_bytes());
        command
    }

    fn lighten(squares: &Set<Square>) -> Vec<u8> {
        let mut encoding = [0u8; 8];
        // TODO: figure out the encoding used
        for square in squares.iter() {
            let (file, rank) = square.coords();
            encoding[(7 - rank as u8) as usize] |= 1 << (7 - file as u8);
        }
        let mut command: Vec<_> = [0x0a, 0x08].into();
        command.extend(encoding);
        command
    }

    fn switch_mode<M: Mode>() -> Vec<u8> {
        let mut command: Vec<_> = [0x21, 0x01].into();
        command.push(M::MODE_BYTE);
        command
    }
}

impl Board<Realtime> {
    /// Connect to Chessnut board in realtime mode
    pub fn realtime() -> Result<Self> {
        let connection = hid::Connection::new()?.set_mode::<Realtime>()?;
        let mut board = Board { connection, lit: Default::default(), mode: Realtime };
        // turn off all lit LEDs
        board.darken()?;
        // check board is sending positions
        board.position()?;
        Ok(board)
    }
}

impl Board<Upload> {
    /// Connect to Chessnut board in file upload mode
    pub fn upload() -> Result<Board<Upload>> {
        let connection = hid::Connection::new()?.set_mode::<Upload>()?;
        let mut board = Board { connection, lit: Default::default(), mode: Upload };
        board.darken()?;
        Ok(board)
    }
}

impl<M: Mode> Board<M> {
    fn execute(&mut self, command: &[u8]) -> Result<()> {
        debug!("-> {}", hex::encode(command));
        self.connection.execute(command)
    }

    /// Get battery level of board
    // This is percent, so 0x64 = 100 is 100%
    pub fn battery_level(&mut self) -> Result<u8> {
        info!(":: battery-level");
        // according to documentation, this command is not needed
        self.execute(Command::BATTERY_LEVEL)?;
        loop {
            // example expected raw response: 2a024d01
            let (indicator, report) = self.connection.report()?;
            if indicator == 0x2a {
                assert_eq!(report.len(), 2);
                let battery_level = report[0];
                info!("...ok {battery_level}");
                return Ok(battery_level);
            }
        }
    }

    /// Beep the board
    pub fn beep(&mut self, beep: Beep) -> Result<()> {
        info!(":: beep {beep:?}");
        self.execute(&Command::beep(beep))?;
        info!("...ok");
        Ok(())
    }

    pub fn beep_default(&mut self) -> Result<()> {
        self.beep(Beep::default())
    }

    /// Bluetooth version of board
    // Example: CNCA101_V103
    // Example expected report: 28 3d 00 434e43413130315f56313033 000...
    // Android app calles this "Firmware"
    pub fn ble_version(&mut self) -> Result<String> {
        info!(":: ble-version");
        let mut i = 0;
        loop {
            if i % 10 == 0 {
                self.execute(Command::BLE_VERSION)?;
            }
            i += 1;
            if let Ok(Response::BleVersion(version)) = self.connection.report()?.try_into() {
                info!("...ok {version}");
                return Ok(version);
            }
        }
    }

    /// Microcontroller version of board
    // Example: CNAIRP_00_240516
    // Example expected report: 28 3d 01 434e414952505f30305f323430353136 000...
    pub fn mcu_version(&mut self) -> Result<String> {
        info!(":: mcu-version");
        let mut i = 0;
        loop {
            if i % 3 == 0 {
                self.execute(Command::MCU_VERSION)?;
            }
            i += 1;
            if let Ok(Response::McuVersion(version)) = self.connection.report()?.try_into() {
                info!("...ok {version}");
                return Ok(version);
            }
        }
    }

    /// Turn an LED on the board on or off
    pub fn set_led(&mut self, square: Square, on: bool) -> Result<()> {
        let mut squares = self.lit.clone();
        if on {
            squares.insert(square);
        } else {
            squares.remove(&square);
        }
        self.lighten(squares)
    }

    /// Turn an LED on the board on
    pub fn led_on(&mut self, square: Square) -> Result<()> {
        self.set_led(square, true)
    }

    /// Turn an LED on the board off
    pub fn led_off(&mut self, square: Square) -> Result<()> {
        self.set_led(square, false)
    }

    /// Turn the supplied squares' LEDs on, and the others off
    pub fn lighten(&mut self, squares: Set<Square>) -> Result<()> {
        info!(":: lighten {squares:?}");
        self.execute(&Command::lighten(&squares))?;
        self.lit = squares;
        info!("...ok");
        Ok(())
    }

    /// Turn off all LEDs on the board
    pub fn darken(&mut self) -> Result<()> {
        self.lighten(Set::default())
    }

    pub fn disco(&mut self, count: usize) -> Result<()> {
        use crate::util::Squares;

        for _ in 0..count {
            self.lighten(Squares::light())?;
            self.lighten(Squares::dark())?;
        }
        Ok(())
    }

    // /// Number of games stored on board
    // pub fn game_count(&mut self) -> Result<usize> {
    //     loop {
    //         self.execute(Command::GAME_COUNT)?;
    //         let (indicator, report) = self.connection.report()?;
    //         if indicator != 0x01 && indicator != 0x2a {
    //             println!("report: {}", hex::encode(&report));
    //             return Ok(0);
    //         } else {
    //             // println!("skipping");
    //         }
    //     }
    // }

    /// Current position on the board
    pub fn position(&mut self) -> Result<Position> {
        info!(":: position");
        loop {
            if let Ok(Response::Position(position)) = self.connection.report()?.try_into() {
                info!("...ok {position}");
                return Ok(position);
            }
        }
    }
}

impl Board<Upload> {
    /// Load the games on the board, optionally deleting
    pub fn games(&mut self, _delete: bool) -> Result<Vec<String>> {
        todo!();
    }
}

impl Board<Realtime> {
    pub fn to_upload(self) -> Result<Board<Upload>> {
        let Board { connection, lit, .. } = self;
        let connection = connection.set_mode::<Upload>()?;
        Ok(Board { connection, lit, mode: Upload })
    }
}

impl Board<Upload> {
    pub fn to_realtime(self) -> Result<Board<Realtime>> {
        let Board { connection, lit, .. } = self;
        let connection = connection.set_mode::<Realtime>()?;
        Ok(Board { connection, lit, mode: Realtime })
    }
}

pub trait Mode: private::Mode {}
impl<M: private::Mode> Mode for M {}

mod private {
    pub trait Mode {
        const MODE: &str;
        const MODE_BYTE: u8;
    }

    impl Mode for crate::Realtime {
        const MODE: &str = "realtime";
        const MODE_BYTE: u8 = 0x00;
    }

    impl Mode for crate::Upload {
        const MODE: &str = "upload";
        const MODE_BYTE: u8 = 0x01;
    }
}

#[derive(Copy, Clone, Debug)]
pub struct Realtime;

#[derive(Copy, Clone, Debug)]
pub struct Upload;

#[derive(Copy, Clone, Debug)]
pub struct Beep {
    duration: Duration,
    frequency: u16,
}

impl Beep {
    pub const DEFAULT_DURATION: Duration = Duration::from_millis(200);
    pub const DEFAULT_FREQUENCY: u16 = 1000;

    pub fn new() -> Self {
        Self { duration: Self::DEFAULT_DURATION, frequency: Self::DEFAULT_FREQUENCY }
    }

    pub fn set_duration(mut self, duration: Duration) -> Self {
        self.duration = duration;
        self
    }

    pub fn set_frequency(mut self, frequency: u16) -> Self {
        self.frequency = frequency;
        self
    }
}

impl Default for Beep {
    fn default() -> Self {
        Self::new()
    }
}

// #[cfg(test)]
// mod tests {}