diff --git a/README.md b/README.md index ad2ee5c..516ff05 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,27 @@ sudo make install ```bash # List connected Azeron devices -sudo azeron-cli list +azeron-cli list -# Show current button mappings -sudo azeron-cli show-mappings +# Show detailed device info and stick config +azeron-cli info + +# Show current button mappings (Single, Long, Double actions) +azeron-cli show-mappings # Remap a button (e.g., button 5 to 'W' key) -sudo azeron-cli map-button 5 KEY_W +azeron-cli map-button 5 KEY_W +# Map long press action +azeron-cli map-button 5 KEY_E --long -# Save current configuration to a profile -sudo azeron-cli save-profile my_gaming_profile +# Configure analog stick +azeron-cli set-stick --mode analog --angle 15 --deadzone 5 + +# Set global timing delays +azeron-cli set-delays --long 500 --double 200 + +# Save current configuration to an onboard profile (0, 1, or 2) +azeron-cli save-profile 1 ``` ### Setting up udev Rules diff --git a/TODO.md b/TODO.md index a9665c9..5717f38 100644 --- a/TODO.md +++ b/TODO.md @@ -1,18 +1,18 @@ # Azeron Cyborg Linux Support - Remaining Tasks ## Protocol Implementation -- [ ] **Joystick Bulk Write (`0x26EC`)**: Implement `azeron_protocol_set_stick_config` using the new bulk write format. +- [x] **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). +- [x] **Global Timings (`0x2000`)**: Implement a function to set Long Press and Double Click delays. +- [x] **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 --double `. -- [ ] **Action Feedback**: Update `show-mappings` to display all three actions (Single, Long, Double) per button. +- [x] **Joystick Commands**: Add `set-stick-mode` and `set-stick-angle`. +- [x] **Timing Commands**: Add `set-delays --long --double `. +- [x] **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. diff --git a/azeron-cli/main.c b/azeron-cli/main.c index 1ddcf95..ea93b27 100644 --- a/azeron-cli/main.c +++ b/azeron-cli/main.c @@ -26,6 +26,7 @@ int cmd_export_config(int argc, char *argv[]); int cmd_import_config(int argc, char *argv[]); int cmd_set_stick(int argc, char *argv[]); int cmd_read_raw(int argc, char *argv[]); +int cmd_set_delays(int argc, char *argv[]); /* Command structure */ struct command { @@ -45,6 +46,7 @@ static struct command commands[] = { {"export-config", "Export configuration to file", cmd_export_config}, {"import-config", "Import configuration from file", cmd_import_config}, {"set-stick", "Configure analog stick settings", cmd_set_stick}, + {"set-delays", "Set global timing delays", cmd_set_delays}, {"read-raw", "Read raw memory from device", cmd_read_raw}, {NULL, NULL, NULL} }; @@ -189,6 +191,7 @@ int cmd_info(int argc, char *argv[]) printf("\nStick Configuration:\n"); printf("--------------------\n"); printf("Mode: %s\n", azeron_stick_mode_string(stick.mode)); + printf("Angle: %d\n", stick.angle); printf("Deadzone: %d%%\n", stick.deadzone); printf("Sensitivity: %d\n", stick.sensitivity); printf("Response Curve: %d\n", stick.response_curve); @@ -251,23 +254,41 @@ int cmd_show_mappings(int argc, char *argv[]) printf("Button Mappings:\n"); printf("================\n\n"); - printf("%-10s %-15s %s\n", "Button", "Type", "Mapping"); - printf("%-10s %-15s %s\n", "------", "----", "-------"); + printf("%-8s %-15s %-15s %-15s\n", "Button", "Single", "Long", "Double"); + printf("%-8s %-15s %-15s %-15s\n", "------", "------", "----", "------"); for (i = 0; i < 30; i++) { - struct azeron_button_mapping mapping; - ret = azeron_device_get_button_mapping(device, i, &mapping); + struct azeron_button_mapping s_mapping, l_mapping, d_mapping; + char s_str[32], l_str[32], d_str[32]; + + /* Single */ + s_mapping.action = AZERON_ACTION_SINGLE; + ret = azeron_device_get_button_mapping(device, i, &s_mapping); if (ret == AZERON_SUCCESS) { - printf("%-10d %-15s 0x%02x\n", - i + 1, - azeron_button_type_string(mapping.type), - mapping.key_code); - } else { - printf("%-10d %-15s \n", - i + 1, - "unknown", - azeron_error_string(ret)); - } + const char *key = azeron_keycode_to_string(s_mapping.key_code); + if (key) snprintf(s_str, sizeof(s_str), "%s", key); + else snprintf(s_str, sizeof(s_str), "0x%02x", s_mapping.key_code); + } else snprintf(s_str, sizeof(s_str), "err"); + + /* Long */ + l_mapping.action = AZERON_ACTION_LONG; + ret = azeron_device_get_button_mapping(device, i, &l_mapping); + if (ret == AZERON_SUCCESS) { + const char *key = azeron_keycode_to_string(l_mapping.key_code); + if (key) snprintf(l_str, sizeof(l_str), "%s", key); + else snprintf(l_str, sizeof(l_str), "0x%02x", l_mapping.key_code); + } else snprintf(l_str, sizeof(l_str), "err"); + + /* Double */ + d_mapping.action = AZERON_ACTION_DOUBLE; + ret = azeron_device_get_button_mapping(device, i, &d_mapping); + if (ret == AZERON_SUCCESS) { + const char *key = azeron_keycode_to_string(d_mapping.key_code); + if (key) snprintf(d_str, sizeof(d_str), "%s", key); + else snprintf(d_str, sizeof(d_str), "0x%02x", d_mapping.key_code); + } else snprintf(d_str, sizeof(d_str), "err"); + + printf("%-8d %-15s %-15s %-15s\n", i + 1, s_str, l_str, d_str); } azeron_device_close(device); @@ -602,6 +623,8 @@ int cmd_set_stick(int argc, char *argv[]) {"deadzone", required_argument, 0, 'z'}, {"sensitivity", required_argument, 0, 's'}, {"curve", required_argument, 0, 'c'}, + {"mode", required_argument, 0, 'm'}, + {"angle", required_argument, 0, 'a'}, {"invert-x", no_argument, 0, 'x'}, {"invert-y", no_argument, 0, 'y'}, {"help", no_argument, 0, 'h'}, @@ -613,16 +636,29 @@ int cmd_set_stick(int argc, char *argv[]) /* Initialize defaults from current config if possible, or zeros */ memset(&stick, 0, sizeof(stick)); - bool dz_set = false, sens_set = false, curve_set = false; + bool dz_set = false, sens_set = false, curve_set = false, mode_set = false, angle_set = false; + bool x_set = false, y_set = false; - while ((opt = getopt_long(argc, argv, "d:z:s:c:xyh", long_options, &option_index)) != -1) { + while ((opt = getopt_long(argc, argv, "d:z:s:c:m:a:xyh", long_options, &option_index)) != -1) { switch (opt) { case 'd': device_index = atoi(optarg); break; case 'z': stick.deadzone = atoi(optarg); dz_set = true; break; case 's': stick.sensitivity = atoi(optarg); sens_set = true; break; case 'c': stick.response_curve = atoi(optarg); curve_set = true; break; - case 'x': stick.invert_x = true; break; - case 'y': stick.invert_y = true; break; + case 'm': + if (strcmp(optarg, "analog") == 0) stick.mode = AZERON_STICK_ANALOG; + else if (strcmp(optarg, "digital_4") == 0) stick.mode = AZERON_STICK_DIGITAL_4; + else if (strcmp(optarg, "digital_8") == 0) stick.mode = AZERON_STICK_DIGITAL_8; + else if (strcmp(optarg, "mouse") == 0) stick.mode = AZERON_STICK_MOUSE; + else { + fprintf(stderr, "Error: Invalid mode '%s'. Use: analog, digital_4, digital_8, mouse\n", optarg); + return 1; + } + mode_set = true; + break; + case 'a': stick.angle = atoi(optarg); angle_set = true; break; + case 'x': stick.invert_x = true; x_set = true; break; + case 'y': stick.invert_y = true; y_set = true; break; case 'h': printf("Usage: %s set-stick [options]\n", argv[0]); printf("Options:\n"); @@ -630,6 +666,8 @@ int cmd_set_stick(int argc, char *argv[]) printf(" -z, --deadzone <0-100> Set deadzone percentage\n"); printf(" -s, --sensitivity <0-255> Set sensitivity\n"); printf(" -c, --curve <0-255> Set response curve\n"); + printf(" -m, --mode Set mode (analog, digital_4, digital_8, mouse)\n"); + printf(" -a, --angle <0-255> Set stick angle\n"); printf(" -x, --invert-x Invert X axis\n"); printf(" -y, --invert-y Invert Y axis\n"); return 0; @@ -659,6 +697,10 @@ int cmd_set_stick(int argc, char *argv[]) if (!dz_set) stick.deadzone = current.deadzone; if (!sens_set) stick.sensitivity = current.sensitivity; if (!curve_set) stick.response_curve = current.response_curve; + if (!mode_set) stick.mode = current.mode; + if (!angle_set) stick.angle = current.angle; + if (!x_set) stick.invert_x = current.invert_x; + if (!y_set) stick.invert_y = current.invert_y; } printf("Updating stick configuration...\n"); @@ -752,6 +794,74 @@ int cmd_read_raw(int argc, char *argv[]) return 0; } +/* Set global timing delays */ +int cmd_set_delays(int argc, char *argv[]) +{ + struct azeron_device *device; + int ret; + int device_index = 0; + uint16_t long_press = 500; + uint16_t double_click = 200; + + /* Parse options */ + static struct option long_options[] = { + {"device", required_argument, 0, 'd'}, + {"long", required_argument, 0, 'l'}, + {"double", required_argument, 0, 'b'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "d:l:b:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'd': device_index = atoi(optarg); break; + case 'l': long_press = (uint16_t)atoi(optarg); break; + case 'b': double_click = (uint16_t)atoi(optarg); break; + case 'h': + printf("Usage: %s set-delays [options]\n", argv[0]); + printf("Options:\n"); + printf(" -d, --device Select device (default: 0)\n"); + printf(" -l, --long Set long press delay in ms (default: 500)\n"); + printf(" -b, --double Set double click delay in ms (default: 200)\n"); + return 0; + default: + fprintf(stderr, "Unknown option. Use --help for usage.\n"); + return 1; + } + } + + ret = azeron_init(); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to initialize library: %s\n", azeron_error_string(ret)); + return 1; + } + + ret = azeron_device_open_index(&device, device_index); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to open device %d: %s\n", device_index, azeron_error_string(ret)); + azeron_exit(); + return 1; + } + + printf("Setting global delays: Long Press = %dms, Double Click = %dms...\n", long_press, double_click); + ret = azeron_device_set_global_timings(device, long_press, double_click); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to set global timings: %s\n", azeron_error_string(ret)); + azeron_device_close(device); + azeron_exit(); + return 1; + } + + printf("Delays updated successfully.\n"); + + azeron_device_close(device); + azeron_exit(); + + return 0; +} + /* Main function */ int main(int argc, char *argv[]) { diff --git a/docs/protocol.md b/docs/protocol.md index e91f285..07a7be5 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -2,10 +2,10 @@ ## Overview -This document describes the USB configuration protocol for the Azeron Cyborg keypad (USB ID: 16d0:113c). The protocol has been reverse-engineered through USB traffic analysis and is now ready for implementation. +This document describes the USB configuration protocol for the Azeron Cyborg keypad (USB ID: 16d0:113c). The protocol has been reverse-engineered through USB traffic analysis and is now implemented in libazeron. **Protocol Status:** ✅ Fully Reverse-Engineered -**Implementation Status:** 🔄 Ready for Development +**Implementation Status:** ✅ Implemented in libazeron ## USB Device Analysis @@ -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 (except offsets) +- **Endianness**: Little-endian for offsets and delays, Big-endian for Command IDs. ### Packet Format @@ -68,12 +68,20 @@ Bytes 6-63: Data payload (varies by command) ### Command Reference (Cyborg Model) #### 0x122a - Get Status (Heartbeat) -**Purpose:** Periodic device status check. +**Purpose:** Periodic device status check. Returns current profile configuration. +- **Payload Index 0:** Status (0x01 = Success) +- **Payload Index 1:** Active Profile ID (0, 1, 2) +- **Payload Index 3:** Joystick Mode +- **Payload Index 8:** Stick Angle +- **Payload Index 10:** Deadzone +- **Payload Index 11:** Response Curve +- **Payload Index 12:** Sensitivity +- **Payload Index 13:** Inversion flags (Bit 0: X, Bit 1: Y) #### 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) +- **Bytes 54-55 (Payload):** Long Press Delay (ms, little-endian, e.g., `f401` = 500ms) +- **Bytes 56-57 (Payload):** Double Press Delay (ms, little-endian, e.g., `c800` = 200ms) #### 0x20F6 - Set Single Press #### 0x20F8 - Set Long Press @@ -88,8 +96,9 @@ Bytes 6-63: Data payload (varies by command) #### 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) +- **Byte 3 (Payload):** Joystick Mode (0x00=Analog, 0x01=WASD4, 0x02=WASD8, 0x03=Mouse) +- **Byte 8 (Payload):** Stick Angle (0-255) +- **Byte 10 (Payload):** Deadzone (0-100) #### 0x26ED - Commit Bulk Write **Purpose:** Persists bulk changes to EEPROM. @@ -101,24 +110,20 @@ Bytes 6-63: Data payload (varies by command) #### 0x12C8 - Read Configuration **Purpose:** Read full device configuration. -#### 0x26FC - Write Profile Data -**Purpose:** Write profile configuration to device (does not persist). - -#### 0x26FD - Save Profile -**Purpose:** Commit profile to device EEPROM. +#### 0x26FB - Read Configuration (Cyborg) +**Purpose:** Read specific memory blocks. ## Button Mapping Reference The Azeron Cyborg has **30 configurable buttons** plus **1 analog joystick**. ### Button Numbering -Buttons are numbered 1-30 in the configuration data: +Buttons are numbered 1-30 in the configuration data. Each button has 3 actions (Single, Long, Double), each 4 bytes. -- **Bytes 18-21:** Button 1 (typically the main thumb button) -- **Bytes 22-25:** Button 2 -- **Bytes 26-29:** Button 3 +- **Profile Offset 8-11:** Button 1 Single +- **Profile Offset 12-15:** Button 1 Long +- **Profile Offset 16-19:** Button 1 Double - ... (continues linearly) -- **Bytes 134-137:** Button 30 ### Key Type Codes @@ -131,219 +136,43 @@ Buttons are numbered 1-30 in the configuration data: | 0xf4 | Media | Media control key | | 0xf5 | Layer | Layer switch | -### Key Code Values - -**Keyboard (0xf0):** -- 0x04 = A, 0x05 = B, 0x06 = C, ... (USB HID keyboard codes) -- 0x1d = W, 0x1e = S, 0x1f = A, 0x20 = D (WASD) -- 0x28 = Return, 0x2c = Space, 0x2b = Tab - -**Mouse (0xf1):** -- 0x01 = Left button, 0x02 = Right button, 0x04 = Middle button -- 0x10 = Wheel up, 0x20 = Wheel down - -**Gamepad (0xf2):** -- 0x01 = Button 1, 0x02 = Button 2, ... -- 0x30 = D-pad up, 0x31 = D-pad down, etc. - -### Modifier Flags - -| Bit | Flag | Key | -|-----|------|-----| -| 0 | 0x01 | Left Ctrl | -| 1 | 0x02 | Left Shift | -| 2 | 0x04 | Left Alt | -| 3 | 0x08 | Left GUI | -| 4 | 0x10 | Right Ctrl | -| 5 | 0x20 | Right Shift | -| 6 | 0x40 | Right Alt | -| 7 | 0x80 | Right GUI | - ## Analog Joystick Configuration The analog joystick is configured separately from the 30 buttons. -**Joystick Data Bytes (58-61):** +**Joystick Profile Data (Indices relative to payload start):** ``` -Byte 58: Dead zone (0-100%) -Byte 59: Sensitivity curve (0=linear, 1=exponential, 2=custom) -Byte 60: X-axis inversion (0=normal, 1=inverted) -Byte 61: Y-axis inversion (0=normal, 1=inverted) +Index 3: Joystick Mode (0x00=Analog, 0x01=WASD4, 0x02=WASD8, 0x03=Mouse) +Index 8: Stick Angle (0-255) +Index 10: Dead zone (0-100%) +Index 11: Sensitivity curve (0=linear, 1=exponential, etc.) +Index 12: Sensitivity (0-100) +Index 13: Inversion flags (Bit 0: X, Bit 1: Y) ``` -**Joystick Modes:** -- **0x00 = Analog:** Standard analog stick behavior -- **0x01 = 4-way:** Digital 4-direction pad -- **0x02 = 8-way:** Digital 8-direction pad -- **0x03 = Mouse:** Mouse emulation mode - ## Profile Management The device supports **3 profiles** (Profile 0, 1, 2). -### Profile Switching Sequence - -When switching profiles, the software sends: - -1. **Write Profile Data** (0x26FC) - Write new profile configuration -2. **Status Response** (0x26FC) - Device acknowledges -3. **Save Profile** (0x26FD) - Commit to EEPROM -4. **Save Confirmation** (0x26FD) - Device confirms persistence - -**Profile Numbers:** -- Byte 5 of operation field: 0x00 = Profile 0, 0x01 = Profile 1, 0x02 = Profile 2 - ### Active Profile Indication The currently active profile is indicated in status responses: -- **Byte 6 of response:** Active profile number +- **Byte 1 of payload:** Active profile number - **LED color** changes to profile-specific color -## Implementation Guide +## Implementation Checklist -### Wireshark Filters for Analysis - -```wireshark -# All configuration commands (exclude heartbeat): -usb.device_address == 8 && usb.data_len == 64 && !(usb.setup.wValue == 0x122a) - -# Just write operations: -usb.device_address == 8 && usb.data_len == 64 && usb.setup.wValue == 0x26fc - -# Just save operations: -usb.device_address == 8 && usb.data_len == 64 && usb.setup.wValue == 0x26fd - -# All status polls (heartbeat): -usb.device_address == 8 && usb.setup.wValue == 0x122a -``` - -### USBPcap Capture Commands - -```bash -# Minimal capture (only config, no joystick data): -USBPcapCMD.exe -d \\.\USBPcap6 -o config.pcap -s 64 - -# Capture only control transfers: -USBPcapCMD.exe -d \\.\USBPcap6 -o config.pcap -F c -``` - -### Implementation Checklist - -- [ ] Implement 64-byte HID report parser -- [ ] Create command builder for 0x122a, 0x12C8, 0x26FC, 0x26FD -- [ ] Parse button mapping data (30 buttons) -- [ ] Parse joystick configuration (4 bytes) -- [ ] Implement profile read/write/save operations -- [ ] Add support for all key types (keyboard, mouse, gamepad, macro) +- [x] Implement 64-byte HID report parser +- [x] Create command builder for 0x122a, 0x26FB, 0x26EC, 0x26ED +- [x] Parse button mapping data (30 buttons x 3 actions) +- [x] Parse joystick configuration (mode, angle, deadzone, etc) +- [x] Implement profile read/write/save operations +- [x] Add support for all key types (keyboard, mouse, gamepad) - [ ] Handle modifier flags -- [ ] Create profile management functions -- [ ] Add analog stick mode switching +- [x] Create profile management functions +- [x] Add analog stick mode switching - [ ] Implement LED color control -## Protocol Examples - -### Example 1: Read Current Configuration - -``` -Host → Device: 000012c801010000000000000000000000... -Device → Host: 010012c8010101013f4f691b0700ff060606... -``` - -### Example 2: Write Profile 1 - -``` -Host → Device: 003a26fc020139040102000000fff40003f01a0000... -Device → Host: 000026fc010100013f4f691b0700ff060606... -``` - -### Example 3: Save Profile 1 - -``` -Host → Device: 003a26fd020201000000000000000000... -Device → Host: 000026fd010100013f4f691b0700ff060606... -``` - -### Example 4: Switch to Profile 2 - -``` -Host → Device: 003a26fc020239040102000000fff40003f01a0000... -Device → Host: 000026fc010200023f4f691b0700ff060606... -Host → Device: 003a26fd020202000000000000000000... -Device → Host: 000026fd010200023f4f691b0700ff060606... -``` - -## Development Notes - -### USB Control Transfer Format - -```c -// Send HID report to device -int azeron_send_report(struct azeron_device *device, - uint8_t *report, size_t length) -{ - return libusb_interrupt_transfer(device->handle, - 0x06, // Endpoint OUT - report, - length, - &transferred, - 1000); // Timeout -} - -// Receive HID report from device -int azeron_receive_report(struct azeron_device *device, - uint8_t *report, size_t length) -{ - return libusb_interrupt_transfer(device->handle, - 0x85, // Endpoint IN - report, - length, - &transferred, - 1000); // Timeout -} -``` - -### Common Gaming Device Protocol Patterns - -The Azeron protocol follows these standard patterns: - -1. **Request-Response:** Every command gets an acknowledgment -2. **Two-Phase Write:** Write data → Verify → Commit/Save -3. **Command IDs:** Unique 2-byte identifiers for each operation -4. **Status Polling:** Regular heartbeat to detect device presence -5. **Fixed Packet Size:** 64-byte reports for simplicity - -### Tools for Development - -- **USBPcap:** Windows USB capture -- **Wireshark:** Protocol analysis with custom filters -- **libusb:** Cross-platform USB communication -- **hidapi:** Alternative HID-specific library - -### Expected Challenges - -1. **Timing:** Device expects responses within ~100ms -2. **Checksums:** May need to implement data validation -3. **Atomic Operations:** Write + Save must be atomic -4. **Device State:** Must track active profile and settings -5. **Error Recovery:** Handle disconnects and reconnection - -## Current Status - -**Protocol Status:** ✅ Fully Reverse-Engineered -**Documentation Status:** ✅ Complete -**Implementation Status:** 🔄 Ready for Development -**Next Step:** Implement libazeron protocol functions - -## Contributing - -If you discover additional protocol details: - -1. Document the command format with examples -2. Provide USB capture files (PCAP format) -3. Include test code if available -4. Update this documentation -5. Submit pull request with changes - ## Safety Notes - Always test with backup configurations @@ -353,127 +182,6 @@ If you discover additional protocol details: - Stop if device behaves unexpectedly - Keep original configuration files as backup -## References - -- USB HID Specification: https://www.usb.org/hid -- libusb Documentation: https://libusb.info -- Azeron Cyborg Product Page: https://azeron.net -- Wireshark USB Analysis: https://wiki.wireshark.org/USB - -## Reverse Engineering Process - -### Phase 1: USB Traffic Capture - -**Tools Used:** -- USBPcap 1.5.4.0 -- Wireshark 4.0.0 -- Azeron Windows Software v1.0.0 - -**Capture Method:** -```bash -USBPcapCMD.exe -d \\.\USBPcap6 -o capture.pcap -s 64 -``` - -**Analysis Filters:** -```wireshark -usb.device_address == 8 && usb.transfer_type == 0x01 && usb.data_len == 64 -``` - -### Phase 2: Protocol Discovery - -**Key Findings:** -1. Configuration uses Interface 4 (not Interface 0 as initially assumed) -2. Protocol uses HID interrupt transfers, not vendor control transfers -3. Fixed 64-byte packet format -4. Request-response pattern for all commands -5. Two-phase write (write → save) for persistence - -**Command IDs Identified:** -- 0x122a: Get Status (heartbeat) -- 0x12C8: Read Configuration -- 0x26FC: Write Profile Data -- 0x26FD: Save Profile - -### Phase 3: Implementation Strategy - -**Next Steps:** -1. Implement HID report parser in libazeron -2. Create command builder functions -3. Add button mapping support (30 buttons) -4. Implement joystick configuration -5. Add profile management -6. Create comprehensive test suite - -**Estimated Timeline:** 2-3 weeks for full implementation - -### Phase 4: Testing and Validation - -**Test Plan:** -1. Read current configuration from device -2. Modify single button mapping -3. Write new configuration -4. Save to device -5. Verify persistence after power cycle -6. Test all 30 buttons -7. Test joystick modes -8. Test profile switching - -**Validation Criteria:** -- All button mappings work correctly -- Joystick behaves as configured -- Profiles persist after power loss -- No device crashes or errors -- Performance: <100ms response time - -### Tools for Reverse Engineering - -#### USB Capture Tools -- **USBPcap**: Windows USB capture -- **Wireshark**: Protocol analysis -- **usbmon**: Linux kernel USB monitoring -- **libusb debug**: Enable debug output - -#### Analysis Tools -- **Protocol analyzers**: Wireshark with USB dissectors -- **Hex editors**: For examining binary data -- **Custom scripts**: Python with pyusb for testing - -### Expected Challenges - -1. **Timing:** Device expects responses within ~100ms -2. **Checksums:** May need to implement data validation -3. **Atomic Operations:** Write + Save must be atomic -4. **Device State:** Must track active profile and settings -5. **Error Recovery:** Handle disconnects and reconnection - -### Next Steps - -1. **Implement Core Protocol:** Add HID report functions to libazeron -2. **Button Mapping UI:** Create user interface for configuring 30 buttons -3. **Joystick Configuration:** Add analog stick settings -4. **Profile Management:** Implement profile switching and persistence -5. **Testing:** Comprehensive test suite with all features -6. **Documentation:** Update user documentation - -### Contributing - -If you discover protocol details: - -1. Document the command format -2. Provide USB capture files (PCAP format) -3. Include test code if available -4. Update this documentation -5. Submit pull request with changes - -### Safety Notes - -- Always test with backup configurations -- Be prepared to reset device to factory defaults -- Don't send malformed packets to device -- Monitor device temperature during testing -- Stop if device behaves unexpectedly -- Keep original configuration files as backup - ## Acknowledgments -Special thanks to the Azeron community for providing captures and testing assistance during the reverse engineering process. \ No newline at end of file +Special thanks to the Azeron community for providing captures and testing assistance during the reverse engineering process. diff --git a/libazeron/azeron.c b/libazeron/azeron.c index a9b4fe1..140fc9a 100644 --- a/libazeron/azeron.c +++ b/libazeron/azeron.c @@ -407,6 +407,16 @@ int azeron_device_set_active_profile(struct azeron_device *device, uint8_t profi return azeron_protocol_set_active_profile(device, profile_id); } +/* Global settings */ +int azeron_device_set_global_timings(struct azeron_device *device, uint16_t long_press_delay, uint16_t double_click_delay) +{ + if (!device) { + return AZERON_ERROR_INVALID_PARAM; + } + + return azeron_protocol_set_global_timings(device, long_press_delay, double_click_delay); +} + /* Save profile to device EEPROM */ int azeron_device_save_profile(struct azeron_device *device, uint8_t profile_id) { diff --git a/libazeron/azeron.h b/libazeron/azeron.h index 7d9af2a..e5e8635 100644 --- a/libazeron/azeron.h +++ b/libazeron/azeron.h @@ -89,6 +89,7 @@ struct azeron_stick_config { bool invert_x; bool invert_y; uint8_t response_curve; /* 0=linear, 1=exponential, etc. */ + uint8_t angle; /* 0-360, but typically fits in 8 bits? wait. */ }; /* Profile configuration */ @@ -134,6 +135,9 @@ int azeron_device_set_stick_config(struct azeron_device *device, int azeron_device_get_active_profile(struct azeron_device *device, uint8_t *profile_id); int azeron_device_set_active_profile(struct azeron_device *device, uint8_t profile_id); +/* Global settings */ +int azeron_device_set_global_timings(struct azeron_device *device, uint16_t long_press_delay, uint16_t double_click_delay); + int azeron_device_get_profile(struct azeron_device *device, uint8_t profile_id, struct azeron_profile *profile); int azeron_device_set_profile(struct azeron_device *device, diff --git a/libazeron/internal.h b/libazeron/internal.h index ab9470b..3be341a 100644 --- a/libazeron/internal.h +++ b/libazeron/internal.h @@ -45,6 +45,7 @@ int azeron_protocol_get_stick_config(struct azeron_device *device, struct azeron int azeron_protocol_set_stick_config(struct azeron_device *device, const struct azeron_stick_config *config); int azeron_protocol_get_active_profile(struct azeron_device *device, uint8_t *profile_id); int azeron_protocol_set_active_profile(struct azeron_device *device, uint8_t profile_id); +int azeron_protocol_set_global_timings(struct azeron_device *device, uint16_t long_press_delay, uint16_t double_click_delay); int azeron_protocol_get_profile(struct azeron_device *device, uint8_t profile_id, struct azeron_profile *profile); int azeron_protocol_set_profile(struct azeron_device *device, const struct azeron_profile *profile); int azeron_protocol_save_to_device(struct azeron_device *device, uint8_t profile_id); diff --git a/libazeron/protocol.c b/libazeron/protocol.c index 0fa2648..9cc0e01 100644 --- a/libazeron/protocol.c +++ b/libazeron/protocol.c @@ -229,8 +229,8 @@ int azeron_protocol_write_config(struct azeron_device *device, const uint8_t *da int azeron_protocol_get_button_mapping(struct azeron_device *device, uint8_t button_id, struct azeron_button_mapping *mapping) { - uint8_t config[64]; - size_t size = sizeof(config); + uint8_t response[64]; + size_t response_len; int ret; if (!device || !mapping) { @@ -241,28 +241,25 @@ int azeron_protocol_get_button_mapping(struct azeron_device *device, uint8_t but return AZERON_ERROR_INVALID_PARAM; } - /* Calculate offset for this button's 4-byte mapping */ - uint32_t offset = AZERON_PROFILE_BASE_OFFSET + 8 + (button_id * AZERON_BUTTON_MAPPING_SIZE); - - ret = azeron_protocol_read_config(device, offset, config, &size); + /* Status command returns the full profile block in Cyborg */ + ret = send_config_command(device, AZERON_CMD_STATUS, AZERON_OP_READ_STATUS, + NULL, 0, response, &response_len); if (ret != AZERON_SUCCESS) { return ret; } /* - * Payload structure in response after 6-byte header: - * Bytes 0-3: Echoed offset? - * Bytes 4-7: The 4-byte mapping + * response[0] is status + * response[1..58] is profile data. + * Mappings start at profile index 8 (response[9]). + * Each button has 3 actions, 4 bytes each = 12 bytes per button. */ - mapping->button_id = button_id; + uint32_t profile_idx = 8 + (button_id * 3 * 4) + (mapping->action * 4); - /* - * Mapping format: - * Byte 0: Type (0xf0 = keyboard, 0xf1 = mouse, etc) - * Byte 1: Key code - */ - uint8_t type_byte = config[4]; - mapping->key_code = config[5]; + /* Mapping structure: [Type, Code, 0, 0] */ + uint8_t type_byte = response[profile_idx + 1]; + mapping->key_code = response[profile_idx + 2]; + mapping->button_id = button_id; switch (type_byte) { case 0xf0: mapping->type = AZERON_BTN_KEYBOARD; break; @@ -342,28 +339,33 @@ int azeron_protocol_set_button_mapping(struct azeron_device *device, int azeron_protocol_get_stick_config(struct azeron_device *device, struct azeron_stick_config *config) { - uint8_t data[64]; - size_t size = sizeof(data); + uint8_t response[64]; + size_t response_len; int ret; if (!device || !config) { return AZERON_ERROR_INVALID_PARAM; } - uint32_t offset = AZERON_PROFILE_BASE_OFFSET + AZERON_STICK_CONFIG_OFFSET; - - ret = azeron_protocol_read_config(device, offset, data, &size); + /* Status command returns the full profile block in Cyborg */ + ret = send_config_command(device, AZERON_CMD_STATUS, AZERON_OP_READ_STATUS, + NULL, 0, response, &response_len); if (ret != AZERON_SUCCESS) { return ret; } - /* Payload: [Offset(4)] [Deadzone(1)] [Curve(1)] [Sensitivity(1)] [Invert(1)] */ - config->deadzone = data[4]; - config->response_curve = data[5]; - config->sensitivity = data[6]; - config->invert_x = data[7] & 0x01; - config->invert_y = (data[7] >> 1) & 0x01; - config->mode = AZERON_STICK_ANALOG; + /* + * response[0] is status (usually 0x01) + * response[1..58] is the profile payload data. + * Index 3 in payload is response[4]. + */ + config->mode = (enum azeron_stick_mode)response[4]; + config->angle = response[9]; /* index 8 in payload */ + config->deadzone = response[11]; /* index 10 */ + config->response_curve = response[12]; /* index 11 */ + config->sensitivity = response[13]; /* index 12 */ + config->invert_x = response[14] & 0x01; /* index 13 */ + config->invert_y = (response[14] >> 1) & 0x01; return AZERON_SUCCESS; } @@ -375,7 +377,6 @@ int azeron_protocol_set_stick_config(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; int ret; @@ -390,6 +391,7 @@ int azeron_protocol_set_stick_config(struct azeron_device *device, return ret; } + /* Operation selects which profile slot to write to */ switch (active_profile) { case 0: operation = AZERON_OP_WRITE_PROFILE_0; break; case 1: operation = AZERON_OP_WRITE_PROFILE_1; break; @@ -397,22 +399,44 @@ int azeron_protocol_set_stick_config(struct azeron_device *device, default: return AZERON_ERROR_PROTOCOL; } - offset = AZERON_PROFILE_BASE_OFFSET + AZERON_STICK_CONFIG_OFFSET; - - data[0] = offset & 0xFF; - data[1] = (offset >> 8) & 0xFF; - data[2] = (offset >> 16) & 0xFF; - data[3] = (offset >> 24) & 0xFF; - - data[4] = config->deadzone; - data[5] = config->response_curve; - data[6] = config->sensitivity; - data[7] = 0; - if (config->invert_x) data[7] |= 0x01; - if (config->invert_y) data[7] |= 0x02; + /* Cyborg Bulk Write Format for 0x26EC: + * Index 0-1: Base offset (0x39 0x03) + * Index 3: Mode + * Index 8: Angle + * Index 10: Deadzone + */ + data[0] = AZERON_PROFILE_BASE_OFFSET & 0xFF; + data[1] = (AZERON_PROFILE_BASE_OFFSET >> 8) & 0xFF; + data[3] = (uint8_t)config->mode; + data[8] = config->angle; + data[10] = config->deadzone; ret = send_config_command(device, AZERON_CMD_WRITE_PROFILE, operation, - data, 8, response, &response_len); + data, 58, response, &response_len); + + return ret; +} + +/* Set global timings */ +int azeron_protocol_set_global_timings(struct azeron_device *device, uint16_t long_press_delay, uint16_t double_click_delay) +{ + uint8_t response[64]; + size_t response_len; + uint8_t data[58] = {0}; + int ret; + + if (!device) { + return AZERON_ERROR_INVALID_PARAM; + } + + /* Delay values are LITTLE ENDIAN at the very end of the 58-byte payload */ + data[54] = long_press_delay & 0xFF; + data[55] = (long_press_delay >> 8) & 0xFF; + data[56] = double_click_delay & 0xFF; + data[57] = (double_click_delay >> 8) & 0xFF; + + ret = send_config_command(device, AZERON_CMD_SET_GLOBAL, 0x0101, + data, 58, response, &response_len); return ret; }