summaryrefslogtreecommitdiff
path: root/src/class/vendor/vendor_device.c
blob: c4a550ef07666b58ddbc6151c5184e08770b5e85 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
/*
 * SPDX-FileCopyrightText: Copyright (c) 2019 Ha Thach (tinyusb.org)
 * SPDX-License-Identifier: MIT
 *
 * This file is part of the TinyUSB stack.
 */

#include "tusb_option.h"

#if (CFG_TUD_ENABLED && CFG_TUD_VENDOR)

#include "device/usbd.h"
#include "device/usbd_pvt.h"

#include "vendor_device.h"

//--------------------------------------------------------------------+
// MACRO CONSTANT TYPEDEF
//--------------------------------------------------------------------+
typedef struct {
  uint8_t rhport;
  uint8_t itf_num;

  #if CFG_TUD_VENDOR_EP_INT_OUT
  uint8_t  ep_int_out;
  uint16_t int_rx_xfer_len;
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
  uint8_t  ep_int_in;
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_OUT
  uint8_t  ep_iso_out;
  uint16_t iso_rx_xfer_len;
  const tusb_desc_endpoint_t* iso_out_desc; // for deactivation on altsetting de-selection
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_IN
  uint8_t  ep_iso_in;
  const tusb_desc_endpoint_t* iso_in_desc; // for deactivation on altsetting de-selection
  #endif
  #if CFG_TUD_VENDOR_ALT_SETTINGS // implies non-buffered: fields cleared by bus reset
  uint8_t        cur_alt;
  const uint8_t* p_itf_desc; // whole interface block incl. all altsettings (static app descriptor)
  uint16_t       itf_desc_len;
  #endif

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  /*------------- From this point, data is not cleared by bus reset -------------*/
  tu_edpt_stream_t tx_stream;
  tu_edpt_stream_t rx_stream;
  uint8_t          tx_ff_buf[CFG_TUD_VENDOR_TX_BUFSIZE];
  uint8_t          rx_ff_buf[CFG_TUD_VENDOR_RX_BUFSIZE];
  #else
  uint8_t  ep_in;
  uint8_t  ep_out;
  uint16_t rx_xfer_len;
  #endif
} vendord_interface_t;

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
    // The reset region is everything before the streams; tx_stream is the first preserved field
    // (see the struct comment), so its offset is exactly that boundary regardless of which endpoint
    // gates are enabled.
    #define ITF_MEM_RESET_SIZE offsetof(vendord_interface_t, tx_stream)
  #else
    #define ITF_MEM_RESET_SIZE sizeof(vendord_interface_t)
  #endif

static vendord_interface_t _vendord_itf[CFG_TUD_VENDOR];

// Skip local EP buffer if dedicated hw FIFO is supported or no fifo mode
#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 || !CFG_TUD_VENDOR_TXRX_BUFFERED
typedef struct {
  TUD_EPBUF_DEF(epout, CFG_TUD_VENDOR_RX_EPSIZE);
  TUD_EPBUF_DEF(epin, CFG_TUD_VENDOR_TX_EPSIZE);
} vendord_epbuf_t;

CFG_TUD_MEM_SECTION static vendord_epbuf_t _vendord_epbuf[CFG_TUD_VENDOR];
#endif

#if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN
typedef struct {
  #if CFG_TUD_VENDOR_EP_INT_OUT
  TUD_EPBUF_DEF(int_out, CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE);
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
  TUD_EPBUF_DEF(int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE);
  #endif
} vendord_int_epbuf_t;

CFG_TUD_MEM_SECTION static vendord_int_epbuf_t _vendord_int_epbuf[CFG_TUD_VENDOR];
#endif

#if CFG_TUD_VENDOR_EP_ISO_OUT || CFG_TUD_VENDOR_EP_ISO_IN
typedef struct {
  #if CFG_TUD_VENDOR_EP_ISO_OUT
  TUD_EPBUF_DEF(iso_out, CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE);
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_IN
  TUD_EPBUF_DEF(iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE);
  #endif
} vendord_iso_epbuf_t;

CFG_TUD_MEM_SECTION static vendord_iso_epbuf_t _vendord_iso_epbuf[CFG_TUD_VENDOR];
#endif

//--------------------------------------------------------------------+
// Weak stubs: invoked if no strong implementation is available
//--------------------------------------------------------------------+
TU_ATTR_WEAK void tud_vendor_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) {
  (void)idx;
  (void)buffer;
  (void)bufsize;
}

TU_ATTR_WEAK void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) {
  (void)idx;
  (void) sent_bytes;
}

