Skip to main content

vlfd_rs/
session.rs

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
364/// A rolling VeriComm pipeline that keeps up to `capacity` transfers in flight.
365///
366/// All transfers in one window have the same word length chosen up front.
367/// Submit frames with [`Self::submit`] and retire them in order with
368/// [`Self::receive_into`]. Dropping the window cancels any remaining transfers
369/// and recycles their buffers back into the parent [`IoSession`].
370pub 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    /// Opens a fixed-size rolling transfer window that can keep `capacity`
499    /// VeriComm transfers of `words` words outstanding at once.
500    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    /// Queues one transfer into the rolling window.
901    pub fn submit(&mut self, tx: &[u16]) -> Result<()> {
902        self.submit_with_profile(tx, None)
903    }
904
905    /// Queues one transfer and returns a stage profile for that submission.
906    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    /// Retires the oldest in-flight transfer into `output`.
913    pub fn receive_into(&mut self, output: &mut [u16]) -> Result<()> {
914        self.receive_into_with_profile(output, None)
915    }
916
917    /// Retires the oldest in-flight transfer and returns its stage profile.
918    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}