Skip to content
Draft
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
36 changes: 33 additions & 3 deletions BLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,17 @@ The characteristic value uses the same binary format as the mDNS protocol for co
The characteristic value uses the same binary format as the mDNS protocol for consistency:

```
[Message_Type] [Version] [App_ID_Length] [App_ID...] [App_Version_Length] [App_Version...] [Button_Count] [Button_IDs...]
[Message_Type] [Version] [Device_Type] [App_ID_Length] [App_ID...]
[App_Version_Length] [App_Version...] [Button_Count] [Button_IDs...]
[Hint_Count] [Hint_1...] [Hint_2...] ...
```

- **Message_Type** (1 byte): Always `0x04` for app information messages
- **Version** (1 byte): Format version, currently `0x01`
- **Device_Type** (1 byte): Device type identifier
- `0x01` = Controller (physical game controller)
- `0x02` = App (software application)
- Indicates sender type
- **App_ID_Length** (1 byte): Length of the App ID string (0-32 characters)
- **App_ID** (variable): UTF-8 encoded app identifier string
- Should be lowercase, alphanumeric with optional hyphens/underscores
Expand All @@ -170,14 +176,36 @@ The characteristic value uses the same binary format as the mDNS protocol for co
- **Button_IDs** (variable): Array of button ID bytes
- Each byte represents a supported button ID from [Button Mapping](PROTOCOL.md#button-mapping)
- Devices can use this to provide visual feedback or customize layouts
- **Hint_Count** (1 byte): Number of button hints (0-255)
- If 0, no button hints provided
- **Hint** (variable, repeated): Each hint consists of:
- **Button_ID** (1 byte): The button this hint applies to
- **Label_Length** (1 byte): Length of label string (0-32 characters)
- **Label** (variable): UTF-8 encoded label for the button
- Helps apps interpret generic button ranges (0x50-0x5F, 0x60-0x6F) and analog enums (0x20, 0x40)

**Example Data:**

```
// App: "zwift", Version: "1.52.0", Buttons: [0x01, 0x02, 0x10, 0x14]
[0x04, 0x01, 0x05, 'z', 'w', 'i', 'f', 't', 0x06, '1', '.', '5', '2', '.', '0', 0x04, 0x01, 0x02, 0x10, 0x14]
// App: "zwift", Type: app (0x02), Version: "1.52.0", Buttons: [0x01, 0x02, 0x10, 0x14], no hints
[0x04, 0x01, 0x02, 0x05, 'z', 'w', 'i', 'f', 't', 0x06, '1', '.', '5', '2', '.', '0', 0x04, 0x01, 0x02, 0x10, 0x14, 0x00]

// Controller with button hints for Emote (0x20) and Camera (0x40)
// Type: controller (0x01)
// Hint count: 2
// Hint 1: Button 0x20 (32), Label "Emote" (5 bytes)
// Hint 2: Button 0x40 (64), Label "Camera" (6 bytes)
[0x04, 0x01, 0x01, ..., 0x02, 0x20, 0x05, 'E', 'm', 'o', 't', 'e', 0x40, 0x06, 'C', 'a', 'm', 'e', 'r', 'a']
```

**Button Hints for Analog Enums:**

Apps should use `button_hints` to provide labels for buttons like:
- **Emote (0x20)**: Label could be "Emote" or "Wave/Thumbs Up"
- **Camera View (0x40)**: Label could be "Camera" or "Cycle View"

For analog enum buttons, the label provides context about the button's purpose. The actual enum value meanings (e.g., 0=none, 1=wave) are communicated through the button state values as defined in the protocol.

**Write Behavior:**

- Apps SHOULD send this information immediately after connecting to the device
Expand All @@ -197,6 +225,8 @@ The characteristic value uses the same binary format as the mDNS protocol for co
- Device customizes button layouts for popular apps
- Device logs connection history for diagnostics
- Device provides app-specific haptic patterns or feedback
- Device interprets analog enum values (0x20 Emote, 0x40 Camera) based on app's hints
- Device shows custom labels for generic buttons (0x50-0x5F, 0x60-0x6F)

**Maximum Payload Size:**

Expand Down
37 changes: 33 additions & 4 deletions MDNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,17 @@ Sent by the app to inform the device about the app's identity and capabilities.
**Data Format:**

```
[Message_Type] [Version] [App_ID_Length] [App_ID...] [App_Version_Length] [App_Version...] [Button_Count] [Button_IDs...]
[Message_Type] [Version] [Device_Type] [App_ID_Length] [App_ID...]
[App_Version_Length] [App_Version...] [Button_Count] [Button_IDs...]
[Hint_Count] [Hint_1...] [Hint_2...] ...
```

- **Message_Type** (1 byte): Always `0x04` for app information messages
- **Version** (1 byte): Format version, currently `0x01`
- **Device_Type** (1 byte): Device type identifier
- `0x01` = Controller (physical game controller)
- `0x02` = App (software application)
- Indicates sender type
- **App_ID_Length** (1 byte): Length of the App ID string (0-32 characters)
- **App_ID** (variable): UTF-8 encoded app identifier string
- Should be lowercase, alphanumeric with optional hyphens/underscores
Expand All @@ -201,21 +207,44 @@ Sent by the app to inform the device about the app's identity and capabilities.
- **Button_IDs** (variable): Array of button ID bytes
- Each byte represents a supported button ID from [Button Mapping](PROTOCOL.md#button-mapping)
- Devices can use this to provide visual feedback or customize layouts
- **Hint_Count** (1 byte): Number of button hints (0-255)
- If 0, no button hints provided
- **Hint** (variable, repeated): Each hint consists of:
- **Button_ID** (1 byte): The button this hint applies to
- **Label_Length** (1 byte): Length of label string (0-32 characters)
- **Label** (variable): UTF-8 encoded label for the button
- Helps apps interpret generic button ranges (0x50-0x5F, 0x60-0x6F) and analog enums (0x20, 0x40)

**Example Data:**

```
// App: "zwift", Version: "1.52.0", Buttons: [0x01, 0x02, 0x10, 0x14]
[0x04, 0x01, 0x05, 'z', 'w', 'i', 'f', 't', 0x06, '1', '.', '5', '2', '.', '0', 0x04, 0x01, 0x02, 0x10, 0x14]
// App: "zwift", Type: app (0x02), Version: "1.52.0", Buttons: [0x01, 0x02, 0x10, 0x14], no hints
[0x04, 0x01, 0x02, 0x05, 'z', 'w', 'i', 'f', 't', 0x06, '1', '.', '5', '2', '.', '0', 0x04, 0x01, 0x02, 0x10, 0x14, 0x00]

// Controller with button hints for Emote (0x20) and Camera (0x40)
// Type: controller (0x01)
// Hint count: 2
// Hint 1: Button 0x20 (32), Label "Emote" (5 bytes)
// Hint 2: Button 0x40 (64), Label "Camera" (6 bytes)
[0x04, 0x01, 0x01, ..., 0x02, 0x20, 0x05, 'E', 'm', 'o', 't', 'e', 0x40, 0x06, 'C', 'a', 'm', 'e', 'r', 'a']
```

**Button Hints for Analog Enums:**

Apps should use `button_hints` to provide labels for buttons like:
- **Emote (0x20)**: Label could be "Emote" or "Wave/Thumbs Up"
- **Camera View (0x40)**: Label could be "Camera" or "Cycle View"

For analog enum buttons, the label provides context about the button's purpose. The actual enum value meanings (e.g., 0=none, 1=wave) are communicated through the button state values as defined in the protocol.

**Usage:**
- Apps SHOULD send this message immediately after establishing the TCP connection
- Apps MAY send updated information if capabilities change during the session
- Devices SHOULD handle the absence of this message gracefully (assume all buttons supported)
- The app information is cleared when the TCP connection is closed
- Apps should provide button hints for analog enums (0x20 Emote, 0x40 Camera) and generic buttons (0x50-0x5F, 0x60-0x6F) to help devices display appropriate labels

**Note:** This message is **optional** for apps to implement, but the information is important for devices to provide the best user experience (e.g., highlighting supported buttons, customizing layouts for specific apps).
**Note:** This message is **optional** for apps to implement, but the information is important for devices to provide the best user experience (e.g., highlighting supported buttons, customizing layouts for specific apps, displaying meaningful button labels).

---

Expand Down
46 changes: 19 additions & 27 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,44 +74,25 @@ OpenBikeControl defines standard button IDs for common actions. Device manufactu

| Button ID | Action | Description |
|-----------|--------|-------------|
| `0x10` | Up | Navigate up / Steer left |
| `0x11` | Down | Navigate down / Steer right |
| `0x12` | Left | Navigate left / Look left |
| `0x13` | Right | Navigate right / Look right |
| `0x10` | Up | Navigate up |
| `0x11` | Down | Navigate down |
| `0x12` | Left | Navigate left / Steer left |
| `0x13` | Right | Navigate right / Steer right |
| `0x14` | Select/Confirm | Confirm selection |
| `0x15` | Back/Cancel | Go back / Cancel |
| `0x16` | Menu | Open menu |
| `0x17` | Home | Return to home screen |

#### Social/Emotes (0x20-0x2F)

| Button ID | Action | Description |
|-----------|-------------|-------------|
| `0x20` | Wave | Wave to other riders |
| `0x21` | Thumbs Up | Give thumbs up |
| `0x22` | Hammer Time | Activate power-up |
| `0x23` | Bell | Ring bell |
| `0x24` | Screenshot | Take screenshot |

#### Training Controls (0x30-0x3F)

| Button ID | Action | Description |
|-----------|--------|-------------|
| `0x30` | ERG Up | Increase ERG mode power |
| `0x31` | ERG Down | Decrease ERG mode power |
| `0x32` | Skip Interval | Skip to next workout interval |
| `0x33` | Pause | Pause workout |
| `0x34` | Resume | Resume workout |
| `0x35` | Lap | Mark lap |
| `0x20` | Emote | Analog emote selector (enum 0–31). State value indicates emote ID: 0 = none/neutral, 1 = wave, 2 = thumbs up, 3 = hammer time, 4 = bell, etc. Apps define mappings. |

#### View Controls (0x40-0x4F)

| Button ID | Action | Description |
|-----------|--------|-------------|
| `0x40` | Camera Angle | Cycle camera view |
| `0x41` | Camera 1 | Switch to camera 1 |
| `0x42` | Camera 2 | Switch to camera 2 |
| `0x43` | Camera 3 | Switch to camera 3 |
| `0x40` | Switch Camera View | Analog camera selector (enum 0–31). State value indicates camera view: 0 = camera 1, 1 = camera 2, 2 = camera 3, etc. Apps define available views. |
| `0x44` | HUD Toggle | Show/hide HUD |
| `0x45` | Map Toggle | Show/hide map |

Expand All @@ -123,11 +104,22 @@ OpenBikeControl defines standard button IDs for common actions. Device manufactu
| `0x51` | Power-up 2 | Activate power-up slot 2 |
| `0x52` | Power-up 3 | Activate power-up slot 3 |

#### Custom/Reserved (0x60-0xFF)
**Generic App-Driven Digital Buttons (0x50-0x5F):**
- Buttons in this range are generic digital inputs
- Apps may assign custom actions via `button_hints` in App Information
- Useful for app-specific features without protocol changes

#### Analog App-Driven Inputs (0x60-0x6F)

| Range | Purpose |
|-------|---------|
| `0x60-0x6F` | Generic analog inputs for app-driven mapping. Apps define semantics via `button_hints`. State values 0–255 represent analog positions or enum values. |

#### Custom/Reserved (0x70-0xFF)

| Range | Purpose |
|-------|---------|
| `0x60-0x7F` | Reserved for future standard actions |
| `0x70-0x7F` | Reserved for future standard actions |
| `0x80-0x9F` | App-specific custom actions |
| `0xA0-0xFF` | Manufacturer-specific custom actions |

Expand Down
77 changes: 67 additions & 10 deletions devices/example-corp/sc-100/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,79 @@
"description": "Go back or cancel current action"
},
{
"physical_button": "Left Trigger",
"button_id": 48,
"standard_action": "ERG Down",
"physical_button": "Emote Dial",
"button_id": 32,
"_comment": "button_id 32 = 0x20 in protocol hex notation",
"standard_action": "Emote Selector",
"type": "analog",
"range": "0-255",
"description": "Analog trigger to decrease ERG mode power (0=released, 255=fully pressed)"
"range": "0-31",
"description": "Analog emote selector (enum 0-31). Value indicates emote ID: 0=none, 1=wave, 2=thumbs up, 3=hammer time, 4=bell, etc."
},
{
"physical_button": "Right Trigger",
"button_id": 49,
"standard_action": "ERG Up",
"physical_button": "Camera Switch",
"button_id": 64,
"_comment": "button_id 64 = 0x40 in protocol hex notation",
"standard_action": "Camera View Selector",
"type": "analog",
"range": "0-255",
"description": "Analog trigger to increase ERG mode power (0=released, 255=fully pressed)"
"range": "0-31",
"description": "Analog camera view selector (enum 0-31). Value indicates camera: 0=camera 1, 1=camera 2, 2=camera 3, etc."
}
],
"button_hints": {
"_comment": "Note: Wire protocol uses simplified binary format (button_id + label only). This JSON structure is for device configuration and documentation.",
"1": {
"role_hint": "gear_shift_up",
"label": "Shift Up"
},
"2": {
"role_hint": "gear_shift_down",
"label": "Shift Down"
},
"16": {
"role_hint": "navigate_up",
"label": "Up"
},
"17": {
"role_hint": "navigate_down",
"label": "Down"
},
"18": {
"role_hint": "navigate_left",
"label": "Left"
},
"19": {
"role_hint": "navigate_right",
"label": "Right"
},
"20": {
"role_hint": "select_confirm",
"label": "Select"
},
"21": {
"role_hint": "back_cancel",
"label": "Back"
},
"32": {
"role_hint": "emote_selector",
"label": "Emote",
"enum_values": {
"0": "None",
"1": "Wave",
"2": "Thumbs Up",
"3": "Hammer Time",
"4": "Bell"
}
},
"64": {
"role_hint": "camera_selector",
"label": "Camera View",
"enum_values": {
"0": "Camera 1",
"1": "Camera 2",
"2": "Camera 3"
}
}
},
"ble_services": {
"primary_service": "d273f680-d548-419d-b9d1-fa0472345229",
"button_characteristic": "d273f681-d548-419d-b9d1-fa0472345229",
Expand Down
50 changes: 33 additions & 17 deletions examples/python/mock_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,45 @@ async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWrit
(1.0, 0x01), # Shift Up after 1s
(2.0, 0x02), # Shift Down after 2s
(3.0, 0x14), # Select after 3s
(4.0, 0x20), # Wave after 4s
(4.0, 0x20, 1), # Emote: Wave (enum value 1) after 4s
(5.0, 0x40, 0), # Camera: View 1 (enum value 0) after 5s
]

