/* * Azeron Linux Configuration Library - Protocol Implementation * Copyright (C) 2024 Azeron Linux Project * * SPDX-License-Identifier: MIT */ #include "azeron.h" #include "internal.h" #include #include #include /* Configuration Interface Constants */ #define AZERON_CONFIG_ENDPOINT_OUT 0x06 #define AZERON_CONFIG_ENDPOINT_IN 0x85 #define AZERON_CONFIG_PACKET_SIZE 64 /* Command IDs */ #define AZERON_CMD_STATUS 0x122a #define AZERON_CMD_READ_CONFIG 0x26FB #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 #define AZERON_OP_READ_CONFIG 0x0101 #define AZERON_OP_WRITE_PROFILE_0 0x0200 #define AZERON_OP_WRITE_PROFILE_1 0x0201 #define AZERON_OP_WRITE_PROFILE_2 0x0202 #define AZERON_OP_SAVE_PROFILE 0x0202 /* Offsets for profile data */ #define AZERON_PROFILE_BASE_OFFSET 0x0339 #define AZERON_BUTTON_MAPPING_SIZE 4 #define AZERON_STICK_CONFIG_OFFSET 58 /* Helper to dump hex data */ static void dump_hex(const char *label, const uint8_t *data, size_t len) { #ifdef AZERON_DEBUG fprintf(stderr, "[AZERON DEBUG] %s (%zu bytes):", label, len); for (size_t i = 0; i < len; i++) { fprintf(stderr, " %02x", data[i]); } fprintf(stderr, "\n"); #endif } /* Build a standard 64-byte configuration packet */ static int build_config_packet(uint8_t *packet, uint16_t command, uint16_t operation, const uint8_t *data, size_t data_len) { if (!packet || data_len > 58) { return AZERON_ERROR_INVALID_PARAM; } /* Clear packet */ memset(packet, 0, AZERON_CONFIG_PACKET_SIZE); /* Build header */ packet[0] = 0x00; /* Request packet */ packet[1] = 0x3a; /* Payload length (fixed 58 bytes for config) */ packet[2] = (command >> 8) & 0xFF; /* Command ID high byte */ packet[3] = command & 0xFF; /* Command ID low byte */ packet[4] = (operation >> 8) & 0xFF; /* Operation high byte */ packet[5] = operation & 0xFF; /* Operation low byte */ /* Copy data payload if provided */ if (data && data_len > 0) { memcpy(&packet[6], data, data_len); } return AZERON_SUCCESS; } /* Send a configuration command and wait for response */ static int send_config_command(struct azeron_device *device, uint16_t command, uint16_t operation, const uint8_t *request_data, size_t request_len, uint8_t *response, size_t *response_len) { uint8_t request_packet[AZERON_CONFIG_PACKET_SIZE]; uint8_t response_packet[AZERON_CONFIG_PACKET_SIZE]; int ret; if (!device || !response || !response_len) { return AZERON_ERROR_INVALID_PARAM; } /* Build request packet */ ret = build_config_packet(request_packet, command, operation, request_data, request_len); if (ret != AZERON_SUCCESS) { return ret; } dump_hex("Request", request_packet, AZERON_CONFIG_PACKET_SIZE); /* Send request */ ret = azeron_device_write(device, AZERON_CONFIG_ENDPOINT_OUT, request_packet, AZERON_CONFIG_PACKET_SIZE, AZERON_USB_TIMEOUT); if (ret < 0) { AZERON_ERROR("Failed to send command 0x%04x: %s", command, azeron_error_string(ret)); return ret; } /* Wait for device to process - configuration read might take longer */ usleep(50000); /* 50ms */ /* Read response */ ret = azeron_device_read(device, AZERON_CONFIG_ENDPOINT_IN, response_packet, AZERON_CONFIG_PACKET_SIZE, AZERON_USB_TIMEOUT); if (ret < 0) { AZERON_ERROR("Failed to read response for command 0x%04x: %s", command, azeron_error_string(ret)); return ret; } dump_hex("Response", response_packet, AZERON_CONFIG_PACKET_SIZE); /* Validate response */ if (ret != AZERON_CONFIG_PACKET_SIZE) { AZERON_ERROR("Invalid response size: %d (expected %d)", ret, AZERON_CONFIG_PACKET_SIZE); return AZERON_ERROR_PROTOCOL; } /* Check response type */ if (response_packet[0] != 0x01 && response_packet[0] != 0x00) { AZERON_ERROR("Invalid response type: 0x%02x", response_packet[0]); return AZERON_ERROR_PROTOCOL; } /* Check command ID matches */ uint16_t response_command = (response_packet[2] << 8) | response_packet[3]; if (response_command != command) { AZERON_ERROR("Command mismatch: sent 0x%04x, got 0x%04x", command, response_command); return AZERON_ERROR_PROTOCOL; } /* Check status - Byte 6 seems to be 0x01 for success in most responses */ if (response_packet[6] != 0x01 && response_packet[6] != 0x00) { AZERON_ERROR("Command failed with status: 0x%02x", response_packet[6]); return AZERON_ERROR_PROTOCOL; } /* Copy response data (skip 6-byte header) */ *response_len = ret - 6; if (*response_len > 0) { memcpy(response, &response_packet[6], *response_len); } AZERON_LOG("Command 0x%04x completed successfully", command); return AZERON_SUCCESS; } /* Protocol initialization */ int azeron_protocol_init(struct azeron_device *device) { uint8_t response[64]; size_t response_len; int ret; if (!device) { return AZERON_ERROR_INVALID_PARAM; } /* Send status command to verify communication */ ret = send_config_command(device, AZERON_CMD_STATUS, AZERON_OP_READ_STATUS, NULL, 0, response, &response_len); if (ret != AZERON_SUCCESS) { AZERON_ERROR("Failed to initialize protocol: %s", azeron_error_string(ret)); return ret; } AZERON_LOG("Protocol initialized successfully"); return AZERON_SUCCESS; } /* Read configuration from device at specific offset */ int azeron_protocol_read_config(struct azeron_device *device, uint32_t offset, uint8_t *data, size_t *size) { uint8_t response[64]; size_t response_len; uint8_t request_data[58] = {0}; int ret; if (!device || !data || !size) { return AZERON_ERROR_INVALID_PARAM; } /* Set offset in request (LITTLE ENDIAN) */ request_data[0] = offset & 0xFF; request_data[1] = (offset >> 8) & 0xFF; request_data[2] = (offset >> 16) & 0xFF; request_data[3] = (offset >> 24) & 0xFF; ret = send_config_command(device, AZERON_CMD_READ_CONFIG, AZERON_OP_READ_CONFIG, request_data, 4, response, &response_len); if (ret != AZERON_SUCCESS) { return ret; } if (response_len > *size) { response_len = *size; } memcpy(data, response, response_len); *size = response_len; return AZERON_SUCCESS; } /* Write configuration to device */ int azeron_protocol_write_config(struct azeron_device *device, const uint8_t *data, size_t size) { (void)device; (void)data; (void)size; AZERON_LOG("Write config - not yet implemented"); return AZERON_ERROR_UNSUPPORTED; } /* Get button mapping */ int azeron_protocol_get_button_mapping(struct azeron_device *device, uint8_t button_id, struct azeron_button_mapping *mapping) { uint8_t response[64]; size_t response_len; int ret; if (!device || !mapping) { return AZERON_ERROR_INVALID_PARAM; } if (button_id >= AZERON_MAX_BUTTONS) { return AZERON_ERROR_INVALID_PARAM; } /* 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; } /* * 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. */ uint32_t profile_idx = 8 + (button_id * 3 * 4) + (mapping->action * 4); /* 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; case 0xf1: mapping->type = AZERON_BTN_MOUSE; break; case 0xf2: mapping->type = AZERON_BTN_GAMEPAD; break; default: mapping->type = AZERON_BTN_KEYBOARD; break; } mapping->macro = NULL; mapping->layer_target = 0; return AZERON_SUCCESS; } /* Set button mapping */ int azeron_protocol_set_button_mapping(struct azeron_device *device, const struct azeron_button_mapping *mapping) { uint8_t response[64]; size_t response_len; uint8_t data[58] = {0}; uint16_t command; int ret; if (!device || !mapping) { return AZERON_ERROR_INVALID_PARAM; } /* 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; } /* 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[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[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[23] = mapping->key_code & 0xFF; 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; } /* Get stick configuration */ int azeron_protocol_get_stick_config(struct azeron_device *device, struct azeron_stick_config *config) { uint8_t response[64]; size_t response_len; int ret; if (!device || !config) { return AZERON_ERROR_INVALID_PARAM; } /* 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; } /* * 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; } /* Set stick configuration */ int azeron_protocol_set_stick_config(struct azeron_device *device, const struct azeron_stick_config *config) { uint8_t response[64]; size_t response_len; uint8_t data[58] = {0}; uint16_t operation; uint8_t active_profile; int ret; if (!device || !config) { return AZERON_ERROR_INVALID_PARAM; } /* Get active profile */ ret = azeron_protocol_get_active_profile(device, &active_profile); if (ret != AZERON_SUCCESS) { 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; case 2: operation = AZERON_OP_WRITE_PROFILE_2; break; default: return AZERON_ERROR_PROTOCOL; } /* 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, 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; } /* Get active profile */ int azeron_protocol_get_active_profile(struct azeron_device *device, uint8_t *profile_id) { uint8_t response[64]; size_t response_len; int ret; if (!device || !profile_id) { return AZERON_ERROR_INVALID_PARAM; } ret = send_config_command(device, AZERON_CMD_STATUS, AZERON_OP_READ_STATUS, NULL, 0, response, &response_len); if (ret != AZERON_SUCCESS) { return ret; } /* Byte 1 of payload data (skipping 6-byte header) is profile ID */ *profile_id = response[1]; AZERON_LOG("Active profile: %d", *profile_id); return AZERON_SUCCESS; } /* Set active profile */ int azeron_protocol_set_active_profile(struct azeron_device *device, uint8_t profile_id) { return azeron_protocol_save_to_device(device, profile_id); } /* Get profile */ int azeron_protocol_get_profile(struct azeron_device *device, uint8_t profile_id, struct azeron_profile *profile) { (void)device; (void)profile_id; (void)profile; return AZERON_ERROR_UNSUPPORTED; } /* Set profile */ int azeron_protocol_set_profile(struct azeron_device *device, const struct azeron_profile *profile) { (void)device; (void)profile; return AZERON_ERROR_UNSUPPORTED; } /* Save configuration to device */ int azeron_protocol_save_to_device(struct azeron_device *device, uint8_t profile_id) { uint8_t response[64]; size_t response_len; int ret; if (device == NULL) { return AZERON_ERROR_INVALID_PARAM; } if (profile_id > 2) { AZERON_ERROR("Invalid profile ID: %d (must be 0, 1 or 2)", profile_id); return AZERON_ERROR_INVALID_PARAM; } uint8_t save_data[58] = {0}; save_data[0] = profile_id; ret = send_config_command(device, AZERON_CMD_SAVE_PROFILE, AZERON_OP_SAVE_PROFILE, save_data, 1, response, &response_len); return ret; }