#if CFG_TUD_VENDOR_EP_INT_OUT
TU_ATTR_WEAK void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) {
  (void)idx;
  (void)buffer;
  (void)bufsize;
}
#endif

#if CFG_TUD_VENDOR_EP_INT_IN
TU_ATTR_WEAK void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) {
  (void)idx;
  (void)sent_bytes;
}
#endif

#if CFG_TUD_VENDOR_EP_ISO_OUT
TU_ATTR_WEAK void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) {
  (void)idx;
  (void)buffer;
  (void)bufsize;
}
#endif

#if CFG_TUD_VENDOR_EP_ISO_IN
TU_ATTR_WEAK void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) {
  (void)idx;
  (void)sent_bytes;
}
#endif

bool tud_vendor_n_mounted(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_itf = &_vendord_itf[idx];

  // bulk may be absent (interrupt-only vendor interface): count the interrupt endpoints too
  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  bool mounted = (p_itf->rx_stream.ep_addr != 0) || (p_itf->tx_stream.ep_addr != 0);
  #else
  bool mounted = (p_itf->ep_out != 0) || (p_itf->ep_in != 0);
  #endif
  #if CFG_TUD_VENDOR_EP_INT_OUT
  mounted = mounted || (p_itf->ep_int_out != 0);
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
  mounted = mounted || (p_itf->ep_int_in != 0);
  #endif
  // an altsetting may expose only isochronous endpoints; count them so apps that gate an iso
  // pump on tud_vendor_mounted() still arm it
  #if CFG_TUD_VENDOR_EP_ISO_OUT
  mounted = mounted || (p_itf->ep_iso_out != 0);
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_IN
  mounted = mounted || (p_itf->ep_iso_in != 0);
  #endif
  return mounted;
}

//--------------------------------------------------------------------+
// Read API
//--------------------------------------------------------------------+
  #if CFG_TUD_VENDOR_TXRX_BUFFERED
uint32_t tud_vendor_n_available(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return tu_edpt_stream_read_available(&p_itf->rx_stream);
}

bool tud_vendor_n_peek(uint8_t idx, uint8_t *u8) {
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return tu_edpt_stream_peek(&p_itf->rx_stream, u8);
}

uint32_t tud_vendor_n_read(uint8_t idx, void *buffer, uint32_t bufsize) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return tu_edpt_stream_read(&p_itf->rx_stream, buffer, bufsize);
}

void tud_vendor_n_read_flush(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, );
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  tu_edpt_stream_clear(&p_itf->rx_stream);
  tu_edpt_stream_read_xfer(&p_itf->rx_stream);
}
  #endif

// Shared non-buffered transfer helpers for the bulk / interrupt / isochronous endpoints, which are
// identical apart from the endpoint, its epbuf and its buffer size. TU_ATTR_UNUSED: in buffered
// mode with the int/iso gates off none is referenced, and clang/IAR error on an unused static.
TU_ATTR_UNUSED static inline uint32_t vendord_ep_write(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf,
                                                       uint32_t bufsize, const void *buffer, uint32_t len) {
  TU_VERIFY(ep > 0, 0); // must be opened
  TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep), 0);
  const uint32_t xact_len = tu_min32(len, bufsize);
  memcpy(epbuf, buffer, xact_len);
  TU_ASSERT(usbd_edpt_xfer(p_itf->rhport, ep, epbuf, (uint16_t) xact_len, false), 0);
  return xact_len;
}

TU_ATTR_UNUSED static inline uint32_t vendord_ep_write_available(vendord_interface_t *p_itf, uint8_t ep, uint32_t bufsize) {
  TU_VERIFY(ep > 0, 0); // must be opened
  return usbd_edpt_busy(p_itf->rhport, ep) ? 0 : bufsize;
}

TU_ATTR_UNUSED static inline bool vendord_ep_read_xfer(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf, uint16_t xfer_len) {
  TU_VERIFY(ep > 0); // must be opened
  TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep));
  return usbd_edpt_xfer(p_itf->rhport, ep, epbuf, xfer_len, false);
}

  #if CFG_TUD_VENDOR_RX_MANUAL_XFER
bool tud_vendor_n_read_xfer(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_itf = &_vendord_itf[idx];

    #if CFG_TUD_VENDOR_TXRX_BUFFERED
  return tu_edpt_stream_read_xfer(&p_itf->rx_stream);

    #else
  // Non-FIFO mode (0 while an altsetting without a bulk OUT ep is active)
  return vendord_ep_read_xfer(p_itf, p_itf->ep_out, _vendord_epbuf[idx].epout, p_itf->rx_xfer_len);
    #endif
}
  #endif


