| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- #include "ctrl_link.h"
- #include "radio.h"
- #include "ctrl_bonds.h"
- #include "report45.h"
- #include "deck_input.h"
- #include "triton.h"
- #include <Adafruit_TinyUSB.h>
- #include <Arduino.h>
- #include <string.h>
- int g_linkSlot = -1;
- unsigned long g_linkAliveMs = 0;
- // Link is considered up while we've heard a poll within this window (the puck polls ~250 Hz).
- #define LINK_ALIVE_MS 1000u
- // RX dwell per loop. One transaction (catch a poll, reply) per loop -> ~200 Hz, plenty for input, and
- // short enough that USB/CDC servicing isn't starved. Tunable on hardware.
- #define RX_WIN_US 4500u
- // Time the puck's RX window stays open for our reply; keep the turnaround well under this.
- static uint8_t g_seq = 0;
- // Last time we caught a frame on the SESSION address (NOT the ch2 discovery beacon). This is the
- // truthful "we're actually exchanging with the puck" signal that drives ctrlLinkUp() / the UI
- // "available" tile -- hearing the discovery beacon alone does NOT mean the session works.
- static unsigned long g_sessRxMs = 0;
- // SESSION-CHANNEL HUNT. The E1 host frame advertises a "primary" channel, but that channel mostly
- // carries the puck's E1/E2 keepalive -- the ACTIVE session (E3 input poll + our F1 reply) rides a small
- // channel set, NOT necessarily the advertised primary (proven by the real-puck capture sniff1.json:
- // E1/E2 on ch68, the E3/F1 session on ch48; and by puck_sniffer.ino's session-hunt). So once we adopt a
- // session we DWELL across {primary, then these candidates} on the session address until a poll (E2/E3)
- // appears, then PARK on that channel. If the parked channel goes dry (QoS hop / puck moved), un-park and
- // hunt again. Mirrors puck_sniffer.ino HUNT_CH.
- static const uint8_t HUNT_CH[] = { 2, 80, 18, 46, 76, 22, 68, 52, 55, 56, 57 };
- static uint8_t g_huntIdx = 0;
- static uint8_t g_parkCh = 0; // 0 = hunting; non-zero = parked on this channel
- static unsigned long g_huntStepMs = 0;
- #define HUNT_DWELL_MS 40u // per-channel dwell while hunting
- #define RECONNECT_MS \
- 4000u // dry this long -> abandon the session, re-catch E1 on ch2
- // ---- build replies into rftx (ESB DPL RAM layout: [LENGTH][S1][payload...]) ----
- static void buildF1()
- {
- // payload = [0xF1][tlen][ttype=6][report45(46)] -> length = 3 + REPORT45_LEN
- rftx[0] = (uint8_t)(3 + REPORT45_LEN); // LENGTH (payload byte count)
- rftx[1] = (uint8_t)((((g_pid++) & 3) << 1) | 1); // S1: PID<<1 | NO_ACK
- rftx[2] = 0xF1;
- rftx[3] = REPORT45_LEN; // TLV len
- rftx[4] = 6; // TLV type 6 (HID report)
- // neutral report (idle controller) when not forwarding or input has gone stale
- bool fresh = deckForwarding() && (millis() - deckLastInputMs() < 200u);
- if (fresh) {
- buildReport45(rftx + 5, g_seq++);
- } else {
- memset(rftx + 5, 0, REPORT45_LEN);
- rftx[5] = 0x45;
- rftx[6] = g_seq++;
- }
- }
- static void buildF3()
- {
- // reply to E7 awake/version probe: rf_link reads the version at rfrx[6] (payload byte 4).
- rftx[0] = 5;
- rftx[1] = (uint8_t)((((g_pid++) & 3) << 1) | 1);
- rftx[2] = 0xF3;
- rftx[3] = 0;
- rftx[4] = 0;
- rftx[5] = 0;
- rftx[6] = 1; // protocol/version byte
- }
- // Transmit whatever is staged in rftx on the currently-configured channel/address, then return to RX
- // config. Mirrors rf_link's TX path; radio is left disabled at exit.
- static void txStaged()
- {
- NRF_RADIO->TASKS_DISABLE = 1;
- RWAIT_DISABLED();
- NRF_RADIO->EVENTS_DISABLED = 0;
- NRF_RADIO->PACKETPTR = (uint32_t)rftx;
- NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
- RADIO_SHORTS_END_DISABLE_Msk;
- NRF_RADIO->EVENTS_DISABLED = 0;
- NRF_RADIO->TASKS_TXEN = 1;
- RWAIT_DISABLED();
- NRF_RADIO->EVENTS_DISABLED = 0;
- NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
- }
- // One RX->maybe-reply transaction on (base,prefix,ch). Returns the received type byte (rfrx[2]) or 0.
- // `reply` selects whether we answer polls (true once connected) or just listen (discovery).
- static uint8_t rxOnce(const uint8_t *base, uint8_t prefix, uint8_t ch,
- uint16_t winUs, bool reply)
- {
- rfConfig(ch);
- rfSetAddr(base, prefix);
- NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
- rfrx[0] = 0;
- NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk;
- NRF_RADIO->EVENTS_END = 0;
- NRF_RADIO->TASKS_RXEN = 1;
- uint32_t t0 = micros();
- while (!NRF_RADIO->EVENTS_END && (uint32_t)(micros() - t0) < winUs) {
- }
- uint8_t type = 0;
- if (NRF_RADIO->EVENTS_END) {
- NRF_RADIO->EVENTS_END = 0;
- g_rfRxCount++;
- bool crcok = NRF_RADIO->CRCSTATUS & 1;
- uint8_t rxlen = rfrx[0];
- if (crcok && rxlen && rxlen <= 96) {
- type = rfrx[2];
- if (reply) {
- // E1 = host-frame keepalive (no reply expected); follow channel hops.
- if (type == 0xE1) {
- uint8_t newCh = rfrx[11];
- if (newCh && newCh != g_sessCh)
- g_sessCh = newCh;
- } else if (type == 0xE7) {
- buildF3();
- txStaged();
- } else if (type >= 0xE0 && type <= 0xEF) {
- // E3 GET (and any other E-type poll): answer with input.
- buildF1();
- txStaged();
- }
- }
- return type;
- }
- }
- NRF_RADIO->TASKS_DISABLE = 1;
- RWAIT_DISABLED();
- NRF_RADIO->EVENTS_DISABLED = 0;
- return type;
- }
- // Listen on the shared "ibex"/ch2 rendezvous for a discovery E1 that matches a stored bond. On a match,
- // adopt the advertised session base/prefix/channel and switch to connected.
- static void discoveryScan()
- {
- uint8_t t = rxOnce(g_rfBase, g_rfPrefix, 2, RX_WIN_US, false);
- if (t != 0xE1)
- return;
- const uint8_t *proteus =
- rfrx + 3; // payload [proteus_uuid 4][ibex_uuid 4]
- const uint8_t *ibex = rfrx + 7;
- int slot = ctrlBondMatch(proteus, ibex);
- if (slot < 0) {
- // We hear the puck's beacon but its UUIDs don't match any stored bond -- the controller
- // side of the pairing didn't land, or we're paired to a different puck. Very useful signal.
- static unsigned long lastMiss = 0;
- if (Serial.availableForWrite() > 40 &&
- millis() - lastMiss > 1000) {
- lastMiss = millis();
- Serial.printf(
- "# E1 heard, no bond match: puuid=%02X%02X%02X%02X iuuid=%02X%02X%02X%02X\n",
- proteus[0], proteus[1], proteus[2], proteus[3],
- ibex[0], ibex[1], ibex[2], ibex[3]);
- }
- return;
- }
- // adopt the advertised session address (rfHostFrameOnce layout: ch@[11], base@[15..19], prefix@[19])
- uint8_t advCh = rfrx[11];
- memcpy(g_sessBase, rfrx + 15, 4);
- g_sessPrefix = rfrx[19];
- if (advCh)
- g_sessCh = advCh;
- g_linkSlot = slot;
- g_linkAliveMs = millis();
- g_parkCh = 0; // start hunting the session channel set from scratch
- g_huntIdx = 0;
- g_huntStepMs = millis();
- deckText("link: adopted session");
- if (Serial.availableForWrite() > 40)
- Serial.printf(
- "# LINK adopted slot=%d base=%02X%02X%02X%02X/%02X ch=%u\n",
- slot, g_sessBase[0], g_sessBase[1], g_sessBase[2],
- g_sessBase[3], g_sessPrefix, g_sessCh);
- }
- bool ctrlLinkUp()
- {
- // "up" = we caught a real SESSION-address frame recently (a poll/keepalive on the adopted
- // base+channel), not merely the ch2 discovery beacon -- so the UI tile only goes tappable when
- // the puck is genuinely polling us.
- return g_linkSlot >= 0 && (millis() - g_sessRxMs) < LINK_ALIVE_MS;
- }
- void ctrlLinkTask()
- {
- // nothing to do until at least one puck is bonded
- bool anyBond = false;
- for (int i = 0; i < NBOND; i++)
- if (g_bond[i].used)
- anyBond = true;
- if (!anyBond) {
- static unsigned long lastNb = 0;
- if (Serial.availableForWrite() > 40 &&
- millis() - lastNb > 2000) {
- lastNb = millis();
- Serial.println(
- "# no bonds stored -- pair first (controller never got 0xEE)");
- }
- g_linkSlot = -1;
- return;
- }
- // 1 Hz status line for a serial monitor: searching vs connected, channel, RX count.
- {
- static unsigned long stMs = 0;
- static uint32_t lastRx = 0;
- if (Serial.availableForWrite() > 50 &&
- millis() - stMs >= 1000) {
- stMs = millis();
- Serial.printf(
- "# link slot=%d %s ch=%u park=%u rx=%lu/s\n",
- g_linkSlot,
- ctrlLinkUp() ? "CONNECTED" : "searching",
- g_sessCh, g_parkCh,
- (unsigned long)(g_rfRxCount - lastRx));
- lastRx = g_rfRxCount;
- }
- }
- // No session adopted yet -> hunt the E1 host frame on the shared ch2/"ibex" rendezvous.
- if (g_linkSlot < 0) {
- discoveryScan();
- return;
- }
- // Adopted a session, but the puck has gone fully dry for a long time -> abandon it and re-catch the
- // E1 (its primary channel may have changed). Only after RECONNECT_MS so a hunt sweep has a chance.
- if ((millis() - g_linkAliveMs) >= RECONNECT_MS) {
- g_linkSlot = -1;
- g_parkCh = 0;
- g_huntIdx = 0;
- deckText("link: lost, rescanning");
- if (Serial.availableForWrite() > 40)
- Serial.println("# LINK lost, rescanning ch2");
- return;
- }
- // Pick the channel to listen on: parked channel if we have one, else sweep {primary, HUNT_CH...}.
- uint8_t tryCh = g_parkCh ? g_parkCh :
- (g_huntIdx == 0 ?
- g_sessCh :
- HUNT_CH[(g_huntIdx - 1) %
- (uint8_t)sizeof HUNT_CH]);
- uint8_t t = rxOnce(g_sessBase, g_sessPrefix, tryCh, RX_WIN_US, true);
- if (t == 0xE1) {
- // keepalive beacon on the session base: refreshes the advertised primary channel (rxOnce already
- // copied it into g_sessCh) + proves the puck is present, but it is NOT a poll -> keep hunting.
- g_linkAliveMs = millis();
- } else if (t >= 0xE0 && t <= 0xEF) {
- // a real poll (E2/E3/E7) -> the session lives on THIS channel; PARK and answer it from here.
- bool wasUp = (millis() - g_sessRxMs) < LINK_ALIVE_MS;
- g_parkCh = tryCh;
- g_sessCh = tryCh;
- g_linkAliveMs = g_sessRxMs = millis();
- if (!wasUp) {
- deckText("link: session live");
- if (Serial.availableForWrite() > 40)
- Serial.printf(
- "# SESSION live: type=%02X ch=%u base=%02X%02X%02X%02X/%02X\n",
- t, tryCh, g_sessBase[0], g_sessBase[1],
- g_sessBase[2], g_sessBase[3],
- g_sessPrefix);
- }
- } else {
- // nothing this dwell. parked channel gone dry -> resume hunting (QoS hop / puck moved)
- if (g_parkCh && (millis() - g_sessRxMs) >= LINK_ALIVE_MS)
- g_parkCh = 0;
- if (!g_parkCh && (millis() - g_huntStepMs) >= HUNT_DWELL_MS) {
- g_huntStepMs = millis();
- g_huntIdx++;
- }
- }
- }
|