async def simulate_buttons():
"""Background task to simulate button presses."""
for delay, button_id in button_sequence:
for item in button_sequence:
if len(item) == 2:
delay, button_id = item
analog_value = None
else:
delay, button_id, analog_value = item

await asyncio.sleep(delay)
if writer.is_closing():
break

press_msg, release_msg = device.simulate_button_press(button_id)

# Send press
writer.write(press_msg)
await writer.drain()
print(f" → Sent button press: 0x{button_id:02X}")

# Wait a bit
await asyncio.sleep(0.1)

# Send release
writer.write(release_msg)
await writer.drain()
print(f" → Sent button release: 0x{button_id:02X}")
if analog_value is not None:
# Analog/enum button - send analog value directly
msg = encode_button_state([(button_id, analog_value)])
writer.write(msg)
await writer.drain()
print(f" → Sent analog/enum button: 0x{button_id:02X} = {analog_value}")
else:
# Digital button - press and release
press_msg, release_msg = device.simulate_button_press(button_id)

# Send press
writer.write(press_msg)
await writer.drain()
print(f" → Sent button press: 0x{button_id:02X}")

# Wait a bit
await asyncio.sleep(0.1)

# Send release
writer.write(release_msg)
await writer.drain()
print(f" → Sent button release: 0x{button_id:02X}")

# Start button simulation
button_task = asyncio.create_task(simulate_buttons())
Expand Down Expand Up @@ -311,7 +326,8 @@ async def start_mock_server(host='0.0.0.0', port=8080):
print(" 1s: Shift Up (0x01)")
print(" 2s: Shift Down (0x02)")
print(" 3s: Select (0x14)")
print(" 4s: Wave (0x20)")
print(" 4s: Emote = Wave (0x20, enum value 1)")
print(" 5s: Camera = View 1 (0x40, enum value 0)")
print()
print("Connect using the TCP trainer app:")
print(" python tcp_trainer_app.py")
Expand Down
Loading