//--------------------------------------------------------------------+
// Write API
//--------------------------------------------------------------------+
uint32_t tud_vendor_n_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  return tu_edpt_stream_write(&p_itf->tx_stream, buffer, (uint16_t)bufsize);

  #else
  // non-fifo mode: direct transfer (ep_in is 0 while an altsetting without a bulk IN ep is active)
  return vendord_ep_write(p_itf, p_itf->ep_in, _vendord_epbuf[idx].epin, CFG_TUD_VENDOR_TX_EPSIZE, buffer, bufsize);
  #endif
}

uint32_t tud_vendor_n_write_available(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  return tu_edpt_stream_write_available(&p_itf->tx_stream);

  #else
  return vendord_ep_write_available(p_itf, p_itf->ep_in, CFG_TUD_VENDOR_TX_EPSIZE);
  #endif
}

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
uint32_t tud_vendor_n_write_flush(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return tu_edpt_stream_write_xfer(&p_itf->tx_stream);
}

bool tud_vendor_n_write_clear(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  tu_edpt_stream_clear(&p_itf->tx_stream);
  return true;
}
#endif

//--------------------------------------------------------------------+
// Interrupt endpoint API
//--------------------------------------------------------------------+
#if CFG_TUD_VENDOR_EP_INT_OUT
bool tud_vendor_n_int_read_xfer(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_read_xfer(p_itf, p_itf->ep_int_out, _vendord_int_epbuf[idx].int_out, p_itf->int_rx_xfer_len);
}
#endif

#if CFG_TUD_VENDOR_EP_INT_IN
uint32_t tud_vendor_n_int_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_write(p_itf, p_itf->ep_int_in, _vendord_int_epbuf[idx].int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, buffer, bufsize);
}

uint32_t tud_vendor_n_int_write_available(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_write_available(p_itf, p_itf->ep_int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE);
}
#endif

//--------------------------------------------------------------------+
// Isochronous endpoint API
//--------------------------------------------------------------------+
#if CFG_TUD_VENDOR_EP_ISO_OUT
bool tud_vendor_n_iso_read_xfer(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_read_xfer(p_itf, p_itf->ep_iso_out, _vendord_iso_epbuf[idx].iso_out, p_itf->iso_rx_xfer_len);
}
#endif

#if CFG_TUD_VENDOR_EP_ISO_IN
uint32_t tud_vendor_n_iso_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_write(p_itf, p_itf->ep_iso_in, _vendord_iso_epbuf[idx].iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, buffer, bufsize);
}

uint32_t tud_vendor_n_iso_write_available(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_itf = &_vendord_itf[idx];
  return vendord_ep_write_available(p_itf, p_itf->ep_iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE);
}
#endif

#if CFG_TUD_VENDOR_ALT_SETTINGS
uint8_t tud_vendor_n_alt(uint8_t idx) {
  TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
  return _vendord_itf[idx].cur_alt;
}
#endif

//--------------------------------------------------------------------+
// USBD Driver API
//--------------------------------------------------------------------+
void vendord_init(void) {
  tu_memclr(_vendord_itf, sizeof(_vendord_itf));

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  for (uint8_t i = 0; i < CFG_TUD_VENDOR; i++) {
    vendord_interface_t *p_itf = &_vendord_itf[i];

    #if CFG_TUD_EDPT_DEDICATED_HWFIFO
    uint8_t *epout_buf = NULL;
    uint8_t *epin_buf  = NULL;
    #else
    uint8_t *epout_buf = _vendord_epbuf[i].epout;
    uint8_t *epin_buf  = _vendord_epbuf[i].epin;
    #endif

    uint8_t *rx_ff_buf = p_itf->rx_ff_buf;
    tu_edpt_stream_init(&p_itf->rx_stream, false, false, false, rx_ff_buf, CFG_TUD_VENDOR_RX_BUFSIZE, epout_buf);

    uint8_t *tx_ff_buf = p_itf->tx_ff_buf;
    tu_edpt_stream_init(&p_itf->tx_stream, false, true, false, tx_ff_buf, CFG_TUD_VENDOR_TX_BUFSIZE, epin_buf);
  }
  #endif
}

bool vendord_deinit(void) {
  #if CFG_TUD_VENDOR_TXRX_BUFFERED
  for (uint8_t i = 0; i < CFG_TUD_VENDOR; i++) {
    vendord_interface_t *p_itf = &_vendord_itf[i];
    tu_edpt_stream_deinit(&p_itf->rx_stream);
    tu_edpt_stream_deinit(&p_itf->tx_stream);
  }
  #endif
  return true;
}

