usb_mount.cpp 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #include "usb_mount.h"
  2. #include "bonds.h"
  3. #include "fault_diag.h" // faultDiagTrace() -- flight recorder
  4. #include <Arduino.h>
  5. uint8_t g_usbMountCount = 0;
  6. int8_t g_usbToBond[NSLOT];
  7. int8_t g_bondToUsb[NSLOT];
  8. static bool g_dynOn = false;
  9. static uint8_t g_maxSlots = NSLOT;
  10. // A controller counts as "connected" (mount-worthy) while it has replied recently. More lenient than the
  11. // 300ms link-up check so a brief RF gap doesn't unmount it; the debounce below adds further hysteresis.
  12. #define CONN_UP_MS 1200u
  13. // The connected set must hold steady this long before we re-enumerate -- absorbs RF blips and the staggered
  14. // way multiple controllers connect at boot (one re-enumeration once the set settles, not one per controller).
  15. #define MOUNT_DEBOUNCE_MS 2000u
  16. // Lazy removal: a disconnected slot's interface is only dropped (and the device re-enumerated down) once NO
  17. // controller has sent input for this long -- i.e. during genuine downtime. Re-enumeration tears down/rebuilds
  18. // USB from the loop task and is risky to do mid-use, so we never shrink while anything is being played; we
  19. // just let dead slots linger as idle interfaces and clean them up when the rig goes quiet. Additions are NOT
  20. // gated by this -- a newly-connected controller grows the set right away.
  21. #define IDLE_CLEANUP_MS 60000u
  22. static uint8_t connectedMask()
  23. {
  24. unsigned long now = millis();
  25. uint8_t m = 0;
  26. for (int s = 0; s < NSLOT; s++)
  27. if (g_slot[s].used && g_connReplyMs[s] != 0 &&
  28. (now - g_connReplyMs[s]) < CONN_UP_MS)
  29. m |= (uint8_t)(1u << s);
  30. return m;
  31. }
  32. static uint8_t mountedMask()
  33. {
  34. uint8_t m = 0;
  35. for (uint8_t u = 0; u < g_usbMountCount; u++)
  36. if (g_usbToBond[u] >= 0)
  37. m |= (uint8_t)(1u << g_usbToBond[u]);
  38. return m;
  39. }
  40. // Compute the order-preserving, budget-capped mount list for `mask` into out[], returning the count. Ordering
  41. // follows WHEN each controller was mounted: controllers already mounted keep their current USB-slot order (a
  42. // departure compacts the rest down but never reorders survivors), and newly-connected controllers are appended
  43. // at the end (ascending bond order among simultaneous joiners). Does NOT mutate the live map.
  44. static uint8_t computeOrder(uint8_t mask, int8_t out[NSLOT])
  45. {
  46. uint8_t n = 0;
  47. // 1. keep currently-mounted controllers that are still connected, in their existing order
  48. for (uint8_t u = 0; u < g_usbMountCount && n < g_maxSlots; u++) {
  49. int8_t b = g_usbToBond[u];
  50. if (b >= 0 && (mask & (1u << b)))
  51. out[n++] = b;
  52. }
  53. // 2. append newly-connected controllers (not already mounted)
  54. for (int s = 0; s < NSLOT && n < g_maxSlots; s++) {
  55. if (!(mask & (1u << s)))
  56. continue;
  57. bool have = false;
  58. for (uint8_t i = 0; i < n; i++)
  59. if (out[i] == (int8_t)s) {
  60. have = true;
  61. break;
  62. }
  63. if (!have)
  64. out[n++] = (int8_t)s;
  65. }
  66. return n;
  67. }
  68. static uint8_t maskOfList(const int8_t *list, uint8_t n)
  69. {
  70. uint8_t m = 0;
  71. for (uint8_t i = 0; i < n; i++)
  72. if (list[i] >= 0)
  73. m |= (uint8_t)(1u << list[i]);
  74. return m;
  75. }
  76. static void commitOrder(const int8_t *list, uint8_t n)
  77. {
  78. for (int i = 0; i < NSLOT; i++) {
  79. g_usbToBond[i] = -1;
  80. g_bondToUsb[i] = -1;
  81. }
  82. for (uint8_t u = 0; u < n; u++) {
  83. g_usbToBond[u] = list[u];
  84. g_bondToUsb[list[u]] = (int8_t)u;
  85. }
  86. g_usbMountCount = n;
  87. }
  88. void usbMountRebuildMap()
  89. {
  90. int8_t tmp[NSLOT];
  91. uint8_t n = computeOrder(connectedMask(), tmp);
  92. commitOrder(tmp, n);
  93. }
  94. void usbMountEnable(bool on, uint8_t maxSlots)
  95. {
  96. g_dynOn = on;
  97. g_maxSlots = maxSlots ? (maxSlots <= NSLOT ? maxSlots : NSLOT) : 1;
  98. for (int i = 0; i < NSLOT; i++)
  99. g_usbToBond[i] = g_bondToUsb[i] = -1;
  100. g_usbMountCount = 0;
  101. }
  102. void usbMountTask()
  103. {
  104. if (!g_dynOn)
  105. return;
  106. static uint8_t lastMask = 0xFF; // force first compare
  107. static unsigned long stableSince = 0;
  108. unsigned long now = millis();
  109. // ADD IMMEDIATELY, REMOVE LAZILY. Re-enumeration (usbReenumerate) tears down + rebuilds the whole USB
  110. // config from the loop task; doing that while the high-priority usbd task is mid-tud_task races its
  111. // internal state -> HardFault/reboot. So we only ever SHRINK the interface set during genuine downtime:
  112. // - active (some controller sent input within IDLE_CLEANUP_MS): target = connected UNION already-mounted
  113. // -> the set only grows. A controller that drops (e.g. an errant ReversePuck disconnect) keeps its
  114. // now-idle interface and reconnecting reuses it with NO re-enumeration. task() gates input per slot,
  115. // so an idle interface just streams nothing.
  116. // - idle (no controller input for a full minute): target = exactly the connected set -> any lingering
  117. // dead slot is dropped and we re-enumerate down, safely, because nobody is playing.
  118. // New controllers grow the set right away (after the usual debounce) regardless of idle state.
  119. unsigned long lastAny = 0;
  120. for (int s = 0; s < NSLOT; s++)
  121. if (g_slot[s].used && g_connReplyMs[s] > lastAny)
  122. lastAny = g_connReplyMs[s];
  123. bool idle = (lastAny == 0) || (now - lastAny) >= IDLE_CLEANUP_MS;
  124. uint8_t want = idle ? connectedMask() :
  125. (uint8_t)(connectedMask() | mountedMask());
  126. if (want != lastMask) { // set is moving -> restart the stability timer
  127. lastMask = want;
  128. stableSince = now;
  129. return;
  130. }
  131. // `want` has been steady since stableSince. Re-enumerate once it has held long enough AND the resulting
  132. // (order-preserving, capped) mount SET differs from what is currently mounted. With the union above,
  133. // while active that can only happen when a NEW controller needs an interface (grow); a shrink only ever
  134. // comes from the idle branch dropping a long-dead slot.
  135. if ((now - stableSince) < MOUNT_DEBOUNCE_MS)
  136. return;
  137. int8_t tmp[NSLOT];
  138. uint8_t n = computeOrder(want, tmp);
  139. if (maskOfList(tmp, n) == mountedMask())
  140. return; // same set already mounted (order is preserved across joins/leaves)
  141. commitOrder(tmp, n);
  142. faultDiagTrace(FR_MOUNT, g_usbMountCount);
  143. usbReenumerate(g_usbMountCount);
  144. }