517 lines
16 KiB
C
517 lines
16 KiB
C
/*
|
|
* Azeron Linux Configuration Library - Protocol Implementation
|
|
* Copyright (C) 2024 Azeron Linux Project
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include "azeron.h"
|
|
#include "internal.h"
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
|
|
/* 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;
|
|
}
|