void vendord_reset(uint8_t rhport) {
  (void) rhport;

  for(uint8_t i=0; i<CFG_TUD_VENDOR; i++) {
    vendord_interface_t* p_itf = &_vendord_itf[i];
    tu_memclr(p_itf, ITF_MEM_RESET_SIZE);

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
    tu_edpt_stream_clear(&p_itf->rx_stream);
    tu_edpt_stream_close(&p_itf->rx_stream);
    tu_edpt_stream_clear(&p_itf->tx_stream);
    tu_edpt_stream_close(&p_itf->tx_stream);
  #endif
  }
}

// Find vendor interface by endpoint address
static uint8_t find_vendor_itf(uint8_t ep_addr) {
  for (uint8_t idx = 0; idx < CFG_TUD_VENDOR; idx++) {
    const vendord_interface_t *p_vendor = &_vendord_itf[idx];
    if (ep_addr == 0) {
      // find unused interface slot
  #if CFG_TUD_VENDOR_ALT_SETTINGS
      // an opened interface parked in an altsetting without endpoints (the mandatory empty alt 0)
      // has all ep fields 0, so the endpoint fields cannot distinguish free from open: use p_itf_desc
      if (p_vendor->p_itf_desc == NULL) {
        return idx;
      }
  #elif CFG_TUD_VENDOR_TXRX_BUFFERED
      // A slot is free only if none of its endpoints are assigned; bulk may be absent
      // (an interrupt-only vendor interface), so check the interrupt endpoints too.
      if (p_vendor->rx_stream.ep_addr == 0 && p_vendor->tx_stream.ep_addr == 0
      #if CFG_TUD_VENDOR_EP_INT_OUT
          && p_vendor->ep_int_out == 0
      #endif
      #if CFG_TUD_VENDOR_EP_INT_IN
          && p_vendor->ep_int_in == 0
      #endif
      ) {
        return idx;
      }
  #else
      // A slot is free only if none of its endpoints are assigned. Bulk may be absent (an
      // interrupt-only vendor interface), so the interrupt endpoints must be checked too.
      if (p_vendor->ep_out == 0 && p_vendor->ep_in == 0
      #if CFG_TUD_VENDOR_EP_INT_OUT
          && p_vendor->ep_int_out == 0
      #endif
      #if CFG_TUD_VENDOR_EP_INT_IN
          && p_vendor->ep_int_in == 0
      #endif
      ) {
        return idx;
      }
  #endif
    } else {
  #if CFG_TUD_VENDOR_EP_INT_OUT
      if (ep_addr == p_vendor->ep_int_out) {
        return idx;
      }
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
      if (ep_addr == p_vendor->ep_int_in) {
        return idx;
      }
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_OUT
      if (ep_addr == p_vendor->ep_iso_out) {
        return idx;
      }
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_IN
      if (ep_addr == p_vendor->ep_iso_in) {
        return idx;
      }
  #endif
  #if CFG_TUD_VENDOR_TXRX_BUFFERED
      if (ep_addr == p_vendor->rx_stream.ep_addr || ep_addr == p_vendor->tx_stream.ep_addr) {
        return idx;
      }
  #else
      if (ep_addr == p_vendor->ep_out || ep_addr == p_vendor->ep_in) {
        return idx;
      }
  #endif
    }
  }
  return 0xff;
}

#if CFG_TUD_VENDOR_ALT_SETTINGS

// Reserve an isochronous endpoint at open time. Ports with a dedicated iso allocator
// (TUP_DCD_EDPT_ISO_ALLOC) reserve the FIFO here and (re)activate on altsetting selection;
// ports with dcd_edpt_close instead open it once here (open == allocate + activate).
static inline bool vendord_iso_ep_alloc(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
  #ifdef TUP_DCD_EDPT_ISO_ALLOC
  return usbd_edpt_iso_alloc(rhport, desc_ep->bEndpointAddress, tu_edpt_packet_size(desc_ep));
  #else
  return usbd_edpt_open(rhport, desc_ep);
  #endif
}

// Deactivate a de-selected altsetting's isochronous endpoint: abort any in-flight
// transfer and release its usbd claim so a stale completion cannot fire into a
// no-longer-tracked endpoint (iso cannot be stalled like bulk/interrupt below). With the
// iso-alloc API, (re)activation with the endpoint's descriptor is the abort/scrub
// primitive; without it, close does (the next selection re-opens).
static inline void vendord_iso_ep_deactivate(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
  if (desc_ep != NULL) {
  #ifdef TUP_DCD_EDPT_ISO_ALLOC
    usbd_edpt_iso_activate(rhport, desc_ep);
  #else
    usbd_edpt_close(rhport, desc_ep->bEndpointAddress);
  #endif
  }
}

