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#[derive(Debug)]
44pub struct Board<M: Mode> {
45 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 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 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 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 board.darken()?;
102 board.position()?;
104 Ok(board)
105 }
106}
107
108impl Board<Upload> {
109 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 pub fn battery_level(&mut self) -> Result<u8> {
127 info!(":: battery-level");
128 self.execute(Command::BATTERY_LEVEL)?;
130 loop {
131 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 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 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 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 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 pub fn led_on(&mut self, square: Square) -> Result<()> {
204 self.set_led(square, true)
205 }
206
207 pub fn led_off(&mut self, square: Square) -> Result<()> {
209 self.set_led(square, false)
210 }
211
212 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 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 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 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