chessnut/
lib.rs

1use core::time::Duration;
2
3use std::collections::BTreeSet as Set;
4
5#[macro_use(debug, error, info, warn)]
6extern crate tracing;
7
8pub use shakmaty::{Color, Piece, Role, Square, board::Board as Position};
9use thiserror::Error;
10
11mod hid;
12pub mod util;
13
14pub type Result<T, E = Error> = core::result::Result<T, E>;
15
16pub fn init_tracing() {
17    use tracing_subscriber::{
18        EnvFilter, fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _,
19    };
20    tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_env("LOG_LEVEL")).init();
21}
22
23#[derive(Debug, Error)]
24#[non_exhaustive]
25pub enum Error {
26    #[error("board connect error: {0:?}")]
27    Connect(hidapi::HidError),
28    #[error("board read error: {0:?}")]
29    Read(hidapi::HidError),
30    #[error("board write error: {0:?}")]
31    Write(hidapi::HidError),
32    #[error("no Chessnut board found")]
33    NoBoardFound,
34    #[error("Chessnut board not turned on")]
35    BoardNotOn,
36    #[error("invalid report: {0}")]
37    InvalidReport(&'static str),
38    #[error("unknown report: {0}")]
39    UnknownReport(u8),
40}
41
42/// Connected Chessnut board
43#[derive(Debug)]
44pub struct Board<M: Mode> {
45    // TODO: turn into ble/hid enum
46    connection: hid::Connection,
47    lit: Set<Square>,
48    #[allow(dead_code)]
49    mode: M,
50}
51
52#[derive(Debug)]
53pub enum Response {
54    BatteryLevel(u8),
55    BleVersion(String),
56    McuVersion(String),
57    Position(Position),
58}
59
60struct Command;
61impl Command {
62    const BATTERY_LEVEL: &[u8] = &[0x29, 0x01, 0x00];
63    // const GAME_COUNT: &[u8] = &[0x31, 0x01, 0x00];
64    const MCU_VERSION: &[u8] = &[0x27, 0x01, 0x01];
65    const BLE_VERSION: &[u8] = &[0x27, 0x01, 0x00];
66
67    fn beep(beep: Beep) -> Vec<u8> {
68        let Beep { duration, frequency } = beep;
69        let duration = duration.as_millis() as u16;
70        let mut command: Vec<_> = [0x0b, 0x04].into();
71        command.extend(frequency.to_be_bytes());
72        command.extend(duration.to_be_bytes());
73        command
74    }
75
76    fn lighten(squares: &Set<Square>) -> Vec<u8> {
77        let mut encoding = [0u8; 8];
78        // TODO: figure out the encoding used
79        for square in squares.iter() {
80            let (file, rank) = square.coords();
81            encoding[(7 - rank as u8) as usize] |= 1 << (7 - file as u8);
82        }
83        let mut command: Vec<_> = [0x0a, 0x08].into();
84        command.extend(encoding);
85        command
86    }
87
88    fn switch_mode<M: Mode>() -> Vec<u8> {
89        let mut command: Vec<_> = [0x21, 0x01].into();
90        command.push(M::MODE_BYTE);
91        command
92    }
93}
94
95impl Board<Realtime> {
96    /// Connect to Chessnut board in realtime mode
97    pub fn realtime() -> Result<Self> {
98        let connection = hid::Connection::new()?.set_mode::<Realtime>()?;
99        let mut board = Board { connection, lit: Default::default(), mode: Realtime };
100        // turn off all lit LEDs
101        board.darken()?;
102        // check board is sending positions
103        board.position()?;
104        Ok(board)
105    }
106}
107
108impl Board<Upload> {
109    /// Connect to Chessnut board in file upload mode
110    pub fn upload() -> Result<Board<Upload>> {
111        let connection = hid::Connection::new()?.set_mode::<Upload>()?;
112        let mut board = Board { connection, lit: Default::default(), mode: Upload };
113        board.darken()?;
114        Ok(board)
115    }
116}
117
118impl<M: Mode> Board<M> {
119    fn execute(&mut self, command: &[u8]) -> Result<()> {
120        debug!("-> {}", hex::encode(command));
121        self.connection.execute(command)
122    }
123
124    /// Get battery level of board
125    // This is percent, so 0x64 = 100 is 100%
126    pub fn battery_level(&mut self) -> Result<u8> {
127        info!(":: battery-level");
128        // according to documentation, this command is not needed
129        self.execute(Command::BATTERY_LEVEL)?;
130        loop {
131            // example expected raw response: 2a024d01
132            let (indicator, report) = self.connection.report()?;
133            if indicator == 0x2a {
134                assert_eq!(report.len(), 2);
135                let battery_level = report[0];
136                info!("...ok {battery_level}");
137                return Ok(battery_level);
138            }
139        }
140    }
141
142    /// Beep the board
143    pub fn beep(&mut self, beep: Beep) -> Result<()> {
144        info!(":: beep {beep:?}");
145        self.execute(&Command::beep(beep))?;
146        info!("...ok");
147        Ok(())
148    }
149
150    pub fn beep_default(&mut self) -> Result<()> {
151        self.beep(Beep::default())
152    }
153
154    /// Bluetooth version of board
155    // Example: CNCA101_V103
156    // Example expected report: 28 3d 00 434e43413130315f56313033 000...
157    // Android app calles this "Firmware"
158    pub fn ble_version(&mut self) -> Result<String> {
159        info!(":: ble-version");
160        let mut i = 0;
161        loop {
162            if i % 10 == 0 {
163                self.execute(Command::BLE_VERSION)?;
164            }
165            i += 1;
166            if let Ok(Response::BleVersion(version)) = self.connection.report()?.try_into() {
167                info!("...ok {version}");
168                return Ok(version);
169            }
170        }
171    }
172
173    /// Microcontroller version of board
174    // Example: CNAIRP_00_240516
175    // Example expected report: 28 3d 01 434e414952505f30305f323430353136 000...
176    pub fn mcu_version(&mut self) -> Result<String> {
177        info!(":: mcu-version");
178        let mut i = 0;
179        loop {
180            if i % 3 == 0 {
181                self.execute(Command::MCU_VERSION)?;
182            }
183            i += 1;
184            if let Ok(Response::McuVersion(version)) = self.connection.report()?.try_into() {
185                info!("...ok {version}");
186                return Ok(version);
187            }
188        }
189    }
190
191    /// Turn an LED on the board on or off
192    pub fn set_led(&mut self, square: Square, on: bool) -> Result<()> {
193        let mut squares = self.lit.clone();
194        if on {
195            squares.insert(square);
196        } else {
197            squares.remove(&square);
198        }
199        self.lighten(squares)
200    }
201
202    /// Turn an LED on the board on
203    pub fn led_on(&mut self, square: Square) -> Result<()> {
204        self.set_led(square, true)
205    }
206
207    /// Turn an LED on the board off
208    pub fn led_off(&mut self, square: Square) -> Result<()> {
209        self.set_led(square, false)
210    }
211
212    /// Turn the supplied squares' LEDs on, and the others off
213    pub fn lighten(&mut self, squares: Set<Square>) -> Result<()> {
214        info!(":: lighten {squares:?}");
215        self.execute(&Command::lighten(&squares))?;
216        self.lit = squares;
217        info!("...ok");
218        Ok(())
219    }
220
221    /// Turn off all LEDs on the board
222    pub fn darken(&mut self) -> Result<()> {
223        self.lighten(Set::default())
224    }
225
226    pub fn disco(&mut self, count: usize) -> Result<()> {
227        use crate::util::Squares;
228
229        for _ in 0..count {
230            self.lighten(Squares::light())?;
231            self.lighten(Squares::dark())?;
232        }
233        Ok(())
234    }
235
236    // /// Number of games stored on board
237    // pub fn game_count(&mut self) -> Result<usize> {
238    //     loop {
239    //         self.execute(Command::GAME_COUNT)?;
240    //         let (indicator, report) = self.connection.report()?;
241    //         if indicator != 0x01 && indicator != 0x2a {
242    //             println!("report: {}", hex::encode(&report));
243    //             return Ok(0);
244    //         } else {
245    //             // println!("skipping");
246    //         }
247    //     }
248    // }
249
250    /// Current position on the board
251    pub fn position(&mut self) -> Result<Position> {
252        info!(":: position");
253        loop {
254            if let Ok(Response::Position(position)) = self.connection.report()?.try_into() {
255                info!("...ok {position}");
256                return Ok(position);
257            }
258        }
259    }
260}
261
262impl Board<Upload> {
263    /// Load the games on the board, optionally deleting
264    pub fn games(&mut self, _delete: bool) -> Result<Vec<String>> {
265        todo!();
266    }
267}
268
269impl Board<Realtime> {
270    pub fn to_upload(self) -> Result<Board<Upload>> {
271        let Board { connection, lit, .. } = self;
272        let connection = connection.set_mode::<Upload>()?;
273        Ok(Board { connection, lit, mode: Upload })
274    }
275}
276
277impl Board<Upload> {
278    pub fn to_realtime(self) -> Result<Board<Realtime>> {
279        let Board { connection, lit, .. } = self;
280        let connection = connection.set_mode::<Realtime>()?;
281        Ok(Board { connection, lit, mode: Realtime })
282    }
283}
284
285pub trait Mode: private::Mode {}
286impl<M: private::Mode> Mode for M {}
287
288mod private {
289    pub trait Mode {
290        const MODE: &str;
291        const MODE_BYTE: u8;
292    }
293
294    impl Mode for crate::Realtime {
295        const MODE: &str = "realtime";
296        const MODE_BYTE: u8 = 0x00;
297    }
298
299    impl Mode for crate::Upload {
300        const MODE: &str = "upload";
301        const MODE_BYTE: u8 = 0x01;
302    }
303}
304
305#[derive(Copy, Clone, Debug)]
306pub struct Realtime;
307
308#[derive(Copy, Clone, Debug)]
309pub struct Upload;
310
311#[derive(Copy, Clone, Debug)]
312pub struct Beep {
313    duration: Duration,
314    frequency: u16,
315}
316
317impl Beep {
318    pub const DEFAULT_DURATION: Duration = Duration::from_millis(200);
319    pub const DEFAULT_FREQUENCY: u16 = 1000;
320
321    pub fn new() -> Self {
322        Self { duration: Self::DEFAULT_DURATION, frequency: Self::DEFAULT_FREQUENCY }
323    }
324
325    pub fn set_duration(mut self, duration: Duration) -> Self {
326        self.duration = duration;
327        self
328    }
329
330    pub fn set_frequency(mut self, frequency: u16) -> Self {
331        self.frequency = frequency;
332        self
333    }
334}
335
336impl Default for Beep {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342// #[cfg(test)]
343// mod tests {}