// (Re)activate an isochronous endpoint on altsetting selection.
static inline bool vendord_iso_ep_activate(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
  #ifdef TUP_DCD_EDPT_ISO_ALLOC
  return usbd_edpt_iso_activate(rhport, desc_ep); // resets ep_status, aborting any stale transfer
  #else
  // No iso alloc/activate API: close (which zeros ep_status and frees a stale claim left by a
  // prior selection) then re-open, so a re-selected altsetting starts from a clean state.
  usbd_edpt_close(rhport, desc_ep->bEndpointAddress);
  return usbd_edpt_open(rhport, desc_ep);
  #endif
}

// Abort any in-flight transfer on a tracked endpoint and release its usbd claim; no-op for an
// unset (0) address. stall disables the endpoint in the dcd, clear-stall resets it to DATA0.
static inline void vendord_abort_ep(uint8_t rhport, uint8_t ep_addr) {
  if (ep_addr) {
    usbd_edpt_stall(rhport, ep_addr);
    usbd_edpt_clear_stall(rhport, ep_addr);
  }
}

// Select an altsetting. Endpoints were hardware-opened once at vendord_open (dcds like
// dwc2 allocate FIFO linearly and cannot close/re-open endpoints dynamically): switching
// only re-targets the API to the selected altsetting's endpoints. Bulk/interrupt
// endpoints get a stall/clear-stall cycle, which portably aborts any in-flight transfer
// (stall disables the endpoint in the dcd) and resets the data toggle to DATA0 as
// SET_INTERFACE requires. Isochronous endpoints are (re)activated, which does the same.
// Single pass: the current altsetting's endpoints are dropped only once the target altsetting
// is confirmed present, so a SET_INTERFACE to an unknown alt leaves the interface intact.
static bool vendord_set_alt(uint8_t rhport, uint8_t idx, uint8_t alt) {
  vendord_interface_t *p_vendor = &_vendord_itf[idx];
  const uint8_t* p_desc = p_vendor->p_itf_desc;
  const uint8_t* desc_end = p_desc + p_vendor->itf_desc_len;
  bool in_target_alt = false;
  bool alt_found = false;

  while (tu_desc_in_bounds(p_desc, desc_end)) {
    const uint8_t desc_type = tu_desc_type(p_desc);
    if (desc_type == TUSB_DESC_INTERFACE) {
      in_target_alt = (((const tusb_desc_interface_t*)p_desc)->bAlternateSetting == alt);
      if (in_target_alt && !alt_found) {
        alt_found = true;
        // target altsetting confirmed present: abort then drop the previous altsetting's endpoints,
        // so an endpoint absent from the target altsetting can't stay armed and keep its usbd
        // claim in the dcd. (Endpoints the target altsetting reuses are reset again below;
        // a double reset is harmless.)
        vendord_abort_ep(rhport, p_vendor->ep_in);
        vendord_abort_ep(rhport, p_vendor->ep_out);
  #if CFG_TUD_VENDOR_EP_INT_OUT
        vendord_abort_ep(rhport, p_vendor->ep_int_out);
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
        vendord_abort_ep(rhport, p_vendor->ep_int_in);
  #endif
        p_vendor->ep_in = 0;
        p_vendor->ep_out = 0;
  #if CFG_TUD_VENDOR_EP_INT_OUT
        p_vendor->ep_int_out = 0;
  #endif
  #if CFG_TUD_VENDOR_EP_INT_IN
        p_vendor->ep_int_in = 0;
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_OUT
        vendord_iso_ep_deactivate(rhport, p_vendor->iso_out_desc);
        p_vendor->iso_out_desc = NULL;
        p_vendor->ep_iso_out = 0;
  #endif
  #if CFG_TUD_VENDOR_EP_ISO_IN
        vendord_iso_ep_deactivate(rhport, p_vendor->iso_in_desc);
        p_vendor->iso_in_desc = NULL;
        p_vendor->ep_iso_in = 0;
  #endif
      }
    } else if (in_target_alt && desc_type == TUSB_DESC_ENDPOINT) {
      const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;
      const uint8_t ep_addr = desc_ep->bEndpointAddress;
      const bool is_in = tu_edpt_dir(ep_addr) == TUSB_DIR_IN;
      (void) is_in;

      switch (desc_ep->bmAttributes.xfer) {
        case TUSB_XFER_BULK:
          // abort in-flight transfer + reset data toggle
          usbd_edpt_stall(rhport, ep_addr);
          usbd_edpt_clear_stall(rhport, ep_addr);
          if (is_in) {
            p_vendor->ep_in = ep_addr;
          } else {
            p_vendor->ep_out = ep_addr;
            p_vendor->rx_xfer_len =
              CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep);
  #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
            TU_ASSERT(usbd_edpt_xfer(rhport, p_vendor->ep_out, _vendord_epbuf[idx].epout,
                                     p_vendor->rx_xfer_len, false));
  #endif
          }
          break;

  #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT
        case TUSB_XFER_INTERRUPT:
          // stall/clear only for the enabled direction (an endpoint of a disabled
          // direction was never opened, so must not be poked in the dcd)
    #if CFG_TUD_VENDOR_EP_INT_IN
          if (is_in) {
            usbd_edpt_stall(rhport, ep_addr);
            usbd_edpt_clear_stall(rhport, ep_addr);
            p_vendor->ep_int_in = ep_addr;
          }
    #endif
    #if CFG_TUD_VENDOR_EP_INT_OUT
          if (!is_in) {
            usbd_edpt_stall(rhport, ep_addr);
            usbd_edpt_clear_stall(rhport, ep_addr);
            p_vendor->ep_int_out = ep_addr;
            p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep);
          }
    #endif
          break;
  #endif

  #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT
        case TUSB_XFER_ISOCHRONOUS:
    #if CFG_TUD_VENDOR_EP_ISO_IN
          if (is_in) {
            TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep));
            p_vendor->ep_iso_in = ep_addr;
            p_vendor->iso_in_desc = desc_ep; // points into p_itf_desc (static app descriptor)
          }
    #endif
    #if CFG_TUD_VENDOR_EP_ISO_OUT
          if (!is_in) {
            TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep));
            p_vendor->ep_iso_out = ep_addr;
            p_vendor->iso_out_desc = desc_ep; // points into p_itf_desc (static app descriptor)
            p_vendor->iso_rx_xfer_len = tu_edpt_packet_size(desc_ep);
          }
    #endif
          break;
  #endif

        default:
          break; // unsupported endpoint type / direction gate disabled: ignore
      }
    }
    p_desc = tu_desc_next(p_desc);
  }

  TU_VERIFY(alt_found); // unknown alt: endpoints were never cleared, current altsetting intact
  p_vendor->cur_alt = alt;
  return true;
}

uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) {
  TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0);
  const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len;

  const uint8_t idx = find_vendor_itf(0);
  TU_ASSERT(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_vendor = &_vendord_itf[idx];
  p_vendor->rhport     = rhport;
  p_vendor->itf_num    = desc_itf->bInterfaceNumber;
  // p_itf_desc is assigned only after the parse succeeds: find_vendor_itf() treats a non-NULL
  // p_itf_desc as an occupied slot, so setting it before a mid-parse TU_ASSERT could fail would
  // leak the slot (a retry would find no free interface until the next bus reset).

  // Consume every altsetting of this interface and hardware-open each endpoint exactly once
  // (bulk/interrupt via usbd_edpt_open, isochronous FIFO-allocated; iso activation and the
  // toggle reset happen on altsetting selection). An endpoint address that recurs in another
  // altsetting must carry an identical configuration, since it is opened only on first sight;
  // reconfiguring the same address per-alt is not supported and is rejected here.
  uint8_t seen_type[CFG_TUD_ENDPPOINT_MAX][2];
  uint16_t seen_mps[CFG_TUD_ENDPPOINT_MAX][2];
  tu_memclr(seen_type, sizeof(seen_type)); // 0 == TUSB_XFER_CONTROL, never used as a data ep here
  const uint8_t* p_desc = tu_desc_next(desc_itf);
  while (tu_desc_in_bounds(p_desc, desc_end)) {
    const uint8_t desc_type = tu_desc_type(p_desc);
    if (desc_type == TUSB_DESC_INTERFACE_ASSOCIATION) {
      break;
    }
    if (desc_type == TUSB_DESC_INTERFACE) {
      if (((const tusb_desc_interface_t*)p_desc)->bInterfaceNumber != p_vendor->itf_num) {
        break; // next interface
      }
    } else if (desc_type == TUSB_DESC_ENDPOINT) {
      const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;
      const uint8_t epnum = tu_edpt_number(desc_ep->bEndpointAddress);
      const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
      const uint8_t xfer = desc_ep->bmAttributes.xfer;
      const uint16_t mps = tu_edpt_packet_size(desc_ep);
      TU_ASSERT(epnum < CFG_TUD_ENDPPOINT_MAX, 0);

      if (seen_type[epnum][dir] != 0) {
        // reused address in a later altsetting: must be an exact match (opened only once)
        TU_ASSERT(seen_type[epnum][dir] == xfer && seen_mps[epnum][dir] == mps, 0);
      } else {
        switch (xfer) {
          case TUSB_XFER_BULK:
            TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
            break;

  #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT
          case TUSB_XFER_INTERRUPT:
    #if CFG_TUD_VENDOR_EP_INT_IN
            if (dir == TUSB_DIR_IN) {
              TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0);
              TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
            }
    #endif
    #if CFG_TUD_VENDOR_EP_INT_OUT
            if (dir == TUSB_DIR_OUT) {
              TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0);
              TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
            }
    #endif
            break;
  #endif

  #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT
          case TUSB_XFER_ISOCHRONOUS:
    #if CFG_TUD_VENDOR_EP_ISO_IN
            if (dir == TUSB_DIR_IN) {
              TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, 0);
              TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0);
            }
    #endif
    #if CFG_TUD_VENDOR_EP_ISO_OUT
            if (dir == TUSB_DIR_OUT) {
              TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE, 0);
              TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0);
            }
    #endif
            break;
  #endif

          default:
            break; // unsupported endpoint type / direction gate disabled: ignore
        }
        seen_type[epnum][dir] = xfer;
        seen_mps[epnum][dir] = mps;
      }
    }
    p_desc = tu_desc_next(p_desc);
  }
  // parse succeeded: commit the descriptor pointer (marks the slot occupied) before selecting alt 0
  p_vendor->p_itf_desc   = (const uint8_t*) desc_itf;
  p_vendor->itf_desc_len = (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf);

  // default altsetting active until the host selects another
  TU_ASSERT(vendord_set_alt(rhport, idx, 0), 0);
  return p_vendor->itf_desc_len;
}

