From 02675436229328a9688797cf1311a4b15d42f401 Mon Sep 17 00:00:00 2001 From: Aodhan Collins Date: Sun, 22 Feb 2026 12:51:32 +0000 Subject: [PATCH] Initial commit --- CMakeLists.txt | 30 +++ README.md | 81 ++++++ azeron-cli/CMakeLists.txt | 20 ++ azeron-cli/commands.c | 19 ++ azeron-cli/main.c | 376 ++++++++++++++++++++++++++ azeron-cli/utils.c | 17 ++ docs/development.md | 165 ++++++++++++ docs/installation.md | 196 ++++++++++++++ docs/protocol.md | 333 +++++++++++++++++++++++ libazeron/CMakeLists.txt | 38 +++ libazeron/azeron.c | 420 +++++++++++++++++++++++++++++ libazeron/azeron.h | 151 +++++++++++ libazeron/azeron.pc.in | 11 + libazeron/device.c | 218 +++++++++++++++ libazeron/internal.h | 61 +++++ libazeron/protocol.c | 108 ++++++++ libazeron/utils.c | 229 ++++++++++++++++ overview.txt | 1 + plan.md | 128 +++++++++ scripts/build.sh | 272 +++++++++++++++++++ scripts/udev-rules/99-azeron.rules | 14 + 21 files changed, 2888 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 azeron-cli/CMakeLists.txt create mode 100644 azeron-cli/commands.c create mode 100644 azeron-cli/main.c create mode 100644 azeron-cli/utils.c create mode 100644 docs/development.md create mode 100644 docs/installation.md create mode 100644 docs/protocol.md create mode 100644 libazeron/CMakeLists.txt create mode 100644 libazeron/azeron.c create mode 100644 libazeron/azeron.h create mode 100644 libazeron/azeron.pc.in create mode 100644 libazeron/device.c create mode 100644 libazeron/internal.h create mode 100644 libazeron/protocol.c create mode 100644 libazeron/utils.c create mode 100644 overview.txt create mode 100644 plan.md create mode 100644 scripts/build.sh create mode 100644 scripts/udev-rules/99-azeron.rules diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c04639b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.10) +project(azeron-linux VERSION 1.0.0 LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Find required packages +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBUSB REQUIRED libusb-1.0>=1.0.16) +pkg_check_modules(JSON REQUIRED json-c) + +# Include directories +include_directories(${LIBUSB_INCLUDE_DIRS}) +include_directories(${JSON_INCLUDE_DIRS}) + +# Library directory +add_subdirectory(libazeron) + +# CLI tool +add_subdirectory(azeron-cli) + +# Install udev rules +install(FILES scripts/udev-rules/99-azeron.rules + DESTINATION /etc/udev/rules.d + COMPONENT udev) + +# Install documentation +install(FILES README.md plan.md + DESTINATION share/doc/azeron-linux + COMPONENT doc) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad2ee5c --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Azeron Cyborg Linux Configuration Software + +Linux configuration software for the Azeron Cyborg gaming keypad (USB ID: 16d0:113c). + +## Overview + +The Azeron Cyborg keypad works on Linux as a standard HID device, but lacks configuration software for remapping buttons, adjusting analog stick settings, and managing profiles. This project provides both command-line and GUI tools to configure your Azeron device on Linux. + +## Features + +- **Button Remapping**: Assign keyboard keys, mouse buttons, and macros to any button +- **Analog Stick Configuration**: Adjust deadzones, sensitivity, and response curves +- **Profile Management**: Create, save, and switch between multiple configuration profiles +- **Cross-Platform**: Works on any Linux distribution +- **No Root Required**: Proper udev rules for user-level access + +## Project Structure + +- `libazeron/` - Core library for device communication +- `azeron-cli/` - Command-line configuration tool +- `azeron-gui/` - Graphical user interface (coming soon) +- `docs/` - Documentation and user guides +- `scripts/` - Helper scripts and udev rules + +## Quick Start + +### Prerequisites + +- Linux with libusb-1.0 installed +- For Fedora: `sudo dnf install libusb1-devel json-c-devel` +- For Ubuntu/Debian: `sudo apt-get install libusb-1.0-0-dev libjson-c-dev` + +### Building from Source + +```bash +git clone +cd azeron-linux +mkdir build && cd build +cmake .. +make +sudo make install +``` + +### Using the Command-Line Tool + +```bash +# List connected Azeron devices +sudo azeron-cli list + +# Show current button mappings +sudo azeron-cli show-mappings + +# Remap a button (e.g., button 5 to 'W' key) +sudo azeron-cli map-button 5 KEY_W + +# Save current configuration to a profile +sudo azeron-cli save-profile my_gaming_profile +``` + +### Setting up udev Rules + +To use the tools without sudo, install the udev rules: + +```bash +sudo cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +## Development + +See [docs/development.md](docs/development.md) for development setup and contribution guidelines. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- Azeron LTD for creating the Cyborg keypad +- The Linux gaming community for testing and feedback diff --git a/azeron-cli/CMakeLists.txt b/azeron-cli/CMakeLists.txt new file mode 100644 index 0000000..94ec2fe --- /dev/null +++ b/azeron-cli/CMakeLists.txt @@ -0,0 +1,20 @@ +# azeron-cli - Command-line tool for Azeron configuration + +set(AZERON_CLI_SOURCES + main.c + commands.c + utils.c +) + +add_executable(azeron-cli ${AZERON_CLI_SOURCES}) + +# Link libraries +target_link_libraries(azeron-cli azeron ${LIBUSB_LIBRARIES} ${JSON_LIBRARIES}) + +# Include directories +target_include_directories(azeron-cli PRIVATE ${CMAKE_SOURCE_DIR}/libazeron) + +# Install executable +install(TARGETS azeron-cli + RUNTIME DESTINATION bin +) diff --git a/azeron-cli/commands.c b/azeron-cli/commands.c new file mode 100644 index 0000000..5aecae8 --- /dev/null +++ b/azeron-cli/commands.c @@ -0,0 +1,19 @@ +/* + * Azeron CLI - Command implementations + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "azeron.h" + +/* Command implementations will go here as they are developed */ +/* For now, this file contains placeholder functions */ + +void commands_init(void) +{ + /* Initialize command system */ +} diff --git a/azeron-cli/main.c b/azeron-cli/main.c new file mode 100644 index 0000000..6ff4f80 --- /dev/null +++ b/azeron-cli/main.c @@ -0,0 +1,376 @@ +/* + * Azeron CLI - Command-line tool for Azeron device configuration + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include "azeron.h" + +#define AZERON_CLI_VERSION "1.0.0" + +/* Command function prototypes */ +int cmd_list(int argc, char *argv[]); +int cmd_info(int argc, char *argv[]); +int cmd_map_button(int argc, char *argv[]); +int cmd_show_mappings(int argc, char *argv[]); +int cmd_set_profile(int argc, char *argv[]); +int cmd_save_profile(int argc, char *argv[]); +int cmd_load_profile(int argc, char *argv[]); +int cmd_export_config(int argc, char *argv[]); +int cmd_import_config(int argc, char *argv[]); + +/* Command structure */ +struct command { + const char *name; + const char *description; + int (*func)(int argc, char *argv[]); +}; + +static struct command commands[] = { + {"list", "List connected Azeron devices", cmd_list}, + {"info", "Show device information", cmd_info}, + {"map-button", "Map a button to a key or action", cmd_map_button}, + {"show-mappings", "Show current button mappings", cmd_show_mappings}, + {"set-profile", "Set active profile", cmd_set_profile}, + {"save-profile", "Save current configuration to profile", cmd_save_profile}, + {"load-profile", "Load configuration from profile", cmd_load_profile}, + {"export-config", "Export configuration to file", cmd_export_config}, + {"import-config", "Import configuration from file", cmd_import_config}, + {NULL, NULL, NULL} +}; + +/* Print usage information */ +static void print_usage(const char *program_name) +{ + printf("Azeron CLI - Configuration tool for Azeron devices\n"); + printf("Version: %s\n\n", AZERON_CLI_VERSION); + printf("Usage: %s [options]\n\n", program_name); + printf("Commands:\n"); + + for (int i = 0; commands[i].name != NULL; i++) { + printf(" %-15s %s\n", commands[i].name, commands[i].description); + } + + printf("\nExamples:\n"); + printf(" %s list\n", program_name); + printf(" %s info\n", program_name); + printf(" %s map-button 5 KEY_W\n", program_name); + printf(" %s set-profile 1\n", program_name); + printf("\nFor help with a specific command:\n"); + printf(" %s --help\n", program_name); +} + +/* List connected devices */ +int cmd_list(int argc, char *argv[]) +{ + struct azeron_device_info *devices; + size_t count; + int ret; + int i; + + (void)argc; + (void)argv; + + ret = azeron_init(); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to initialize library: %s\n", azeron_error_string(ret)); + return 1; + } + + ret = azeron_device_list(&devices, &count); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to list devices: %s\n", azeron_error_string(ret)); + azeron_exit(); + return 1; + } + + if (count == 0) { + printf("No Azeron devices found.\n"); + printf("Make sure your device is connected and you have proper permissions.\n"); + } else { + printf("Found %zu Azeron device(s):\n\n", count); + + for (i = 0; i < count; i++) { + printf("Device %d:\n", i); + printf(" Product: %s\n", devices[i].product); + printf(" Manufacturer: %s\n", devices[i].manufacturer); + printf(" Serial: %s\n", devices[i].serial_number); + printf(" USB ID: %04x:%04x\n", devices[i].vendor_id, devices[i].product_id); + printf(" Firmware: %d.%d\n", + devices[i].firmware_version >> 8, + devices[i].firmware_version & 0xFF); + printf("\n"); + } + } + + azeron_device_list_free(devices, count); + azeron_exit(); + + return 0; +} + +/* Show device information */ +int cmd_info(int argc, char *argv[]) +{ + struct azeron_device *device; + struct azeron_device_info info; + int ret; + int device_index = 0; + + /* Parse options */ + static struct option long_options[] = { + {"device", required_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "d:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'd': + device_index = atoi(optarg); + break; + case 'h': + printf("Usage: %s info [options]\n", argv[0]); + printf("Options:\n"); + printf(" -d, --device Select device by index (default: 0)\n"); + printf(" -h, --help Show this help message\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; + } + + ret = azeron_device_get_info(device, &info); + if (ret != AZERON_SUCCESS) { + fprintf(stderr, "Failed to get device info: %s\n", azeron_error_string(ret)); + azeron_device_close(device); + azeron_exit(); + return 1; + } + + printf("Device Information:\n"); + printf("===================\n\n"); + printf("Product: %s\n", info.product); + printf("Manufacturer: %s\n", info.manufacturer); + printf("Serial Number: %s\n", info.serial_number); + printf("USB ID: %04x:%04x\n", info.vendor_id, info.product_id); + printf("Firmware: %d.%d\n", info.firmware_version >> 8, info.firmware_version & 0xFF); + printf("Profiles: %d\n", info.num_profiles); + printf("Active Profile: %d\n", info.active_profile); + + azeron_device_close(device); + azeron_exit(); + + return 0; +} + +/* Show current button mappings */ +int cmd_show_mappings(int argc, char *argv[]) +{ + struct azeron_device *device; + int ret; + int device_index = 0; + int i; + + /* Parse options */ + static struct option long_options[] = { + {"device", required_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "d:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'd': + device_index = atoi(optarg); + break; + case 'h': + printf("Usage: %s show-mappings [options]\n", argv[0]); + printf("Options:\n"); + printf(" -d, --device Select device by index (default: 0)\n"); + printf(" -h, --help Show this help message\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("Button Mappings:\n"); + printf("================\n\n"); + printf("%-10s %-15s %s\n", "Button", "Type", "Mapping"); + printf("%-10s %-15s %s\n", "------", "----", "-------"); + + /* For now, show placeholder since protocol isn't implemented yet */ + for (i = 1; i <= 24; i++) { + printf("%-10d %-15s %s\n", i, "keyboard", ""); + } + + printf("\nNote: Button mapping functionality requires protocol reverse engineering.\n"); + printf("This is a placeholder implementation.\n"); + + azeron_device_close(device); + azeron_exit(); + + return 0; +} + +/* Map button (placeholder) */ +int cmd_map_button(int argc, char *argv[]) +{ + if (argc < 3) { + fprintf(stderr, "Usage: %s map-button \n", argv[0]); + fprintf(stderr, "Example: %s map-button 5 KEY_W\n", argv[0]); + return 1; + } + + fprintf(stderr, "Button mapping is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Set active profile (placeholder) */ +int cmd_set_profile(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s set-profile \n", argv[0]); + fprintf(stderr, "Example: %s set-profile 1\n", argv[0]); + return 1; + } + + fprintf(stderr, "Profile switching is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Save profile (placeholder) */ +int cmd_save_profile(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s save-profile \n", argv[0]); + return 1; + } + + fprintf(stderr, "Profile saving is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Load profile (placeholder) */ +int cmd_load_profile(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s load-profile \n", argv[0]); + return 1; + } + + fprintf(stderr, "Profile loading is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Export config (placeholder) */ +int cmd_export_config(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s export-config \n", argv[0]); + return 1; + } + + fprintf(stderr, "Config export is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Import config (placeholder) */ +int cmd_import_config(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s import-config \n", argv[0]); + return 1; + } + + fprintf(stderr, "Config import is not yet implemented.\n"); + fprintf(stderr, "Protocol reverse engineering is required for this feature.\n"); + + return 1; +} + +/* Main function */ +int main(int argc, char *argv[]) +{ + const char *command; + int i; + + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + command = argv[1]; + + /* Help command */ + if (strcmp(command, "help") == 0 || strcmp(command, "--help") == 0 || strcmp(command, "-h") == 0) { + print_usage(argv[0]); + return 0; + } + + /* Version command */ + if (strcmp(command, "version") == 0 || strcmp(command, "--version") == 0 || strcmp(command, "-v") == 0) { + printf("azeron-cli version %s\n", AZERON_CLI_VERSION); + return 0; + } + + /* Find and execute command */ + for (i = 0; commands[i].name != NULL; i++) { + if (strcmp(command, commands[i].name) == 0) { + return commands[i].func(argc - 1, argv + 1); + } + } + + fprintf(stderr, "Unknown command: %s\n", command); + fprintf(stderr, "Use '%s help' for usage information.\n", argv[0]); + + return 1; +} diff --git a/azeron-cli/utils.c b/azeron-cli/utils.c new file mode 100644 index 0000000..3efc457 --- /dev/null +++ b/azeron-cli/utils.c @@ -0,0 +1,17 @@ +/* + * Azeron CLI - Utility functions + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +/* CLI utility functions will be implemented here */ + +void cli_utils_init(void) +{ + /* Initialize CLI utilities */ +} diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..21e0c36 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,165 @@ +# Development Guide + +## Project Architecture + +This project uses a userspace approach to configure the Azeron Cyborg keypad. Since Linux already recognizes the device as a HID device, we don't need a kernel driver. Instead, we communicate directly with the device using libusb to send configuration commands. + +## Directory Structure + +- `libazeron/` - Core C library for device communication +- `azeron-cli/` - Command-line interface tool +- `azeron-gui/` - Python GUI application (planned) +- `docs/` - Documentation +- `scripts/` - Helper scripts and udev rules + +## Building from Source + +### Prerequisites + +- CMake 3.10 or higher +- C compiler (GCC or Clang) +- libusb-1.0 development files +- json-c development files + +#### Fedora/RHEL +```bash +sudo dnf install cmake gcc libusb1-devel json-c-devel +``` + +#### Ubuntu/Debian +```bash +sudo apt-get install cmake build-essential libusb-1.0-0-dev libjson-c-dev +``` + +### Build Instructions + +```bash +mkdir build +cd build +cmake .. +make +sudo make install +``` + +### Build Options + +```bash +# Debug build +cmake -DCMAKE_BUILD_TYPE=Debug .. + +# Specify installation prefix +cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. + +# Build without udev rules +cmake -DINSTALL_UDEV_RULES=OFF .. +``` + +## Development Workflow + +### 1. Core Library Development + +The `libazeron` library is the foundation. It handles: +- USB device detection and connection +- Configuration protocol implementation +- Error handling and logging + +Key files: +- `libazeron/azeron.h` - Public API +- `libazeron/azeron.c` - Implementation +- `libazeron/protocol.md` - Protocol documentation + +### 2. Testing Protocol + +Since we may not have Windows software to reverse engineer from, we'll use a systematic approach: + +1. **Device Enumeration**: List all USB descriptors and endpoints +2. **Control Transfers**: Test standard USB control transfers +3. **Interrupt Transfers**: Monitor interrupt endpoints for data +4. **Configuration Commands**: Try to discover configuration commands +5. **Response Analysis**: Parse device responses + +Useful tools: +- `lsusb -v -d 16d0:113c` - Device information +- `usbhid-dump` - HID report descriptors +- `evtest` - Input event testing +- Wireshark with USBPcap - Protocol analysis (if Windows available) + +### 3. Adding New Features + +1. Implement feature in `libazeron` first +2. Add CLI support in `azeron-cli` +3. Add GUI support in `azeron-gui` (if applicable) +4. Update documentation +5. Add tests + +## Code Style + +- C code follows Linux kernel style (indent with tabs) +- Functions should be documented with Doxygen comments +- Error handling: check all return values +- Memory management: free all allocated memory +- Use const where appropriate + +## Debugging + +### Enable Debug Output + +```bash +# Set debug environment variable +export AZERON_DEBUG=1 +azeron-cli list +``` + +### USB Debugging + +```bash +# Monitor USB traffic (requires root) +sudo usbmon -f -t > usb_trace.txt + +# Use with azeron-cli to capture configuration commands +``` + +### Common Issues + +1. **Permission Denied**: Install udev rules or run with sudo +2. **Device Not Found**: Check USB connection and device permissions +3. **Configuration Not Applying**: Ensure device is in configuration mode + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## Testing + +### Unit Tests + +```bash +cd build +make test +``` + +### Manual Testing + +1. Connect Azeron device +2. Run `azeron-cli list` to verify detection +3. Test button mapping: `azeron-cli map-button 5 KEY_W` +4. Verify mapping works in game/application +5. Test profile save/load functionality + +## Release Process + +1. Update version in CMakeLists.txt +2. Update CHANGELOG.md +3. Create git tag: `git tag -a v1.0.0 -m "Release version 1.0.0"` +4. Push tag: `git push origin v1.0.0` +5. Create release packages + +## Resources + +- [libusb API Documentation](https://libusb.info/) +- [Linux Input Subsystem](https://www.kernel.org/doc/html/latest/input/) +- [USB HID Specification](https://www.usb.org/hid) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..1dec3fa --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,196 @@ +# Installation Guide + +## Quick Install + +### Fedora/RHEL/CentOS + +```bash +# Install dependencies +sudo dnf install libusb1-devel json-c-devel + +# Build from source +git clone +cd azeron-linux +mkdir build && cd build +cmake .. +make +sudo make install + +# Install udev rules for non-root access +sudo cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +### Ubuntu/Debian + +```bash +# Install dependencies +sudo apt-get update +sudo apt-get install build-essential cmake libusb-1.0-0-dev libjson-c-dev + +# Build from source +git clone +cd azeron-linux +mkdir build && cd build +cmake .. +make +sudo make install + +# Install udev rules for non-root access +sudo cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +### Arch Linux + +```bash +# Install dependencies +sudo pacman -S base-devel cmake libusb json-c + +# Build from source (same as above) +``` + +## Dependencies + +### Required +- **libusb-1.0** (>= 1.0.16) - USB device communication +- **json-c** - Configuration file format support +- **CMake** (>= 3.10) - Build system +- **C compiler** - GCC or Clang + +### Optional (for GUI) +- **Python 3** - For GUI application +- **PyQt5 or PyGTK** - GUI toolkit + +## Building from Source + +### Step 1: Install Dependencies + +Choose your distribution from the sections above and install the required packages. + +### Step 2: Clone Repository + +```bash +git clone +cd azeron-linux +``` + +### Step 3: Build + +```bash +mkdir build +cd build +cmake .. +make -j$(nproc) +``` + +### Step 4: Install + +```bash +sudo make install +``` + +This installs: +- `libazeron.so` - Core library (to /usr/local/lib) +- `azeron-cli` - Command-line tool (to /usr/local/bin) +- Header files (to /usr/local/include) +- udev rules (to /etc/udev/rules.d) +- Documentation (to /usr/local/share/doc) + +### Step 5: Configure Permissions + +For non-root access to the USB device: + +```bash +sudo cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +Then unplug and reconnect your Azeron device. + +## Verification + +Verify installation: + +```bash +# Check if azeron-cli is installed +which azeron-cli + +# List connected devices +azeron-cli list + +# Should show something like: +# Device 0: Azeron Cyborg Keypad (16d0:113c) +``` + +## Troubleshooting + +### "command not found: azeron-cli" + +The install directory may not be in your PATH. Add it: + +```bash +echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bashrc +source ~/.bashrc +``` + +Or use the full path: `/usr/local/bin/azeron-cli` + +### "Permission denied" when accessing device + +1. Ensure udev rules are installed correctly +2. Check rule file permissions: `ls -l /etc/udev/rules.d/99-azeron.rules` +3. Reconnect the device after installing rules +4. As a temporary workaround, use sudo: `sudo azeron-cli list` + +### "libazeron.so: cannot open shared object file" + +The library path may not be configured. Add it: + +```bash +echo '/usr/local/lib' | sudo tee /etc/ld.so.conf.d/azeron.conf +sudo ldconfig +``` + +### Device not detected + +1. Check USB connection: `lsusb | grep 16d0` +2. Verify device appears: `lsusb -v -d 16d0:113c` +3. Check kernel messages: `dmesg | tail -20` +4. Ensure no other program is using the device + +### Build errors + +1. Ensure all dependencies are installed +2. Check CMake version: `cmake --version` (needs >= 3.10) +3. Check compiler version: `gcc --version` +4. Look for missing development packages + +## Uninstallation + +```bash +cd build +sudo make uninstall +``` + +To also remove udev rules: + +```bash +sudo rm /etc/udev/rules.d/99-azeron.rules +sudo udevadm control --reload-rules +``` + +## Package Managers + +### Future Plans + +We plan to provide packages for: +- Fedora COPR repository +- Ubuntu PPA +- Arch Linux AUR +- openSUSE OBS + +Check back soon or help us create packages! diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..70f6abc --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,333 @@ +# Azeron Configuration Protocol Documentation + +## Overview + +This document describes the approach to reverse engineering the USB configuration protocol for the Azeron Cyborg keypad (USB ID: 16d0:113c). The device uses a vendor-specific USB interface for configuration, which needs to be understood to implement full configuration support. + +## USB Device Analysis + +### Device Descriptor Summary +- **Vendor ID**: 0x16d0 (MCS) +- **Product ID**: 0x113c (Azeron Keypad) +- **Configuration**: 1 configuration, 5 interfaces +- **Power**: 500mA (bus-powered) + +### Interface Breakdown + +1. **Interface 0**: Vendor-specific (0xFF) + - Endpoints: 0x81 (IN), 0x01 (OUT) + - **Purpose**: Likely main configuration interface + - **Packet size**: 32 bytes + +2. **Interface 1**: HID + - Endpoint: 0x82 (IN) + - **Purpose**: Main button input + - **Packet size**: 16 bytes + +3. **Interface 2**: HID Boot Mouse + - Endpoint: 0x83 (IN) + - **Purpose**: Mouse emulation + - **Packet size**: 7 bytes + +4. **Interface 3**: HID + - Endpoint: 0x84 (IN) + - **Purpose**: Analog stick input + - **Packet size**: 16 bytes + +5. **Interface 4**: HID with IN/OUT + - Endpoints: 0x85 (IN), 0x06 (OUT) + - **Purpose**: LED/control interface + - **Packet size**: 64 bytes + +## Protocol Reverse Engineering Approach + +### Phase 1: USB Traffic Capture + +#### Option A: Windows Software Capture (Recommended) + +If you have access to Windows and the Azeron configuration software: + +1. **Setup**: + ```bash + # Install USBPcap on Windows + # Install Wireshark + ``` + +2. **Capture Process**: + - Start USBPcap capture on the Azeron device + - Open Azeron Windows software + - Perform configuration changes: + - Map a button to different key + - Change analog stick settings + - Switch profiles + - Save configuration + - Stop capture and save the data + +3. **Analysis**: + - Look for control transfers to Interface 0 + - Identify command patterns + - Map request types and data formats + +#### Option B: Linux Exploration + +Without Windows software, we can try to discover the protocol: + +1. **Basic Communication Test**: + ```bash + # Use the azeron-cli tool to attempt communication + ./build/azeron-cli list + + # Try to read from configuration endpoint + # (This will require implementing test functions) + ``` + +2. **USB Control Transfer Testing**: + - Test standard USB requests + - Try vendor-specific requests + - Monitor device responses + +### Phase 2: Protocol Discovery + +#### Common USB Configuration Patterns + +Most gaming devices use similar patterns: + +1. **Configuration Read**: + ``` + Request Type: 0xC0 (Vendor IN) + Request: 0x01-0xFF (varies by device) + Value: 0x0000 + Index: Interface number (0) + Data: Response buffer + ``` + +2. **Configuration Write**: + ``` + Request Type: 0x40 (Vendor OUT) + Request: 0x01-0xFF (varies by device) + Value: 0x0000 + Index: Interface number (0) + Data: Command/data buffer + ``` + +#### Expected Command Structure + +Based on similar devices, the protocol likely includes: + +1. **Read Current Configuration**: + - Command to read all button mappings + - Command to read analog stick settings + - Command to read profile information + +2. **Write Configuration**: + - Command to set button mapping + - Command to set analog stick parameters + - Command to save configuration to device + +3. **Profile Management**: + - Command to switch active profile + - Command to read/write profile data + +### Phase 3: Implementation Strategy + +#### Step 1: Basic Communication + +Add test functions to the library: + +```c +// In libazeron/protocol.c +int azeron_protocol_test_read(struct azeron_device *device) +{ + uint8_t buffer[64]; + int ret; + + // Try various vendor requests + for (int req = 0x01; req <= 0xFF; req++) { + ret = azeron_device_control_transfer(device, + 0xC0, // Vendor IN + req, // Request + 0x0000, // Value + 0x0000, // Index (Interface 0) + buffer, sizeof(buffer), + 1000); + if (ret > 0) { + printf("Request 0x%02x: %d bytes\n", req, ret); + // Print buffer contents + } + } + + return AZERON_SUCCESS; +} +``` + +#### Step 2: Button Mapping Discovery + +The button mapping likely uses: +- Button ID (1-32) +- Key type (keyboard, mouse, gamepad, macro) +- Key code or action +- Modifiers (shift, ctrl, alt) + +Expected data structure: +```c +struct button_mapping { + uint8_t button_id; + uint8_t key_type; + uint16_t key_code; + uint8_t modifiers; +}; +``` + +#### Step 3: Analog Stick Configuration + +Analog stick settings likely include: +- Dead zone (0-100%) +- Sensitivity curve (linear, exponential) +- X/Y inversion flags +- Mode (analog, 4-way digital, 8-way digital, mouse) + +#### Step 4: Profile Management + +Profile commands likely: +- Read profile (0-2) +- Write profile +- Set active profile +- Save to device EEPROM + +### Phase 4: Testing and Validation + +#### Test Plan + +1. **Basic Detection**: + - Verify device is detected + - Check all interfaces are accessible + +2. **Configuration Read**: + - Read current button mappings + - Verify against known configuration + +3. **Configuration Write**: + - Change single button mapping + - Verify change persists + - Test in-game/application + +4. **Profile Management**: + - Create multiple profiles + - Switch between profiles + - Verify profile persistence + +### Development Notes + +#### USB Control Transfer Format + +```c +// Vendor request to interface 0 +int azeron_protocol_send_command(struct azeron_device *device, + uint8_t request, + uint16_t value, + uint16_t index, + uint8_t *data, + size_t size, + int timeout) +{ + return libusb_control_transfer(device->handle, + 0x40, // Vendor OUT + request, + value, + index, + data, + size, + timeout); +} + +int azeron_protocol_receive_response(struct azeron_device *device, + uint8_t request, + uint16_t value, + uint16_t index, + uint8_t *data, + size_t size, + int timeout) +{ + return libusb_control_transfer(device->handle, + 0xC0, // Vendor IN + request, + value, + index, + data, + size, + timeout); +} +``` + +#### Common Gaming Device Protocol Patterns + +1. **Init/Handshake**: + - Send init command + - Receive device info/acknowledgment + +2. **Read Configuration**: + - Send read command with offset/address + - Receive configuration data + - May require multiple transfers for full config + +3. **Write Configuration**: + - Send write command with data + - Receive acknowledgment + - Send save command to persist + +4. **Profile Operations**: + - Select profile (0-2) + - Read/write profile data + - Set as active profile + +### 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. **Encryption/Obfuscation**: Configuration may be encrypted +2. **Checksums**: Data may include CRC/checksums +3. **Command Sequences**: May require specific command sequences +4. **Timing Requirements**: Some devices have strict timing +5. **Device Protection**: May have write protection mechanisms + +### Next Steps + +1. **Capture USB Traffic**: Get Windows software captures +2. **Analyze Patterns**: Identify command structure +3. **Implement Protocol**: Add functions to libazeron +4. **Test Incrementally**: Start with simple commands +5. **Document Findings**: Update this document with actual protocol + +### Contributing + +If you discover protocol details: + +1. Document the command format +2. Provide example USB captures +3. Include test code if available +4. Update this documentation + +### Safety Notes + +- Always test with backup configurations +- Be prepared to reset device to factory defaults +- Don't write untested commands to device +- Monitor device temperature during testing +- Stop if device behaves unexpectedly + +## Current Status + +**Protocol Status**: Not yet reverse engineered +**Implementation Status**: Placeholder functions only +**Next Step**: USB traffic capture and analysis diff --git a/libazeron/CMakeLists.txt b/libazeron/CMakeLists.txt new file mode 100644 index 0000000..d5e6b65 --- /dev/null +++ b/libazeron/CMakeLists.txt @@ -0,0 +1,38 @@ +# libazeron - Core Azeron device library + +set(LIBAZERON_SOURCES + azeron.c + protocol.c + device.c + utils.c +) + +set(LIBAZERON_HEADERS + azeron.h + internal.h +) + +# Create shared library +add_library(azeron SHARED ${LIBAZERON_SOURCES}) + +# Link libraries +target_link_libraries(azeron ${LIBUSB_LIBRARIES} ${JSON_LIBRARIES}) + +# Set properties +set_target_properties(azeron PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1 + PUBLIC_HEADER "${LIBAZERON_HEADERS}" +) + +# Install library +install(TARGETS azeron + LIBRARY DESTINATION lib + PUBLIC_HEADER DESTINATION include/azeron +) + +# Install pkg-config file +configure_file(azeron.pc.in azeron.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/azeron.pc + DESTINATION lib/pkgconfig +) diff --git a/libazeron/azeron.c b/libazeron/azeron.c new file mode 100644 index 0000000..ea076a8 --- /dev/null +++ b/libazeron/azeron.c @@ -0,0 +1,420 @@ +/* + * Azeron Linux Configuration Library + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include "azeron.h" +#include "internal.h" +#include +#include +#include +#include +#include + +static bool g_initialized = false; +static libusb_context *g_context = NULL; + +/* Initialize the library */ +int azeron_init(void) +{ + int ret; + + if (g_initialized) { + return AZERON_SUCCESS; + } + + ret = libusb_init(&g_context); + if (ret < 0) { + AZERON_ERROR("Failed to initialize libusb: %s", azeron_usb_error_string(ret)); + return AZERON_ERROR_INIT; + } + +#ifdef AZERON_DEBUG + libusb_set_debug(g_context, LIBUSB_LOG_LEVEL_INFO); +#endif + + g_initialized = true; + AZERON_LOG("Library initialized"); + + return AZERON_SUCCESS; +} + +/* Cleanup the library */ +void azeron_exit(void) +{ + if (!g_initialized) { + return; + } + + if (g_context) { + libusb_exit(g_context); + g_context = NULL; + } + + g_initialized = false; + AZERON_LOG("Library exited"); +} + +/* Convert error code to string */ +const char *azeron_error_string(int error) +{ + switch (error) { + case AZERON_SUCCESS: + return "Success"; + case AZERON_ERROR_INIT: + return "Initialization error"; + case AZERON_ERROR_NOT_FOUND: + return "Device not found"; + case AZERON_ERROR_ACCESS: + return "Access denied"; + case AZERON_ERROR_IO: + return "I/O error"; + case AZERON_ERROR_PROTOCOL: + return "Protocol error"; + case AZERON_ERROR_INVALID_PARAM: + return "Invalid parameter"; + case AZERON_ERROR_NO_MEM: + return "Out of memory"; + case AZERON_ERROR_UNSUPPORTED: + return "Unsupported operation"; + default: + return "Unknown error"; + } +} + +/* Convert libusb error to azeron error */ +int azeron_libusb_to_azeron_error(int libusb_error) +{ + switch (libusb_error) { + case LIBUSB_SUCCESS: + return AZERON_SUCCESS; + case LIBUSB_ERROR_IO: + return AZERON_ERROR_IO; + case LIBUSB_ERROR_INVALID_PARAM: + return AZERON_ERROR_INVALID_PARAM; + case LIBUSB_ERROR_ACCESS: + return AZERON_ERROR_ACCESS; + case LIBUSB_ERROR_NO_DEVICE: + return AZERON_ERROR_NOT_FOUND; + case LIBUSB_ERROR_NOT_FOUND: + return AZERON_ERROR_NOT_FOUND; + case LIBUSB_ERROR_BUSY: + return AZERON_ERROR_ACCESS; + case LIBUSB_ERROR_TIMEOUT: + return AZERON_ERROR_IO; + case LIBUSB_ERROR_OVERFLOW: + return AZERON_ERROR_IO; + case LIBUSB_ERROR_PIPE: + return AZERON_ERROR_PROTOCOL; + case LIBUSB_ERROR_INTERRUPTED: + return AZERON_ERROR_IO; + case LIBUSB_ERROR_NO_MEM: + return AZERON_ERROR_NO_MEM; + case LIBUSB_ERROR_NOT_SUPPORTED: + return AZERON_ERROR_UNSUPPORTED; + default: + return AZERON_ERROR_IO; + } +} + +/* Get USB error string */ +const char *azeron_usb_error_string(int error) +{ + return libusb_error_name(error); +} + +/* Extract device info from libusb device */ +void azeron_device_info_from_libusb(struct azeron_device *device, libusb_device *libusb_dev) +{ + struct libusb_device_descriptor desc; + int ret; + + ret = libusb_get_device_descriptor(libusb_dev, &desc); + if (ret < 0) { + AZERON_ERROR("Failed to get device descriptor: %s", azeron_usb_error_string(ret)); + return; + } + + device->info.vendor_id = desc.idVendor; + device->info.product_id = desc.idProduct; + device->info.firmware_version = desc.bcdDevice; + + /* Get string descriptors if possible */ + if (device->handle) { + if (desc.iSerialNumber > 0) { + libusb_get_string_descriptor_ascii(device->handle, desc.iSerialNumber, + (unsigned char *)device->info.serial_number, + sizeof(device->info.serial_number)); + } + if (desc.iManufacturer > 0) { + libusb_get_string_descriptor_ascii(device->handle, desc.iManufacturer, + (unsigned char *)device->info.manufacturer, + sizeof(device->info.manufacturer)); + } + if (desc.iProduct > 0) { + libusb_get_string_descriptor_ascii(device->handle, desc.iProduct, + (unsigned char *)device->info.product, + sizeof(device->info.product)); + } + } +} + +/* List all Azeron devices */ +int azeron_device_list(struct azeron_device_info **devices, size_t *count) +{ + libusb_device **dev_list; + ssize_t num_devs; + size_t azeron_count = 0; + struct azeron_device_info *azeron_devices = NULL; + int i; + + if (!devices || !count) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!g_initialized) { + return AZERON_ERROR_INIT; + } + + num_devs = libusb_get_device_list(g_context, &dev_list); + if (num_devs < 0) { + AZERON_ERROR("Failed to get device list: %s", azeron_usb_error_string(num_devs)); + return azeron_libusb_to_azeron_error(num_devs); + } + + /* First pass: count Azeron devices */ + for (i = 0; i < num_devs; i++) { + struct libusb_device_descriptor desc; + int ret = libusb_get_device_descriptor(dev_list[i], &desc); + if (ret >= 0 && desc.idVendor == AZERON_VENDOR_ID && desc.idProduct == AZERON_PRODUCT_ID) { + azeron_count++; + } + } + + if (azeron_count == 0) { + libusb_free_device_list(dev_list, 1); + *devices = NULL; + *count = 0; + return AZERON_SUCCESS; + } + + /* Allocate memory for device info */ + azeron_devices = calloc(azeron_count, sizeof(struct azeron_device_info)); + if (!azeron_devices) { + libusb_free_device_list(dev_list, 1); + return AZERON_ERROR_NO_MEM; + } + + /* Second pass: fill device info */ + azeron_count = 0; + for (i = 0; i < num_devs; i++) { + struct libusb_device_descriptor desc; + int ret = libusb_get_device_descriptor(dev_list[i], &desc); + if (ret >= 0 && desc.idVendor == AZERON_VENDOR_ID && desc.idProduct == AZERON_PRODUCT_ID) { + struct azeron_device tmp_device = {0}; + int open_ret; + + tmp_device.context = g_context; + open_ret = libusb_open(dev_list[i], &tmp_device.handle); + if (open_ret >= 0) { + azeron_device_info_from_libusb(&tmp_device, dev_list[i]); + libusb_close(tmp_device.handle); + } else { + /* Still fill basic info even if we can't open */ + tmp_device.info.vendor_id = desc.idVendor; + tmp_device.info.product_id = desc.idProduct; + tmp_device.info.firmware_version = desc.bcdDevice; + snprintf(tmp_device.info.product, sizeof(tmp_device.info.product), "Azeron Keypad"); + } + + memcpy(&azeron_devices[azeron_count], &tmp_device.info, sizeof(struct azeron_device_info)); + azeron_count++; + } + } + + libusb_free_device_list(dev_list, 1); + + *devices = azeron_devices; + *count = azeron_count; + + AZERON_LOG("Found %zu Azeron device(s)", azeron_count); + + return AZERON_SUCCESS; +} + +/* Free device list */ +void azeron_device_list_free(struct azeron_device_info *devices, size_t count) +{ + (void)count; /* Not used currently */ + free(devices); +} + +/* Open device by VID/PID */ +int azeron_device_open(struct azeron_device **device, uint16_t vendor_id, uint16_t product_id) +{ + struct azeron_device *dev; + int ret; + + if (!device) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!g_initialized) { + return AZERON_ERROR_INIT; + } + + dev = calloc(1, sizeof(struct azeron_device)); + if (!dev) { + return AZERON_ERROR_NO_MEM; + } + + dev->context = g_context; + pthread_mutex_init(&dev->mutex, NULL); + + ret = libusb_open_device_with_vid_pid(g_context, vendor_id, product_id); + if (!ret) { + AZERON_ERROR("Failed to open device %04x:%04x", vendor_id, product_id); + free(dev); + return AZERON_ERROR_NOT_FOUND; + } + + dev->handle = ret; + azeron_device_info_from_libusb(dev, libusb_get_device(dev->handle)); + + *device = dev; + + AZERON_LOG("Device opened: %s (%04x:%04x)", dev->info.product, vendor_id, product_id); + + return AZERON_SUCCESS; +} + +/* Open device by index */ +int azeron_device_open_index(struct azeron_device **device, size_t index) +{ + libusb_device **dev_list; + ssize_t num_devs; + int i; + size_t azeron_idx = 0; + int ret; + + if (!device) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!g_initialized) { + return AZERON_ERROR_INIT; + } + + num_devs = libusb_get_device_list(g_context, &dev_list); + if (num_devs < 0) { + return azeron_libusb_to_azeron_error(num_devs); + } + + for (i = 0; i < num_devs; i++) { + struct libusb_device_descriptor desc; + ret = libusb_get_device_descriptor(dev_list[i], &desc); + if (ret >= 0 && desc.idVendor == AZERON_VENDOR_ID && desc.idProduct == AZERON_PRODUCT_ID) { + if (azeron_idx == index) { + struct azeron_device *dev = calloc(1, sizeof(struct azeron_device)); + if (!dev) { + libusb_free_device_list(dev_list, 1); + return AZERON_ERROR_NO_MEM; + } + + dev->context = g_context; + pthread_mutex_init(&dev->mutex, NULL); + + ret = libusb_open(dev_list[i], &dev->handle); + if (ret < 0) { + AZERON_ERROR("Failed to open device at index %zu: %s", index, azeron_usb_error_string(ret)); + free(dev); + libusb_free_device_list(dev_list, 1); + return azeron_libusb_to_azeron_error(ret); + } + + azeron_device_info_from_libusb(dev, dev_list[i]); + libusb_free_device_list(dev_list, 1); + + *device = dev; + + AZERON_LOG("Device opened at index %zu: %s", index, dev->info.product); + + return AZERON_SUCCESS; + } + azeron_idx++; + } + } + + libusb_free_device_list(dev_list, 1); + return AZERON_ERROR_NOT_FOUND; +} + +/* Close device */ +void azeron_device_close(struct azeron_device *device) +{ + if (!device) { + return; + } + + if (device->claimed) { + azeron_device_release(device); + } + + if (device->handle) { + libusb_close(device->handle); + } + + pthread_mutex_destroy(&device->mutex); + free(device); + + AZERON_LOG("Device closed"); +} + +/* Get device information */ +int azeron_device_get_info(struct azeron_device *device, struct azeron_device_info *info) +{ + if (!device || !info) { + return AZERON_ERROR_INVALID_PARAM; + } + + memcpy(info, &device->info, sizeof(struct azeron_device_info)); + return AZERON_SUCCESS; +} + +/* Button type to string */ +const char *azeron_button_type_string(enum azeron_button_type type) +{ + switch (type) { + case AZERON_BTN_KEYBOARD: + return "keyboard"; + case AZERON_BTN_MOUSE: + return "mouse"; + case AZERON_BTN_GAMEPAD: + return "gamepad"; + case AZERON_BTN_MACRO: + return "macro"; + case AZERON_BTN_LAYER_SWITCH: + return "layer_switch"; + default: + return "unknown"; + } +} + +/* Stick mode to string */ +const char *azeron_stick_mode_string(enum azeron_stick_mode mode) +{ + switch (mode) { + case AZERON_STICK_ANALOG: + return "analog"; + case AZERON_STICK_DIGITAL_4: + return "digital_4"; + case AZERON_STICK_DIGITAL_8: + return "digital_8"; + case AZERON_STICK_MOUSE: + return "mouse"; + default: + return "unknown"; + } +} diff --git a/libazeron/azeron.h b/libazeron/azeron.h new file mode 100644 index 0000000..fe63dc7 --- /dev/null +++ b/libazeron/azeron.h @@ -0,0 +1,151 @@ +/* + * Azeron Linux Configuration Library + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#ifndef AZERON_H +#define AZERON_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define AZERON_VENDOR_ID 0x16d0 +#define AZERON_PRODUCT_ID 0x113c + +#define AZERON_MAX_BUTTONS 32 +#define AZERON_MAX_PROFILES 3 + +/* Error codes */ +enum azeron_error { + AZERON_SUCCESS = 0, + AZERON_ERROR_INIT = -1, + AZERON_ERROR_NOT_FOUND = -2, + AZERON_ERROR_ACCESS = -3, + AZERON_ERROR_IO = -4, + AZERON_ERROR_PROTOCOL = -5, + AZERON_ERROR_INVALID_PARAM = -6, + AZERON_ERROR_NO_MEM = -7, + AZERON_ERROR_UNSUPPORTED = -8, +}; + +/* Button types that can be mapped */ +enum azeron_button_type { + AZERON_BTN_KEYBOARD = 0, + AZERON_BTN_MOUSE, + AZERON_BTN_GAMEPAD, + AZERON_BTN_MACRO, + AZERON_BTN_LAYER_SWITCH, +}; + +/* Analog stick modes */ +enum azeron_stick_mode { + AZERON_STICK_ANALOG = 0, + AZERON_STICK_DIGITAL_4, + AZERON_STICK_DIGITAL_8, + AZERON_STICK_MOUSE, +}; + +/* Device information */ +struct azeron_device_info { + uint16_t vendor_id; + uint16_t product_id; + char serial_number[64]; + char manufacturer[128]; + char product[128]; + uint8_t firmware_version; + uint8_t num_profiles; + uint8_t active_profile; +}; + +/* Button mapping */ +struct azeron_button_mapping { + uint8_t button_id; + enum azeron_button_type type; + uint16_t key_code; /* Linux input event code */ + char *macro; /* For macro type */ + uint8_t layer_target; /* For layer switch type */ +}; + +/* Analog stick configuration */ +struct azeron_stick_config { + enum azeron_stick_mode mode; + uint8_t deadzone; /* 0-100 */ + uint8_t sensitivity; /* 0-100 */ + bool invert_x; + bool invert_y; + uint8_t response_curve; /* 0=linear, 1=exponential, etc. */ +}; + +/* Profile configuration */ +struct azeron_profile { + uint8_t profile_id; + char name[64]; + struct azeron_button_mapping buttons[AZERON_MAX_BUTTONS]; + uint8_t num_buttons; + struct azeron_stick_config stick_config; +}; + +/* Opaque device handle */ +struct azeron_device; + +/* Library initialization */ +int azeron_init(void); +void azeron_exit(void); +const char *azeron_error_string(int error); + +/* Device management */ +int azeron_device_list(struct azeron_device_info **devices, size_t *count); +void azeron_device_list_free(struct azeron_device_info *devices, size_t count); + +int azeron_device_open(struct azeron_device **device, uint16_t vendor_id, uint16_t product_id); +int azeron_device_open_index(struct azeron_device **device, size_t index); +void azeron_device_close(struct azeron_device *device); + +int azeron_device_get_info(struct azeron_device *device, struct azeron_device_info *info); + +/* Button mapping */ +int azeron_device_get_button_mapping(struct azeron_device *device, uint8_t button_id, + struct azeron_button_mapping *mapping); +int azeron_device_set_button_mapping(struct azeron_device *device, + const struct azeron_button_mapping *mapping); + +/* Analog stick configuration */ +int azeron_device_get_stick_config(struct azeron_device *device, + struct azeron_stick_config *config); +int azeron_device_set_stick_config(struct azeron_device *device, + const struct azeron_stick_config *config); + +/* Profile management */ +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); + +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, + const struct azeron_profile *profile); + +/* Configuration import/export */ +int azeron_device_export_config(struct azeron_device *device, const char *filename); +int azeron_device_import_config(struct azeron_device *device, const char *filename); + +int azeron_device_export_config_json(struct azeron_device *device, struct json_object **json); +int azeron_device_import_config_json(struct azeron_device *device, struct json_object *json); + +/* Utility functions */ +const char *azeron_button_type_string(enum azeron_button_type type); +const char *azeron_stick_mode_string(enum azeron_stick_mode mode); +int azeron_keycode_from_string(const char *key_name); +const char *azeron_keycode_to_string(int keycode); + +#ifdef __cplusplus +} +#endif + +#endif /* AZERON_H */ diff --git a/libazeron/azeron.pc.in b/libazeron/azeron.pc.in new file mode 100644 index 0000000..ad7ae0f --- /dev/null +++ b/libazeron/azeron.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: azeron +Description: Azeron device configuration library +Version: @PROJECT_VERSION@ +Libs: -L${libdir} -lazeron +Cflags: -I${includedir} +Requires: libusb-1.0 json-c diff --git a/libazeron/device.c b/libazeron/device.c new file mode 100644 index 0000000..ced180f --- /dev/null +++ b/libazeron/device.c @@ -0,0 +1,218 @@ +/* + * Azeron Linux Configuration Library - Device Communication + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include "azeron.h" +#include "internal.h" +#include +#include + +/* Claim device interfaces */ +int azeron_device_claim(struct azeron_device *device) +{ + int ret; + + if (!device || !device->handle) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (device->claimed) { + return AZERON_SUCCESS; + } + + pthread_mutex_lock(&device->mutex); + + /* Claim all interfaces - the device has 5 interfaces (0-4) */ + for (int i = 0; i <= 4; i++) { + ret = libusb_claim_interface(device->handle, i); + if (ret < 0) { + AZERON_ERROR("Failed to claim interface %d: %s", i, azeron_usb_error_string(ret)); + + /* Release already claimed interfaces */ + for (int j = 0; j < i; j++) { + libusb_release_interface(device->handle, j); + } + + pthread_mutex_unlock(&device->mutex); + return azeron_libusb_to_azeron_error(ret); + } + } + + device->claimed = true; + pthread_mutex_unlock(&device->mutex); + + AZERON_LOG("Device interfaces claimed"); + + return AZERON_SUCCESS; +} + +/* Release device interfaces */ +int azeron_device_release(struct azeron_device *device) +{ + int ret; + int overall_ret = AZERON_SUCCESS; + + if (!device || !device->handle) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!device->claimed) { + return AZERON_SUCCESS; + } + + pthread_mutex_lock(&device->mutex); + + /* Release all interfaces */ + for (int i = 0; i <= 4; i++) { + ret = libusb_release_interface(device->handle, i); + if (ret < 0) { + AZERON_ERROR("Failed to release interface %d: %s", i, azeron_usb_error_string(ret)); + overall_ret = azeron_libusb_to_azeron_error(ret); + } + } + + device->claimed = false; + pthread_mutex_unlock(&device->mutex); + + AZERON_LOG("Device interfaces released"); + + return overall_ret; +} + +/* Read data from endpoint */ +int azeron_device_read(struct azeron_device *device, uint8_t endpoint, uint8_t *data, size_t size, int timeout) +{ + int transferred; + int ret; + + if (!device || !device->handle || !data) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!device->claimed) { + ret = azeron_device_claim(device); + if (ret != AZERON_SUCCESS) { + return ret; + } + } + + pthread_mutex_lock(&device->mutex); + + ret = libusb_interrupt_transfer(device->handle, endpoint, data, size, &transferred, timeout); + + pthread_mutex_unlock(&device->mutex); + + if (ret < 0) { + AZERON_ERROR("Failed to read from endpoint 0x%02x: %s", endpoint, azeron_usb_error_string(ret)); + return azeron_libusb_to_azeron_error(ret); + } + + AZERON_LOG("Read %d bytes from endpoint 0x%02x", transferred, endpoint); + + return transferred; +} + +/* Write data to endpoint */ +int azeron_device_write(struct azeron_device *device, uint8_t endpoint, const uint8_t *data, size_t size, int timeout) +{ + int transferred; + int ret; + + if (!device || !device->handle || !data) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!device->claimed) { + ret = azeron_device_claim(device); + if (ret != AZERON_SUCCESS) { + return ret; + } + } + + pthread_mutex_lock(&device->mutex); + + ret = libusb_interrupt_transfer(device->handle, endpoint, (uint8_t *)data, size, &transferred, timeout); + + pthread_mutex_unlock(&device->mutex); + + if (ret < 0) { + AZERON_ERROR("Failed to write to endpoint 0x%02x: %s", endpoint, azeron_usb_error_string(ret)); + return azeron_libusb_to_azeron_error(ret); + } + + AZERON_LOG("Wrote %d bytes to endpoint 0x%02x", transferred, endpoint); + + return transferred; +} + +/* Perform control transfer */ +int azeron_device_control_transfer(struct azeron_device *device, uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, uint8_t *data, size_t size, int timeout) +{ + int ret; + + if (!device || !device->handle) { + return AZERON_ERROR_INVALID_PARAM; + } + + if (!device->claimed) { + ret = azeron_device_claim(device); + if (ret != AZERON_SUCCESS) { + return ret; + } + } + + pthread_mutex_lock(&device->mutex); + + ret = libusb_control_transfer(device->handle, request_type, request, value, index, data, size, timeout); + + pthread_mutex_unlock(&device->mutex); + + if (ret < 0) { + AZERON_ERROR("Control transfer failed: %s", azeron_usb_error_string(ret)); + return azeron_libusb_to_azeron_error(ret); + } + + AZERON_LOG("Control transfer: %d bytes transferred", ret); + + return ret; +} + +/* Get button type string */ +const char *azeron_button_type_string(enum azeron_button_type type) +{ + switch (type) { + case AZERON_BTN_KEYBOARD: + return "keyboard"; + case AZERON_BTN_MOUSE: + return "mouse"; + case AZERON_BTN_GAMEPAD: + return "gamepad"; + case AZERON_BTN_MACRO: + return "macro"; + case AZERON_BTN_LAYER_SWITCH: + return "layer_switch"; + default: + return "unknown"; + } +} + +/* Get stick mode string */ +const char *azeron_stick_mode_string(enum azeron_stick_mode mode) +{ + switch (mode) { + case AZERON_STICK_ANALOG: + return "analog"; + case AZERON_STICK_DIGITAL_4: + return "digital_4"; + case AZERON_STICK_DIGITAL_8: + return "digital_8"; + case AZERON_STICK_MOUSE: + return "mouse"; + default: + return "unknown"; + } +} diff --git a/libazeron/internal.h b/libazeron/internal.h new file mode 100644 index 0000000..5c97174 --- /dev/null +++ b/libazeron/internal.h @@ -0,0 +1,61 @@ +/* + * Azeron Linux Configuration Library - Internal Header + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#ifndef AZERON_INTERNAL_H +#define AZERON_INTERNAL_H + +#include "azeron.h" +#include +#include + +#define AZERON_USB_TIMEOUT 1000 +#define AZERON_MAX_STRING_LENGTH 256 + +/* Debug logging */ +#ifdef AZERON_DEBUG +#define AZERON_LOG(fmt, ...) fprintf(stderr, "[AZERON] " fmt "\n", ##__VA_ARGS__) +#else +#define AZERON_LOG(fmt, ...) do {} while (0) +#endif + +/* Error logging */ +#define AZERON_ERROR(fmt, ...) fprintf(stderr, "[AZERON ERROR] " fmt "\n", ##__VA_ARGS__) + +/* Device structure */ +struct azeron_device { + libusb_device_handle *handle; + libusb_context *context; + struct azeron_device_info info; + pthread_mutex_t mutex; + bool claimed; +}; + +/* Protocol functions */ +int azeron_protocol_init(struct azeron_device *device); +int azeron_protocol_read_config(struct azeron_device *device, uint8_t *data, size_t *size); +int azeron_protocol_write_config(struct azeron_device *device, const uint8_t *data, size_t size); +int azeron_protocol_get_button_mapping(struct azeron_device *device, uint8_t button_id, struct azeron_button_mapping *mapping); +int azeron_protocol_set_button_mapping(struct azeron_device *device, const struct azeron_button_mapping *mapping); +int azeron_protocol_get_stick_config(struct azeron_device *device, struct azeron_stick_config *config); +int azeron_protocol_set_stick_config(struct azeron_device *device, const struct azeron_stick_config *config); +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); + +/* Device functions */ +int azeron_device_claim(struct azeron_device *device); +int azeron_device_release(struct azeron_device *device); +int azeron_device_read(struct azeron_device *device, uint8_t endpoint, uint8_t *data, size_t size, int timeout); +int azeron_device_write(struct azeron_device *device, uint8_t endpoint, const uint8_t *data, size_t size, int timeout); +int azeron_device_control_transfer(struct azeron_device *device, uint8_t request_type, uint8_t request, uint16_t value, uint16_t index, uint8_t *data, size_t size, int timeout); + +/* Utility functions */ +const char *azeron_usb_error_string(int error); +int azeron_libusb_to_azeron_error(int libusb_error); +void azeron_device_info_from_libusb(struct azeron_device *device, libusb_device *libusb_dev); + +#endif /* AZERON_INTERNAL_H */ diff --git a/libazeron/protocol.c b/libazeron/protocol.c new file mode 100644 index 0000000..11d4655 --- /dev/null +++ b/libazeron/protocol.c @@ -0,0 +1,108 @@ +/* + * Azeron Linux Configuration Library - Protocol Implementation + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include "azeron.h" +#include "internal.h" +#include + +/* Protocol initialization */ +int azeron_protocol_init(struct azeron_device *device) +{ + (void)device; + AZERON_LOG("Protocol initialization - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Read configuration from device */ +int azeron_protocol_read_config(struct azeron_device *device, uint8_t *data, size_t *size) +{ + (void)device; + (void)data; + (void)size; + AZERON_LOG("Read config - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* 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) +{ + (void)device; + (void)button_id; + (void)mapping; + AZERON_LOG("Get button mapping - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Set button mapping */ +int azeron_protocol_set_button_mapping(struct azeron_device *device, + const struct azeron_button_mapping *mapping) +{ + (void)device; + (void)mapping; + AZERON_LOG("Set button mapping - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Get stick configuration */ +int azeron_protocol_get_stick_config(struct azeron_device *device, + struct azeron_stick_config *config) +{ + (void)device; + (void)config; + AZERON_LOG("Get stick config - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Set stick configuration */ +int azeron_protocol_set_stick_config(struct azeron_device *device, + const struct azeron_stick_config *config) +{ + (void)device; + (void)config; + AZERON_LOG("Set stick config - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* 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; + AZERON_LOG("Get profile - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Set profile */ +int azeron_protocol_set_profile(struct azeron_device *device, + const struct azeron_profile *profile) +{ + (void)device; + (void)profile; + AZERON_LOG("Set profile - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Save configuration to device */ +int azeron_protocol_save_to_device(struct azeron_device *device) +{ + (void)device; + AZERON_LOG("Save to device - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} diff --git a/libazeron/utils.c b/libazeron/utils.c new file mode 100644 index 0000000..886acad --- /dev/null +++ b/libazeron/utils.c @@ -0,0 +1,229 @@ +/* + * Azeron Linux Configuration Library - Utility Functions + * Copyright (C) 2024 Azeron Linux Project + * + * SPDX-License-Identifier: MIT + */ + +#include "azeron.h" +#include +#include + +/* Key name to keycode mapping table */ +static struct { + const char *name; + int keycode; +} keymap[] = { + /* Letters */ + {"KEY_A", KEY_A}, {"KEY_B", KEY_B}, {"KEY_C", KEY_C}, {"KEY_D", KEY_D}, + {"KEY_E", KEY_E}, {"KEY_F", KEY_F}, {"KEY_G", KEY_G}, {"KEY_H", KEY_H}, + {"KEY_I", KEY_I}, {"KEY_J", KEY_J}, {"KEY_K", KEY_K}, {"KEY_L", KEY_L}, + {"KEY_M", KEY_M}, {"KEY_N", KEY_N}, {"KEY_O", KEY_O}, {"KEY_P", KEY_P}, + {"KEY_Q", KEY_Q}, {"KEY_R", KEY_R}, {"KEY_S", KEY_S}, {"KEY_T", KEY_T}, + {"KEY_U", KEY_U}, {"KEY_V", KEY_V}, {"KEY_W", KEY_W}, {"KEY_X", KEY_X}, + {"KEY_Y", KEY_Y}, {"KEY_Z", KEY_Z}, + + /* Numbers */ + {"KEY_1", KEY_1}, {"KEY_2", KEY_2}, {"KEY_3", KEY_3}, {"KEY_4", KEY_4}, + {"KEY_5", KEY_5}, {"KEY_6", KEY_6}, {"KEY_7", KEY_7}, {"KEY_8", KEY_8}, + {"KEY_9", KEY_9}, {"KEY_0", KEY_0}, + + /* Function keys */ + {"KEY_F1", KEY_F1}, {"KEY_F2", KEY_F2}, {"KEY_F3", KEY_F3}, {"KEY_F4", KEY_F4}, + {"KEY_F5", KEY_F5}, {"KEY_F6", KEY_F6}, {"KEY_F7", KEY_F7}, {"KEY_F8", KEY_F8}, + {"KEY_F9", KEY_F9}, {"KEY_F10", KEY_F10}, {"KEY_F11", KEY_F11}, {"KEY_F12", KEY_F12}, + + /* Special keys */ + {"KEY_ESC", KEY_ESC}, + {"KEY_TAB", KEY_TAB}, + {"KEY_CAPSLOCK", KEY_CAPSLOCK}, + {"KEY_LEFTSHIFT", KEY_LEFTSHIFT}, {"KEY_RIGHTSHIFT", KEY_RIGHTSHIFT}, + {"KEY_LEFTCTRL", KEY_LEFTCTRL}, {"KEY_RIGHTCTRL", KEY_RIGHTCTRL}, + {"KEY_LEFTALT", KEY_LEFTALT}, {"KEY_RIGHTALT", KEY_RIGHTALT}, + {"KEY_SPACE", KEY_SPACE}, + {"KEY_ENTER", KEY_ENTER}, + {"KEY_BACKSPACE", KEY_BACKSPACE}, + {"KEY_DELETE", KEY_DELETE}, + {"KEY_INSERT", KEY_INSERT}, + {"KEY_HOME", KEY_HOME}, + {"KEY_END", KEY_END}, + {"KEY_PAGEUP", KEY_PAGEUP}, + {"KEY_PAGEDOWN", KEY_PAGEDOWN}, + + /* Arrow keys */ + {"KEY_UP", KEY_UP}, + {"KEY_DOWN", KEY_DOWN}, + {"KEY_LEFT", KEY_LEFT}, + {"KEY_RIGHT", KEY_RIGHT}, + + /* Numpad */ + {"KEY_NUMLOCK", KEY_NUMLOCK}, + {"KEY_KP0", KEY_KP0}, {"KEY_KP1", KEY_KP1}, {"KEY_KP2", KEY_KP2}, {"KEY_KP3", KEY_KP3}, + {"KEY_KP4", KEY_KP4}, {"KEY_KP5", KEY_KP5}, {"KEY_KP6", KEY_KP6}, {"KEY_KP7", KEY_KP7}, + {"KEY_KP8", KEY_KP8}, {"KEY_KP9", KEY_KP9}, + {"KEY_KPSLASH", KEY_KPSLASH}, {"KEY_KPASTERISK", KEY_KPASTERISK}, + {"KEY_KPMINUS", KEY_KPMINUS}, {"KEY_KPPLUS", KEY_KPPLUS}, + {"KEY_KPENTER", KEY_KPENTER}, {"KEY_KPDOT", KEY_KPDOT}, + + /* Mouse buttons */ + {"BTN_LEFT", BTN_LEFT}, + {"BTN_RIGHT", BTN_RIGHT}, + {"BTN_MIDDLE", BTN_MIDDLE}, + {"BTN_SIDE", BTN_SIDE}, + {"BTN_EXTRA", BTN_EXTRA}, + + {NULL, 0} +}; + +/* Convert key name to keycode */ +int azeron_keycode_from_string(const char *key_name) +{ + int i; + + if (!key_name) { + return -1; + } + + /* First try exact match */ + for (i = 0; keymap[i].name != NULL; i++) { + if (strcmp(key_name, keymap[i].name) == 0) { + return keymap[i].keycode; + } + } + + /* Try without KEY_ prefix */ + if (strncmp(key_name, "KEY_", 4) != 0) { + char prefixed[64]; + snprintf(prefixed, sizeof(prefixed), "KEY_%s", key_name); + for (i = 0; keymap[i].name != NULL; i++) { + if (strcmp(prefixed, keymap[i].name) == 0) { + return keymap[i].keycode; + } + } + } + + return -1; +} + +/* Convert keycode to string */ +const char *azeron_keycode_to_string(int keycode) +{ + int i; + + for (i = 0; keymap[i].name != NULL; i++) { + if (keymap[i].keycode == keycode) { + return keymap[i].name; + } + } + + return "UNKNOWN"; +} + +/* Export configuration to JSON */ +int azeron_device_export_config_json(struct azeron_device *device, struct json_object **json) +{ + struct json_object *root; + struct json_object *profiles_array; + struct azeron_device_info info; + int ret; + int i; + + if (!device || !json) { + return AZERON_ERROR_INVALID_PARAM; + } + + root = json_object_new_object(); + if (!root) { + return AZERON_ERROR_NO_MEM; + } + + /* Add device info */ + ret = azeron_device_get_info(device, &info); + if (ret == AZERON_SUCCESS) { + json_object_object_add(root, "vendor_id", json_object_new_int(info.vendor_id)); + json_object_object_add(root, "product_id", json_object_new_int(info.product_id)); + json_object_object_add(root, "serial_number", json_object_new_string(info.serial_number)); + json_object_object_add(root, "firmware_version", json_object_new_int(info.firmware_version)); + } + + /* Create profiles array */ + profiles_array = json_object_new_array(); + if (!profiles_array) { + json_object_put(root); + return AZERON_ERROR_NO_MEM; + } + + /* Add placeholder profiles - actual implementation would read from device */ + for (i = 0; i < 3; i++) { + struct json_object *profile = json_object_new_object(); + char name[32]; + + snprintf(name, sizeof(name), "Profile %d", i + 1); + json_object_object_add(profile, "id", json_object_new_int(i)); + json_object_object_add(profile, "name", json_object_new_string(name)); + json_object_object_add(profile, "active", json_object_new_boolean(i == 0)); + + json_object_array_add(profiles_array, profile); + } + + json_object_object_add(root, "profiles", profiles_array); + + *json = root; + return AZERON_SUCCESS; +} + +/* Import configuration from JSON */ +int azeron_device_import_config_json(struct azeron_device *device, struct json_object *json) +{ + (void)device; + (void)json; + + AZERON_LOG("Import config from JSON - not yet implemented"); + return AZERON_ERROR_UNSUPPORTED; +} + +/* Export configuration to file */ +int azeron_device_export_config(struct azeron_device *device, const char *filename) +{ + struct json_object *json; + int ret; + + if (!device || !filename) { + return AZERON_ERROR_INVALID_PARAM; + } + + ret = azeron_device_export_config_json(device, &json); + if (ret != AZERON_SUCCESS) { + return ret; + } + + ret = json_object_to_file_ext(filename, json, JSON_C_TO_STRING_PRETTY); + json_object_put(json); + + if (ret != 0) { + return AZERON_ERROR_IO; + } + + return AZERON_SUCCESS; +} + +/* Import configuration from file */ +int azeron_device_import_config(struct azeron_device *device, const char *filename) +{ + struct json_object *json; + int ret; + + if (!device || !filename) { + return AZERON_ERROR_INVALID_PARAM; + } + + json = json_object_from_file(filename); + if (!json) { + return AZERON_ERROR_IO; + } + + ret = azeron_device_import_config_json(device, json); + json_object_put(json); + + return ret; +} diff --git a/overview.txt b/overview.txt new file mode 100644 index 0000000..336c9ea --- /dev/null +++ b/overview.txt @@ -0,0 +1 @@ +The Azeron Cyborg is a keypad with a single analog stick that allows the user to map keyboard/controller inputs to one of it's many buttons for gaming. It uses custom software to map the keys to the desired input. \ No newline at end of file diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..e4272cf --- /dev/null +++ b/plan.md @@ -0,0 +1,128 @@ +# Azeron Cyborg Linux Configuration Software - Project Plan + +## Overview +The Azeron Cyborg keypad (USB ID: 16d0:113c) is already recognized by Linux as a HID device and basic functionality works. However, Linux users need configuration software to remap buttons, configure analog sticks, and manage profiles - similar to the Windows software. + +## Current Status +- **Device**: Azeron Cyborg Keypad (MCS, Vendor ID: 0x16d0, Product ID: 0x113c) +- **Linux Support**: Device creates multiple input devices (/dev/input/event*, /dev/input/js*, /dev/input/mouse*) +- **Working Features**: Basic input with pre-configured mappings (stored in device firmware) +- **Missing**: Configuration software to modify mappings, analog stick settings, and profiles + +## USB Device Analysis +The device has 5 interfaces: +1. **Interface 0**: Vendor-specific (0xFF) - Likely configuration interface +2. **Interface 1**: HID - Main input interface (buttons) +3. **Interface 2**: HID Boot Mouse - Mouse emulation +4. **Interface 3**: HID - Analog stick input +5. **Interface 4**: HID with IN/OUT endpoints - Likely LED/control interface + +## Architecture Decision +**Userspace Application Approach** (not kernel driver needed): +- Linux already recognizes device as HID +- Configuration is done through USB control transfers +- Userspace app can communicate with vendor-specific interface +- No kernel module required - more portable and easier to maintain + +## Project Structure +``` +azeron-linux/ +├── libazeron/ # Core library for device communication +│ ├── azeron.c # USB communication and protocol handling +│ ├── azeron.h # Library API header +│ └── protocol.md # Documented protocol findings +├── azeron-cli/ # Command-line configuration tool +│ ├── main.c # CLI application +│ └── Makefile +├── azeron-gui/ # GUI configuration application +│ ├── main.py # Python GUI (Qt/GTK) +│ ├── ui/ # UI files +│ └── requirements.txt +├── docs/ # Documentation +│ ├── installation.md +│ ├── usage.md +│ └── development.md +├── scripts/ # Helper scripts +│ ├── udev-rules/ # udev rules for device permissions +│ └── build.sh # Build script +└── tests/ # Test suite + └── test_protocol.c +``` + +## Implementation Phases + +### Phase 1: Core Library (libazeron) +- [ ] Set up build system (CMake/autotools) +- [ ] Implement USB device detection and connection +- [ ] Create device communication abstraction layer +- [ ] Implement basic USB control transfers +- [ ] Add error handling and logging + +### Phase 2: Protocol Reverse Engineering +- [ ] Capture USB traffic from Windows software (if available) +- [ ] Analyze vendor-specific interface (Interface 0) +- [ ] Document configuration command structure +- [ ] Map button IDs and analog stick data formats +- [ ] Understand profile storage and retrieval + +### Phase 3: Command-Line Tool (azeron-cli) +- [ ] List connected Azeron devices +- [ ] Read current button mappings +- [ ] Remap buttons (assign keyboard keys, mouse buttons, etc.) +- [ ] Configure analog stick (deadzone, sensitivity, curves) +- [ ] Manage profiles (save, load, switch) +- [ ] Export/import configurations + +### Phase 4: GUI Application (azeron-gui) +- [ ] Create visual button mapping interface +- [ ] Implement drag-and-drop or click-to-configure UI +- [ ] Add analog stick calibration wizard +- [ ] Profile management with visual indicators +- [ ] Real-time configuration testing +- [ ] Save/load configuration files + +### Phase 5: Advanced Features +- [ ] LED control and effects (if supported) +- [ ] Macro recording and playback +- [ ] Analog stick mode switching (digital vs analog) +- [ ] Multiple device support +- [ ] Configuration backup/restore + +### Phase 6: Testing and Documentation +- [ ] Unit tests for core library +- [ ] Integration tests for CLI and GUI +- [ ] User documentation and tutorials +- [ ] Packaging (RPM for Fedora, DEB for Ubuntu) +- [ ] udev rules for non-root access + +## Technical Requirements + +### Dependencies +- **libusb-1.0**: For USB communication +- **libevdev**: For input event handling (testing) +- **GTK+ 3/Qt 5**: For GUI application +- **json-c**: For configuration file format +- **pkg-config**: For build configuration + +### Development Tools +- GCC/Clang C compiler +- CMake or GNU Autotools +- Git for version control +- Wireshark (for protocol analysis) +- evtest/jstest (for testing input) + +## Success Criteria +1. Users can remap all buttons on the Azeron keypad +2. Analog stick can be configured (deadzone, sensitivity, response curves) +3. Multiple profiles can be created, saved, and switched +4. Configuration persists on the device (like Windows software) +5. Both CLI and GUI tools work reliably +6. No root access required (proper udev rules) +7. Documentation covers installation and common use cases + +## Next Steps +1. Set up development environment and project structure +2. Begin implementing core USB communication library +3. Start protocol reverse engineering (with or without Windows capture) +4. Create basic CLI tool for testing +5. Iterate on functionality based on testing feedback \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..b9647e1 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,272 @@ +#!/bin/bash + +# Azeron Linux Build Script +# Builds the project and optionally installs it + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +BUILD_TYPE="Release" +INSTALL_PREFIX="/usr/local" +BUILD_DIR="build" +INSTALL_UDEV="yes" +RUN_TESTS="no" + +# Print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Show usage +usage() { + cat << EOF +Usage: $0 [options] + +Options: + -h, --help Show this help message + -d, --debug Build in debug mode (default: Release) + -p, --prefix PATH Installation prefix (default: /usr/local) + -b, --build-dir DIR Build directory (default: build) + -t, --test Run tests after building + -n, --no-udev Skip udev rules installation + -c, --clean Clean build directory before building + -i, --install Install after building + +Examples: + $0 # Build only + $0 -i # Build and install + $0 -d -t # Debug build with tests + $0 -c -i # Clean build and install + +EOF +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -d|--debug) + BUILD_TYPE="Debug" + shift + ;; + -p|--prefix) + INSTALL_PREFIX="$2" + shift 2 + ;; + -b|--build-dir) + BUILD_DIR="$2" + shift 2 + ;; + -t|--test) + RUN_TESTS="yes" + shift + ;; + -n|--no-udev) + INSTALL_UDEV="no" + shift + ;; + -c|--clean) + if [ -d "$BUILD_DIR" ]; then + print_info "Cleaning build directory..." + rm -rf "$BUILD_DIR" + fi + shift + ;; + -i|--install) + INSTALL="yes" + shift + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Check dependencies +check_dependencies() { + print_info "Checking dependencies..." + + # Check for cmake + if ! command -v cmake &> /dev/null; then + print_error "cmake is required but not installed" + exit 1 + fi + + # Check for C compiler + if ! command -v gcc &> /dev/null && ! command -v clang &> /dev/null; then + print_error "C compiler (gcc or clang) is required but not found" + exit 1 + fi + + # Check for pkg-config + if ! command -v pkg-config &> /dev/null; then + print_error "pkg-config is required but not installed" + exit 1 + fi + + # Check for libusb-1.0 + if ! pkg-config --exists libusb-1.0; then + print_error "libusb-1.0 is required but not installed" + print_info "Install it with:" + print_info " Fedora: sudo dnf install libusb1-devel" + print_info " Ubuntu: sudo apt-get install libusb-1.0-0-dev" + print_info " Arch: sudo pacman -S libusb" + exit 1 + fi + + # Check for json-c + if ! pkg-config --exists json-c; then + print_error "json-c is required but not installed" + print_info "Install it with:" + print_info " Fedora: sudo dnf install json-c-devel" + print_info " Ubuntu: sudo apt-get install libjson-c-dev" + print_info " Arch: sudo pacman -S json-c" + exit 1 + fi + + print_info "All dependencies found" +} + +# Build the project +build_project() { + print_info "Building project..." + print_info "Build type: $BUILD_TYPE" + print_info "Install prefix: $INSTALL_PREFIX" + + # Create build directory + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + + # Configure with cmake + print_info "Configuring with cmake..." + cmake .. -DCMAKE_BUILD_TYPE="$BUILD_TYPE" -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" + + # Build + print_info "Building..." + make -j$(nproc) + + cd .. + + print_info "Build completed successfully" +} + +# Run tests +run_tests() { + if [ "$RUN_TESTS" != "yes" ]; then + return + fi + + print_info "Running tests..." + + cd "$BUILD_DIR" + + if [ -f "test_azeron" ]; then + ./test_azeron + else + print_warning "No test executable found" + fi + + cd .. +} + +# Install the project +install_project() { + if [ "$INSTALL" != "yes" ]; then + return + fi + + print_info "Installing project..." + + cd "$BUILD_DIR" + + if [ "$EUID" -ne 0 ]; then + print_warning "Installing without root privileges" + print_warning "You may need to run: sudo $0 --install" + fi + + make install + + # Install udev rules if requested + if [ "$INSTALL_UDEV" = "yes" ]; then + if [ "$EUID" -eq 0 ]; then + print_info "Installing udev rules..." + cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/ + udevadm control --reload-rules + udevadm trigger + print_info "Udev rules installed. Reconnect your Azeron device." + else + print_warning "Skipping udev rules installation (requires root)" + print_info "To install udev rules manually, run:" + print_info " sudo cp scripts/udev-rules/99-azeron.rules /etc/udev/rules.d/" + print_info " sudo udevadm control --reload-rules" + print_info " sudo udevadm trigger" + fi + fi + + cd .. + + print_info "Installation completed" +} + +# Print build summary +print_summary() { + print_info "Build Summary:" + echo " Build type: $BUILD_TYPE" + echo " Build directory: $BUILD_DIR" + echo " Install prefix: $INSTALL_PREFIX" + echo " Udev rules: $INSTALL_UDEV" + echo " Run tests: $RUN_TESTS" + + if [ -d "$BUILD_DIR" ]; then + echo "" + print_info "Binaries built:" + if [ -f "$BUILD_DIR/azeron-cli" ]; then + echo " - azeron-cli (command-line tool)" + fi + if [ -f "$BUILD_DIR/libazeron/libazeron.so" ]; then + echo " - libazeron.so (library)" + fi + fi +} + +# Main execution +main() { + print_info "Azeron Linux Build Script" + echo "" + + check_dependencies + build_project + run_tests + install_project + + echo "" + print_summary + + if [ "$INSTALL" != "yes" ]; then + echo "" + print_info "To install the software, run: $0 --install" + print_info "Or cd $BUILD_DIR && sudo make install" + fi +} + +# Run main function +main "$@" diff --git a/scripts/udev-rules/99-azeron.rules b/scripts/udev-rules/99-azeron.rules new file mode 100644 index 0000000..d0fd964 --- /dev/null +++ b/scripts/udev-rules/99-azeron.rules @@ -0,0 +1,14 @@ +# udev rules for Azeron Cyborg keypad +# Allows non-root users to access the device + +# Azeron Cyborg Keypad (16d0:113c) +SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="113c", MODE="0666", GROUP="plugdev", TAG+="uaccess" + +# HID raw devices +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="113c", MODE="0666", GROUP="plugdev", TAG+="uaccess" + +# Input devices +SUBSYSTEM=="input", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="113c", MODE="0666", GROUP="input", TAG+="uaccess" + +# USB device node +SUBSYSTEM=="usb", ATTR{idVendor}=="16d0", ATTR{idProduct}=="113c", MODE="0666", GROUP="plugdev", TAG+="uaccess"