feat: implement initial cyborg multi-action button mappings

- Added support for Single, Long, and Double press actions in libazeron.
- Mapped Cyborg surgical command IDs (0x20F6, 0x20F8, 0x204A).
- Updated azeron-cli to support --long and --double mapping flags.
- Updated protocol documentation with newly discovered Cyborg commands.
- Added TODO.md for remaining joystick and timing tasks.
This commit is contained in:
2026-02-22 19:08:13 +00:00
parent db5c3505da
commit 18f84a538a
12 changed files with 37131 additions and 152 deletions

20
TODO.md Normal file
View File

@@ -0,0 +1,20 @@
# Azeron Cyborg Linux Support - Remaining Tasks
## Protocol Implementation
- [ ] **Joystick Bulk Write (`0x26EC`)**: Implement `azeron_protocol_set_stick_config` using the new bulk write format.
- Map mode byte (index 3).
- Map stick angle (index 8).
- Map deadzone/sensitivity.
- [ ] **Global Timings (`0x2000`)**: Implement a function to set Long Press and Double Click delays.
- [ ] **Verify Read Configuration**: Confirm if `0x26FB` works for Cyborg or if a new read command is required (Heartbeat showed `0x12EA/EB` pairs).
- [ ] **Surgical Button Persistence**: Verify if `0x204B` (Commit) is sufficient for all surgical updates or if `0x26ED` is needed.
## CLI Enhancements
- [ ] **Joystick Commands**: Add `set-stick-mode` and `set-stick-angle`.
- [ ] **Timing Commands**: Add `set-delays --long <ms> --double <ms>`.
- [ ] **Action Feedback**: Update `show-mappings` to display all three actions (Single, Long, Double) per button.
## Testing & Validation
- [ ] **Linux Verification**: Test all surgical updates (`0x20F6/F8/4A`) using `libusb` on Linux.
- [ ] **Persistence Check**: Ensure settings survive a device power cycle.
- [ ] **Input Monitoring**: Verify that Linux input events (EV_KEY) correctly match the new mappings.

View File