#else // !CFG_TUD_VENDOR_ALT_SETTINGS

uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) {
  TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0);
  const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len;
  const uint8_t* p_desc = tu_desc_next(desc_itf);

  // Find available interface
  const uint8_t idx = find_vendor_itf(0);
  TU_ASSERT(idx < CFG_TUD_VENDOR, 0);
  vendord_interface_t *p_vendor = &_vendord_itf[idx];
  p_vendor->rhport  = rhport;
  p_vendor->itf_num = desc_itf->bInterfaceNumber;

  while (tu_desc_in_bounds(p_desc, desc_end)) {
    const uint8_t desc_type = tu_desc_type(p_desc);
    if (desc_type == TUSB_DESC_INTERFACE || desc_type == TUSB_DESC_INTERFACE_ASSOCIATION) {
      break; // end of this interface
    } else if (desc_type == TUSB_DESC_ENDPOINT) {
      const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;

  #if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN
      if (desc_ep->bmAttributes.xfer == TUSB_XFER_INTERRUPT) {
        const bool is_int_in = tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN;
        (void) is_int_in;
    #if CFG_TUD_VENDOR_EP_INT_IN
        if (is_int_in) {
          TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0);
          TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
          p_vendor->ep_int_in = desc_ep->bEndpointAddress;
        }
    #endif
    #if CFG_TUD_VENDOR_EP_INT_OUT
        if (!is_int_in) {
          TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0);
          TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
          p_vendor->ep_int_out = desc_ep->bEndpointAddress;
          p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep);
        }
    #endif
        p_desc = tu_desc_next(p_desc);
        continue;
      }
  #endif

      TU_ASSERT(usbd_edpt_open(rhport, desc_ep));

      uint16_t rx_xfer_len = CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep);

  #if CFG_TUD_VENDOR_TXRX_BUFFERED
      // open endpoint stream
      if (tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN) {
        tu_edpt_stream_t *tx_stream = &p_vendor->tx_stream;
        tu_edpt_stream_open(tx_stream, rhport, desc_ep, CFG_TUD_VENDOR_TX_EPSIZE);
        tu_edpt_stream_write_xfer(tx_stream); // flush pending data
      } else {
        tu_edpt_stream_t *rx_stream = &p_vendor->rx_stream;
        tu_edpt_stream_open(rx_stream, rhport, desc_ep, rx_xfer_len);
    #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
        TU_ASSERT(tu_edpt_stream_read_xfer(rx_stream) > 0, 0); // prepare for incoming data
    #endif
      }
  #else
      p_vendor->rx_xfer_len = rx_xfer_len;
      // Non-FIFO mode: store endpoint info
      if (tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN) {
        p_vendor->ep_in     = desc_ep->bEndpointAddress;
      } else {
        p_vendor->ep_out     = desc_ep->bEndpointAddress;
    #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
        // Prepare for incoming data
        TU_ASSERT(usbd_edpt_xfer(rhport, p_vendor->ep_out, _vendord_epbuf[idx].epout, rx_xfer_len, false), 0);
    #endif
      }
  #endif
    }

    p_desc = tu_desc_next(p_desc);
  }

  return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf);
}

