Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/websocket-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -952,18 +952,30 @@ subscription ControllerEvents($sessionId: ID!) {
| Event | Description |
|-------|-------------|
| `LedUpdate` | LED commands for current climb (RGB values and positions) |
| `ControllerQueueSync` | Full queue state sync (sent on connection and queue changes) |
| `ControllerPing` | Keep-alive ping (not currently implemented) |

### LedUpdate Fields

| Field | Type | Description |
|-------|------|-------------|
| `commands` | Array | LED positions with RGB values |
| `queueItemUuid` | String | UUID of the queue item (for navigation) |
| `climbUuid` | String | Unique identifier for the climb |
| `climbName` | String | Display name of the climb |
| `climbGrade` | String | The climb difficulty/grade (e.g., "V5", "6a/V3") |
| `gradeColor` | String | Hex color for the grade (e.g., "#00FF00") |
| `boardPath` | String | Board configuration path for context-aware operations (e.g., "kilter/1/12/1,2,3/40") |
| `angle` | Int | Board angle in degrees |
| `clientId` | String | Identifier of client that initiated the change (used by ESP32 to decide whether to disconnect BLE client - if clientId matches ESP32's MAC, it was self-initiated via BLE) |
| `navigation` | Object | Navigation context with previousClimbs, nextClimb, currentIndex, totalCount |

### ControllerQueueSync Fields

| Field | Type | Description |
|-------|------|-------------|
| `queue` | Array | Array of queue items with uuid, climbUuid, name, grade, gradeColor |
| `currentIndex` | Int | Index of the current climb in the queue (-1 if none) |

### LED Color Mapping

Expand All @@ -986,12 +998,42 @@ mutation SetClimbFromLeds($sessionId: ID!, $frames: String) {
}
}

# Navigate queue via hardware buttons (previous/next)
# queueItemUuid is preferred for direct navigation (most reliable)
# direction is used as fallback when queueItemUuid not found
mutation NavigateQueue($sessionId: ID!, $direction: String!, $queueItemUuid: String) {
navigateQueue(sessionId: $sessionId, direction: $direction, queueItemUuid: $queueItemUuid) {
uuid
climb {
name
difficulty
}
}
}

# Heartbeat to update lastSeenAt
mutation Heartbeat($sessionId: ID!) {
controllerHeartbeat(sessionId: $sessionId)
}
```

### Queue Navigation

The `navigateQueue` mutation allows ESP32 controllers to browse the queue via hardware buttons:

| Parameter | Type | Description |
|-----------|------|-------------|
| `sessionId` | ID! | Session to navigate within |
| `direction` | String! | "next" or "previous" (fallback if queueItemUuid not found) |
| `queueItemUuid` | String | Direct navigation to specific queue item (preferred) |

**Navigation Flow:**
1. ESP32 maintains local queue state from `ControllerQueueSync` events
2. On button press, ESP32 calculates the target item locally (optimistic update)
3. ESP32 sends `navigateQueue` with the target `queueItemUuid`
4. Backend updates current climb and broadcasts `CurrentClimbChanged`
5. ESP32 receives `LedUpdate` with new climb data

### Manual Authorization

If auto-authorization doesn't apply (e.g., controller owner is not in session), use:
Expand Down
75 changes: 75 additions & 0 deletions embedded/libs/aurora-protocol/src/aurora_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,81 @@ void AuroraProtocol::decodeLedDataV3(const uint8_t* data, size_t length, std::ve
}
}

void AuroraProtocol::encodeLedCommands(const LedCommand* commands, int count,
std::vector<std::vector<uint8_t>>& packets) {
packets.clear();

if (count == 0) {
return;
}

// Use V3 format (3 bytes per LED) - most compatible with current boards
// Max BLE packet size is ~240 bytes, with framing overhead we can fit ~75 LEDs per packet
// To be safe, use 60 LEDs per packet
const int LEDS_PER_PACKET = 60;

int totalPackets = (count + LEDS_PER_PACKET - 1) / LEDS_PER_PACKET;
int ledIndex = 0;

for (int packetNum = 0; packetNum < totalPackets; packetNum++) {
int ledsInThisPacket = min(LEDS_PER_PACKET, count - ledIndex);

// Determine command byte based on packet position
uint8_t command;
if (totalPackets == 1) {
command = CMD_V3_PACKET_ONLY; // 'T' - single packet
} else if (packetNum == 0) {
command = CMD_V3_PACKET_FIRST; // 'R' - first packet
} else if (packetNum == totalPackets - 1) {
command = CMD_V3_PACKET_LAST; // 'S' - last packet
} else {
command = CMD_V3_PACKET_MIDDLE; // 'Q' - middle packet
}

// Build the data portion (command + LED data)
std::vector<uint8_t> data;
data.push_back(command);

for (int i = 0; i < ledsInThisPacket; i++) {
const LedCommand& cmd = commands[ledIndex + i];

// Position (little-endian)
data.push_back(cmd.position & 0xFF);
data.push_back((cmd.position >> 8) & 0xFF);

// Color: RRRGGGBB format (3 bits red, 3 bits green, 2 bits blue)
// Convert 8-bit colors to packed format
uint8_t r3 = (cmd.r * 7) / 255; // Scale 0-255 to 0-7
uint8_t g3 = (cmd.g * 7) / 255; // Scale 0-255 to 0-7
uint8_t b2 = (cmd.b * 3) / 255; // Scale 0-255 to 0-3
uint8_t color = (r3 << 5) | (g3 << 2) | b2;
data.push_back(color);
}

// Build the complete framed packet
// Format: [SOH, length, checksum, STX, ...data..., ETX]
uint8_t dataLength = data.size();

// Calculate checksum (XOR of all data bytes with 0xFF)
uint8_t checksum = 0;
for (uint8_t byte : data) {
checksum = (checksum + byte) & 0xFF;
}
checksum ^= 0xFF;

std::vector<uint8_t> packet;
packet.push_back(FRAME_SOH); // 0x01
packet.push_back(dataLength); // Length of data
packet.push_back(checksum); // Checksum
packet.push_back(FRAME_STX); // 0x02
packet.insert(packet.end(), data.begin(), data.end());
packet.push_back(FRAME_ETX); // 0x03

packets.push_back(packet);
ledIndex += ledsInThisPacket;
}
}

bool AuroraProtocol::processMessage(uint8_t command, const uint8_t* data, size_t length) {
std::vector<LedCommand> commands;

Expand Down
11 changes: 11 additions & 0 deletions embedded/libs/aurora-protocol/src/aurora_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ class AuroraProtocol {
// Enable/disable debug output
void setDebug(bool enabled);

/**
* Encode LED commands into Aurora protocol format for sending to a board.
* Creates one or more BLE packets in the proper framed format.
*
* @param commands Array of LED commands to encode
* @param count Number of commands
* @param packets Output vector that will be filled with encoded packets
* Each packet is a complete framed message ready for BLE transmission
*/
static void encodeLedCommands(const LedCommand* commands, int count, std::vector<std::vector<uint8_t>>& packets);

private:
// Raw data buffer for incoming BLE packets
std::vector<uint8_t> rawBuffer;
Expand Down
22 changes: 16 additions & 6 deletions embedded/libs/ble-proxy/src/ble_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,41 @@ BLEClientConnection::BLEClientConnection()

bool BLEClientConnection::connect(NimBLEAddress address) {
if (state == BLEClientState::CONNECTED || state == BLEClientState::CONNECTING) {
Logger.logln("BLEClient: Already connected or connecting");
Logger.logln("BLEClient: Already connecting");
return false;
}

targetAddress = address;
state = BLEClientState::CONNECTING;

Logger.logln("BLEClient: Connecting to %s", address.toString().c_str());
// Log address details
uint8_t addrType = address.getType();
Logger.logln("BLEClient: Target addr: %s", address.toString().c_str());
Logger.logln("BLEClient: Addr type: %d (0=pub, 1=rand)", addrType);

// Create client if needed
if (!pClient) {
pClient = NimBLEDevice::createClient();
pClient->setClientCallbacks(this);
pClient->setConnectionParams(12, 12, 0, 51);
pClient->setConnectTimeout(CLIENT_CONNECT_TIMEOUT_MS / 1000);
pClient->setConnectTimeout(5); // 5 seconds - connections should be fast
}

// Attempt connection
if (!pClient->connect(address)) {
Logger.logln("BLEClient: Connection failed");
Logger.logln("BLEClient: Calling connect()...");

// Attempt connection with explicit address type
if (!pClient->connect(address, true)) {
Logger.logln("BLEClient: connect() returned false");
state = BLEClientState::DISCONNECTED;
reconnectTime = millis() + CLIENT_RECONNECT_DELAY_MS;
// Notify callback of connection failure so proxy can update state
if (connectCallback) {
connectCallback(false);
}
return false;
}

Logger.logln("BLEClient: connect() returned true");
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions embedded/libs/ble-proxy/src/ble_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
#define NUS_TX_CHARACTERISTIC "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

// Connection timing
#define CLIENT_CONNECT_TIMEOUT_MS 10000
#define CLIENT_RECONNECT_DELAY_MS 5000
#define CLIENT_CONNECT_TIMEOUT_MS 5000 // 5 second timeout (connections should be fast)
#define CLIENT_RECONNECT_DELAY_MS 3000

enum class BLEClientState { IDLE, CONNECTING, CONNECTED, RECONNECTING, DISCONNECTED };

Expand Down
Loading
Loading