1use crate::VeriCommFrame;
2use crate::config::Config;
3use crate::constants;
4use crate::error::{Error, Result};
5use crate::licence::Licence;
6use crate::usb::{BoardInfo, BoardSelector, Probe};
7use crate::usb::{Endpoint, TransportConfig, UsbDevice};
8use nusb::{
9 Endpoint as UsbEndpoint,
10 transfer::{Buffer, Bulk, Completion, EndpointDirection, In, Out},
11};
12use std::collections::VecDeque;
13use std::thread;
14use std::time::{Duration, Instant};
15
16const CONTROL_COMMAND_PREFIX: u8 = 0x01;
17const VERICOMM_TRANSFER_PACKET_BYTES: usize = 8;
18const MAX_PIPELINE_DEPTH: usize = 512;
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct TransferStageProfile {
22 pub calls: u64,
23 pub transfers: u64,
24 pub validation: Duration,
25 pub setup: Duration,
26 pub submit: Duration,
27 pub wait_write: Duration,
28 pub wait_read: Duration,
29 pub decode_copy: Duration,
30}
31
32impl TransferStageProfile {
33 pub fn merge(&mut self, other: &Self) {
34 self.calls = self.calls.saturating_add(other.calls);
35 self.transfers = self.transfers.saturating_add(other.transfers);
36 self.validation = self.validation.saturating_add(other.validation);
37 self.setup = self.setup.saturating_add(other.setup);
38 self.submit = self.submit.saturating_add(other.submit);
39 self.wait_write = self.wait_write.saturating_add(other.wait_write);
40 self.wait_read = self.wait_read.saturating_add(other.wait_read);
41 self.decode_copy = self.decode_copy.saturating_add(other.decode_copy);
42 }
43
44 pub fn total_duration(&self) -> Duration {
45 self.validation
46 .saturating_add(self.setup)
47 .saturating_add(self.submit)
48 .saturating_add(self.wait_write)
49 .saturating_add(self.wait_read)
50 .saturating_add(self.decode_copy)
51 }
52}
53
54#[derive(Debug, Clone, Copy)]
55enum TransferProfileStage {
56 Validation,
57 Setup,
58 Submit,
59 WaitWrite,
60 WaitRead,
61 DecodeCopy,
62}
63
64struct TransferProfiler<'a> {
65 profile: Option<&'a mut TransferStageProfile>,
66}
67
68impl<'a> TransferProfiler<'a> {
69 fn new(profile: Option<&'a mut TransferStageProfile>, transfers: usize) -> Self {
70 let mut profiler = Self { profile };
71 if let Some(profile) = profiler.profile.as_deref_mut() {
72 profile.calls = profile.calls.saturating_add(1);
73 profile.transfers = profile.transfers.saturating_add(transfers as u64);
74 }
75 profiler
76 }
77
78 fn borrow(profile: Option<&'a mut TransferStageProfile>) -> Self {
79 Self { profile }
80 }
81
82 fn add(&mut self, stage: TransferProfileStage, elapsed: Duration) {
83 let Some(profile) = self.profile.as_deref_mut() else {
84 return;
85 };
86
87 match stage {
88 TransferProfileStage::Validation => {
89 profile.validation = profile.validation.saturating_add(elapsed);
90 }
91 TransferProfileStage::Setup => {
92 profile.setup = profile.setup.saturating_add(elapsed);
93 }
94 TransferProfileStage::Submit => {
95 profile.submit = profile.submit.saturating_add(elapsed);
96 }
97 TransferProfileStage::WaitWrite => {
98 profile.wait_write = profile.wait_write.saturating_add(elapsed);
99 }
100 TransferProfileStage::WaitRead => {
101 profile.wait_read = profile.wait_read.saturating_add(elapsed);
102 }
103 TransferProfileStage::DecodeCopy => {
104 profile.decode_copy = profile.decode_copy.saturating_add(elapsed);
105 }
106 }
107 }
108}
109
110pub struct Board {
111 usb: UsbDevice,
112 config: Config,
113 crypto: CryptoState,
114 initialized: bool,
115 mode: BoardMode,
116}
117
118impl Board {
119 pub fn enumerate() -> Result<Vec<BoardInfo>> {
120 Probe::new().boards()
121 }
122
123 pub fn open() -> Result<Self> {
124 Self::open_with_transport(TransportConfig::default())
125 }
126
127 pub fn open_with_transport(transport: TransportConfig) -> Result<Self> {
128 Self::open_selected_with_transport(&BoardSelector::Only, transport)
129 }
130
131 pub fn open_selected(selector: &BoardSelector) -> Result<Self> {
132 Self::open_selected_with_transport(selector, TransportConfig::default())
133 }
134
135 pub fn open_selected_with_transport(
136 selector: &BoardSelector,
137 transport: TransportConfig,
138 ) -> Result<Self> {
139 let mut usb = UsbDevice::with_transport_config(transport)?;
140 usb.open_selected(constants::DW_VID, constants::DW_PID, selector)?;
141
142 let mut board = Self {
143 usb,
144 config: Config::new(),
145 crypto: CryptoState::default(),
146 initialized: false,
147 mode: BoardMode::Unknown,
148 };
149 board.initialize()?;
150 Ok(board)
151 }
152
153 pub fn transport(&self) -> &TransportConfig {
154 self.usb.transport_config()
155 }
156
157 pub fn config(&self) -> &Config {
158 &self.config
159 }
160
161 pub fn mode(&self) -> BoardMode {
162 self.mode
163 }
164
165 pub fn is_initialized(&self) -> bool {
166 self.initialized
167 }
168
169 pub fn initialize(&mut self) -> Result<()> {
170 match self.initialize_once() {
171 Ok(()) => Ok(()),
172 Err(err) if should_retry_initialize(&err) => {
173 self.try_recover_control_plane()?;
174 self.initialize_once()
175 }
176 Err(err) => Err(err),
177 }
178 }
179
180 fn initialize_once(&mut self) -> Result<()> {
181 self.read_encrypt_table()?;
182 self.crypto.decode_table();
183 self.refresh_config()?;
184 Ok(())
185 }
186
187 pub fn refresh_config(&mut self) -> Result<&Config> {
188 self.sync_delay()?;
189 self.usb
190 .write_bytes(Endpoint::Command, &[CONTROL_COMMAND_PREFIX, 0x01])?;
191
192 let mut words = [0u16; Config::WORD_COUNT];
193 self.usb.read_words(Endpoint::FifoRead, &mut words)?;
194 self.activate_control()?;
195 self.crypto.decrypt_words(&mut words);
196 self.config = Config::from_words(words);
197 self.initialized = true;
198 self.mode = BoardMode::Control;
199 Ok(&self.config)
200 }
201
202 pub fn write_config(&mut self) -> Result<()> {
203 self.sync_delay()?;
204 let mut words = *self.config.words();
205 self.crypto.encrypt_words(&mut words);
206 self.usb
207 .write_bytes(Endpoint::Command, &[CONTROL_COMMAND_PREFIX, 0x11])?;
208 self.usb.write_words(Endpoint::FifoWrite, &words)?;
209 self.activate_control()?;
210 self.initialized = true;
211 self.mode = BoardMode::Control;
212 Ok(())
213 }
214
215 pub fn configure_io(&mut self, settings: &IoConfig) -> Result<IoSession<'_>> {
216 self.ensure_ready()?;
217
218 let actual_version = self.config.smims_version_raw();
219 if actual_version < constants::SMIMS_VERSION {
220 return Err(Error::VersionMismatch {
221 expected: constants::SMIMS_VERSION,
222 actual: actual_version,
223 });
224 }
225 if !self.config.is_programmed() {
226 return Err(Error::NotProgrammed);
227 }
228 if !self.config.vericomm_ability() {
229 return Err(Error::FeatureUnavailable("vericomm"));
230 }
231
232 let licence_key = settings.licence.key_for(self.config.security_key());
233 self.config.set_licence_key(licence_key);
234 self.config
235 .set_vericomm_clock_high_delay(settings.clock_high_delay);
236 self.config
237 .set_vericomm_clock_low_delay(settings.clock_low_delay);
238 self.config.set_vericomm_isv(settings.vericomm_isv);
239 self.config
240 .set_vericomm_clock_check_enabled(settings.clock_check_enabled);
241 self.config.set_mode_selector(settings.mode_selector);
242 self.write_config()?;
243 self.activate_mode(BoardMode::VeriComm)?;
244
245 Ok(IoSession {
246 board: self,
247 pipeline_write: None,
248 pipeline_read: None,
249 single_tx_buffer: None,
250 single_rx_buffer: None,
251 tx_pool: Vec::new(),
252 rx_pool: Vec::new(),
253 finished: false,
254 })
255 }
256
257 pub fn programmer(&mut self) -> Result<ProgramSession<'_>> {
258 self.ensure_ready()?;
259 self.activate_mode(BoardMode::FpgaProgrammer)?;
260 Ok(ProgramSession { board: self })
261 }
262
263 pub fn close(mut self) -> Result<()> {
264 self.usb.close()
265 }
266
267 pub(crate) fn encrypt_words(&mut self, words: &mut [u16]) {
268 self.crypto.encrypt_words(words);
269 }
270
271 pub(crate) fn fifo_write_words(&self, words: &[u16]) -> Result<()> {
272 self.usb.write_words(Endpoint::FifoWrite, words)
273 }
274
275 pub(crate) fn command_active(&mut self) -> Result<()> {
276 self.activate_control()
277 }
278
279 pub(crate) fn activate_control(&mut self) -> Result<()> {
280 self.sync_delay()?;
281 self.usb
282 .write_bytes(Endpoint::Command, &[CONTROL_COMMAND_PREFIX, 0x00])?;
283 self.mode = BoardMode::Control;
284 Ok(())
285 }
286
287 fn engine_reset(&mut self) -> Result<()> {
288 self.usb.write_bytes(Endpoint::Command, &[0x02])?;
289 self.mode = BoardMode::Unknown;
290 Ok(())
291 }
292
293 fn try_recover_control_plane(&mut self) -> Result<()> {
294 self.usb.clear_halt_all()?;
295 self.engine_reset()?;
296 thread::sleep(Duration::from_millis(2));
297 Ok(())
298 }
299
300 fn ensure_ready(&mut self) -> Result<()> {
301 if !self.initialized {
302 self.initialize()?;
303 }
304 Ok(())
305 }
306
307 fn ensure_mode(&self, expected: BoardMode) -> Result<()> {
308 if self.mode != expected {
309 return Err(Error::InvalidMode {
310 expected: expected.as_str(),
311 actual: self.mode.as_str(),
312 });
313 }
314 Ok(())
315 }
316
317 fn activate_mode(&mut self, mode: BoardMode) -> Result<()> {
318 let Some(command) = mode.command_byte() else {
319 return Err(Error::UnexpectedResponse("unsupported mode command"));
320 };
321 self.sync_delay()?;
322 self.usb
323 .write_bytes(Endpoint::Command, &[CONTROL_COMMAND_PREFIX, command])?;
324 self.mode = mode;
325 Ok(())
326 }
327
328 fn read_encrypt_table(&mut self) -> Result<()> {
329 self.sync_delay()?;
330 self.usb
331 .write_bytes(Endpoint::Command, &[CONTROL_COMMAND_PREFIX, 0x0f])?;
332 self.usb
333 .read_words(Endpoint::FifoRead, self.crypto.table_mut())
334 }
335
336 fn sync_delay(&self) -> Result<()> {
337 let start = Instant::now();
338 let sync_timeout = self.transport().sync_timeout;
339 let mut buffer = [0u8; 1];
340
341 while start.elapsed() <= sync_timeout {
342 self.usb.write_bytes(Endpoint::Command, &buffer)?;
343 self.usb.read_bytes(Endpoint::Sync, &mut buffer)?;
344 if buffer[0] != 0 {
345 return Ok(());
346 }
347 }
348
349 Err(Error::Timeout("sync_delay"))
350 }
351}
352
353pub struct IoSession<'a> {
354 board: &'a mut Board,
355 pipeline_write: Option<UsbEndpoint<Bulk, Out>>,
356 pipeline_read: Option<UsbEndpoint<Bulk, In>>,
357 single_tx_buffer: Option<Buffer>,
358 single_rx_buffer: Option<Buffer>,
359 tx_pool: Vec<Buffer>,
360 rx_pool: Vec<Buffer>,
361 finished: bool,
362}
363
364pub struct IoTransferWindow<'session, 'board> {
371 io: &'session mut IoSession<'board>,
372 frame_words: usize,
373 frame_bytes: usize,
374 read_request_bytes: usize,
375 capacity: usize,
376 pending_reads: VecDeque<PendingWindowRead>,
377 pending_writes: usize,
378}
379
380struct PendingWindowRead {
381 buffer_id: usize,
382 completion: Option<Completion>,
383}
384
385impl PendingWindowRead {
386 fn new(buffer_id: usize) -> Self {
387 Self {
388 buffer_id,
389 completion: None,
390 }
391 }
392
393 fn is_waiting(&self) -> bool {
394 self.completion.is_none()
395 }
396
397 fn complete(&mut self, completion: Completion) -> Result<()> {
398 if self.completion.is_some() {
399 return Err(Error::UnexpectedResponse(
400 "pipeline read completion matched an already completed transfer",
401 ));
402 }
403 self.completion = Some(completion);
404 Ok(())
405 }
406
407 fn into_completion(mut self) -> Option<Completion> {
408 self.completion.take()
409 }
410}
411
412impl<'a> IoSession<'a> {
413 fn cleanup(&mut self) -> Result<()> {
414 if let Some(pipeline_write) = self.pipeline_write.as_mut() {
415 pipeline_write.cancel_all();
416 }
417 if let Some(pipeline_read) = self.pipeline_read.as_mut() {
418 pipeline_read.cancel_all();
419 }
420 self.pipeline_write = None;
421 self.pipeline_read = None;
422 self.single_tx_buffer = None;
423 self.single_rx_buffer = None;
424 self.tx_pool.clear();
425 self.rx_pool.clear();
426 self.board.try_recover_control_plane()?;
427 self.board.activate_control()
428 }
429
430 fn ensure_pipeline_endpoints(&mut self) -> Result<()> {
431 if self.pipeline_write.is_none() {
432 self.pipeline_write = Some(self.board.usb.open_out_endpoint(Endpoint::FifoWrite)?);
433 }
434 if self.pipeline_read.is_none() {
435 self.pipeline_read = Some(self.board.usb.open_in_endpoint(Endpoint::FifoRead)?);
436 }
437 Ok(())
438 }
439
440 fn take_single_tx_buffer(&mut self, tx_bytes: usize) -> Buffer {
441 if let Some(mut buffer) = self.single_tx_buffer.take() {
442 if buffer.capacity() >= tx_bytes.max(1) {
443 buffer.clear();
444 return buffer;
445 }
446 }
447
448 self.pipeline_write
449 .as_mut()
450 .expect("pipeline write endpoint should be initialized")
451 .allocate(tx_bytes.max(1))
452 }
453
454 fn take_single_rx_buffer(&mut self, request_bytes: usize) -> Buffer {
455 if let Some(mut buffer) = self.single_rx_buffer.take() {
456 if buffer.capacity() >= request_bytes.max(1) {
457 buffer.clear();
458 buffer.set_requested_len(request_bytes.max(1));
459 return buffer;
460 }
461 }
462
463 let mut buffer = self
464 .pipeline_read
465 .as_mut()
466 .expect("pipeline read endpoint should be initialized")
467 .allocate(request_bytes.max(1));
468 buffer.set_requested_len(request_bytes.max(1));
469 buffer
470 }
471
472 fn prepare_pools(&mut self, pipeline_depth: usize, tx_bytes: usize, rx_bytes: usize) {
473 let tx_bytes = tx_bytes.max(1);
474 let rx_bytes = rx_bytes.max(1);
475
476 let pipeline_write = self
477 .pipeline_write
478 .as_mut()
479 .expect("pipeline write endpoint should be initialized");
480 let pipeline_read = self
481 .pipeline_read
482 .as_mut()
483 .expect("pipeline read endpoint should be initialized");
484
485 discard_undersized_buffers(&mut self.tx_pool, tx_bytes);
486 discard_undersized_buffers(&mut self.rx_pool, rx_bytes);
487
488 while self.tx_pool.len() < pipeline_depth {
489 self.tx_pool.push(pipeline_write.allocate(tx_bytes));
490 }
491 while self.rx_pool.len() < pipeline_depth {
492 let mut buffer = pipeline_read.allocate(rx_bytes);
493 buffer.set_requested_len(rx_bytes);
494 self.rx_pool.push(buffer);
495 }
496 }
497
498 pub fn transfer_window(
501 &mut self,
502 words: usize,
503 capacity: usize,
504 ) -> Result<IoTransferWindow<'_, 'a>> {
505 if capacity == 0 {
506 return Err(Error::InvalidBufferLength {
507 context: "vericomm transfer window",
508 expected: 1,
509 actual: 0,
510 });
511 }
512
513 validate_transfer_buffers(
514 words,
515 words,
516 usize::from(self.board.config.fifo_size_words()),
517 )?;
518 self.board.ensure_mode(BoardMode::VeriComm)?;
519 self.ensure_pipeline_endpoints()?;
520
521 let frame_bytes = words * std::mem::size_of::<u16>();
522 let read_request_bytes = request_bytes_for_words(
523 self.pipeline_read
524 .as_ref()
525 .expect("pipeline read endpoint should be initialized")
526 .max_packet_size(),
527 words,
528 );
529 let capacity = capacity.min(MAX_PIPELINE_DEPTH);
530 self.prepare_pools(capacity, frame_bytes, read_request_bytes);
531
532 Ok(IoTransferWindow {
533 io: self,
534 frame_words: words,
535 frame_bytes,
536 read_request_bytes,
537 capacity,
538 pending_reads: VecDeque::with_capacity(capacity),
539 pending_writes: 0,
540 })
541 }
542
543 fn submit_window_transfer(&mut self, tx: &[u16], read_request_bytes: usize) -> usize {
544 let tx_buffer = self.tx_pool.pop().expect("tx pool should be primed");
545 let rx_buffer = self.rx_pool.pop().expect("rx pool should be primed");
546 let rx_buffer_id = buffer_identity(&rx_buffer);
547 submit_pipeline_read(
548 self.pipeline_read
549 .as_mut()
550 .expect("pipeline read endpoint should be initialized"),
551 rx_buffer,
552 read_request_bytes,
553 );
554 submit_pipeline_write(
555 &mut self.board.crypto,
556 self.pipeline_write
557 .as_mut()
558 .expect("pipeline write endpoint should be initialized"),
559 tx,
560 tx_buffer,
561 );
562 rx_buffer_id
563 }
564
565 fn discard_window_pending_transfers(&mut self, pending_writes: usize, pending_reads: usize) {
566 const DRAIN_TIMEOUT: Duration = Duration::from_millis(10);
567
568 if let Some(endpoint) = self.pipeline_write.as_mut() {
569 endpoint.cancel_all();
570 for _ in 0..pending_writes {
571 let Some(completion) = endpoint.wait_next_complete(DRAIN_TIMEOUT) else {
572 break;
573 };
574 self.tx_pool.push(completion.buffer);
575 }
576 }
577
578 if let Some(endpoint) = self.pipeline_read.as_mut() {
579 endpoint.cancel_all();
580 for _ in 0..pending_reads {
581 let Some(completion) = endpoint.wait_next_complete(DRAIN_TIMEOUT) else {
582 break;
583 };
584 self.rx_pool.push(completion.buffer);
585 }
586 }
587 }
588 fn transfer_with_profile(
589 &mut self,
590 tx: &[u16],
591 rx: &mut [u16],
592 profile: Option<&mut TransferStageProfile>,
593 ) -> Result<()> {
594 let mut profiler = TransferProfiler::new(profile, 1);
595
596 let stage_started = Instant::now();
597 validate_transfer_buffers(
598 tx.len(),
599 rx.len(),
600 usize::from(self.board.config.fifo_size_words()),
601 )?;
602 self.board.ensure_mode(BoardMode::VeriComm)?;
603 profiler.add(TransferProfileStage::Validation, stage_started.elapsed());
604
605 let stage_started = Instant::now();
606 self.ensure_pipeline_endpoints()?;
607
608 let tx_byte_len = std::mem::size_of_val(tx);
609 let request_bytes = aligned_request_len(
610 self.pipeline_read
611 .as_ref()
612 .expect("pipeline read endpoint should be initialized")
613 .max_packet_size(),
614 tx_byte_len,
615 );
616 profiler.add(TransferProfileStage::Setup, stage_started.elapsed());
617
618 let stage_started = Instant::now();
619 let rx_buffer = self.take_single_rx_buffer(request_bytes);
620 submit_pipeline_read(
621 self.pipeline_read
622 .as_mut()
623 .expect("pipeline read endpoint should be initialized"),
624 rx_buffer,
625 request_bytes,
626 );
627
628 let mut tx_buffer = self.take_single_tx_buffer(tx_byte_len);
629 let tx_bytes = tx_buffer.extend_fill(tx_byte_len, 0);
630 words_to_bytes(tx, tx_bytes);
631 self.board
632 .crypto
633 .encrypt_words(bytes_as_words_mut(tx_bytes));
634 self.pipeline_write
635 .as_mut()
636 .expect("pipeline write endpoint should be initialized")
637 .submit(tx_buffer);
638 profiler.add(TransferProfileStage::Submit, stage_started.elapsed());
639
640 let timeout = self.board.transport().usb_timeout;
641 let stage_started = Instant::now();
642 let tx_completion = match self
643 .pipeline_write
644 .as_mut()
645 .expect("pipeline write endpoint should be initialized")
646 .wait_next_complete(timeout)
647 {
648 Some(completion) => completion,
649 None => {
650 let tx_cancelled = cancel_pending_transfer(
651 self.pipeline_write
652 .as_mut()
653 .expect("pipeline write endpoint should be initialized"),
654 );
655 self.single_tx_buffer = Some(tx_cancelled.buffer);
656 let rx_cancelled = cancel_pending_transfer(
657 self.pipeline_read
658 .as_mut()
659 .expect("pipeline read endpoint should be initialized"),
660 );
661 self.single_rx_buffer = Some(rx_cancelled.buffer);
662 return Err(Error::Timeout("nusb_bulk_write"));
663 }
664 };
665 let tx_status = tx_completion.status;
666 let tx_buffer = tx_completion.buffer;
667 self.single_tx_buffer = Some(tx_buffer);
668 tx_status.map_err(|err| transfer_error(err, "nusb_bulk_write"))?;
669 profiler.add(TransferProfileStage::WaitWrite, stage_started.elapsed());
670
671 let stage_started = Instant::now();
672 let rx_completion = match self
673 .pipeline_read
674 .as_mut()
675 .expect("pipeline read endpoint should be initialized")
676 .wait_next_complete(timeout)
677 {
678 Some(completion) => completion,
679 None => {
680 let rx_cancelled = cancel_pending_transfer(
681 self.pipeline_read
682 .as_mut()
683 .expect("pipeline read endpoint should be initialized"),
684 );
685 self.single_rx_buffer = Some(rx_cancelled.buffer);
686 return Err(Error::Timeout("nusb_bulk_read"));
687 }
688 };
689 let actual_len = rx_completion.actual_len;
690 let rx_status = rx_completion.status;
691 let mut rx_buffer = rx_completion.buffer;
692 rx_status.map_err(|err| transfer_error(err, "nusb_bulk_read"))?;
693 profiler.add(TransferProfileStage::WaitRead, stage_started.elapsed());
694
695 let stage_started = Instant::now();
696 if actual_len < tx_byte_len {
697 self.single_rx_buffer = Some(rx_buffer);
698 return Err(Error::UnexpectedResponse(
699 "blocking read returned short payload",
700 ));
701 }
702 self.board
703 .crypto
704 .decrypt_words(bytes_as_words_mut(&mut rx_buffer[..tx_byte_len]));
705 rx.copy_from_slice(bytes_as_words(&rx_buffer[..tx_byte_len]));
706 self.single_rx_buffer = Some(rx_buffer);
707 profiler.add(TransferProfileStage::DecodeCopy, stage_started.elapsed());
708 Ok(())
709 }
710
711 pub fn transfer(&mut self, tx: &[u16], rx: &mut [u16]) -> Result<()> {
712 self.transfer_with_profile(tx, rx, None)
713 }
714
715 pub fn transfer_profiled_into(
716 &mut self,
717 tx: &[u16],
718 rx: &mut [u16],
719 ) -> Result<TransferStageProfile> {
720 let mut profile = TransferStageProfile::default();
721 self.transfer_with_profile(tx, rx, Some(&mut profile))?;
722 Ok(profile)
723 }
724
725 pub fn transfer_into(&mut self, tx: &[u16], rx: &mut [u16]) -> Result<()> {
726 self.transfer(tx, rx)
727 }
728
729 pub fn transfer_frame(&mut self, tx: VeriCommFrame) -> Result<VeriCommFrame> {
730 let mut rx = VeriCommFrame::ZERO;
731 self.transfer(tx.words(), rx.words_mut())?;
732 Ok(rx)
733 }
734
735 pub fn finish(mut self) -> Result<()> {
736 let result = self.cleanup();
737 self.finished = true;
738 result
739 }
740}
741
742impl Drop for IoSession<'_> {
743 fn drop(&mut self) {
744 if !self.finished {
745 let _ = self.cleanup();
746 }
747 }
748}
749
750impl<'session, 'board> IoTransferWindow<'session, 'board> {
751 fn submit_with_profile(
752 &mut self,
753 tx: &[u16],
754 profile: Option<&mut TransferStageProfile>,
755 ) -> Result<()> {
756 if self.is_full() {
757 return Err(Error::PipelineFull {
758 capacity: self.capacity,
759 });
760 }
761
762 let mut profiler = TransferProfiler::new(profile, 1);
763
764 let stage_started = Instant::now();
765 validate_window_frame_words(
766 self.frame_words,
767 tx.len(),
768 "vericomm transfer window submit",
769 )?;
770 profiler.add(TransferProfileStage::Validation, stage_started.elapsed());
771
772 let stage_started = Instant::now();
773 let buffer_id = self.io.submit_window_transfer(tx, self.read_request_bytes);
774 self.pending_reads
775 .push_back(PendingWindowRead::new(buffer_id));
776 self.pending_writes += 1;
777 profiler.add(TransferProfileStage::Submit, stage_started.elapsed());
778 Ok(())
779 }
780
781 fn receive_into_with_profile(
782 &mut self,
783 output: &mut [u16],
784 profile: Option<&mut TransferStageProfile>,
785 ) -> Result<()> {
786 if self.pending_reads.is_empty() {
787 return Err(Error::PipelineEmpty);
788 }
789 validate_window_frame_words(
790 self.frame_words,
791 output.len(),
792 "vericomm transfer window receive",
793 )?;
794
795 let mut profiler = TransferProfiler::borrow(profile);
796
797 let stage_started = Instant::now();
798 self.reclaim_write_buffer()?;
799 profiler.add(TransferProfileStage::WaitWrite, stage_started.elapsed());
800
801 let stage_started = Instant::now();
802 let Completion {
803 buffer: read_buffer,
804 actual_len,
805 status,
806 } = self.collect_oldest_read_completion()?;
807 if let Err(err) = status {
808 self.io.rx_pool.push(read_buffer);
809 return Err(transfer_error(err, "pipeline_read"));
810 }
811 profiler.add(TransferProfileStage::WaitRead, stage_started.elapsed());
812
813 let stage_started = Instant::now();
814 if actual_len < self.frame_bytes {
815 self.io.rx_pool.push(read_buffer);
816 return Err(Error::UnexpectedResponse(
817 "pipeline read returned short payload",
818 ));
819 }
820 bytes_into_words(&read_buffer[..self.frame_bytes], output);
821 self.io.board.crypto.decrypt_words(output);
822 self.io.rx_pool.push(read_buffer);
823 profiler.add(TransferProfileStage::DecodeCopy, stage_started.elapsed());
824 Ok(())
825 }
826
827 fn reclaim_write_buffer(&mut self) -> Result<()> {
828 let Completion { buffer, status, .. } = self
829 .io
830 .pipeline_write
831 .as_mut()
832 .expect("pipeline write endpoint should be initialized")
833 .wait_next_complete(self.io.board.transport().usb_timeout)
834 .ok_or(Error::Timeout("pipeline_write"))?;
835 self.pending_writes = self.pending_writes.saturating_sub(1);
836 self.io.tx_pool.push(buffer);
837 status.map_err(|err| transfer_error(err, "pipeline_write"))
838 }
839
840 fn collect_oldest_read_completion(&mut self) -> Result<Completion> {
841 while self
842 .pending_reads
843 .front()
844 .is_some_and(PendingWindowRead::is_waiting)
845 {
846 let completion = self
847 .io
848 .pipeline_read
849 .as_mut()
850 .expect("pipeline read endpoint should be initialized")
851 .wait_next_complete(self.io.board.transport().usb_timeout)
852 .ok_or(Error::Timeout("pipeline_read"))?;
853 store_window_read_completion(&mut self.pending_reads, completion)?;
854 }
855 Ok(self
856 .pending_reads
857 .pop_front()
858 .and_then(PendingWindowRead::into_completion)
859 .expect("front transfer should have a completed read"))
860 }
861
862 fn recycle_completed_read_buffers(&mut self) -> usize {
863 let mut pending_read_completions = 0usize;
864
865 while let Some(pending) = self.pending_reads.pop_front() {
866 if let Some(completion) = pending.into_completion() {
867 self.io.rx_pool.push(completion.buffer);
868 } else {
869 pending_read_completions += 1;
870 }
871 }
872
873 pending_read_completions
874 }
875
876 pub fn words(&self) -> usize {
877 self.frame_words
878 }
879
880 pub fn capacity(&self) -> usize {
881 self.capacity
882 }
883
884 pub fn pending(&self) -> usize {
885 self.pending_reads.len()
886 }
887
888 pub fn available(&self) -> usize {
889 self.capacity.saturating_sub(self.pending())
890 }
891
892 pub fn is_empty(&self) -> bool {
893 self.pending_reads.is_empty()
894 }
895
896 pub fn is_full(&self) -> bool {
897 self.pending() >= self.capacity
898 }
899
900 pub fn submit(&mut self, tx: &[u16]) -> Result<()> {
902 self.submit_with_profile(tx, None)
903 }
904
905 pub fn submit_profiled(&mut self, tx: &[u16]) -> Result<TransferStageProfile> {
907 let mut profile = TransferStageProfile::default();
908 self.submit_with_profile(tx, Some(&mut profile))?;
909 Ok(profile)
910 }
911
912 pub fn receive_into(&mut self, output: &mut [u16]) -> Result<()> {
914 self.receive_into_with_profile(output, None)
915 }
916
917 pub fn receive_into_profiled(&mut self, output: &mut [u16]) -> Result<TransferStageProfile> {
919 let mut profile = TransferStageProfile::default();
920 self.receive_into_with_profile(output, Some(&mut profile))?;
921 Ok(profile)
922 }
923}
924
925impl Drop for IoTransferWindow<'_, '_> {
926 fn drop(&mut self) {
927 if !self.pending_reads.is_empty() {
928 let pending_read_completions = self.recycle_completed_read_buffers();
929 self.io
930 .discard_window_pending_transfers(self.pending_writes, pending_read_completions);
931 self.pending_writes = 0;
932 }
933 }
934}
935
936pub struct ProgramSession<'a> {
937 board: &'a mut Board,
938}
939
940impl ProgramSession<'_> {
941 pub fn write_bitstream_words(&mut self, words: &[u16]) -> Result<()> {
942 let chunk_len = bitstream_chunk_words(self.board.config())?;
943 let mut encrypted = words.to_vec();
944 self.board.encrypt_words(&mut encrypted);
945 for chunk in encrypted.chunks(chunk_len) {
946 self.board.fifo_write_words(chunk)?;
947 }
948 Ok(())
949 }
950
951 pub fn finish(self) -> Result<()> {
952 self.board.command_active()?;
953 self.board.refresh_config()?;
954 if !self.board.config().is_programmed() {
955 return Err(Error::NotProgrammed);
956 }
957 Ok(())
958 }
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq)]
962pub enum BoardMode {
963 Closed,
964 Unknown,
965 Control,
966 VeriComm,
967 FpgaProgrammer,
968 VeriInstrument,
969 VeriLink,
970 VeriSoc,
971 VeriCommPro,
972 VeriSdk,
973 FlashRead,
974 FlashWrite,
975}
976
977impl BoardMode {
978 pub fn as_str(self) -> &'static str {
979 match self {
980 Self::Closed => "closed",
981 Self::Unknown => "unknown",
982 Self::Control => "control",
983 Self::VeriComm => "vericomm",
984 Self::FpgaProgrammer => "fpga_programmer",
985 Self::VeriInstrument => "veri_instrument",
986 Self::VeriLink => "veri_link",
987 Self::VeriSoc => "veri_soc",
988 Self::VeriCommPro => "vericomm_pro",
989 Self::VeriSdk => "veri_sdk",
990 Self::FlashRead => "flash_read",
991 Self::FlashWrite => "flash_write",
992 }
993 }
994
995 fn command_byte(self) -> Option<u8> {
996 Some(match self {
997 Self::Control => 0x00,
998 Self::FpgaProgrammer => 0x02,
999 Self::VeriComm => 0x03,
1000 Self::VeriSdk => 0x04,
1001 Self::FlashRead => 0x05,
1002 Self::VeriInstrument => 0x08,
1003 Self::VeriLink => 0x09,
1004 Self::VeriSoc => 0x0a,
1005 Self::VeriCommPro => 0x0b,
1006 Self::FlashWrite => 0x15,
1007 Self::Closed | Self::Unknown => return None,
1008 })
1009 }
1010}
1011
1012#[derive(Debug, Clone)]
1013pub struct IoConfig {
1014 pub clock_high_delay: u16,
1015 pub clock_low_delay: u16,
1016 pub vericomm_isv: u8,
1017 pub clock_check_enabled: bool,
1018 pub mode_selector: u8,
1019 pub licence: Licence,
1020}
1021
1022impl IoConfig {
1023 pub const fn new(licence: Licence) -> Self {
1024 Self {
1025 clock_high_delay: 11,
1026 clock_low_delay: 11,
1027 vericomm_isv: 0,
1028 clock_check_enabled: false,
1029 mode_selector: 0,
1030 licence,
1031 }
1032 }
1033}
1034
1035#[derive(Debug, Clone, Default)]
1036struct CryptoState {
1037 table: [u16; 32],
1038 encode_index: usize,
1039 decode_index: usize,
1040}
1041
1042impl CryptoState {
1043 fn table_mut(&mut self) -> &mut [u16; 32] {
1044 &mut self.table
1045 }
1046
1047 fn decode_table(&mut self) {
1048 self.table[0] = !self.table[0];
1049 for idx in 1..self.table.len() {
1050 let prev = self.table[idx - 1];
1051 self.table[idx] ^= prev;
1052 }
1053 self.reset_indices();
1054 }
1055
1056 fn encrypt_words(&mut self, buffer: &mut [u16]) {
1057 let key = &self.table[0..16];
1058 let mut index = self.encode_index;
1059 for word in buffer.iter_mut() {
1060 *word ^= key[index];
1061 index = (index + 1) & 0x0f;
1062 }
1063 self.encode_index = index;
1064 }
1065
1066 fn decrypt_words(&mut self, buffer: &mut [u16]) {
1067 let key = &self.table[16..32];
1068 let mut index = self.decode_index;
1069 for word in buffer.iter_mut() {
1070 *word ^= key[index];
1071 index = (index + 1) & 0x0f;
1072 }
1073 self.decode_index = index;
1074 }
1075
1076 fn reset_indices(&mut self) {
1077 self.encode_index = 0;
1078 self.decode_index = 0;
1079 }
1080}
1081
1082fn validate_transfer_buffers(
1083 write_words: usize,
1084 read_words: usize,
1085 fifo_capacity_words: usize,
1086) -> Result<()> {
1087 if write_words != read_words {
1088 return Err(Error::InvalidBufferLength {
1089 context: "vericomm transfer",
1090 expected: write_words,
1091 actual: read_words,
1092 });
1093 }
1094
1095 if write_words > fifo_capacity_words {
1096 return Err(Error::BufferTooLarge {
1097 context: "vericomm transfer",
1098 max_words: fifo_capacity_words,
1099 actual_words: write_words,
1100 });
1101 }
1102
1103 let write_bytes = write_words * std::mem::size_of::<u16>();
1104 if write_bytes % VERICOMM_TRANSFER_PACKET_BYTES != 0 {
1105 return Err(Error::InvalidBufferLength {
1106 context: "vericomm transfer packet alignment",
1107 expected: write_words
1108 .next_multiple_of(VERICOMM_TRANSFER_PACKET_BYTES / std::mem::size_of::<u16>()),
1109 actual: write_words,
1110 });
1111 }
1112
1113 Ok(())
1114}
1115
1116fn validate_window_frame_words(
1117 expected_words: usize,
1118 actual_words: usize,
1119 context: &'static str,
1120) -> Result<()> {
1121 if expected_words != actual_words {
1122 return Err(Error::InvalidBufferLength {
1123 context,
1124 expected: expected_words,
1125 actual: actual_words,
1126 });
1127 }
1128
1129 Ok(())
1130}
1131
1132pub(crate) fn bitstream_chunk_words(config: &Config) -> Result<usize> {
1133 let fifo_words = usize::from(config.fifo_size_words());
1134 if fifo_words == 0 {
1135 return Err(Error::UnexpectedResponse(
1136 "device reported zero-length programming FIFO",
1137 ));
1138 }
1139 Ok(fifo_words)
1140}
1141
1142fn aligned_request_len(max_packet_size: usize, payload_bytes: usize) -> usize {
1143 let payload_bytes = payload_bytes
1144 .next_multiple_of(VERICOMM_TRANSFER_PACKET_BYTES)
1145 .max(max_packet_size.max(1));
1146 let rem = payload_bytes % max_packet_size.max(1);
1147 if rem == 0 {
1148 payload_bytes
1149 } else {
1150 payload_bytes + (max_packet_size - rem)
1151 }
1152}
1153
1154fn request_bytes_for_words(max_packet_size: usize, word_len: usize) -> usize {
1155 aligned_request_len(max_packet_size, word_len * std::mem::size_of::<u16>())
1156}
1157
1158fn discard_undersized_buffers(pool: &mut Vec<Buffer>, min_capacity: usize) {
1159 pool.retain(|buffer| buffer.capacity() >= min_capacity);
1160}
1161
1162fn buffer_identity(buffer: &Buffer) -> usize {
1163 buffer.as_ptr() as usize
1164}
1165
1166fn store_window_read_completion(
1167 pending_reads: &mut VecDeque<PendingWindowRead>,
1168 completion: Completion,
1169) -> Result<()> {
1170 let buffer_id = buffer_identity(&completion.buffer);
1171 let Some(pending) = pending_reads
1172 .iter_mut()
1173 .find(|pending| pending.buffer_id == buffer_id)
1174 else {
1175 return Err(Error::UnexpectedResponse(
1176 "pipeline read completion did not match a pending transfer",
1177 ));
1178 };
1179
1180 pending.complete(completion)
1181}
1182
1183fn submit_pipeline_write(
1184 crypto: &mut CryptoState,
1185 endpoint: &mut UsbEndpoint<Bulk, Out>,
1186 tx: &[u16],
1187 mut buffer: Buffer,
1188) {
1189 buffer.clear();
1190 let byte_len = std::mem::size_of_val(tx);
1191 buffer.extend_fill(byte_len, 0);
1192 words_to_bytes(tx, &mut buffer[..byte_len]);
1193 crypto.encrypt_words(bytes_as_words_mut(&mut buffer[..byte_len]));
1194 endpoint.submit(buffer);
1195}
1196
1197fn submit_pipeline_read(
1198 endpoint: &mut UsbEndpoint<Bulk, In>,
1199 mut buffer: Buffer,
1200 request_bytes: usize,
1201) {
1202 buffer.clear();
1203 buffer.set_requested_len(request_bytes);
1204 endpoint.submit(buffer);
1205}
1206
1207fn bytes_into_words(bytes: &[u8], out: &mut [u16]) {
1208 for (index, chunk) in bytes.chunks_exact(2).take(out.len()).enumerate() {
1209 out[index] = u16::from_le_bytes([chunk[0], chunk[1]]);
1210 }
1211}
1212
1213fn bytes_as_words(bytes: &[u8]) -> &[u16] {
1214 unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u16, bytes.len() / 2) }
1215}
1216
1217fn transfer_error(err: nusb::transfer::TransferError, context: &'static str) -> Error {
1218 Error::Usb {
1219 source: Box::new(std::io::Error::other(format!("{context}: {err}"))),
1220 context,
1221 }
1222}
1223
1224fn words_to_bytes(words: &[u16], out: &mut [u8]) {
1225 for (index, word) in words.iter().copied().enumerate() {
1226 let [lo, hi] = word.to_le_bytes();
1227 out[index * 2] = lo;
1228 out[index * 2 + 1] = hi;
1229 }
1230}
1231
1232fn bytes_as_words_mut(bytes: &mut [u8]) -> &mut [u16] {
1233 unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u16, bytes.len() / 2) }
1234}
1235
1236fn cancel_pending_transfer<Dir>(endpoint: &mut UsbEndpoint<Bulk, Dir>) -> Completion
1237where
1238 Dir: EndpointDirection,
1239{
1240 endpoint.cancel_all();
1241 loop {
1242 if let Some(completion) = endpoint.wait_next_complete(Duration::from_secs(1)) {
1243 return completion;
1244 }
1245 }
1246}
1247
1248fn should_retry_initialize(err: &Error) -> bool {
1249 matches!(err, Error::Timeout(_) | Error::Usb { .. })
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use nusb::transfer::Buffer;
1255
1256 #[test]
1257 fn words_to_bytes_roundtrip() {
1258 let words = [0x1234u16, 0xabcd];
1259 let mut bytes = [0u8; 4];
1260 super::words_to_bytes(&words, &mut bytes);
1261 assert_eq!(bytes, [0x34, 0x12, 0xcd, 0xab]);
1262 }
1263
1264 #[test]
1265 fn aligned_request_len_rounds_up_to_packet_boundary() {
1266 assert_eq!(super::aligned_request_len(512, 513), 1024);
1267 assert_eq!(super::aligned_request_len(512, 512), 512);
1268 }
1269
1270 #[test]
1271 fn request_bytes_for_words_keep_frame_request_size() {
1272 assert_eq!(super::request_bytes_for_words(512, 256), 512);
1273 assert_eq!(super::request_bytes_for_words(512, 512), 1024);
1274 }
1275
1276 #[test]
1277 fn discard_undersized_buffers_drops_stale_pool_entries() {
1278 let mut pool = vec![
1279 Buffer::from(vec![0u8; 512]),
1280 Buffer::from(vec![0u8; 1024]),
1281 Buffer::from(vec![0u8; 256]),
1282 ];
1283
1284 super::discard_undersized_buffers(&mut pool, 600);
1285
1286 let capacities = pool.iter().map(Buffer::capacity).collect::<Vec<_>>();
1287 assert_eq!(capacities, vec![1024]);
1288 }
1289
1290 #[test]
1291 fn fixed_window_length_error_shape_is_stable() {
1292 let err = Error::InvalidBufferLength {
1293 context: "vericomm transfer window submit",
1294 expected: 1,
1295 actual: 0,
1296 };
1297 assert_eq!(
1298 err.to_string(),
1299 "invalid buffer length for `vericomm transfer window submit` (expected 1, got 0)"
1300 );
1301 }
1302
1303 #[test]
1304 fn io_session_struct_caches_endpoint_adapters() {
1305 let type_name = std::any::type_name::<super::IoSession<'_>>();
1306 assert!(type_name.contains("IoSession"));
1307 }
1308
1309 #[test]
1310 fn io_transfer_window_type_is_stable() {
1311 let type_name = std::any::type_name::<super::IoTransferWindow<'_, '_>>();
1312 assert!(type_name.contains("IoTransferWindow"));
1313 }
1314
1315 #[test]
1316 fn pipeline_error_shapes_are_stable() {
1317 assert_eq!(
1318 Error::PipelineEmpty.to_string(),
1319 "transfer pipeline has no pending transfers"
1320 );
1321 assert_eq!(
1322 Error::PipelineFull { capacity: 4 }.to_string(),
1323 "transfer pipeline is full (capacity 4 outstanding transfers)"
1324 );
1325 }
1326
1327 #[test]
1328 fn transfer_window_rejects_wrong_frame_length() {
1329 let err = super::validate_window_frame_words(256, 128, "vericomm transfer window submit")
1330 .unwrap_err();
1331 assert_eq!(
1332 err.to_string(),
1333 "invalid buffer length for `vericomm transfer window submit` (expected 256, got 128)"
1334 );
1335 }
1336
1337 #[test]
1338 fn transfer_window_matches_reads_by_buffer_identity() {
1339 let mut pending = VecDeque::new();
1340 let rx_a = Buffer::from(vec![0u8; 512]);
1341 let rx_b = Buffer::from(vec![0u8; 512]);
1342 let id_a = super::buffer_identity(&rx_a);
1343 let id_b = super::buffer_identity(&rx_b);
1344 pending.push_back(super::PendingWindowRead::new(id_a));
1345 pending.push_back(super::PendingWindowRead::new(id_b));
1346
1347 super::store_window_read_completion(
1348 &mut pending,
1349 nusb::transfer::Completion {
1350 buffer: rx_b,
1351 actual_len: 512,
1352 status: Ok(()),
1353 },
1354 )
1355 .expect("read completion should match the second transfer");
1356
1357 assert!(pending[0].completion.is_none());
1358 assert_eq!(
1359 super::buffer_identity(&pending[1].completion.as_ref().unwrap().buffer),
1360 id_b
1361 );
1362 }
1363
1364 #[test]
1365 fn initialize_retry_only_triggers_for_transport_failures() {
1366 assert!(super::should_retry_initialize(&Error::Timeout(
1367 "sync_delay"
1368 )));
1369 assert!(super::should_retry_initialize(&Error::Usb {
1370 source: Box::new(std::io::Error::other("boom")),
1371 context: "nusb_bulk_read",
1372 }));
1373 assert!(!super::should_retry_initialize(&Error::NotProgrammed));
1374 assert!(!super::should_retry_initialize(&Error::VersionMismatch {
1375 expected: 0x0220,
1376 actual: 0x0000,
1377 }));
1378 }
1379
1380 use super::{Board, BoardMode, CryptoState, IoConfig, validate_transfer_buffers};
1381 use crate::error::Error;
1382 use crate::licence::Licence;
1383 use crate::usb::TransportConfig;
1384 use std::collections::VecDeque;
1385 use std::time::Duration;
1386
1387 #[test]
1388 fn board_accepts_custom_transport_config() {
1389 let transport = TransportConfig {
1390 usb_timeout: Duration::from_millis(250),
1391 sync_timeout: Duration::from_millis(750),
1392 reset_on_open: true,
1393 clear_halt_on_open: false,
1394 };
1395 let board = Board::open_with_transport(transport);
1396 assert!(
1397 board.is_err()
1398 || board
1399 .as_ref()
1400 .map(|b| b.transport() == &transport)
1401 .unwrap_or(false)
1402 );
1403 }
1404
1405 #[test]
1406 fn encrypted_transfer_buffer_is_copied_before_mutation() {
1407 let mut crypto = CryptoState::default();
1408 crypto.table[0] = 0x00ff;
1409 let input = [0x1234u16, 0xabcd];
1410 let mut encrypted = input;
1411 crypto.encrypt_words(&mut encrypted);
1412
1413 assert_eq!(input, [0x1234, 0xabcd]);
1414 assert_eq!(encrypted, [0x12cb, 0xabcd]);
1415 }
1416
1417 #[test]
1418 fn vericomm_transfer_requires_matching_buffer_lengths() {
1419 let err = validate_transfer_buffers(4, 3, 16).expect_err("validation should fail");
1420 match err {
1421 Error::InvalidBufferLength {
1422 context,
1423 expected,
1424 actual,
1425 } => {
1426 assert_eq!(context, "vericomm transfer");
1427 assert_eq!(expected, 4);
1428 assert_eq!(actual, 3);
1429 }
1430 other => panic!("unexpected error: {other}"),
1431 }
1432 }
1433
1434 #[test]
1435 fn vericomm_transfer_rejects_oversize_payloads() {
1436 let err = validate_transfer_buffers(17, 17, 16).expect_err("validation should fail");
1437 match err {
1438 Error::BufferTooLarge {
1439 context,
1440 max_words,
1441 actual_words,
1442 } => {
1443 assert_eq!(context, "vericomm transfer");
1444 assert_eq!(max_words, 16);
1445 assert_eq!(actual_words, 17);
1446 }
1447 other => panic!("unexpected error: {other}"),
1448 }
1449 }
1450
1451 #[test]
1452 fn io_config_constructor_keeps_protocol_tuning_explicit() {
1453 let cfg = IoConfig::new(Licence::CustomerId(0xf805));
1454 assert_eq!(cfg.clock_high_delay, 11);
1455 assert_eq!(cfg.clock_low_delay, 11);
1456 assert_eq!(cfg.licence, Licence::CustomerId(0xf805));
1457 }
1458
1459 #[test]
1460 fn board_mode_labels_are_stable() {
1461 assert_eq!(BoardMode::Control.as_str(), "control");
1462 assert_eq!(BoardMode::VeriComm.as_str(), "vericomm");
1463 }
1464}