#endif // CFG_TUD_VENDOR_ALT_SETTINGS

// Handle interface standard requests (GET/SET_INTERFACE when altsettings are enabled),
// delegate everything else to the application callback as before.
bool vendord_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) {
#if CFG_TUD_VENDOR_ALT_SETTINGS
  if (request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD &&
      request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_INTERFACE) {
    const uint8_t itf_num = tu_u16_low(request->wIndex);
    uint8_t idx;
    for (idx = 0; idx < CFG_TUD_VENDOR; idx++) {
      if (_vendord_itf[idx].itf_num == itf_num && _vendord_itf[idx].p_itf_desc != NULL) {
        break;
      }
    }
    if (idx < CFG_TUD_VENDOR) {
      if (request->bRequest == TUSB_REQ_SET_INTERFACE) {
        if (stage == CONTROL_STAGE_SETUP) {
          TU_VERIFY(vendord_set_alt(rhport, idx, tu_u16_low(request->wValue)));
          return tud_control_status(rhport, request);
        }
        return true;
      } else if (request->bRequest == TUSB_REQ_GET_INTERFACE) {
        if (stage == CONTROL_STAGE_SETUP) {
          return tud_control_xfer(rhport, request, &_vendord_itf[idx].cur_alt, 1);
        }
        return true;
      }
    }
  }
#endif
  return tud_vendor_control_xfer_cb(rhport, stage, request);
}

bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) {
  (void)rhport;
  (void)result;
  const uint8_t idx = find_vendor_itf(ep_addr);
  TU_VERIFY(idx < CFG_TUD_VENDOR);
  vendord_interface_t *p_vendor = &_vendord_itf[idx];

#if CFG_TUD_VENDOR_EP_INT_OUT
  if (ep_addr == p_vendor->ep_int_out) {
    // not re-armed automatically: application calls tud_vendor_n_int_read_xfer()
    tud_vendor_int_rx_cb(idx, _vendord_int_epbuf[idx].int_out, xferred_bytes);
    return true;
  }
#endif
#if CFG_TUD_VENDOR_EP_INT_IN
  if (ep_addr == p_vendor->ep_int_in) {
    tud_vendor_int_tx_cb(idx, xferred_bytes);
    return true;
  }
#endif
#if CFG_TUD_VENDOR_EP_ISO_OUT
  if (ep_addr == p_vendor->ep_iso_out) {
    // not re-armed automatically: application calls tud_vendor_n_iso_read_xfer()
    tud_vendor_iso_rx_cb(idx, _vendord_iso_epbuf[idx].iso_out, xferred_bytes);
    return true;
  }
#endif
#if CFG_TUD_VENDOR_EP_ISO_IN
  if (ep_addr == p_vendor->ep_iso_in) {
    tud_vendor_iso_tx_cb(idx, xferred_bytes);
    return true;
  }
#endif

#if CFG_TUD_VENDOR_TXRX_BUFFERED
  if (ep_addr == p_vendor->rx_stream.ep_addr) {
    // Put received data to FIFO
    tu_edpt_stream_read_xfer_complete(&p_vendor->rx_stream, xferred_bytes);
    tud_vendor_rx_cb(idx, NULL, 0);
    #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
    tu_edpt_stream_read_xfer(&p_vendor->rx_stream); // prepare next data
    #endif
  } else if (ep_addr == p_vendor->tx_stream.ep_addr) {
    // Send complete
    tud_vendor_tx_cb(idx, (uint16_t)xferred_bytes);

    // try to send more if possible
    if (0 == tu_edpt_stream_write_xfer(&p_vendor->tx_stream)) {
      // If there is no data left, a ZLP should be sent if xferred_bytes is multiple of EP Packet size and not zero
      tu_edpt_stream_write_zlp_if_needed(&p_vendor->tx_stream, xferred_bytes);
    }
  }
  #else
  if (ep_addr == p_vendor->ep_out) {
    // Non-FIFO mode: invoke callback with buffer
    tud_vendor_rx_cb(idx, _vendord_epbuf[idx].epout, xferred_bytes);
    #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
    usbd_edpt_xfer(rhport, p_vendor->ep_out, _vendord_epbuf[idx].epout, p_vendor->rx_xfer_len, false);
    #endif
  } else if (ep_addr == p_vendor->ep_in) {
    // Send complete
    tud_vendor_tx_cb(idx, (uint16_t)xferred_bytes);
  }
  #endif

  return true;
}

#endif