@@ -285,10 +285,14 @@ int cmd_map_button(int argc, char *argv[])
int device_index = 0;
int button_id;
const char *key_name;
enum azeron_action_type action_type = AZERON_ACTION_SINGLE;
if (argc < 3) {
fprintf(stderr, "Usage: %s map-button <button-id> <key> [options]\n", argv[0]);
fprintf(stderr, "Example: %s map-button 1 KEY_W\n", argv[0]);
fprintf(stderr, "Options:\n");
fprintf(stderr, " --long Map to long press\n");
fprintf(stderr, " --double Map to double press\n");
fprintf(stderr, "Example: %s map-button 1 KEY_W --long\n", argv[0]);
return 1;
}
@@ -303,6 +307,8 @@ int cmd_map_button(int argc, char *argv[])
/* Parse options */
static struct option long_options[] = {
{"device", required_argument, 0, 'd'},
{"long", no_argument, 0, 'l'},
{"double", no_argument, 0, 'b'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
@@ -310,11 +316,17 @@ int cmd_map_button(int argc, char *argv[])
int opt;
int option_index = 0;
optind = 3; /* Skip command, button-id and key */
while ((opt = getopt_long(argc, argv, "d:h", long_options, &option_index)) != -1) {
while ((opt = getopt_long(argc, argv, "d:lbh", long_options, &option_index)) != -1) {
switch (opt) {
case 'd':
device_index = atoi(optarg);
break;
case 'l':
action_type = AZERON_ACTION_LONG;
break;
case 'b':
action_type = AZERON_ACTION_DOUBLE;
break;
case 'h':
printf("Usage: %s map-button <button-id> <key> [options]\n", argv[0]);
return 0;
@@ -339,6 +351,7 @@ int cmd_map_button(int argc, char *argv[])
/* Prepare mapping */
mapping.button_id = (uint8_t)button_id;
mapping.action = action_type;
mapping.type = AZERON_BTN_KEYBOARD;
int keycode = azeron_keycode_from_string(key_name);
@@ -356,7 +369,13 @@ int cmd_map_button(int argc, char *argv[])
mapping.key_code = (uint16_t)keycode;
}
printf("Mapping button %d to key 0x%02x (%s)...\n", button_id + 1, mapping.key_code, key_name);
const char *action_str = "single";
if (action_type == AZERON_ACTION_LONG) action_str = "long";
else if (action_type == AZERON_ACTION_DOUBLE) action_str = "double";
printf("Mapping button %d (%s press) to key 0x%02x (%s)...\n",
button_id + 1, action_str, mapping.key_code, key_name);
ret = azeron_device_set_button_mapping(device, &mapping);
if (ret != AZERON_SUCCESS) {
fprintf(stderr, "Failed to set button mapping: %s\n", azeron_error_string(ret));

18379
captures/ana-joy-capture.json Normal file

File diff suppressed because it is too large Load Diff

5396
captures/azeron-capture.json Normal file

File diff suppressed because it is too large Load Diff

1797
captures/hid-to-ana.json Normal file

File diff suppressed because it is too large Load Diff

11303
captures/joy-capture.json Normal file

File diff suppressed because it is too large Load Diff

122
captures/long_press.txt Normal file
View File

@@ -0,0 +1,122 @@
# Packet returned when performing a long press
Frame 31: Packet, 91 bytes on wire (728 bits), 91 bytes captured (728 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: 2.4.5]
[Destination: host]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20f9867c420
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x01, Direction: PDO -> FDO
URB bus id: 2
Device address: 4
Endpoint: 0x85, Direction: IN
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 64
[bInterfaceClass: HID (0x03)]
HID Data: 42505f31320d0a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Frame 32: Packet, 27 bytes on wire (216 bits), 27 bytes captured (216 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: host]
[Destination: 2.4.5]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20f9867c420
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x00, Direction: FDO -> PDO
URB bus id: 2
Device address: 4
Endpoint: 0x85, Direction: IN
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 0
[Response in: 40]
[bInterfaceClass: HID (0x03)]
Frame 33: Packet, 91 bytes on wire (728 bits), 91 bytes captured (728 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: 2.4.5]
[Destination: host]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20f9861d7e0
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x01, Direction: PDO -> FDO
URB bus id: 2
Device address: 4
Endpoint: 0x85, Direction: IN
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 64
[bInterfaceClass: HID (0x03)]
HID Data: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Frame 34: Packet, 27 bytes on wire (216 bits), 27 bytes captured (216 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: host]
[Destination: 2.4.5]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20f9861d7e0
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x00, Direction: FDO -> PDO
URB bus id: 2
Device address: 4
Endpoint: 0x85, Direction: IN
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 0
[Response in: 45]
[bInterfaceClass: HID (0x03)]
Frame 35: Packet, 91 bytes on wire (728 bits), 91 bytes captured (728 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: host]
[Destination: 2.4.6]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20fb26e3010
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x00, Direction: FDO -> PDO
URB bus id: 2
Device address: 4
Endpoint: 0x06, Direction: OUT
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 64
[Response in: 36]
[bInterfaceClass: HID (0x03)]
HID Data: 5e327e48690a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Frame 36: Packet, 27 bytes on wire (216 bits), 27 bytes captured (216 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: 2.4.6]
[Destination: host]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20fb26e3010
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x01, Direction: PDO -> FDO
URB bus id: 2
Device address: 4
Endpoint: 0x06, Direction: OUT
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 0
[Request in: 35]
[Time from request: 4.877000 milliseconds]
[bInterfaceClass: HID (0x03)]
Frame 37: Packet, 91 bytes on wire (728 bits), 91 bytes captured (728 bits) on interface \\.\USBPcap2, id 0
USB URB
[Source: host]
[Destination: 2.4.6]
USBPcap pseudoheader length: 27
IRP ID: 0xffffe20fa1bb2010
IRP USBD_STATUS: USBD_STATUS_SUCCESS (0x00000000)
URB Function: URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER (0x0009)
IRP information: 0x00, Direction: FDO -> PDO
URB bus id: 2
Device address: 4
Endpoint: 0x06, Direction: OUT
URB transfer type: URB_INTERRUPT (0x01)
Packet Data Length: 64
[Response in: 44]
[bInterfaceClass: HID (0x03)]
HID Data: 001520b10101150101000c0000000001000000000000f01b00000000000000000000000000000000000000000000000000000000000000000000000000000000

7
captures/usb_info.txt Normal file
View File

@@ -0,0 +1,7 @@
# Output from "Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -match '^USB' } | Select-Object FriendlyName, InstanceId"
USB Input Device USB\VID_16D0&PID_113C&MI_04\9&6389280&1&0004
USB Input Device USB\VID_16D0&PID_113C&MI_03\9&6389280&1&0003
USB Input Device USB\VID_16D0&PID_113C&MI_02\9&6389280&1&0002
USB Input Device USB\VID_16D0&PID_113C&MI_01\9&6389280&1&0001
Xbox 360 Controller for Windows USB\VID_16D0&PID_113C&MI_00\9&6389280&1&0000
USB Composite Device USB\VID_16D0&PID_113C\208332B93553

View File

@@ -51,7 +51,7 @@ This document describes the USB configuration protocol for the Azeron Cyborg key
- **Transfer Type**: Interrupt transfers (0x01)
- **Packet Format**: Fixed 64-byte HID reports
- **Command Structure**: Request-response pattern
- **Endianness**: Big-endian for multi-byte values
- **Endianness**: Big-endian for multi-byte values (except offsets)
### Packet Format
@@ -59,135 +59,53 @@ All configuration packets follow this structure:
```
Byte 0: Flags/Type (0x00=request, 0x01=response)
Byte 1: Reserved (0x00)
Byte 1: Reserved/Length (0x3a for config writes)
Bytes 2-3: Command ID (big-endian)
Bytes 4-5: Operation type and parameters
Bytes 6-63: Data payload (varies by command)
```
### Command Reference
### Command Reference (Cyborg Model)
#### 0x122a - Get Status (Heartbeat)
**Purpose:** Periodic device status check (occurs every 1-2 seconds automatically)
**Purpose:** Periodic device status check.
**Request:**
```
OUT 0x06: 0000122a010100000000000000000000...
││ ││││└────┬────┘└────┬──────┘
││ ││││ │ └─ Payload (zeros)
││ ││││ └─ Operation: 0x0101 (read status)
││ ││└─────── Command ID: 0x122a
││ └──────── Reserved: 0x00
└─────────── Type: 0x00 (request)
```
#### 0x2000 - Set Global Settings
**Purpose:** Configure timing delays for actions.
- **Bytes 60-61 (Payload):** Long Press Delay (ms, big-endian, e.g., `01f4` = 500ms)
- **Bytes 62-63 (Payload):** Double Press Delay (ms, big-endian, e.g., `00c8` = 200ms)
**Response:**
```
IN 0x85: 0100122a010100013f4f691b0700ff060606...
││ ││││ │└────┬──────┘└────┬──────┘
││ ││││ │ └─ Device data starts here
││ ││││ └─ Status: 0x01 (success)
││ ││└─────── Command ID: 0x122a
││ └──────── Reserved: 0x00
└─────────── Type: 0x01 (response)
```
#### 0x20F6 - Set Single Press
#### 0x20F8 - Set Long Press
#### 0x204A - Set Double Press
**Purpose:** Surgical update for a specific button action.
- **Byte 10 (Payload):** Button ID (0-based)
- **Byte 22 (Payload):** Key Type (0xf0 = Keyboard)
- **Byte 23 (Payload):** Key Code (HID)
**Device Data Bytes:**
- Bytes 6-7: Profile number (0x0001 = Profile 1)
- Bytes 8-11: Joystick X/Y position
- Bytes 12-15: Button states (bitmask)
- Bytes 16-19: Analog stick settings
- Bytes 20-23: LED color (RGB)
- Bytes 24-63: Additional status data
#### 0x204B - Commit Button Changes
**Purpose:** Makes surgical updates (`0x20F6/F8/4A`) active.
#### 0x26EC - Bulk Write Profile
**Purpose:** Writes a complete profile block.
- **Byte 3 (Payload):** Joystick Mode (0x00 = Analog, 0x01 = WASD)
- **Byte 8 (Payload):** Stick Angle (degrees)
#### 0x26ED - Commit Bulk Write
**Purpose:** Persists bulk changes to EEPROM.
### Command Reference (Classic Model)
*Note: These commands are used by older Azeron versions and may differ from Cyborg.*
#### 0x12C8 - Read Configuration
**Purpose:** Read full device configuration
**Request:**
```
OUT 0x06: 000012c801010000000000000000000000...
││ ││││└────┬────┘
││ ││││ └─ Operation: 0x0101 (read config)
││ ││└─────── Command ID: 0x12c8
└──────────── Standard header
```
**Response:**
```
IN 0x85: 010012c8010101013f4f691b0700ff060606...
││ ││││ │└────┬──────┘
││ ││││ │ └─ Full configuration data (58 bytes)
││ ││││ └─ Status: 0x01 (success)
└──────────── Standard header
```
**Configuration Data Structure:**
- Bytes 6-9: Device signature/version
- Bytes 10-13: Profile 0 settings
- Bytes 14-17: Profile 1 settings
- Bytes 18-21: Profile 2 settings
- Bytes 22-25: Button mapping offsets
- Bytes 26-57: Button mappings (30 buttons × 1 byte each)
- Bytes 58-61: Joystick configuration
- Bytes 62-63: Checksum
**Purpose:** Read full device configuration.
#### 0x26FC - Write Profile Data
**Purpose:** Write profile configuration to device (does not persist)
**Request:**
```
OUT 0x06: 003a26fc020139040102000000fff40003f01a0000...
││ ││││└────┬────┘└────┬──────┘└────┬──────┘
││ ││││ │ │ └─ Button/key codes
││ ││││ │ └─ Offset: 0x0439 (1081)
││ ││││ └─ Operation: 0x0201 (write profile 1)
││ ││└─────── Command ID: 0x26fc
└──────────── Length: 0x003a (58 bytes)
```
**Response:**
```
IN 0x85: 000026fc010100013f4f691b0700ff060606...
Standard header + status + device data
```
**Profile Data Bytes:**
- Bytes 6-9: Offset/address (0x00000439)
- Bytes 10-13: Profile header (0x01020000)
- Bytes 14-17: LED color (0xfff40003 = RGBA)
- Bytes 18-21: Button 1 mapping (0xf01a0000)
- Bytes 22-25: Button 2 mapping (0xf0070000)
- ... (continues for all 30 buttons)
- Bytes 58-61: Joystick settings
- Bytes 62-63: Reserved
**Button Mapping Format:**
```
Byte 0: Key type (0xf0 = keyboard, 0xf1 = mouse, 0xf2 = gamepad, 0xf3 = macro)
Byte 1: Key code (USB HID code)
Byte 2: Modifier flags (shift, ctrl, alt)
Byte 3: Reserved
```
**Purpose:** Write profile configuration to device (does not persist).
#### 0x26FD - Save Profile
**Purpose:** Commit profile to device EEPROM (persists after power-off)
**Request:**
```
OUT 0x06: 003a26fd020201000000000000000000000000...
││ ││││└────┬────┘
││ ││││ └─ Operation: 0x0202 (save profile 1)
││ ││└─────── Command ID: 0x26fd
└──────────── Length: 0x003a (58 bytes, mostly zeros)
```
**Response:**
```
IN 0x85: 000026fd010100013f4f691b0700ff060606...
Standard header + confirmation
```
**Note:** The save command echoes the write command structure but with operation 0x0202 and minimal data payload. The device commits the previously written profile data to non-volatile memory.
**Purpose:** Commit profile to device EEPROM.
## Button Mapping Reference

View File

@@ -44,6 +44,13 @@ enum azeron_button_type {
AZERON_BTN_LAYER_SWITCH,
};
/* Button action types (for Cyborg model) */
enum azeron_action_type {
AZERON_ACTION_SINGLE = 0,
AZERON_ACTION_LONG,
AZERON_ACTION_DOUBLE,
};
/* Analog stick modes */
enum azeron_stick_mode {
AZERON_STICK_ANALOG = 0,
@@ -67,6 +74,7 @@ struct azeron_device_info {
/* Button mapping */
struct azeron_button_mapping {
uint8_t button_id;
enum azeron_action_type action;
enum azeron_button_type type;
uint16_t key_code; /* Linux input event code */
char *macro; /* For macro type */

View File

@@ -19,8 +19,15 @@
/* Command IDs */
#define AZERON_CMD_STATUS 0x122a
#define AZERON_CMD_READ_CONFIG 0x26FB
#define AZERON_CMD_WRITE_PROFILE 0x26FC
#define AZERON_CMD_SAVE_PROFILE 0x26FD
#define AZERON_CMD_WRITE_PROFILE 0x26EC
#define AZERON_CMD_SAVE_PROFILE 0x26ED
/* Cyborg Specific Surgical Commands */
#define AZERON_CMD_SET_SINGLE 0x20F6
#define AZERON_CMD_SET_LONG 0x20F8
#define AZERON_CMD_SET_DOUBLE 0x204A
#define AZERON_CMD_COMMIT_BTN 0x204B
#define AZERON_CMD_SET_GLOBAL 0x2000
/* Operation types */
#define AZERON_OP_READ_STATUS 0x0101
@@ -31,7 +38,7 @@
#define AZERON_OP_SAVE_PROFILE 0x0202
/* Offsets for profile data */
#define AZERON_PROFILE_BASE_OFFSET 0x0439
#define AZERON_PROFILE_BASE_OFFSET 0x0339
#define AZERON_BUTTON_MAPPING_SIZE 4
#define AZERON_STICK_CONFIG_OFFSET 58
@@ -277,53 +284,56 @@ int azeron_protocol_set_button_mapping(struct azeron_device *device,
uint8_t response[64];
size_t response_len;
uint8_t data[58] = {0};
uint32_t offset;
uint16_t operation;
uint8_t active_profile;
uint16_t command;
int ret;
if (!device || !mapping) {
return AZERON_ERROR_INVALID_PARAM;
}
/* Get active profile to know which operation to use */
ret = azeron_protocol_get_active_profile(device, &active_profile);
if (ret != AZERON_SUCCESS) {
return ret;
/* Select surgical command based on action type */
switch (mapping->action) {
case AZERON_ACTION_SINGLE: command = AZERON_CMD_SET_SINGLE; break;
case AZERON_ACTION_LONG: command = AZERON_CMD_SET_LONG; break;
case AZERON_ACTION_DOUBLE: command = AZERON_CMD_SET_DOUBLE; break;
default: return AZERON_ERROR_INVALID_PARAM;
}
switch (active_profile) {
case 0: operation = AZERON_OP_WRITE_PROFILE_0; break;
case 1: operation = AZERON_OP_WRITE_PROFILE_1; break;
case 2: operation = AZERON_OP_WRITE_PROFILE_2; break;
default: return AZERON_ERROR_PROTOCOL;
}
/* Calculate offset: Base + 8 (header + color) + (ButtonID * 4) */
offset = AZERON_PROFILE_BASE_OFFSET + 8 + (mapping->button_id * AZERON_BUTTON_MAPPING_SIZE);
/* Data payload for 0x26FC:
* Bytes 0-3: Offset (LITTLE ENDIAN)
* Bytes 4-7: Mapping (4 bytes)
/* Surgical payload structure for Cyborg:
* Byte 4: 0x01 (fixed?)
* Byte 5: 0x01 (fixed?)
* Byte 10: Button ID
* Byte 11: Sub-action index? (usually 0x01 or 0x02)
* Byte 22: Key Type
* Byte 23: Key Code
*/
data[0] = offset & 0xFF;
data[1] = (offset >> 8) & 0xFF;
data[2] = (offset >> 16) & 0xFF;
data[3] = (offset >> 24) & 0xFF;
data[4] = 0x01;
data[10] = mapping->button_id;
switch (mapping->action) {
case AZERON_ACTION_SINGLE: data[11] = 0x01; break;
case AZERON_ACTION_LONG: data[11] = 0x00; break; /* As seen in capture */
case AZERON_ACTION_DOUBLE: data[11] = 0x00; break; /* As seen in capture */
}
switch (mapping->type) {
case AZERON_BTN_KEYBOARD: data[4] = 0xf0; break;
case AZERON_BTN_MOUSE: data[4] = 0xf1; break;
case AZERON_BTN_GAMEPAD: data[4] = 0xf2; break;
case AZERON_BTN_KEYBOARD: data[22] = 0xf0; break;
case AZERON_BTN_MOUSE: data[22] = 0xf1; break;
case AZERON_BTN_GAMEPAD: data[22] = 0xf2; break;
default: return AZERON_ERROR_UNSUPPORTED;
}
data[5] = mapping->key_code & 0xFF;
data[6] = 0x00; /* No modifiers for now */
data[7] = 0x00;
data[23] = mapping->key_code & 0xFF;
ret = send_config_command(device, AZERON_CMD_WRITE_PROFILE, operation,
data, 8, response, &response_len);
ret = send_config_command(device, command, 0x0101,
data, 24, response, &response_len);
if (ret == AZERON_SUCCESS) {
/* Follow up with commitment command to make it live */
ret = send_config_command(device, AZERON_CMD_COMMIT_BTN, 0x0101,
NULL, 0, response, &response_len);
}
return ret;
}